feat: add MusicLinker plugin (#1048)

* feat: add MusicLinker plugin

* Update constants.ts

* mostly done

* Update index.tsx

* ServerListAPI: Add Priority

---------

Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
Nass 2026-05-07 17:02:07 +02:00 • committed by GitHub
parent f99bacba91
commit e08a20e917
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 118 additions and 34 deletions

View file

@ -25,11 +25,11 @@ export const enum ServerListRenderPosition {
Below,
}
const componentsIn = new Set<ComponentType>();
const componentsAbove = new Set<ComponentType>();
const componentsBelow = new Set<ComponentType>();
const componentsIn = new Map<ComponentType, number>();
const componentsAbove = new Map<ComponentType, number>();
const componentsBelow = new Map<ComponentType, number>();
function getRenderFunctions(position: ServerListRenderPosition) {
function getRenderMap(position: ServerListRenderPosition) {
switch (position) {
case ServerListRenderPosition.Above:
return componentsAbove;
@ -40,21 +40,20 @@ function getRenderFunctions(position: ServerListRenderPosition) {
}
}
export function addServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) {
getRenderFunctions(position).add(renderFunction);
export function addServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType, priority = 0) {
getRenderMap(position).set(renderFunction, priority);
}
export function removeServerListElement(position: ServerListRenderPosition, renderFunction: ComponentType) {
getRenderFunctions(position).delete(renderFunction);
getRenderMap(position).delete(renderFunction);
}
export const renderAll = (position: ServerListRenderPosition) => {
return Array.from(
getRenderFunctions(position),
(Component, i) => (
return Array.from(getRenderMap(position).entries())
.sort((a, b) => b[1] - a[1])
.map(([Component], i) => (
<ErrorBoundary noop key={i}>
<Component />
</ErrorBoundary>
)
);
));
};

View file

@ -631,7 +631,7 @@ export default definePlugin({
start() {
initializeRestartTracking(settings);
addServerListElement(ServerListRenderPosition.Above, this.renderQuestifyButton);
addServerListElement(ServerListRenderPosition.Above, this.renderQuestifyButton, 1);
onceReady.then(() => {
if (!settings.store.disableQuestsEverything) {

File diff suppressed because one or more lines are too long

View file

@ -8,7 +8,7 @@ import { FormSwitch } from "@components/FormSwitch";
import { HeadingTertiary } from "@components/Heading";
import { settings } from ".";
import Providers from "./Providers";
import { Providers } from "./Providers";
export function Settings() {
const blazinglyFastSettings = settings.use(["servicesSettings"]);

View file

@ -10,7 +10,7 @@ import { HeadphonesIcon } from "@components/Icons";
import { Button, useEffect, useState } from "@webpack/common";
import pl, { Native, settings, SongLinkResult } from ".";
import Providers from "./Providers";
import { Providers } from "./Providers";
export default function SongLinker({ url }: { url: string; }) {
const [songData, setSongData] = useState<SongLinkResult>();

View file

@ -4,11 +4,13 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Devs, EquicordDevs } from "@utils/constants";
import { sendMessage } from "@utils/discord";
import definePlugin, { OptionType, PluginNative } from "@utils/types";
import Providers from "./Providers";
import { Providers } from "./Providers";
import { Settings } from "./Settings";
import SongLinker from "./SongLinker";
@ -27,6 +29,11 @@ export const settings = definePluginSettings({
description: "Country used for lookup (Two letter country code)",
default: "US"
},
includeMetadata: {
type: OptionType.BOOLEAN,
description: "Include the track title and artist name as a header.",
default: true,
},
servicesComponent: {
type: OptionType.COMPONENT,
component: () => <Settings />
@ -48,12 +55,40 @@ export type SongLinkResult = {
export const Native = VencordNative.pluginHelpers.SongLink as PluginNative<typeof import("./native")>;
function formatMessage(data: SongLinkResult): string | null {
const lines: string[] = [];
for (const [serviceKey, service] of Object.entries(settings.store.servicesSettings)) {
if (!service.enabled) continue;
const platformData = data.links[serviceKey];
if (!platformData?.url) continue;
const provider = Providers[serviceKey];
const name = provider?.name ?? serviceKey;
lines.push(`- [${name}](<${platformData.url}>)`);
}
if (lines.length === 0) return null;
const parts: string[] = [];
if (settings.store.includeMetadata && data.info?.title && data.info?.artist) {
parts.push(`### **${data.info.title}** — *${data.info.artist}*`);
}
parts.push(lines.join("\n"));
return parts.join("\n");
}
export default definePlugin({
name: "SongLink",
description: "Adds streaming service buttons below song links",
dependencies: ["MessageAccessoriesAPI"],
tags: ["Media", "Utility"],
authors: [Devs.nin0dev],
authors: [Devs.nin0dev, EquicordDevs.NassCT],
settings,
Providers,
cache: ({} as Record<string, SongLinkResult>),
@ -85,5 +120,53 @@ export default definePlugin({
musicLinks.map(item => <SongLinker key={item} url={item} />)
}
</div>;
}
},
commands: [
{
name: "musiclink",
description: "Convert a music link to other streaming platforms.",
inputType: ApplicationCommandInputType.BUILT_IN,
options: [
{
name: "url",
description: "Music link (Spotify, Deezer, YouTube, Tidal, Apple Music, SoundCloud)",
type: ApplicationCommandOptionType.STRING,
required: true,
},
],
execute: async (opts, ctx) => {
const url = findOption<string>(opts, "url", "");
if (!url) {
sendBotMessage(ctx.channel.id, {
content: "Please provide a music link.",
});
return;
}
sendBotMessage(ctx.channel.id, {
content: "This will take a moment...",
});
try {
const data = await Native.getTrackData(url);
const formatted = formatMessage(data);
if (!formatted) {
sendBotMessage(ctx.channel.id, {
content:
"No alternative platforms found for this link.",
});
return;
}
sendMessage(ctx.channel.id, { content: formatted });
} catch (e: any) {
sendBotMessage(ctx.channel.id, {
content: "Failed to resolve music link",
});
}
},
},
],
});

View file

@ -1223,7 +1223,7 @@ export const EquicordDevs = Object.freeze({
},
prism: {
name: "prism",
id: 390884143749136386n,
id: 390884143749136386n
},
square: {
name: "square",
@ -1251,7 +1251,7 @@ export const EquicordDevs = Object.freeze({
},
PWall: {
name: "PWall",
id: 0n,
id: 0n
},
busyboxkitty: {
name: "busyboxkitty",
@ -1259,31 +1259,31 @@ export const EquicordDevs = Object.freeze({
},
BioTomateDE: {
name: "BioTomateDE",
id: 553499669226061844n,
id: 553499669226061844n
},
korzi: {
name: "korzi",
id: 740966310875365416n,
id: 740966310875365416n
},
davri: {
name: "Davri",
id: 457579346282938368n,
id: 457579346282938368n
},
yash: {
name: "yash",
id: 889150838658977874n,
id: 889150838658977874n
},
Leon135: {
name: "Leon135",
id: 309275452231385088n,
id: 309275452231385088n
},
bbpltergiest: {
name: "bbpltergiest",
id: 279448683672502274n,
id: 279448683672502274n
},
mshl: {
name: "m.shl",
id: 1025245410224263258n,
id: 1025245410224263258n
},
ScattrdBlade: {
name: "ScattrdBlade",
@ -1291,11 +1291,11 @@ export const EquicordDevs = Object.freeze({
},
Ape: {
name: "Ape",
id: 722832640360841308n,
id: 722832640360841308n
},
Kiri: {
name: "Kiri",
id: 310525496771346434n,
id: 310525496771346434n
},
pointy: {
name: "pointy",
@ -1307,7 +1307,7 @@ export const EquicordDevs = Object.freeze({
},
Gir0fa: {
name: "gir0fa",
id: 1282734265955520545n,
id: 1282734265955520545n
},
yonn2222: {
name: "yonn2222",
@ -1321,6 +1321,10 @@ export const EquicordDevs = Object.freeze({
name: "sketchmyname",
id: 1412164910443663491n
},
NassCT: {
name: "NassCT",
id: 354996937868705793n
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly