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