Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
cefee6a4be
13 changed files with 277 additions and 101 deletions
|
|
@ -55,6 +55,11 @@ export const CspPolicies: PolicyMap = {
|
|||
// Function Specific
|
||||
"api.github.com": ConnectSrc, // used for updating Vencord itself
|
||||
"ws.audioscrobbler.com": ConnectSrc, // Last.fm API
|
||||
"musicbrainz.org": ConnectSrc,
|
||||
"*.listenbrainz.org": ConnectSrc,
|
||||
"coverartarchive.org": ConnectSrc,
|
||||
"archive.org": ConnectSrc,
|
||||
"*.archive.org": ConnectSrc,
|
||||
"translate-pa.googleapis.com": ConnectSrc, // Google Translate API
|
||||
"*.vencord.dev": ImageSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev)
|
||||
"manti.vendicated.dev": ImageSrc, // ReviewDB API
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
|||
{
|
||||
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
|
||||
replacement: {
|
||||
match: /(?<=\]\}\)),(.{0,40}togglePopout:.+?\}\))\]\}\):null,(?<=\((\i\.\i),\{label:.+?:null,(\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
|
||||
match: /(?<=\]\}\)),(.{0,40}togglePopout:.+?\}\))\]\}\):null,(?<=\((\i),\{label:.+?:null,(\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
|
||||
replace: (_, ReactButton, ButtonComponent, showReactButton, message) => "" +
|
||||
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ export default definePlugin({
|
|||
predicate: () => settings.store.closeAllHomeButton,
|
||||
replacement: {
|
||||
// Close all folders when clicking the home button
|
||||
match: /(?<=onClick:(\(\)=>|function\(\)){)(?=.{0,300}"discodo")/,
|
||||
match: /(?<=onClick:(?:function)?\(\)(?:=>)?{)(?=.{0,300}"discodo")/,
|
||||
replace: "$self.closeFolders();"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ export default definePlugin({
|
|||
replace: "$self.DecorationGridItem=$1;",
|
||||
},
|
||||
{
|
||||
match: /(?=function (\i)\(\i\){let{user:\i,avatarDecoration)/,
|
||||
replace: "$self.DecorationGridDecoration=$1;",
|
||||
match: /(?<=(?:(\i)=)?)(?:\i=>|function (\i)\(\i\)){let{user:\i,avatarDecoration/,
|
||||
replace: (m, arrowFunctionName, functionName) => `$self.DecorationGridDecoration=${arrowFunctionName ?? functionName}${arrowFunctionName ? "" : ";"}${m}`,
|
||||
},
|
||||
// Remove NEW label from decor avatar decorations
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import { Devs } from "@utils/constants.js";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { classes } from "@utils/misc";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Channel, Message } from "@vencord/discord-types";
|
||||
|
|
@ -45,7 +43,6 @@ import {
|
|||
} from "@webpack/common";
|
||||
import { ComponentType, JSX } from "react";
|
||||
|
||||
const cl = classNameFactory("vc-message-link-embeds-");
|
||||
const messageCache = new Map<string, {
|
||||
message?: Message;
|
||||
fetched: boolean;
|
||||
|
|
@ -306,7 +303,12 @@ function ChannelMessageEmbedAccessory({ message, channel }: MessageEmbedProps):
|
|||
}
|
||||
}}
|
||||
renderDescription={() => (
|
||||
<div key={message.id} className={classes(cl("message"), settings.store.messageBackgroundColor && cl("search-result"))}>
|
||||
<div key={message.id} style={!settings.store.messageBackgroundColor ? undefined : {
|
||||
backgroundColor: "var(--background-base-lower)",
|
||||
border: "1px solid var(--border-subtle)",
|
||||
borderRadius: "8px",
|
||||
paddingBottom: "8px",
|
||||
}}>
|
||||
<ChannelMessage
|
||||
id={`message-link-embeds-${message.id}`}
|
||||
message={message}
|
||||
|
|
|
|||
|
|
@ -16,25 +16,38 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { definePluginSettings, migratePluginSetting, migratePluginSettings } from "@api/Settings";
|
||||
import { LinkButton } from "@components/Button";
|
||||
import { Card } from "@components/Card";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { Margins } from "@components/margins";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Activity, ActivityAssets, ActivityButton } from "@vencord/discord-types";
|
||||
import { ActivityFlags, ActivityStatusDisplayType, ActivityType } from "@vencord/discord-types/enums";
|
||||
import { ApplicationAssetUtils, AuthenticationStore, FluxDispatcher, PresenceStore } from "@webpack/common";
|
||||
|
||||
interface TrackData {
|
||||
import { LastFMScrobbler } from "./lastfm";
|
||||
import { ListenBrainzScrobbler } from "./listenbrainz";
|
||||
|
||||
export interface TrackData {
|
||||
name: string;
|
||||
album: string;
|
||||
artist: string;
|
||||
url: string;
|
||||
imageUrl?: string;
|
||||
trackURL?: string;
|
||||
artistURL?: string;
|
||||
albumURL?: string;
|
||||
imageURL?: string;
|
||||
serviceName?: string;
|
||||
}
|
||||
|
||||
export interface ScrobblerBackend {
|
||||
name: string,
|
||||
id: string,
|
||||
|
||||
fetchTrackData(username: string, apiKey?: string): Promise<TrackData | null>;
|
||||
getUserURL(username: string): string;
|
||||
}
|
||||
|
||||
const enum NameFormat {
|
||||
|
|
@ -43,18 +56,15 @@ const enum NameFormat {
|
|||
SongFirst = "song-first",
|
||||
ArtistOnly = "artist",
|
||||
SongOnly = "song",
|
||||
AlbumName = "album"
|
||||
AlbumName = "album",
|
||||
ServiceName = "service-name"
|
||||
}
|
||||
|
||||
// Last.fm API keys are essentially public information and have no access to your account, so including one here is fine.
|
||||
const API_KEY = "790c37d90400163a5a5fe00d6ca32ef0";
|
||||
const LASTFM_API_KEY = "790c37d90400163a5a5fe00d6ca32ef0";
|
||||
const DISCORD_APP_ID = "1108588077900898414";
|
||||
const LASTFM_PLACEHOLDER_IMAGE_HASH = "2a96cbd8b46e442fc41c2b86b821562f";
|
||||
|
||||
const logger = new Logger("LastFMRichPresence");
|
||||
|
||||
let updateInterval: NodeJS.Timeout | undefined;
|
||||
|
||||
async function getApplicationAsset(key: string): Promise<string> {
|
||||
return (await ApplicationAssetUtils.fetchAssetIds(DISCORD_APP_ID, [key]))[0];
|
||||
}
|
||||
|
|
@ -68,17 +78,32 @@ function setActivity(activity: Activity | null) {
|
|||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
scrobblerBackend: {
|
||||
description: "The scrobbler backend to use.",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{
|
||||
"label": "Last.FM",
|
||||
"value": "lastfm",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"label": "ListenBrainz",
|
||||
"value": "listenbrainz"
|
||||
}
|
||||
] as const
|
||||
},
|
||||
apiKey: {
|
||||
displayName: "API Key",
|
||||
description: "Custom Last.fm API key. Not required but highly recommended to avoid rate limiting with our shared key",
|
||||
type: OptionType.STRING,
|
||||
},
|
||||
username: {
|
||||
description: "Last.fm username",
|
||||
description: "Username",
|
||||
type: OptionType.STRING,
|
||||
},
|
||||
shareUsername: {
|
||||
description: "Show link to Last.fm profile",
|
||||
description: "Show link to scrobbler profile",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
},
|
||||
|
|
@ -88,12 +113,12 @@ const settings = definePluginSettings({
|
|||
default: true,
|
||||
},
|
||||
hideWithSpotify: {
|
||||
description: "Hide Last.fm presence if spotify is running",
|
||||
description: "Hide presence if Spotify is running",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
hideWithActivity: {
|
||||
description: "Hide Last.fm presence if you have any other presence",
|
||||
description: "Hide presence if you have any other presence",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
},
|
||||
|
|
@ -130,6 +155,10 @@ const settings = definePluginSettings({
|
|||
value: NameFormat.StatusName,
|
||||
default: true
|
||||
},
|
||||
{
|
||||
label: "Use music service name (falls back to custom status text)",
|
||||
value: NameFormat.ServiceName
|
||||
},
|
||||
{
|
||||
label: "Use format 'artist - song'",
|
||||
value: NameFormat.ArtistFirst
|
||||
|
|
@ -162,8 +191,8 @@ const settings = definePluginSettings({
|
|||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{
|
||||
label: "Use large Last.fm logo",
|
||||
value: "lastfmLogo",
|
||||
label: "Use large scrobbler logo",
|
||||
value: "logo",
|
||||
default: true
|
||||
},
|
||||
{
|
||||
|
|
@ -172,30 +201,34 @@ const settings = definePluginSettings({
|
|||
}
|
||||
],
|
||||
},
|
||||
showLastFmLogo: {
|
||||
displayName: "Show Last.fm Logo",
|
||||
description: "Show the Last.fm logo by the album cover",
|
||||
showLogo: {
|
||||
displayName: "Show Scrobbler Logo",
|
||||
description: "Show the scrobbler service logo by the album cover",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
showAlbumCover: {
|
||||
description: "Show album cover. Disabling this will display a placeholder. Useful if your Music has inappropriate art",
|
||||
description: "Show album cover. Disabling this will display a placeholder. Useful if your music has inappropriate art",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
}
|
||||
});
|
||||
|
||||
migratePluginSettings("MusicRichPresence", "LastFMRichPresence");
|
||||
migratePluginSetting("MusicRichPresence", "showLastFmLogo", "showLogo");
|
||||
export default definePlugin({
|
||||
name: "LastFMRichPresence",
|
||||
description: "Little plugin for Last.fm rich presence",
|
||||
name: "MusicRichPresence",
|
||||
description: "Rich Presence for Last.FM/Listenbrainz",
|
||||
tags: ["Activity", "Media"],
|
||||
authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu],
|
||||
searchTerms: ["lastfm", "LastFMRichPresence"],
|
||||
authors: [Devs.Rini, Devs.Ven, Devs.angelcube, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu],
|
||||
|
||||
settings,
|
||||
|
||||
settingsAboutComponent() {
|
||||
return (
|
||||
<Card>
|
||||
<Heading tag="h2">Last.FM</Heading>
|
||||
<Heading tag="h5">How to create an API key</Heading>
|
||||
<Paragraph>Set <strong>Application name</strong> and <strong>Application description</strong> to anything and leave the rest blank.</Paragraph>
|
||||
<LinkButton size="small" href="https://www.last.fm/api/account/create" className={Margins.top8}>Create API Key</LinkButton>
|
||||
|
|
@ -205,54 +238,11 @@ export default definePlugin({
|
|||
|
||||
start() {
|
||||
this.updatePresence();
|
||||
updateInterval = setInterval(() => { this.updatePresence(); }, 16000);
|
||||
this.updateInterval = setInterval(() => { this.updatePresence(); }, 16000);
|
||||
},
|
||||
|
||||
stop() {
|
||||
clearInterval(updateInterval);
|
||||
updateInterval = undefined;
|
||||
},
|
||||
|
||||
async fetchTrackData(): Promise<TrackData | null> {
|
||||
if (!settings.store.username)
|
||||
return null;
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
method: "user.getrecenttracks",
|
||||
api_key: settings.store.apiKey || API_KEY,
|
||||
user: settings.store.username,
|
||||
limit: "1",
|
||||
format: "json"
|
||||
});
|
||||
|
||||
const res = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`);
|
||||
if (!res.ok) throw `${res.status} ${res.statusText}`;
|
||||
|
||||
const json = await res.json();
|
||||
if (json.error) {
|
||||
logger.error("Error from Last.fm API", `${json.error}: ${json.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const trackData = json.recenttracks?.track[0];
|
||||
|
||||
if (!trackData?.["@attr"]?.nowplaying)
|
||||
return null;
|
||||
|
||||
// why does the json api have xml structure
|
||||
return {
|
||||
name: trackData.name || "Unknown",
|
||||
album: trackData.album["#text"],
|
||||
artist: trackData.artist["#text"] || "Unknown",
|
||||
url: trackData.url,
|
||||
imageUrl: trackData.image?.find((x: any) => x.size === "large")?.["#text"]
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error("Failed to query Last.fm API", e);
|
||||
// will clear the rich presence if API fails
|
||||
return null;
|
||||
}
|
||||
clearInterval(this.updateInterval);
|
||||
},
|
||||
|
||||
async updatePresence() {
|
||||
|
|
@ -260,14 +250,17 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
getLargeImage(track: TrackData): string | undefined {
|
||||
if (settings.store.showAlbumCover && track.imageUrl && !track.imageUrl.includes(LASTFM_PLACEHOLDER_IMAGE_HASH))
|
||||
return track.imageUrl;
|
||||
if (settings.store.showAlbumCover && track.imageURL && !track.imageURL.includes(LASTFM_PLACEHOLDER_IMAGE_HASH))
|
||||
return track.imageURL;
|
||||
|
||||
if (settings.store.missingArt === "placeholder")
|
||||
return "placeholder";
|
||||
},
|
||||
|
||||
async getActivity(): Promise<Activity | null> {
|
||||
if (!settings.store.username)
|
||||
return null;
|
||||
|
||||
if (settings.store.hideWithActivity) {
|
||||
if (PresenceStore.getActivities(AuthenticationStore.getId()).some(a => a.application_id !== DISCORD_APP_ID && a.type !== ActivityType.CUSTOM_STATUS)) {
|
||||
return null;
|
||||
|
|
@ -281,7 +274,9 @@ export default definePlugin({
|
|||
}
|
||||
}
|
||||
|
||||
const trackData = await this.fetchTrackData();
|
||||
const scrobbler = settings.store.scrobblerBackend === "lastfm" ? LastFMScrobbler : ListenBrainzScrobbler;
|
||||
|
||||
const trackData = await scrobbler.fetchTrackData(settings.store.username, settings.store.apiKey || LASTFM_API_KEY);
|
||||
if (!trackData) return null;
|
||||
|
||||
const largeImage = this.getLargeImage(trackData);
|
||||
|
|
@ -289,22 +284,23 @@ export default definePlugin({
|
|||
{
|
||||
large_image: await getApplicationAsset(largeImage),
|
||||
large_text: trackData.album || undefined,
|
||||
...(settings.store.showLastFmLogo && {
|
||||
small_image: await getApplicationAsset("lastfm-small"),
|
||||
small_text: "Last.fm"
|
||||
...(settings.store.showLogo && {
|
||||
small_image: await getApplicationAsset(`${scrobbler.id}-small`),
|
||||
small_text: scrobbler.id
|
||||
}),
|
||||
} : {
|
||||
large_image: await getApplicationAsset("lastfm-large"),
|
||||
large_image: await getApplicationAsset(`${scrobbler.id}-large`),
|
||||
large_text: trackData.album || undefined,
|
||||
};
|
||||
|
||||
const buttons: ActivityButton[] = [];
|
||||
|
||||
if (settings.store.shareUsername)
|
||||
if (settings.store.shareUsername) {
|
||||
buttons.push({
|
||||
label: "Last.fm Profile",
|
||||
url: `https://www.last.fm/user/${settings.store.username}`,
|
||||
label: `${scrobbler.name} Profile`,
|
||||
url: scrobbler.getUserURL(settings.store.username!)
|
||||
});
|
||||
}
|
||||
|
||||
const statusName = (() => {
|
||||
switch (settings.store.nameFormat) {
|
||||
|
|
@ -321,6 +317,11 @@ export default definePlugin({
|
|||
.replaceAll("{artist}", trackData.artist || "")
|
||||
.replaceAll("{album}", trackData.album || "")
|
||||
.replaceAll("{title}", trackData.name || "");
|
||||
case NameFormat.ServiceName:
|
||||
return trackData.serviceName || settings.store.statusName
|
||||
.replaceAll("{artist}", trackData.artist || "")
|
||||
.replaceAll("{album}", trackData.album || "")
|
||||
.replaceAll("{title}", trackData.name || "");
|
||||
default:
|
||||
return settings.store.statusName
|
||||
.replaceAll("{artist}", trackData.artist || "")
|
||||
|
|
@ -353,11 +354,11 @@ export default definePlugin({
|
|||
};
|
||||
|
||||
if (settings.store.clickableLinks) {
|
||||
activity.details_url = trackData.url;
|
||||
activity.state_url = `https://www.last.fm/music/${encodeURIComponent(trackData.artist)}`;
|
||||
activity.details_url = trackData.trackURL;
|
||||
activity.state_url = trackData.artistURL;
|
||||
|
||||
if (trackData.album) {
|
||||
activity.assets!.large_url = `https://www.last.fm/music/${encodeURIComponent(trackData.artist)}/${encodeURIComponent(trackData.album)}`;
|
||||
activity.assets!.large_url = trackData.albumURL;
|
||||
}
|
||||
}
|
||||
|
||||
63
src/plugins/musicRichPresence/lastfm.ts
Normal file
63
src/plugins/musicRichPresence/lastfm.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Logger } from "@utils/Logger";
|
||||
|
||||
import { ScrobblerBackend, TrackData } from ".";
|
||||
|
||||
const logger = new Logger("MusicRichPresence/LastFM");
|
||||
|
||||
const url = (path: string) => `https://www.last.fm${path}`;
|
||||
|
||||
export const LastFMScrobbler: ScrobblerBackend = {
|
||||
name: "Last.FM",
|
||||
id: "lastfm",
|
||||
|
||||
async fetchTrackData(username: string, apiKey: string): Promise<TrackData | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
method: "user.getrecenttracks",
|
||||
api_key: apiKey,
|
||||
user: username,
|
||||
limit: "1",
|
||||
format: "json"
|
||||
});
|
||||
|
||||
const res = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`);
|
||||
if (!res.ok) throw `${res.status} ${res.statusText}`;
|
||||
|
||||
const json = await res.json();
|
||||
if (json.error) {
|
||||
logger.error("Error from Last.fm API", `${json.error}: ${json.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const trackData = json.recenttracks?.track[0];
|
||||
|
||||
if (!trackData?.["@attr"]?.nowplaying)
|
||||
return null;
|
||||
|
||||
// why does the json api have xml structure
|
||||
return {
|
||||
name: trackData.name || "Unknown",
|
||||
album: trackData.album["#text"],
|
||||
artist: trackData.artist["#text"] || "Unknown",
|
||||
trackURL: trackData.url,
|
||||
artistURL: trackData.artist["#text"] ? url(`/music/${encodeURIComponent(trackData.artist["#text"])}`) : undefined,
|
||||
albumURL: url(`/music/${encodeURIComponent(trackData.artist["#text"])}/${encodeURIComponent(trackData.album["#text"])}`),
|
||||
imageURL: trackData.image?.find((x: any) => x.size === "large")?.["#text"]
|
||||
} as TrackData;
|
||||
} catch (e) {
|
||||
logger.error("Failed to query Last.FM API", e);
|
||||
// will clear the rich presence if API fails
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getUserURL(username: string): string {
|
||||
return url(`/user/${username}`);
|
||||
}
|
||||
};
|
||||
105
src/plugins/musicRichPresence/listenbrainz.ts
Normal file
105
src/plugins/musicRichPresence/listenbrainz.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
||||
import { Logger } from "@utils/Logger";
|
||||
|
||||
import { ScrobblerBackend, TrackData } from ".";
|
||||
|
||||
const logger = new Logger("AudioScrobblerRichPresence/ListenBrainz");
|
||||
|
||||
const url = (path: string) => `https://listenbrainz.org${path}`;
|
||||
|
||||
async function fetchCoverArt(releaseGroupMBID: string) {
|
||||
const res = await fetch(`https://coverartarchive.org/release-group/${releaseGroupMBID}`);
|
||||
if (!res.ok) return null;
|
||||
return res.json().then(json => json.images[0].thumbnails.large);
|
||||
}
|
||||
|
||||
async function getUrls(additionalInfo: Record<string, string> | undefined, trackName: string, artistName: string, releaseName: string): Promise<Partial<TrackData>> {
|
||||
// Well tagged music will have MBIDs which we can use directly. These are optional but highly recommended in ListenBrainz scrobbles.
|
||||
// If your music doesn't have these, it's highly recommended to use https://picard.musicbrainz.org/ to automatically add them
|
||||
if (additionalInfo?.recording_mbid) {
|
||||
const { release_group_mbid, release_mbid, recording_mbid, artist_mbids } = additionalInfo;
|
||||
|
||||
return {
|
||||
imageURL: release_group_mbid ? await fetchCoverArt(release_group_mbid) : undefined,
|
||||
trackURL: recording_mbid ? url(`/track/${recording_mbid}`) : undefined,
|
||||
albumURL: release_group_mbid
|
||||
? url(`/release-group/${release_group_mbid}`)
|
||||
: release_mbid
|
||||
? url(`/release/${release_mbid}`)
|
||||
: undefined,
|
||||
artistURL: artist_mbids?.length ? url(`/artist/${artist_mbids[0]}`) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// If no MBIDs are present, try to search for the track on MusicBrainz
|
||||
|
||||
// this needs to be encoded separately—URLSearchParams encodes spaces as "+"
|
||||
const query = encodeURIComponent(`artist:"${artistName}" AND recording:"${trackName}" AND album:${releaseName}`);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
fmt: "json",
|
||||
limit: "1"
|
||||
});
|
||||
|
||||
const metadata = await fetch("https://musicbrainz.org/ws/2/recording/?" + params + "&query=" + query, {
|
||||
headers: { "User-Agent": VENCORD_USER_AGENT }
|
||||
})
|
||||
.then(res => res.ok ? res.json() : Promise.reject(new Error(`${res.status} ${res.statusText}`)))
|
||||
.then(json => json.recordings?.[0]);
|
||||
|
||||
if (!metadata) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const artist = metadata["artist-credit"]?.[0]?.artist;
|
||||
const release = metadata.releases?.[0];
|
||||
|
||||
return {
|
||||
imageURL: release?.["release-group"] ? await fetchCoverArt(release["release-group"].id) : undefined,
|
||||
trackURL: url(`/track/${metadata.id}/`),
|
||||
albumURL: release?.id ? url(`/release/${release.id}/`) : release?.["release-group"]?.id ? url(`/release-group/${release["release-group"].id}/`) : undefined,
|
||||
artistURL: artist?.id ? url(`/artist/${artist.id}/`) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const ListenBrainzScrobbler: ScrobblerBackend = {
|
||||
name: "ListenBrainz",
|
||||
id: "listenbrainz",
|
||||
|
||||
async fetchTrackData(username: string, _apiKey?: string): Promise<TrackData | null> {
|
||||
try {
|
||||
const res = await fetch(`https://api.listenbrainz.org/1/user/${username}/playing-now`);
|
||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
||||
|
||||
const data = await res.json().then(json => json.payload?.listens[0]);
|
||||
if (!data?.playing_now || !data?.track_metadata)
|
||||
return null;
|
||||
|
||||
const { track_name, artist_name, release_name, additional_info } = data.track_metadata;
|
||||
|
||||
const trackData = {
|
||||
name: track_name || "Unknown",
|
||||
artist: artist_name,
|
||||
album: release_name || "Unknown",
|
||||
serviceName: additional_info?.music_service_name || additional_info?.submission_client,
|
||||
...await getUrls(additional_info, track_name, artist_name, release_name)
|
||||
} as TrackData;
|
||||
|
||||
return trackData;
|
||||
} catch (e) {
|
||||
logger.error("Failed to query ListenBrainz API", e);
|
||||
// will clear the rich presence if API fails
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getUserURL(username: string): string {
|
||||
return url(`/user/${username}`);
|
||||
}
|
||||
};
|
||||
|
|
@ -23,7 +23,7 @@ export default definePlugin({
|
|||
name: "NoUnblockToJump",
|
||||
description: "Allows you to jump to messages of blocked or ignored users and likely spammers without unblocking them",
|
||||
tags: ["Utility"],
|
||||
authors: [Devs.dzshn],
|
||||
authors: [Devs.Rini],
|
||||
patches: [
|
||||
{
|
||||
find: "#{intl::UNIGNORE_TO_JUMP_BODY}",
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export default definePlugin({
|
|||
name: "ServerListIndicators",
|
||||
description: "Add online friend count or server count in the server list",
|
||||
tags: ["Servers", "Appearance"],
|
||||
authors: [Devs.dzshn, EquicordDevs.Panniku],
|
||||
authors: [Devs.Rini, EquicordDevs.Panniku],
|
||||
dependencies: ["ServerListAPI"],
|
||||
settings,
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export default definePlugin({
|
|||
name: "ShowHiddenChannels",
|
||||
description: "Show channels that you do not have access to view.",
|
||||
tags: ["Servers", "Utility"],
|
||||
authors: [Devs.BigDuck, Devs.AverageReactEnjoyer, Devs.D3SOX, Devs.Ven, Devs.Nuckyz, Devs.Nickyux, Devs.dzshn, EquicordDevs.Oggetto],
|
||||
authors: [Devs.BigDuck, Devs.AverageReactEnjoyer, Devs.D3SOX, Devs.Ven, Devs.Nuckyz, Devs.Nickyux, Devs.Rini, EquicordDevs.Oggetto],
|
||||
isModified: true,
|
||||
settings,
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export default definePlugin({
|
|||
replacement: [
|
||||
{
|
||||
// if (inlinedCalculatePlayingCount(a,b) >= limit) return;
|
||||
match: /(BURST_REACTION_EFFECT_PLAY:(\i=>|function\(\i\)){.+?if\()(\(\(\i,\i\)=>.+?\(\i,\i\))>=5+?(?=\))/,
|
||||
match: /(BURST_REACTION_EFFECT_PLAY:(?:\i=>|function\(\i\)){.+?if\()(\(?(?:function)?\(\i,\i\)(?:=>)?{.+?\(\i,\i\))>=5+?(?=\))/,
|
||||
replace: (_, rest, playingCount) => `${rest}!$self.shouldPlayBurstReaction(${playingCount})`
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -192,10 +192,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "kemo",
|
||||
id: 715746190813298788n
|
||||
},
|
||||
dzshn: {
|
||||
name: "dzshn",
|
||||
id: 310449948011528192n
|
||||
},
|
||||
Ducko: {
|
||||
name: "Ducko",
|
||||
id: 506482395269169153n
|
||||
|
|
@ -374,8 +370,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
id: 354191516979429376n
|
||||
},
|
||||
Rini: {
|
||||
name: "Rini",
|
||||
id: 1079479184478441643n
|
||||
name: "rini",
|
||||
id: 0n
|
||||
},
|
||||
castdrian: {
|
||||
name: "castdrian",
|
||||
|
|
@ -515,7 +511,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
},
|
||||
Byeoon: {
|
||||
name: "byeoon",
|
||||
id: 1167275288036655133n
|
||||
id: 495683688474869781n
|
||||
},
|
||||
Kaitlyn: {
|
||||
name: "kaitlyn",
|
||||
|
|
@ -666,6 +662,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
name: "NightmareSan",
|
||||
id: 304239816466235392n
|
||||
},
|
||||
angelcube: {
|
||||
name: "angelcube",
|
||||
id: 958505257288208446n
|
||||
},
|
||||
Lunascape: {
|
||||
name: "Lunascape",
|
||||
id: 383365021415243776n
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue