Remove AppleMusicRPC ~ too niche & alternatives exist

This commit is contained in:
Vendicated 2026-09-21 19:46:06 +02:00
parent 452241cf8e
commit 6c7e9cd9a9
No known key found for this signature in database
GPG key ID: D66986BAF75ECF18
3 changed files with 0 additions and 430 deletions

View file

@ -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.

View file

@ -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<typeof import("./native")>;
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 <>
<Paragraph>
For the customizable activity format strings, you can use several special strings to include track data in activities!{" "}
<code>{"{name}"}</code> is replaced with the track name; <code>{"{artist}"}</code> is replaced with the artist(s)' name(s); and <code>{"{album}"}</code> is replaced with the album name.
</Paragraph>
</>;
},
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<Activity | null> {
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,
};
}
});

View file

@ -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(/<meta property="og:image" content="(.+?)">/);
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<TrackData | null> {
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 };
}