diff --git a/src/plugins/appleMusic.desktop/README.md b/src/plugins/appleMusic.desktop/README.md deleted file mode 100644 index 52ab93bfd..000000000 --- a/src/plugins/appleMusic.desktop/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# AppleMusicRichPresence - -This plugin enables Discord rich presence for your Apple Music! (This only works on macOS with the Music app.) - -![Screenshot of the activity in Discord](https://github.com/Vendicated/Vencord/assets/70191398/1f811090-ab5f-4060-a9ee-d0ac44a1d3c0) - -## Configuration - -For the customizable activity format strings, you can use several special strings to include track data in activities! `{name}` is replaced with the track name; `{artist}` is replaced with the artist(s)' name(s); and `{album}` is replaced with the album name. diff --git a/src/plugins/appleMusic.desktop/index.tsx b/src/plugins/appleMusic.desktop/index.tsx deleted file mode 100644 index 5dddb1d55..000000000 --- a/src/plugins/appleMusic.desktop/index.tsx +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { Paragraph } from "@components/Paragraph"; -import { Devs, IS_MAC } from "@utils/constants"; -import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types"; -import { Activity, ActivityAssets, ActivityButton } from "@vencord/discord-types"; -import { ActivityFlags, ActivityStatusDisplayType, ActivityType } from "@vencord/discord-types/enums"; -import { ApplicationAssetUtils, FluxDispatcher } from "@webpack/common"; - -const Native = VencordNative.pluginHelpers.AppleMusicRichPresence as PluginNative; - -export interface TrackData { - name: string; - album?: string; - artist?: string; - - appleMusicLink?: string; - appleMusicArtistLink?: string; - songLink?: string; - - albumArtwork?: string; - artistArtwork?: string; - - playerPosition?: number; - duration?: number; -} - -const enum AssetImageType { - Album = "Album", - Artist = "Artist", - Disabled = "Disabled" -} - -const enum LinkType { - Album = "Album", - Artist = "Artist", - Disabled = "Disabled" -} - -const applicationId = "1239490006054207550"; - -function setActivity(activity: Activity | null) { - FluxDispatcher.dispatch({ - type: "LOCAL_ACTIVITY_UPDATE", - activity, - socketId: "AppleMusic", - }); -} - -const settings = definePluginSettings({ - activityType: { - type: OptionType.SELECT, - description: "Which type of activity", - options: [ - { label: "Playing", value: ActivityType.PLAYING, default: true }, - { label: "Listening", value: ActivityType.LISTENING } - ], - }, - statusDisplayType: { - description: "Show the track / artist name in the member list", - type: OptionType.SELECT, - options: [ - { - label: "Don't show (shows generic listening message)", - value: "off", - default: true - }, - { - label: "Show artist name", - value: "artist" - }, - { - label: "Show track name", - value: "track" - } - ] - }, - refreshInterval: { - type: OptionType.SLIDER, - description: "The interval between activity refreshes (seconds)", - markers: [1, 2, 2.5, 3, 5, 10, 15], - default: 5, - restartNeeded: true, - }, - enableTimestamps: { - type: OptionType.BOOLEAN, - description: "Whether or not to enable timestamps", - default: true, - }, - enableButtons: { - type: OptionType.BOOLEAN, - description: "Whether or not to enable buttons", - default: true, - }, - nameString: { - type: OptionType.STRING, - description: "Activity name format string", - default: "Apple Music" - }, - detailsString: { - type: OptionType.STRING, - description: "Activity details format string", - default: "{name}" - }, - stateString: { - type: OptionType.STRING, - description: "Activity state format string", - default: "{artist} ยท {album}" - }, - detailsLink: { - type: OptionType.SELECT, - description: "Activity details link", - options: [ - { label: "Album", value: LinkType.Album, default: true }, - { label: "Artist", value: LinkType.Artist }, - { label: "Disabled", value: LinkType.Disabled } - ], - }, - stateLink: { - type: OptionType.SELECT, - description: "Activity state link", - options: [ - { label: "Album", value: LinkType.Album }, - { label: "Artist", value: LinkType.Artist, default: true }, - { label: "Disabled", value: LinkType.Disabled } - ], - }, - largeImageType: { - type: OptionType.SELECT, - description: "Activity assets large image type", - options: [ - { label: "Album artwork", value: AssetImageType.Album, default: true }, - { label: "Artist artwork", value: AssetImageType.Artist }, - { label: "Disabled", value: AssetImageType.Disabled } - ], - }, - largeTextString: { - type: OptionType.STRING, - description: "Activity assets large text format string", - default: "{album}" - }, - largeImageLink: { - type: OptionType.SELECT, - description: "Activity assets large image link", - options: [ - { label: "Album", value: LinkType.Album, default: true }, - { label: "Artist", value: LinkType.Artist }, - { label: "Disabled", value: LinkType.Disabled } - ], - }, - smallImageType: { - type: OptionType.SELECT, - description: "Activity assets small image type", - options: [ - { label: "Album artwork", value: AssetImageType.Album }, - { label: "Artist artwork", value: AssetImageType.Artist, default: true }, - { label: "Disabled", value: AssetImageType.Disabled } - ], - }, - smallTextString: { - type: OptionType.STRING, - description: "Activity assets small text format string", - default: "{artist}" - }, - smallImageLink: { - type: OptionType.SELECT, - description: "Activity assets small image link", - options: [ - { label: "Album", value: LinkType.Album }, - { label: "Artist", value: LinkType.Artist, default: true }, - { label: "Disabled", value: LinkType.Disabled } - ], - }, -}); - -function customFormat(formatStr: string, data: TrackData) { - return formatStr - .replaceAll("{name}", data.name) - .replaceAll("{album}", data.album ?? "") - .replaceAll("{artist}", data.artist ?? ""); -} - -function getLink(type: LinkType, data: TrackData) { - return type === LinkType.Album - ? data.appleMusicLink - : type === LinkType.Artist - ? data.appleMusicArtistLink - : undefined; -} - -function getImageAsset(type: AssetImageType, data: TrackData) { - const source = type === AssetImageType.Album - ? data.albumArtwork - : data.artistArtwork; - - if (!source) return undefined; - - return ApplicationAssetUtils.fetchAssetIds(applicationId, [source]).then(ids => ids[0]); -} - -export default definePlugin({ - name: "AppleMusicRichPresence", - description: "Discord rich presence for your Apple Music!", - tags: ["Activity", "Media"], - authors: [Devs.RyanCaoDev], - hidden: !IS_MAC, - reporterTestable: ReporterTestable.None, - - settingsAboutComponent() { - return <> - - For the customizable activity format strings, you can use several special strings to include track data in activities!{" "} - {"{name}"} is replaced with the track name; {"{artist}"} is replaced with the artist(s)' name(s); and {"{album}"} is replaced with the album name. - - ; - }, - - settings, - - start() { - this.updatePresence(); - this.updateInterval = setInterval(() => { this.updatePresence(); }, settings.store.refreshInterval * 1000); - }, - - stop() { - clearInterval(this.updateInterval); - FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null }); - }, - - updatePresence() { - this.getActivity().then(activity => { setActivity(activity); }); - }, - - async getActivity(): Promise { - const trackData = await Native.fetchTrackData(); - if (!trackData) return null; - - const [largeImageAsset, smallImageAsset] = await Promise.all([ - getImageAsset(settings.store.largeImageType, trackData), - getImageAsset(settings.store.smallImageType, trackData) - ]); - - const assets: ActivityAssets = {}; - - const isRadio = Number.isNaN(trackData.duration) && (trackData.playerPosition === 0); - - if (settings.store.largeImageType !== AssetImageType.Disabled) { - assets.large_image = largeImageAsset; - if (!isRadio) assets.large_text = customFormat(settings.store.largeTextString, trackData); - assets.large_url = getLink(settings.store.largeImageLink, trackData); - } - - if (settings.store.smallImageType !== AssetImageType.Disabled) { - assets.small_image = smallImageAsset; - if (!isRadio) assets.small_text = customFormat(settings.store.smallTextString, trackData); - assets.small_url = getLink(settings.store.smallImageLink, trackData); - } - - const buttons: ActivityButton[] = []; - - if (settings.store.enableButtons) { - if (trackData.appleMusicLink) - buttons.push({ - label: "Listen on Apple Music", - url: trackData.appleMusicLink, - }); - - if (trackData.songLink) - buttons.push({ - label: "View on SongLink", - url: trackData.songLink, - }); - } - - return { - application_id: applicationId, - - name: customFormat(settings.store.nameString, trackData), - details: customFormat(settings.store.detailsString, trackData), - state: isRadio ? undefined : customFormat(settings.store.stateString, trackData), - details_url: getLink(settings.store.detailsLink, trackData), - state_url: getLink(settings.store.stateLink, trackData), - - timestamps: (trackData.playerPosition && trackData.duration && settings.store.enableTimestamps) ? { - start: Date.now() - (trackData.playerPosition * 1000), - end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000), - } : undefined, - - assets, - - buttons: !isRadio && buttons.length ? buttons.map(v => v.label) : undefined, - metadata: !isRadio && buttons.length ? { button_urls: buttons.map(v => v.url) } : undefined, - - type: settings.store.activityType, - status_display_type: { - "off": ActivityStatusDisplayType.NAME, - "artist": ActivityStatusDisplayType.STATE, - "track": ActivityStatusDisplayType.DETAILS - }[settings.store.statusDisplayType], - flags: ActivityFlags.INSTANCE, - }; - } -}); diff --git a/src/plugins/appleMusic.desktop/native.ts b/src/plugins/appleMusic.desktop/native.ts deleted file mode 100644 index 0c137e953..000000000 --- a/src/plugins/appleMusic.desktop/native.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent"; -import { execFile } from "child_process"; -import { promisify } from "util"; - -import type { TrackData } from "."; - -const exec = promisify(execFile); - -async function applescript(cmds: string[]) { - const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat()); - return stdout; -} - -interface RemoteData { - appleMusicLink?: string, - appleMusicArtistLink?: string; - songLink?: string, - albumArtwork?: string, - artistArtwork?: string; -} - -let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null; - -async function fetchRemoteData({ id, name, artist, album }: { id: string, name: string, artist: string, album: string; }) { - if (id === cachedRemoteData?.id) { - if ("data" in cachedRemoteData) return cachedRemoteData.data; - if ("failures" in cachedRemoteData && cachedRemoteData.failures >= 5) return null; - } - - try { - const dataUrl = new URL("https://itunes.apple.com/search"); - dataUrl.searchParams.set("term", `${name} ${artist} ${album}`); - dataUrl.searchParams.set("media", "music"); - dataUrl.searchParams.set("entity", "song"); - - const songData = await fetch(dataUrl, { - headers: { - "user-agent": VENCORD_USER_AGENT, - }, - }) - .then(r => r.json()) - .then(data => data.results.find(song => song.collectionName === album) || data.results[0]); - - const artistArtworkURL = await fetch(songData.artistViewUrl) - .then(r => r.text()) - .then(data => { - const match = data.match(//); - return match ? match[1].replace(/[0-9]+x.+/, "220x220bb-60.png") : undefined; - }) - .catch(() => void 0); - - cachedRemoteData = { - id, - data: { - appleMusicLink: songData.trackViewUrl, - appleMusicArtistLink: songData.artistViewUrl, - songLink: `https://song.link/i/${new URL(songData.trackViewUrl).searchParams.get("i")}`, - albumArtwork: (songData.artworkUrl100).replace("100x100", "512x512"), - artistArtwork: artistArtworkURL - } - }; - - return cachedRemoteData.data; - } catch (e) { - console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e); - cachedRemoteData = { - id, - failures: (id === cachedRemoteData?.id && "failures" in cachedRemoteData ? cachedRemoteData.failures : 0) + 1 - }; - return null; - } -} - -export async function fetchTrackData(): Promise { - try { - await exec("pgrep", ["^Music$"]); - } catch (error) { - return null; - } - - const playerState = await applescript(['tell application "Music"', "get player state", "end tell"]) - .then(out => out.trim()); - if (playerState !== "playing") return null; - - const playerPosition = await applescript(['tell application "Music"', "get player position", "end tell"]) - .then(text => Number.parseFloat(text.trim())); - - const stdout = await applescript([ - 'set output to ""', - 'tell application "Music"', - "set t_id to database id of current track", - "set t_name to name of current track", - "set t_album to album of current track", - "set t_artist to artist of current track", - "set t_duration to duration of current track", - 'set output to "" & t_id & "\\n" & t_name & "\\n" & t_album & "\\n" & t_artist & "\\n" & t_duration', - "end tell", - "return output" - ]); - - const [id, name, album, artist, durationStr] = stdout.split("\n").filter(k => !!k); - const duration = Number.parseFloat(durationStr); - - const remoteData = await fetchRemoteData({ id, name, artist, album }); - - return { name, album, artist, playerPosition, duration, ...remoteData }; -}