Remove AppleMusicRPC ~ too niche & alternatives exist
This commit is contained in:
parent
452241cf8e
commit
6c7e9cd9a9
3 changed files with 0 additions and 430 deletions
|
|
@ -1,9 +0,0 @@
|
|||
# AppleMusicRichPresence
|
||||
|
||||
This plugin enables Discord rich presence for your Apple Music! (This only works on macOS with the Music app.)
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
@ -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 };
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue