SongInfo: Add Plugin

This commit is contained in:
thororen1234 2025-10-29 10:44:57 -04:00
parent 1da50a70b7
commit 83958f2135
No known key found for this signature in database
6 changed files with 348 additions and 0 deletions

View file

@ -58,6 +58,7 @@
line-height: 16px;
color: var(--white-500);
text-align: center;
margin-right: 8px;
}
.vc-plugins-dep-name {

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,62 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 nin0
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { FormSwitch } from "@components/FormSwitch";
import { HeadingTertiary } from "@components/Heading";
import { settings } from ".";
import Providers from "./Providers";
export function Settings() {
const blazinglyFastSettings = settings.use(["servicesSettings"]);
return <div style={{
display: "flex",
flexDirection: "column",
gap: 16,
}}>
{
Object.entries(Providers).map(provider => <div key={provider[0]} style={{
borderBottom: "1px solid var(--border-subtle)",
paddingBottom: 8
}}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<img
src={provider[1].logo}
alt={`${provider[1].name} logo`}
style={{ width: 16, height: 16, objectFit: "contain", display: "block" }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
<HeadingTertiary>{provider[1].name}</HeadingTertiary>
</div>
</div>
<div style={{
display: "flex",
flexDirection: "column",
gap: 4
}}>
<FormSwitch
title="Show if available"
hideBorder={true}
key={`${provider[0]}-visible`}
value={blazinglyFastSettings.servicesSettings[provider[0]].enabled}
onChange={v => settings.store.servicesSettings[provider[0]].enabled = v}
/>
{
/* @ts-ignore */
provider[1].native && <FormSwitch
title="Open in desktop app"
hideBorder={true}
key={`${provider[0]}-native`}
value={blazinglyFastSettings.servicesSettings[provider[0]].openInNative}
onChange={v => settings.store.servicesSettings[provider[0]].openInNative = v}
/>
}
</div>
</div>)
}
</div>;
}

View file

@ -0,0 +1,61 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 nin0
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BaseText } from "@components/BaseText";
import { findComponentByCodeLazy } from "@webpack";
import { Button, Card, useEffect, useState } from "@webpack/common";
import pl, { Native, settings, SongLinkResult } from ".";
import Providers from "./Providers";
const HeadphonesIcon = findComponentByCodeLazy("4.1-2.13h1.86A9");
export default function SongLinker({ url }: { url: string; }) {
const [songData, setSongData] = useState<SongLinkResult>();
useEffect(() => {
async function doStuff() {
if (pl.cache[url]) return void setSongData(pl.cache[url]);
const sd = await Native.getTrackData(url);
setSongData(sd);
pl.addToCache(url, sd);
}
doStuff();
});
return <BaseText>
{
songData ? <Card style={{
padding: "10px 15px"
}}>
<div>
<BaseText style={{ display: "flex", alignItems: "center", gap: "6px", fontWeight: 600, fontSize: "1.05rem" }}>
<HeadphonesIcon /> {songData.info?.title} - {songData.info?.artist}
</BaseText>
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: "10px", marginTop: "10px" }}>
{
Object.keys(songData.links).map(service => settings.store.servicesSettings[service].enabled && <Button key={`${service}-${url}`} style={{
width: "20px !important"
// @ts-ignore
}} variant="secondary" onClick={() => {
// FIXME: fix type error
// @ts-expect-error ???
VencordNative.native.openExternal(settings.store.servicesSettings[service].openInNative && Providers[service].native ? songData.links[service].nativeUri : songData.links[service].url);
}}>
<img
src={Providers[service].logo}
alt={`${Providers[service]} logo`}
style={{ width: 16, height: 16, objectFit: "contain", display: "block" }}
/>
</Button>)
}
</div>
</div>
</Card> : <BaseText>Loading song link...</BaseText>
}
</BaseText >;
}

View file

@ -0,0 +1,87 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 nin0
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, PluginNative } from "@utils/types";
import Providers from "./Providers";
import { Settings } from "./Settings";
import SongLinker from "./SongLinker";
export const settings = definePluginSettings({
servicesSettings: {
type: OptionType.CUSTOM,
description: "settings for services",
default: Object.fromEntries(Object.entries(Providers).map(([name, data]) => [name, {
enabled: true,
// @ts-ignore
openInNative: data.native || false
}]))
},
userCountry: {
type: OptionType.STRING,
description: "Country used for lookup (Two letter country code)",
default: "US"
},
servicesComponent: {
type: OptionType.COMPONENT,
component: () => <Settings />
}
});
export type SongLinkResult = {
info?: {
title: string;
artist: string;
};
links: {
[platform: string]: {
url: string;
nativeUri?: string;
};
};
};
export const Native = VencordNative.pluginHelpers.SongLink as PluginNative<typeof import("./native")>;
export default definePlugin({
name: "SongLink",
description: "Adds streaming service buttons below song links",
authors: [Devs.nin0dev],
settings,
Providers,
cache: ({} as Record<string, SongLinkResult>),
addToCache(link, data: SongLinkResult) {
this.cache[link] = data;
},
renderMessageAccessory(props: Record<string, any>) {
const { content }: {
content: string;
} = props.message;
if (!content) return;
const regexes = [
/https:\/\/(?:open|play)\.spotify\.com\/track\/[a-zA-Z0-9]+/, // spotify
/https:\/\/(music|itunes)\.apple\.com\/[a-z]{2}\/album\/\S+/, // apple music/itunes
/https:\/\/music\.youtube\.com\/watch\?v=[0-9A-Za-z_-]+/, // yt music
/https:\/\/tidal\.com\/track\/[0-9]+\/u/ // tidal
];
const musicLinks = content.match(new RegExp(regexes.map(r => r.source).join("|"), "g"));
if (!musicLinks?.length) return;
return <div style={{
display: "flex",
flexDirection: "column",
gap: "10px",
marginTop: "7px"
}}>
{
musicLinks.map(item => <SongLinker key={item} url={item} />)
}
</div>;
}
});

View file

@ -0,0 +1,40 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 nin0
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { RendererSettings } from "@main/settings";
type SongLinkResult = {
info?: {
title: string;
artist: string;
};
links: {
[platform: string]: {
url: string;
nativeUri?: string;
};
};
};
export async function getTrackData(_, trackURL: string): Promise<SongLinkResult> {
const url = new URL("https://api.song.link/v1-alpha.1/links");
url.searchParams.set("url", trackURL);
url.searchParams.set("userCountry", RendererSettings.store.plugins?.SongLink.userCountry || "US");
const raw = await fetch(url.toString()).then(u => u.json());
const [, entry]: any = Object.entries(raw.entitiesByUniqueId)
.find(([key]) => !key.includes("YOUTUBE")) || [];
const possibleTrackInfo = entry
? { title: (entry.title as string), artist: (entry.artistName as string) }
: null;
return {
// @ts-ignore
info: possibleTrackInfo,
links: Object.fromEntries(Object.entries<any>(raw.linksByPlatform).map(([name, data]) => [name, {
url: data.url,
nativeUri: data.nativeAppUriDesktop
}]))
};
}