Last.fm: Reorganize everything
This commit is contained in:
parent
92d511c7b2
commit
85ef273cde
4 changed files with 292 additions and 264 deletions
|
|
@ -1,286 +1,50 @@
|
||||||
import { patcher, plugin } from "@vendetta";
|
import { plugin } from "@vendetta";
|
||||||
import { findByProps, findByStoreName } from '@vendetta/metro';
|
import { findByProps, findByStoreName } from '@vendetta/metro';
|
||||||
import { FluxDispatcher } from '@vendetta/metro/common';
|
import { FluxDispatcher } from '@vendetta/metro/common';
|
||||||
import Constants from "./constants";
|
|
||||||
import Settings from "./settings";
|
|
||||||
|
|
||||||
let stopped: Boolean,
|
import Settings from "./Settings";
|
||||||
lastActivity: Activity,
|
import { flush, initialize, patchActivityType } from "./utils";
|
||||||
updateInterval: NodeJS.Timer,
|
|
||||||
lastTrackUrl: string,
|
|
||||||
activityTypeUnpatch: () => void;
|
|
||||||
|
|
||||||
// Discord activity
|
export const global = {} as {
|
||||||
type Activity = {
|
stopped?: Boolean,
|
||||||
name: string;
|
lastActivity?: Activity,
|
||||||
type: number;
|
updateInterval?: NodeJS.Timer,
|
||||||
details: string;
|
lastTrackUrl?: string,
|
||||||
state: string;
|
activityTypeUnpatch?: () => boolean
|
||||||
timestamps?: {
|
|
||||||
start: number;
|
|
||||||
end?: number;
|
|
||||||
};
|
|
||||||
assets?: ActivityAssets;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResultActivity = {
|
|
||||||
name: string;
|
|
||||||
type: number;
|
|
||||||
details: string;
|
|
||||||
state: string;
|
|
||||||
timestamps?: {
|
|
||||||
start: number;
|
|
||||||
end?: number;
|
|
||||||
};
|
|
||||||
assets?: ActivityAssets;
|
|
||||||
application_id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ActivityAssets = {
|
|
||||||
large_image: string;
|
|
||||||
large_text?: string;
|
|
||||||
} | {
|
|
||||||
small_image: string;
|
|
||||||
small_text?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Track = {
|
|
||||||
name: string;
|
|
||||||
artist: string;
|
|
||||||
album: string;
|
|
||||||
albumArt: string;
|
|
||||||
url: string;
|
|
||||||
date: string;
|
|
||||||
nowPlaying: boolean;
|
|
||||||
loved: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const currentSettings = { ...plugin.storage } as PluginSettings;
|
export const currentSettings = { ...plugin.storage } as PluginSettings;
|
||||||
|
|
||||||
// Discord modules that we need
|
// Discord modules that we need
|
||||||
const UserStore = findByStoreName("UserStore")
|
export const UserStore = findByStoreName("UserStore")
|
||||||
const SET_ACTIVITY = findByProps('SET_ACTIVITY').SET_ACTIVITY;
|
export const SET_ACTIVITY = findByProps('SET_ACTIVITY').SET_ACTIVITY;
|
||||||
|
|
||||||
const verboseLog = (...message: any) => currentSettings.verboseLogging && console.log(...message);
|
export const verboseLog = (...message: any) => currentSettings.verboseLogging && console.log(...message);
|
||||||
|
|
||||||
async function fetchCurrentScrobble(): Promise<Track> {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
'method': 'user.getrecenttracks',
|
|
||||||
'user': currentSettings.username,
|
|
||||||
'api_key': Constants.LFM_API_KEY,
|
|
||||||
'format': 'json',
|
|
||||||
'limit': '1',
|
|
||||||
'extended': '1'
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
const info = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`).then(x => x.json());
|
|
||||||
const lastTrack = info.recenttracks?.track?.[0] || Promise.reject(info);
|
|
||||||
return {
|
|
||||||
name: lastTrack.name,
|
|
||||||
artist: lastTrack.artist.name,
|
|
||||||
album: lastTrack.album['#text'],
|
|
||||||
albumArt: filterAlbumCover(lastTrack.image[3]['#text']),
|
|
||||||
url: lastTrack.url,
|
|
||||||
date: lastTrack.date?.['#text'] ?? 'now',
|
|
||||||
nowPlaying: Boolean(lastTrack['@attr']?.nowplaying),
|
|
||||||
loved: lastTrack.loved === '1',
|
|
||||||
} as Track;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Currently ditches the default album covers (they're ugly)
|
|
||||||
// Might as well use Youtube as fallback too (soontm)
|
|
||||||
function filterAlbumCover(cover: string): string {
|
|
||||||
if (Constants.DEFAULT_COVER_HASHES.some(x => cover.includes(x))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clears the activity
|
|
||||||
function clearActivity(): Promise<ResultActivity> {
|
|
||||||
lastActivity && verboseLog("--> Clearing activity...");
|
|
||||||
return sendRequest(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends the activity details to Discord
|
|
||||||
async function sendRequest(activity: Activity): Promise<ResultActivity> {
|
|
||||||
if (stopped) {
|
|
||||||
console.log("--> Plugin is unloaded, aborting...");
|
|
||||||
updateInterval && clearInterval(updateInterval);
|
|
||||||
activity = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastActivity = activity;
|
|
||||||
|
|
||||||
return await SET_ACTIVITY.handler({
|
|
||||||
isSocketConnected: () => true,
|
|
||||||
socket: {
|
|
||||||
id: 120,
|
|
||||||
application: {
|
|
||||||
id: Constants.APPLICATION_ID,
|
|
||||||
name: activity?.name || "RichPresence"
|
|
||||||
},
|
|
||||||
transport: "ipc"
|
|
||||||
},
|
|
||||||
args: {
|
|
||||||
pid: 120,
|
|
||||||
activity: activity && { ...activity } || null
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetches the current scrobble and sets the activity
|
|
||||||
// This is the main function that is called every 5 seconds (or whatever the user has set)
|
|
||||||
export async function update() {
|
|
||||||
verboseLog("--> Fetching last track...");
|
|
||||||
const lastTrack = await fetchCurrentScrobble().catch((err) => {
|
|
||||||
verboseLog("--> An error occurred while fetching the last track, aborting...");
|
|
||||||
clearActivity();
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!lastTrack.nowPlaying) {
|
|
||||||
verboseLog("--> Last track is not currently playing, aborting...");
|
|
||||||
return await clearActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
verboseLog("--> Setting activity...");
|
|
||||||
|
|
||||||
if (lastTrackUrl === lastTrack.url) {
|
|
||||||
verboseLog("--> Last track is the same as the previous one, aborting...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activity = {
|
|
||||||
name: currentSettings.appName || "Music",
|
|
||||||
type: currentSettings.listeningTo ? 2 : 0,
|
|
||||||
details: lastTrack.name,
|
|
||||||
state: `by ${lastTrack.artist}`,
|
|
||||||
} as Activity;
|
|
||||||
|
|
||||||
lastTrackUrl = lastTrack.url;
|
|
||||||
|
|
||||||
if (currentSettings.showTimestamp) {
|
|
||||||
activity.timestamps = {
|
|
||||||
start: Date.now() / 1000 | 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastTrack.albumArt) {
|
|
||||||
activity.assets = {
|
|
||||||
large_image: lastTrack.albumArt,
|
|
||||||
large_text: `on ${lastTrack.album}`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await sendRequest(activity).catch((err) => {
|
|
||||||
verboseLog("--> An error occurred while setting the activity");
|
|
||||||
clearActivity();
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
verboseLog("--> Successfully set activity!");
|
|
||||||
verboseLog(response);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stops everything
|
|
||||||
export function flush(): Promise<ResultActivity> {
|
|
||||||
console.log("--> Flushing...");
|
|
||||||
lastActivity = null;
|
|
||||||
updateInterval && clearInterval(updateInterval);
|
|
||||||
|
|
||||||
return clearActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializes the plugin
|
|
||||||
export async function initialize() {
|
|
||||||
console.log("--> Initializing...");
|
|
||||||
|
|
||||||
stopped = false;
|
|
||||||
lastActivity = null;
|
|
||||||
lastTrackUrl = null;
|
|
||||||
|
|
||||||
if (updateInterval) clearInterval(updateInterval);
|
|
||||||
if (lastActivity) await clearActivity();
|
|
||||||
|
|
||||||
let tries = 0;
|
|
||||||
|
|
||||||
update().catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
tries++;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Periodically fetches the current scrobble and sets the activity
|
|
||||||
updateInterval = setInterval(
|
|
||||||
() => update()
|
|
||||||
.then(() => tries = 0)
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
if (++tries > 3) {
|
|
||||||
console.error("Failed to fetch/set activity 3 times, aborting...");
|
|
||||||
clearInterval(updateInterval);
|
|
||||||
stopped = true;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
currentSettings.timeInterval
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discord doesn't allow us to set the activity type to "Listening to" so we have to patch it
|
|
||||||
function patchActivityType() {
|
|
||||||
if (activityTypeUnpatch) {
|
|
||||||
console.warn("This is weird... activityTypeUnpatch is already defined?");
|
|
||||||
activityTypeUnpatch();
|
|
||||||
} else {
|
|
||||||
console.log("Patching activity type...");
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes = Object.values(FluxDispatcher._actionHandlers._dependencyGraph.nodes);
|
|
||||||
const { actionHandler } = nodes.find((x: any) => x.name === "LocalActivityStore") as any;
|
|
||||||
|
|
||||||
activityTypeUnpatch = patcher.before("LOCAL_ACTIVITY_UPDATE", actionHandler, ([{ activity }]) => {
|
|
||||||
if (activity.name === lastActivity.name) {
|
|
||||||
activity.type = lastActivity.type;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plugin entry point
|
// Plugin entry point
|
||||||
export default {
|
export default {
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
console.log("Starting last.fm plugin..");
|
console.log("Starting last.fm plugin..");
|
||||||
patchActivityType();
|
global.activityTypeUnpatch = patchActivityType();
|
||||||
|
global.stopped = false;
|
||||||
|
|
||||||
// Options
|
|
||||||
// settings.appName = "Music";
|
|
||||||
// settings.username = "slyde99";
|
|
||||||
// settings.showTimestamp = true;
|
|
||||||
// settings.timeInterval = 5000;
|
|
||||||
// settings.listeningTo = true;
|
|
||||||
// settings.verboseLogging = false;
|
|
||||||
|
|
||||||
// If the user is already logged in (brrr connection), initialize the plugin
|
|
||||||
if (UserStore.getCurrentUser()) {
|
if (UserStore.getCurrentUser()) {
|
||||||
console.log("User is already logged in, initializing...");
|
console.log("User is already logged in, initializing...");
|
||||||
initialize().catch(console.error);
|
initialize().catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just in case the user logs in before the plugin is loaded
|
|
||||||
FluxDispatcher.subscribe("CONNECTION_OPEN", () => {
|
FluxDispatcher.subscribe("CONNECTION_OPEN", () => {
|
||||||
initialize().catch(console.error)
|
initialize().catch(console.error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onUnload: () => {
|
onUnload: () => {
|
||||||
console.log("Stopping last.fm...");
|
console.log("Stopping last.fm...");
|
||||||
|
|
||||||
stopped = true;
|
global.stopped = true;
|
||||||
|
|
||||||
flush();
|
flush();
|
||||||
activityTypeUnpatch?.();
|
global.activityTypeUnpatch?.();
|
||||||
activityTypeUnpatch = null;
|
global.activityTypeUnpatch = null;
|
||||||
},
|
},
|
||||||
settings: Settings
|
settings: Settings
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import { NavigationNative, React, ReactNative } from "@vendetta/metro/common";
|
import { NavigationNative, React, ReactNative } from "@vendetta/metro/common";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { storage } from "@vendetta/plugin";
|
||||||
import { useProxy } from "@vendetta/storage";
|
import { useProxy } from "@vendetta/storage";
|
||||||
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
import { Forms } from "@vendetta/ui/components";
|
import { Forms } from "@vendetta/ui/components";
|
||||||
import { currentSettings, flush, initialize } from ".";
|
|
||||||
|
|
||||||
const { ScrollView, TouchableOpacity } = ReactNative;
|
import { currentSettings } from ".";
|
||||||
const { FormInput, FormDivider, FormSwitchRow, FormText } = Forms;
|
import { flush, initialize } from "./utils";
|
||||||
|
|
||||||
|
const { ScrollView, TouchableOpacity, Text } = ReactNative;
|
||||||
|
const { FormInput, FormDivider, FormSwitchRow, FormText, FormIcon, FormTextColors } = Forms;
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const settings = useProxy(storage) as PluginSettings;
|
const settings = useProxy(storage) as PluginSettings;
|
||||||
|
|
@ -16,9 +19,13 @@ export default function Settings() {
|
||||||
headerRight: () => (
|
headerRight: () => (
|
||||||
<TouchableOpacity onPress={() => {
|
<TouchableOpacity onPress={() => {
|
||||||
for (const key in storage) {
|
for (const key in storage) {
|
||||||
currentSettings[key] = settings[key];
|
if (typeof storage[key] === "boolean" || storage[key]) {
|
||||||
|
currentSettings[key] = settings[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
flush().then(() => initialize()).then(() => navigation.goBack()).catch(console.error);
|
|
||||||
|
console.log("Applying settings...");
|
||||||
|
flush().then(() => initialize());
|
||||||
}}>
|
}}>
|
||||||
<FormText style={{ marginRight: 12 }}>UPDATE</FormText>
|
<FormText style={{ marginRight: 12 }}>UPDATE</FormText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
@ -29,17 +36,18 @@ export default function Settings() {
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<FormInput
|
<FormInput
|
||||||
value={settings.appName}
|
value={settings.appName || undefined}
|
||||||
onChangeText={(value: string) => settings.appName = value.trim()}
|
onChangeText={(value: string) => settings.appName = value.trim()}
|
||||||
title="Application Name"
|
title="Discord Application Name"
|
||||||
placeholder="Music"
|
placeholder="Music"
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
/>
|
/>
|
||||||
<FormDivider />
|
<FormDivider />
|
||||||
<FormInput
|
<FormInput required
|
||||||
value={settings.username}
|
value={settings.username || undefined}
|
||||||
onChangeText={(value: string) => settings.username = value.trim()}
|
onChangeText={(value: string) => settings.username = value.trim()}
|
||||||
title="Username"
|
title="Last.fm username"
|
||||||
|
helpText={!settings.username && <Text style={{ color: "#FF0000" }}>{"This field is required!"}</Text>}
|
||||||
placeholder="wumpus123"
|
placeholder="wumpus123"
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
/>
|
/>
|
||||||
|
|
@ -47,6 +55,7 @@ export default function Settings() {
|
||||||
<FormSwitchRow
|
<FormSwitchRow
|
||||||
label="Show time elapsed"
|
label="Show time elapsed"
|
||||||
subLabel="Show the time elapsed since the song started playing"
|
subLabel="Show the time elapsed since the song started playing"
|
||||||
|
leading={<FormIcon source={getAssetIDByName("clock")} />}
|
||||||
value={settings.showTimestamp}
|
value={settings.showTimestamp}
|
||||||
onValueChange={(value: boolean) => settings.showTimestamp = value}
|
onValueChange={(value: boolean) => settings.showTimestamp = value}
|
||||||
/>
|
/>
|
||||||
|
|
@ -54,6 +63,7 @@ export default function Settings() {
|
||||||
<FormSwitchRow
|
<FormSwitchRow
|
||||||
label="Set status as listening"
|
label="Set status as listening"
|
||||||
subLabel='Set your status as "Listening to" instead of "Playing"'
|
subLabel='Set your status as "Listening to" instead of "Playing"'
|
||||||
|
leading={<FormIcon source={getAssetIDByName("ic_headset_neutral")} />}
|
||||||
value={settings.listeningTo}
|
value={settings.listeningTo}
|
||||||
onValueChange={(value: boolean) => settings.listeningTo = value}
|
onValueChange={(value: boolean) => settings.listeningTo = value}
|
||||||
/>
|
/>
|
||||||
|
|
@ -61,6 +71,7 @@ export default function Settings() {
|
||||||
<FormSwitchRow
|
<FormSwitchRow
|
||||||
label="Verbose logging"
|
label="Verbose logging"
|
||||||
subLabel="Log more information to the console for debugging purposes"
|
subLabel="Log more information to the console for debugging purposes"
|
||||||
|
leading={<FormIcon source={getAssetIDByName("icon-qs-files")} />}
|
||||||
value={settings.verboseLogging}
|
value={settings.verboseLogging}
|
||||||
onValueChange={(value: boolean) => settings.verboseLogging = value}
|
onValueChange={(value: boolean) => settings.verboseLogging = value}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,51 @@ type PluginSettings = {
|
||||||
/** Whether or not to show verbose logging */
|
/** Whether or not to show verbose logging */
|
||||||
verboseLogging: boolean;
|
verboseLogging: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Discord activity, used for SET_ACTIVITY
|
||||||
|
type Activity = {
|
||||||
|
name: string;
|
||||||
|
type: number;
|
||||||
|
details: string;
|
||||||
|
state: string;
|
||||||
|
timestamps?: {
|
||||||
|
start: number;
|
||||||
|
end?: number;
|
||||||
|
};
|
||||||
|
assets?: ActivityAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A type of an object that is returned by the Discord API after setting the activity
|
||||||
|
type ResultActivity = {
|
||||||
|
name: string;
|
||||||
|
type: number;
|
||||||
|
details: string;
|
||||||
|
state: string;
|
||||||
|
timestamps?: {
|
||||||
|
start: number;
|
||||||
|
end?: number;
|
||||||
|
};
|
||||||
|
assets?: ActivityAssets;
|
||||||
|
application_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The assets of the activity
|
||||||
|
type ActivityAssets = {
|
||||||
|
large_image: string;
|
||||||
|
large_text?: string;
|
||||||
|
} | {
|
||||||
|
small_image: string;
|
||||||
|
small_text?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Last.fm track
|
||||||
|
type Track = {
|
||||||
|
name: string;
|
||||||
|
artist: string;
|
||||||
|
album: string;
|
||||||
|
albumArt: string;
|
||||||
|
url: string;
|
||||||
|
date: string;
|
||||||
|
nowPlaying: boolean;
|
||||||
|
loved: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
205
plugins/Last.fm/src/utils.ts
Normal file
205
plugins/Last.fm/src/utils.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
import { patcher } from "@vendetta";
|
||||||
|
import { FluxDispatcher } from "@vendetta/metro/common";
|
||||||
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
import { currentSettings, global, SET_ACTIVITY, verboseLog } from ".";
|
||||||
|
import Constants from "./constants";
|
||||||
|
|
||||||
|
/** Fetches the latest user's scrobble */
|
||||||
|
async function fetchLatestScrobble(): Promise<Track> {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
'method': 'user.getrecenttracks',
|
||||||
|
'user': currentSettings.username,
|
||||||
|
'api_key': Constants.LFM_API_KEY,
|
||||||
|
'format': 'json',
|
||||||
|
'limit': '1',
|
||||||
|
'extended': '1'
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
const info = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`).then(x => x.json());
|
||||||
|
const lastTrack = info.recenttracks?.track?.[0] || Promise.reject(info);
|
||||||
|
return {
|
||||||
|
name: lastTrack.name,
|
||||||
|
artist: lastTrack.artist.name,
|
||||||
|
album: lastTrack.album['#text'],
|
||||||
|
albumArt: handleAlbumCover(lastTrack.image[3]['#text']),
|
||||||
|
url: lastTrack.url,
|
||||||
|
date: lastTrack.date?.['#text'] ?? 'now',
|
||||||
|
nowPlaying: Boolean(lastTrack['@attr']?.nowplaying),
|
||||||
|
loved: lastTrack.loved === '1',
|
||||||
|
} as Track;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Currently ditches the default album covers
|
||||||
|
* @param cover The album cover given by Last.fm
|
||||||
|
*/
|
||||||
|
function handleAlbumCover(cover: string): string {
|
||||||
|
// If the cover is a default one, return null (empty)
|
||||||
|
if (Constants.DEFAULT_COVER_HASHES.some(x => cover.includes(x))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sends the activity details to Discord */
|
||||||
|
async function sendRequest(activity: Activity): Promise<ResultActivity> {
|
||||||
|
if (global.stopped) {
|
||||||
|
console.log("--> Plugin is unloaded, aborting...");
|
||||||
|
global.updateInterval && clearInterval(global.updateInterval);
|
||||||
|
activity = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
global.lastActivity = activity;
|
||||||
|
|
||||||
|
return await SET_ACTIVITY.handler({
|
||||||
|
isSocketConnected: () => true,
|
||||||
|
socket: {
|
||||||
|
id: 120,
|
||||||
|
application: {
|
||||||
|
id: Constants.APPLICATION_ID,
|
||||||
|
name: activity?.name || "RichPresence"
|
||||||
|
},
|
||||||
|
transport: "ipc"
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
pid: 120,
|
||||||
|
activity: activity && { ...activity } || null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears the user's activity */
|
||||||
|
function clearActivity(): Promise<ResultActivity> {
|
||||||
|
global.lastActivity && verboseLog("--> Clearing activity...");
|
||||||
|
return sendRequest(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the current scrobble and sets the activity.
|
||||||
|
* This function is actively called by the updateInterval.
|
||||||
|
*/
|
||||||
|
export async function update() {
|
||||||
|
verboseLog("--> Fetching last track...");
|
||||||
|
|
||||||
|
if (!currentSettings.username) {
|
||||||
|
showToast("Last.fm username is not set!", getAssetIDByName("Small"));
|
||||||
|
await flush(); // Flush as we always needs reinitialization
|
||||||
|
throw new Error("Username is not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastTrack = await fetchLatestScrobble().catch(async (err) => {
|
||||||
|
verboseLog("--> An error occurred while fetching the last track, aborting...");
|
||||||
|
await clearActivity().catch();
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!lastTrack.nowPlaying) {
|
||||||
|
verboseLog("--> Last track is not currently playing, aborting...");
|
||||||
|
return await clearActivity();
|
||||||
|
}
|
||||||
|
|
||||||
|
verboseLog("--> Setting activity...");
|
||||||
|
|
||||||
|
if (global.lastTrackUrl === lastTrack.url) {
|
||||||
|
verboseLog("--> Last track is the same as the previous one, aborting...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activity = {
|
||||||
|
name: currentSettings.appName || "Music",
|
||||||
|
type: currentSettings.listeningTo ? 2 : 0,
|
||||||
|
details: lastTrack.name,
|
||||||
|
state: `by ${lastTrack.artist}`,
|
||||||
|
} as Activity;
|
||||||
|
|
||||||
|
global.lastTrackUrl = lastTrack.url;
|
||||||
|
|
||||||
|
// Set timestamps
|
||||||
|
if (currentSettings.showTimestamp) {
|
||||||
|
activity.timestamps = {
|
||||||
|
start: Date.now() / 1000 | 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTrack.albumArt) {
|
||||||
|
activity.assets = {
|
||||||
|
large_image: lastTrack.albumArt,
|
||||||
|
large_text: `on ${lastTrack.album}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await sendRequest(activity).catch(async (err) => {
|
||||||
|
verboseLog("--> An error occurred while setting the activity");
|
||||||
|
await clearActivity().catch();
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
verboseLog("--> Successfully set activity!");
|
||||||
|
verboseLog(response);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stops everything */
|
||||||
|
export function flush(): Promise<ResultActivity> {
|
||||||
|
console.log("--> Flushing...");
|
||||||
|
global.lastActivity = null;
|
||||||
|
global.updateInterval && clearInterval(global.updateInterval);
|
||||||
|
|
||||||
|
return clearActivity();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Initializes the plugin */
|
||||||
|
export async function initialize() {
|
||||||
|
console.log("--> Initializing...");
|
||||||
|
|
||||||
|
if (global.stopped) {
|
||||||
|
throw new Error("Plugin is already stopped!");
|
||||||
|
}
|
||||||
|
|
||||||
|
global.lastTrackUrl = null;
|
||||||
|
|
||||||
|
global.updateInterval && clearInterval(global.updateInterval);
|
||||||
|
global.lastActivity && await clearActivity();
|
||||||
|
|
||||||
|
let tries = 0;
|
||||||
|
|
||||||
|
update().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
tries++;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Periodically fetches the current scrobble and sets the activity
|
||||||
|
global.updateInterval = setInterval(
|
||||||
|
() => update()
|
||||||
|
.then(() => tries = 0)
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
if (++tries > 3) {
|
||||||
|
console.error("Failed to fetch/set activity 3 times, aborting...");
|
||||||
|
clearInterval(global.updateInterval);
|
||||||
|
global.stopped = true;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
currentSettings.timeInterval
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discord doesn't allow us to set the activity type to "Listening to" so we have to patch it
|
||||||
|
export function patchActivityType(): () => boolean {
|
||||||
|
if (global.activityTypeUnpatch) {
|
||||||
|
console.warn("This is weird... activity type override is already defined?");
|
||||||
|
global.activityTypeUnpatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Patching activity type...");
|
||||||
|
|
||||||
|
const nodes = Object.values(FluxDispatcher._actionHandlers._dependencyGraph.nodes);
|
||||||
|
const { actionHandler } = nodes.find((x: any) => x.name === "LocalActivityStore") as any;
|
||||||
|
|
||||||
|
return patcher.before("LOCAL_ACTIVITY_UPDATE", actionHandler, ([{ activity }]) => {
|
||||||
|
if (activity && activity.name === global.lastActivity.name) {
|
||||||
|
activity.type = global.lastActivity.type;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue