mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-09 14:43:03 -04:00
:3
This commit is contained in:
commit
6acc198c85
2 changed files with 1 additions and 393 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: test
|
name: Test
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
|
|
@ -1,392 +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 { Link } from "@components/Link";
|
|
||||||
import { EquicordDevs } from "@utils/constants";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
|
||||||
import { ApplicationAssetUtils, FluxDispatcher, Forms, React } from "@webpack/common";
|
|
||||||
|
|
||||||
interface ActivityAssets {
|
|
||||||
large_image?: string;
|
|
||||||
large_text?: string;
|
|
||||||
small_image?: string;
|
|
||||||
small_text?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Activity {
|
|
||||||
state?: string;
|
|
||||||
details?: string;
|
|
||||||
timestamps?: {
|
|
||||||
start?: number;
|
|
||||||
end?: number;
|
|
||||||
};
|
|
||||||
assets?: ActivityAssets;
|
|
||||||
buttons?: Array<string>;
|
|
||||||
name: string;
|
|
||||||
application_id: string;
|
|
||||||
metadata?: {
|
|
||||||
button_urls?: Array<string>;
|
|
||||||
};
|
|
||||||
type: ActivityType;
|
|
||||||
url?: string;
|
|
||||||
flags: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Data {
|
|
||||||
activity: Activity;
|
|
||||||
pid?: number;
|
|
||||||
socketId: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const enum ActivityType {
|
|
||||||
PLAYING = 0,
|
|
||||||
STREAMING = 1,
|
|
||||||
LISTENING = 2,
|
|
||||||
WATCHING = 3,
|
|
||||||
COMPETING = 5
|
|
||||||
}
|
|
||||||
interface SpotifyImage {
|
|
||||||
height: number;
|
|
||||||
width: number;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SpotifyElement {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
// track, album or artist
|
|
||||||
type: string;
|
|
||||||
// only on track
|
|
||||||
duration?: number;
|
|
||||||
album?: SpotifyElement;
|
|
||||||
artists?: Array<SpotifyElement>;
|
|
||||||
isLocal?: boolean;
|
|
||||||
// only on album
|
|
||||||
image?: SpotifyImage;
|
|
||||||
// only on artist
|
|
||||||
external_urls?: {
|
|
||||||
spotify: string;
|
|
||||||
};
|
|
||||||
href?: string;
|
|
||||||
uri?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SpotifyEvent {
|
|
||||||
type: string;
|
|
||||||
track: SpotifyElement;
|
|
||||||
connectionId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SpotifyDevice {
|
|
||||||
id: string;
|
|
||||||
is_active: boolean;
|
|
||||||
is_private_session: boolean;
|
|
||||||
is_restricted: boolean;
|
|
||||||
name: string;
|
|
||||||
supports_volume: boolean;
|
|
||||||
type: string;
|
|
||||||
volume_percent: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum NonSpotifyShowOptions {
|
|
||||||
Details,
|
|
||||||
State,
|
|
||||||
LargeImageText,
|
|
||||||
SmallImageText
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
|
||||||
applicationId: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
description: "Application ID (required)",
|
|
||||||
isValid: (value: string) => {
|
|
||||||
if (!value) return "Application ID is required.";
|
|
||||||
if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
musicPlayerNames: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
default: "Music",
|
|
||||||
description: "List of activity names which will get replaced by the song's title (separated by ,)",
|
|
||||||
},
|
|
||||||
forceListeningType: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
description: "Force all music player activities to be \"Listening to\", instead of \"Playing\"",
|
|
||||||
},
|
|
||||||
spotifyActivityName: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
default: "{{Title}}",
|
|
||||||
description: "The activity's name (spotify)"
|
|
||||||
},
|
|
||||||
spotifyActivityDetails: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
default: "{{Title}}",
|
|
||||||
description: "The activity's details field (first line) (spotify)"
|
|
||||||
},
|
|
||||||
spotifyActivityState: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
default: "by {{AllArtistsGrammarSeparated}}",
|
|
||||||
description: "The activity's state field (second line) (spotify)"
|
|
||||||
},
|
|
||||||
spotifyActivityLargeImageText: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
default: "on {{Album}}",
|
|
||||||
description: "The activity's large image text (third line and image hover text) (spotify)"
|
|
||||||
},
|
|
||||||
spotifyButtonOneText: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
default: "Open in Spotify",
|
|
||||||
description: "The activity's first button's text (spotify)"
|
|
||||||
},
|
|
||||||
spotifyButtonTwoText: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
default: "",
|
|
||||||
description: "The activity's second button's text (spotify)"
|
|
||||||
},
|
|
||||||
spotifyButtonOneLink: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
default: "https://open.spotify.com/track/{{Id}}",
|
|
||||||
description: "The activity's first button's link (spotify)"
|
|
||||||
},
|
|
||||||
spotifyButtonTwoLink: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
default: "",
|
|
||||||
description: "The activity's second button's link (spotify)"
|
|
||||||
},
|
|
||||||
nonSpotifyWhatToShow: {
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
options: [
|
|
||||||
{ label: "Details", value: NonSpotifyShowOptions.Details, default: true },
|
|
||||||
{ label: "State", value: NonSpotifyShowOptions.State },
|
|
||||||
{ label: "Large image text", value: NonSpotifyShowOptions.LargeImageText },
|
|
||||||
{ label: "Small image text", value: NonSpotifyShowOptions.SmallImageText },
|
|
||||||
],
|
|
||||||
description: "What to show (non-spotify)"
|
|
||||||
},
|
|
||||||
nonSpotifyRegex: {
|
|
||||||
type: OptionType.STRING,
|
|
||||||
default: "(.+)",
|
|
||||||
description: "Regex pattern to determine what to show using the first captured group (non-spotify)"
|
|
||||||
},
|
|
||||||
cloneSpotifyActivity: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
default: true,
|
|
||||||
description: "Clone Spotify's activity to give it the custom name. Does not remove the original one.",
|
|
||||||
restartNeeded: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function handleUpdate(data: Data) {
|
|
||||||
if (data.activity === null || data.activity.state === undefined) return;
|
|
||||||
const { nonSpotifyWhatToShow: what_to_show, nonSpotifyRegex: regex_option } = settings.store;
|
|
||||||
|
|
||||||
const players = settings.store.musicPlayerNames.split(",").map(x => x.trim());
|
|
||||||
if (!players.includes(data.activity.name)) return;
|
|
||||||
|
|
||||||
if (settings.store.forceListeningType) {
|
|
||||||
data.activity.type = ActivityType.LISTENING;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name: string = "";
|
|
||||||
if (what_to_show === NonSpotifyShowOptions.Details && data.activity.details !== undefined) {
|
|
||||||
name = data.activity.details;
|
|
||||||
} else if (what_to_show === NonSpotifyShowOptions.State && data.activity.state !== undefined) {
|
|
||||||
name = data.activity.state;
|
|
||||||
} else if (what_to_show === NonSpotifyShowOptions.LargeImageText && data.activity.assets?.large_text !== undefined) {
|
|
||||||
name = data.activity.assets.large_text;
|
|
||||||
} else if (what_to_show === NonSpotifyShowOptions.State && data.activity.assets?.small_text !== undefined) {
|
|
||||||
name = data.activity.assets.small_text;
|
|
||||||
}
|
|
||||||
|
|
||||||
const regex_output = new RegExp(regex_option).exec(name);
|
|
||||||
if (regex_output === null || regex_output[1] === undefined) {
|
|
||||||
// no match, use the full string
|
|
||||||
data.activity.name = name;
|
|
||||||
} else {
|
|
||||||
data.activity.name = regex_output[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var playbackStoppedTimeout: number | undefined;
|
|
||||||
|
|
||||||
export default definePlugin({
|
|
||||||
name: "MusicTitleRPC",
|
|
||||||
description: "Makes the song's title appear as the activity name when listening to music.",
|
|
||||||
authors: [EquicordDevs.Blackilykat],
|
|
||||||
start: () => {
|
|
||||||
FluxDispatcher.subscribe("LOCAL_ACTIVITY_UPDATE", handleUpdate);
|
|
||||||
},
|
|
||||||
stop: () => {
|
|
||||||
FluxDispatcher.unsubscribe("LOCAL_ACTIVITY_UPDATE", handleUpdate);
|
|
||||||
},
|
|
||||||
patches: [
|
|
||||||
{
|
|
||||||
find: "let{connectionId:",
|
|
||||||
predicate: () => settings.store.cloneSpotifyActivity,
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /(?=let{connectionId:\i,track:\i}=(\i);)/,
|
|
||||||
replace: "$self.handleSpotifySongChange($1);"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: "SPOTIFY_SET_DEVICES:function(",
|
|
||||||
predicate: () => settings.store.cloneSpotifyActivity,
|
|
||||||
replacement: [
|
|
||||||
{
|
|
||||||
match: /(?<=SPOTIFY_SET_DEVICES:function\((\i)\){)/,
|
|
||||||
replace: "$self.handleSpotifyChangeDevices($1);"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
settings,
|
|
||||||
|
|
||||||
async handleSpotifyChangeDevices(e: { accountId: string, devices: SpotifyDevice[]; }) {
|
|
||||||
const { devices } = e;
|
|
||||||
let playing: boolean = false;
|
|
||||||
devices.forEach(device => {
|
|
||||||
if (device.is_active) playing = true;
|
|
||||||
});
|
|
||||||
if (!playing) {
|
|
||||||
FluxDispatcher.dispatch({
|
|
||||||
type: "LOCAL_ACTIVITY_UPDATE",
|
|
||||||
activity: null,
|
|
||||||
socketId: "MusicTitleRPC:Spotify"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleSpotifySongChange(e: SpotifyEvent) {
|
|
||||||
const {
|
|
||||||
applicationId: application_id,
|
|
||||||
spotifyActivityName: activity_name,
|
|
||||||
spotifyActivityState: activity_state,
|
|
||||||
spotifyActivityDetails: activity_details,
|
|
||||||
spotifyActivityLargeImageText: activity_large_image_text,
|
|
||||||
spotifyButtonOneText: activity_button_one_text,
|
|
||||||
spotifyButtonTwoText: activity_button_two_text,
|
|
||||||
spotifyButtonOneLink: activity_button_one_link,
|
|
||||||
spotifyButtonTwoLink: activity_button_two_link
|
|
||||||
} = settings.store;
|
|
||||||
|
|
||||||
if (application_id === undefined) return;
|
|
||||||
|
|
||||||
let large_image: string | undefined = undefined;
|
|
||||||
if (e.track.album?.image?.url !== undefined) {
|
|
||||||
large_image = (await ApplicationAssetUtils.fetchAssetIds(application_id, [e.track.album.image.url]))[0];
|
|
||||||
}
|
|
||||||
const activity: Activity = {
|
|
||||||
application_id,
|
|
||||||
name: formatSpotifyString(activity_name, e.track),
|
|
||||||
type: ActivityType.LISTENING,
|
|
||||||
flags: 0,
|
|
||||||
details: formatSpotifyString(activity_details, e.track),
|
|
||||||
state: formatSpotifyString(activity_state, e.track),
|
|
||||||
timestamps: {
|
|
||||||
start: Date.now(),
|
|
||||||
end: (Date.now() + (e.track.duration ?? 0))
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
large_image,
|
|
||||||
large_text: formatSpotifyString(activity_large_image_text, e.track)
|
|
||||||
},
|
|
||||||
buttons: [],
|
|
||||||
metadata: {
|
|
||||||
button_urls: []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (activity_button_one_text !== "") {
|
|
||||||
activity.buttons![0] = formatSpotifyString(activity_button_one_text, e.track);
|
|
||||||
activity.metadata!.button_urls![0] = formatSpotifyString(activity_button_one_link, e.track);
|
|
||||||
}
|
|
||||||
if (activity_button_two_text !== "") {
|
|
||||||
activity.buttons![1] = formatSpotifyString(activity_button_two_text, e.track);
|
|
||||||
activity.metadata!.button_urls![1] = formatSpotifyString(activity_button_two_link, e.track);
|
|
||||||
}
|
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
|
||||||
type: "LOCAL_ACTIVITY_UPDATE",
|
|
||||||
activity: activity,
|
|
||||||
socketId: "MusicTitleRPC:Spotify"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (playbackStoppedTimeout) clearTimeout(playbackStoppedTimeout);
|
|
||||||
playbackStoppedTimeout = window.setTimeout(() => {
|
|
||||||
FluxDispatcher.dispatch({
|
|
||||||
type: "LOCAL_ACTIVITY_UPDATE",
|
|
||||||
activity: null,
|
|
||||||
socketId: "MusicTitleRPC:Spotify"
|
|
||||||
});
|
|
||||||
}, (e.track.duration ?? 0) + 5000);
|
|
||||||
},
|
|
||||||
|
|
||||||
settingsAboutComponent: () => {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Forms.FormTitle tag="h3">Getting the Application ID</Forms.FormTitle>
|
|
||||||
<Forms.FormText variant="text-md/normal">
|
|
||||||
To get your application ID, go to the <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> and create an application.
|
|
||||||
</Forms.FormText>
|
|
||||||
<Forms.FormText variant="text-md/normal">
|
|
||||||
If you already created one for CustomRPC, you can use the same ID here.
|
|
||||||
</Forms.FormText>
|
|
||||||
<br />
|
|
||||||
<Forms.FormTitle tag="h3">For spotify users</Forms.FormTitle>
|
|
||||||
<Forms.FormText variant="text-md/normal">
|
|
||||||
If you use spotify, make sure to disable <code>Settings</code> > <code>Connections</code> > <code>Display Spotify as your status</code>. Keeping it enabled will result in two activities showing up.
|
|
||||||
</Forms.FormText>
|
|
||||||
<br />
|
|
||||||
<Forms.FormText variant="text-md/normal">
|
|
||||||
The new activity this plugin creates will be missing some features (time bar, play on spotify and listen along). This is a compromise, not a bug.
|
|
||||||
</Forms.FormText>
|
|
||||||
<br />
|
|
||||||
<Forms.FormText variant="text-md/normal">
|
|
||||||
Text fields in settings for spotify users will have the following strings replaced:<br />
|
|
||||||
<code>{"{{Title}}"}</code>: The song's title<br />
|
|
||||||
<code>{"{{Album}}"}</code>: The song's album's name<br />
|
|
||||||
<code>{"{{FirstArtist}}"}</code>: The first artist's name<br />
|
|
||||||
<code>{"{{AllArtistsCommaSeparated}}"}</code>: Names of all artists, separated by commas (Artist1, Artist2, Artist3)<br />
|
|
||||||
<code>{"{{AllArtistsGrammarSeparated}}"}</code>: Names of all artists, separated according to english grammar (Artist1, Artist2 and Artist3)<br />
|
|
||||||
<code>{"{{Duration}}"}</code>: The song's duration (formatted mm:ss)<br />
|
|
||||||
<code>{"{{Id}}"}</code>: The track id<br />
|
|
||||||
</Forms.FormText>
|
|
||||||
<br />
|
|
||||||
<Forms.FormTitle tag="h3">If you don't see the activity</Forms.FormTitle>
|
|
||||||
<Forms.FormText variant="text-md/normal">
|
|
||||||
Make sure you enabled <code>Settings</code> > <code>Activity privacy</code> > <code>Share your detected activities with others</code>.
|
|
||||||
</Forms.FormText>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
function formatSpotifyString(input: string, song: SpotifyElement): string {
|
|
||||||
if (input === undefined) return "";
|
|
||||||
|
|
||||||
const durationMinutes: number = Math.trunc(song.duration! / 60000);
|
|
||||||
const durationSeconds: number = Math.trunc(song.duration! / 1000) - durationMinutes * 60;
|
|
||||||
return input
|
|
||||||
.replace("{{Title}}", song.name)
|
|
||||||
.replace("{{Album}}", song.album!.name)
|
|
||||||
.replace("{{FirstArtist}}", song.artists![0].name)
|
|
||||||
.replace("{{AllArtistsCommaSeparated}}", song.artists!.map(artist => artist.name).join(", "))
|
|
||||||
.replace("{{AllArtistsGrammarSeparated}}", song.artists!
|
|
||||||
.slice(0, song.artists!.length > 1 ? -1 : undefined)
|
|
||||||
.map(artist => artist.name)
|
|
||||||
.join(", ")
|
|
||||||
+ (song.artists!.length > 1 ? ` and ${song.artists![song.artists!.length - 1].name}` : ""))
|
|
||||||
.replace("{{Duration}}", durationMinutes + ":" + ("00" + durationSeconds).slice(-2))
|
|
||||||
.replace("{{Id}}", song.id);
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue