From ba45ecda56d0f5bc19954f56bdb64b9fd11db0ee Mon Sep 17 00:00:00 2001 From: Sofia Date: Sat, 19 Nov 2022 17:40:52 +0000 Subject: [PATCH] feat(plugin): Last.fm rich presence (#220) Co-authored-by: Ven --- src/plugins/lastfm.tsx | 208 +++++++++++++++++++++++++++++++++++++++++ src/utils/constants.ts | 4 + 2 files changed, 212 insertions(+) create mode 100644 src/plugins/lastfm.tsx diff --git a/src/plugins/lastfm.tsx b/src/plugins/lastfm.tsx new file mode 100644 index 0000000..280c0ee --- /dev/null +++ b/src/plugins/lastfm.tsx @@ -0,0 +1,208 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Sofia Lima + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Link } from "../components/Link"; +import { Devs } from "../utils/constants"; +import { lazyWebpack } from "../utils/misc"; +import definePlugin, { OptionType } from "../utils/types"; +import { Settings, Webpack } from "../Vencord"; +import { FluxDispatcher, Forms } 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 + } + assets?: ActivityAssets + buttons?: Array + name: string + application_id: string + metadata?: { + button_urls?: Array + } + type: Number + flags: Number +} + +interface TrackData { + name: string + album: string + artist: string + url: string + imageUrl?: string +} + +// only relevant enum values +enum ActivityType { + PLAYING = 0, + LISTENING = 2, +} + +enum ActivityFlag { + INSTANCE = 1 << 0, +} + +const applicationId = "1043533871037284423"; + +const presenceStore = lazyWebpack(Webpack.filters.byProps("getLocalPresence")); +const assetManager = Webpack.mapMangledModuleLazy( + "getAssetImage: size must === [number, number] for Twitch", + { + getAsset: Webpack.filters.byCode("apply("), + } +); + +async function getApplicationAsset(key: string): Promise { + return (await assetManager.getAsset(applicationId, [key, undefined]))[0]; +} + +function setActivity(activity?: Activity) { + FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: activity }); +} + +export default definePlugin({ + name: "LastFMRichPresence", + description: "Little plugin for Last.fm rich presence", + authors: [Devs.dzshn], + + settingsAboutComponent: () => ( + <> + How to get an API key + + An API key is required to fetch your current track. To get one, you can + visit this page and + fill in the following information:

+ + Application name: Discord Rich Presence
+ Application description: (personal use)

+ + And copy the API key (not the shared secret!) +
+ + ), + + options: { + username: { + description: "last.fm username", + type: OptionType.STRING, + }, + apiKey: { + description: "last.fm api key", + type: OptionType.STRING, + }, + hideWithSpotify: { + description: "hide last.fm presence if spotify is running", + type: OptionType.BOOLEAN, + default: true, + }, + useListeningStatus: { + description: 'show "Listening to" status instead of "Playing"', + type: OptionType.BOOLEAN, + default: false, + } + }, + + start() { + this.settings = Settings.plugins.LastFMRichPresence; + + this.updateInterval = setInterval(() => { this.updatePresence(); }, 16000); + }, + + stop() { + clearInterval(this.updateInterval); + }, + + async fetchTrackData(): Promise { + if (!this.settings.username || !this.settings.apiKey) return null; + + const response = await fetch(`https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&api_key=${this.settings.apiKey}&user=${this.settings.username}&limit=1&format=json`); + const trackData = (await response.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 || []).filter(x => x.size === "large")[0]?.["#text"] + }; + }, + + async updatePresence() { + if (this.settings.hideWithSpotify) { + for (const activity of presenceStore.getActivities()) { + if (activity.type === ActivityType.LISTENING && activity.application_id !== applicationId) { + // there is already music status (probably only spotify can do this currently) + setActivity(); + return; + } + } + } + + const trackData = await this.fetchTrackData(); + + if (!trackData) { + setActivity(); + return; + } + + const hideAlbumName = !trackData.album || trackData.album === trackData.name; + + let assets: ActivityAssets; + if (trackData.imageUrl) { + assets = { + large_image: await getApplicationAsset(trackData.imageUrl), + large_text: trackData.name, + small_image: await getApplicationAsset("lastfm-small"), + small_text: "Last.fm", + }; + } else { + assets = { + large_image: await getApplicationAsset("lastfm-large"), + large_text: "Last.fm", + }; + } + + setActivity({ + application_id: applicationId, + name: "some music", + + details: trackData.name, + state: hideAlbumName ? trackData.artist : `${trackData.artist} - ${trackData.album}`, + assets, + + buttons: [ "Open in Last.fm" ], + metadata: { + button_urls: [ trackData.url ] + }, + + type: this.settings.useListeningStatus ? ActivityType.LISTENING : ActivityType.PLAYING, + flags: ActivityFlag.INSTANCE, + }); + } +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 7f44ee0..18aaeb8 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -140,5 +140,9 @@ export const Devs = Object.freeze({ kemo: { name: "kemo", id: 299693897859465228n + }, + dzshn: { + name: "dzshn", + id: 310449948011528192n } });