Add option to ignore YouTube Music activity in status

- Add `ignoreYouTubeMusic` to settings and defaults - Skip updating
Discord status if YouTube Music is playing and option is enabled - Add
toggle to settings UI for ignoring YouTube Music
This commit is contained in:
kmmiio99o 2025-10-04 20:37:39 +02:00
parent 735f8aba4c
commit d4f81b1668
4 changed files with 947 additions and 916 deletions

1
plugins/defs.d.ts vendored
View file

@ -37,6 +37,7 @@ export type LFMSettings = {
timeInterval: number | string; timeInterval: number | string;
listeningTo: boolean; listeningTo: boolean;
ignoreSpotify: boolean; ignoreSpotify: boolean;
ignoreYouTubeMusic: boolean;
verboseLogging: boolean; verboseLogging: boolean;
service: ServiceType | undefined; service: ServiceType | undefined;
// Service-specific settings // Service-specific settings

View file

@ -1,79 +1,80 @@
import { ServiceConfig, ServiceType } from "../../defs"; import { ServiceConfig, ServiceType } from "../../defs";
const Constants = { const Constants = {
DEFAULT_APP_NAME: "Music", DEFAULT_APP_NAME: "Music",
DEFAULT_TIME_INTERVAL: 5, DEFAULT_TIME_INTERVAL: 5,
// Discord application ID // Discord application ID
APPLICATION_ID: "1054951789318909972", APPLICATION_ID: "1054951789318909972",
// Don't check more than once every 3 seconds to avoid getting rate limited // Don't check more than once every 3 seconds to avoid getting rate limited
MIN_UPDATE_INTERVAL: 3, MIN_UPDATE_INTERVAL: 3,
// How many times to retry failed API calls // How many times to retry failed API calls
MAX_RETRY_ATTEMPTS: 3, MAX_RETRY_ATTEMPTS: 3,
// Wait 5 seconds between retries // Wait 5 seconds between retries
RETRY_DELAY: 5000, RETRY_DELAY: 5000,
// Configuration for each supported service // Configuration for each supported service
SERVICES: { SERVICES: {
lastfm: { lastfm: {
name: "Last.fm", name: "Last.fm",
baseUrl: "http://ws.audioscrobbler.com/2.0", baseUrl: "http://ws.audioscrobbler.com/2.0",
requiresApiKey: true, requiresApiKey: true,
requiresToken: false, requiresToken: false,
},
librefm: {
name: "Libre.fm",
baseUrl: "http://libre.fm/api",
requiresApiKey: true,
requiresToken: false,
},
listenbrainz: {
name: "ListenBrainz",
baseUrl: "http://api.listenbrainz.org/1",
requiresApiKey: false,
requiresToken: true,
},
} as Record<ServiceType, ServiceConfig>,
// Last.fm/Libre.fm use these hashes for their generic album covers
DEFAULT_COVER_HASHES: [
"2a96cbd8b46e442fc41c2b86b821562f",
"c6f59c1e5e7240a4c0d427abd71f3dbb",
],
// Plugin defaults
DEFAULT_SETTINGS: {
username: "",
apiKey: "",
appName: "Music",
timeInterval: 5,
showTimestamp: true,
listeningTo: true,
ignoreSpotify: true,
verboseLogging: false,
service: undefined as ServiceType | undefined,
librefmUsername: "",
librefmApiKey: "",
listenbrainzUsername: "",
listenbrainzToken: "",
}, },
librefm: {
name: "Libre.fm",
baseUrl: "http://libre.fm/api",
requiresApiKey: true,
requiresToken: false,
},
listenbrainz: {
name: "ListenBrainz",
baseUrl: "http://api.listenbrainz.org/1",
requiresApiKey: false,
requiresToken: true,
},
} as Record<ServiceType, ServiceConfig>,
// Last.fm/Libre.fm API error codes // Last.fm/Libre.fm use these hashes for their generic album covers
API_ERROR_CODES: { DEFAULT_COVER_HASHES: [
2: "Invalid service", "2a96cbd8b46e442fc41c2b86b821562f",
3: "Invalid method", "c6f59c1e5e7240a4c0d427abd71f3dbb",
4: "Invalid format", ],
5: "Invalid parameters",
6: "Invalid resource specified", // Plugin defaults
7: "Invalid session key", DEFAULT_SETTINGS: {
8: "Invalid API key", username: "",
9: "Invalid session", apiKey: "",
10: "Invalid API signature", appName: "Music",
11: "Service offline", timeInterval: 5,
13: "Invalid method signature supplied", showTimestamp: true,
16: "Service temporarily unavailable", listeningTo: true,
26: "Suspended API key", ignoreSpotify: true,
29: "Rate limit exceeded", ignoreYouTubeMusic: true,
} as Record<number, string>, verboseLogging: false,
service: undefined as ServiceType | undefined,
librefmUsername: "",
librefmApiKey: "",
listenbrainzUsername: "",
listenbrainzToken: "",
},
// Last.fm/Libre.fm API error codes
API_ERROR_CODES: {
2: "Invalid service",
3: "Invalid method",
4: "Invalid format",
5: "Invalid parameters",
6: "Invalid resource specified",
7: "Invalid session key",
8: "Invalid API key",
9: "Invalid session",
10: "Invalid API signature",
11: "Service offline",
13: "Invalid method signature supplied",
16: "Service temporarily unavailable",
26: "Suspended API key",
29: "Rate limit exceeded",
} as Record<number, string>,
} as const; } as const;
export default Constants; export default Constants;

View file

@ -4,16 +4,16 @@ import Constants from "./constants";
import { SelfPresenceStore } from "./modules"; import { SelfPresenceStore } from "./modules";
import { clearActivity, fetchAsset, sendRequest } from "./utils/activity"; import { clearActivity, fetchAsset, sendRequest } from "./utils/activity";
import { import {
setDebugInfo, setDebugInfo,
recordSuccessfulUpdate, recordSuccessfulUpdate,
recordServiceError, recordServiceError,
incrementApiCall, incrementApiCall,
} from "./utils/debug"; } from "./utils/debug";
import { serviceFactory } from "./services/ServiceFactory"; import { serviceFactory } from "./services/ServiceFactory";
import { import {
formatDuration, formatDuration,
validateTimestamps, validateTimestamps,
getCurrentTimestamp, getCurrentTimestamp,
} from "./utils/time"; } from "./utils/time";
enum ActivityType { enum ActivityType {
@ -25,382 +25,399 @@ enum ActivityType {
const log = (...message: any[]) => console.log("[Scrobble Plugin]", ...message); const log = (...message: any[]) => console.log("[Scrobble Plugin]", ...message);
const logError = (...message: any[]) => const logError = (...message: any[]) =>
console.error("[Scrobble Plugin] Error:", ...message); console.error("[Scrobble Plugin] Error:", ...message);
const logVerbose = (...message: any[]) => const logVerbose = (...message: any[]) =>
currentSettings.verboseLogging && currentSettings.verboseLogging &&
console.log("[Scrobble Plugin] Verbose:", ...message); console.log("[Scrobble Plugin] Verbose:", ...message);
class PluginManager { class PluginManager {
private static instance: PluginManager; private static instance: PluginManager;
private updateTimer?: any; private updateTimer?: any;
private reconnectTimer?: any; private reconnectTimer?: any;
private consecutiveFailures = 0; private consecutiveFailures = 0;
private isReconnecting = false; private isReconnecting = false;
private currentActivity?: Activity; private currentActivity?: Activity;
private lastUpdateTime = 0; private lastUpdateTime = 0;
private constructor() { private constructor() {
// singleton pattern // singleton pattern
}
public static getInstance(): PluginManager {
if (!PluginManager.instance) {
PluginManager.instance = new PluginManager();
}
return PluginManager.instance;
}
// Check for new tracks and update Discord status when something changes
private async updateActivity() {
if (pluginState.pluginStopped) {
logVerbose("Plugin is stopped, skipping update");
this.stopUpdates();
return;
} }
public static getInstance(): PluginManager { const serviceName = serviceFactory.getCurrentService().getServiceName();
if (!PluginManager.instance) { logVerbose(`Fetching latest track from ${serviceName}...`);
PluginManager.instance = new PluginManager();
// keep track of whether we actually need to update Discord
let willUpdateRPC = false;
try {
// skip if Spotify is playing and user wants to ignore it
if (currentSettings.ignoreSpotify) {
const spotifyActivity = SelfPresenceStore.findActivity(
(act) => act.sync_id,
);
if (spotifyActivity) {
logVerbose("Spotify is currently playing, clearing activity");
setDebugInfo("isSpotifyIgnored", true);
clearActivity();
return;
} }
return PluginManager.instance; setDebugInfo("isSpotifyIgnored", false);
} }
// Check for new tracks and update Discord status when something changes // skip if YouTube Music is playing and user wants to ignore it
private async updateActivity() { if (currentSettings.ignoreYouTubeMusic) {
if (pluginState.pluginStopped) { const youtubeActivity = SelfPresenceStore.findActivity(
logVerbose("Plugin is stopped, skipping update"); (act) =>
this.stopUpdates(); act.name === "YouTube Music" ||
return; act.application_id === "463097721130188830" ||
(act.name && act.name.toLowerCase().includes("youtube music")),
);
if (youtubeActivity) {
logVerbose("YouTube Music is currently playing, clearing activity");
setDebugInfo("isYouTubeMusicIgnored", true);
clearActivity();
return;
} }
setDebugInfo("isYouTubeMusicIgnored", false);
}
const serviceName = serviceFactory.getCurrentService().getServiceName(); incrementApiCall();
logVerbose(`Fetching latest track from ${serviceName}...`); const lastTrack = await serviceFactory
.getCurrentService()
.fetchLatestScrobble();
setDebugInfo("lastTrack", lastTrack);
// keep track of whether we actually need to update Discord if (!lastTrack.nowPlaying) {
let willUpdateRPC = false; logVerbose("Track is not currently playing");
clearActivity();
return;
}
try { // use simple URL comparison like Last.fm plugin
// skip if Spotify is playing and user wants to ignore it if (pluginState.lastTrackUrl === lastTrack.url) {
if (currentSettings.ignoreSpotify) { logVerbose("Track hasn't changed");
const spotifyActivity = SelfPresenceStore.findActivity( recordSuccessfulUpdate();
(act) => act.sync_id, this.consecutiveFailures = 0;
); return;
if (spotifyActivity) { }
logVerbose("Spotify is currently playing, clearing activity");
setDebugInfo("isSpotifyIgnored", true);
clearActivity();
return;
}
setDebugInfo("isSpotifyIgnored", false);
}
incrementApiCall(); willUpdateRPC = true;
const lastTrack = await serviceFactory
.getCurrentService()
.fetchLatestScrobble();
setDebugInfo("lastTrack", lastTrack);
if (!lastTrack.nowPlaying) { log(`🎵 Track changed: ${lastTrack.artist} - ${lastTrack.name}`);
logVerbose("Track is not currently playing");
clearActivity();
return;
}
// use simple URL comparison like Last.fm plugin // set up timestamps for the track
if (pluginState.lastTrackUrl === lastTrack.url) { let activityTimestamps;
logVerbose("Track hasn't changed"); if (
recordSuccessfulUpdate(); lastTrack.nowPlaying &&
this.consecutiveFailures = 0;
return;
}
willUpdateRPC = true;
log(`🎵 Track changed: ${lastTrack.artist} - ${lastTrack.name}`);
// set up timestamps for the track
let activityTimestamps;
if (
lastTrack.nowPlaying &&
currentSettings.showTimestamp && currentSettings.showTimestamp &&
lastTrack.from lastTrack.from
) { ) {
// figure out when this track actually started // figure out when this track actually started
const now = getCurrentTimestamp(); const now = getCurrentTimestamp();
let startTime = lastTrack.from; let startTime = lastTrack.from;
// if the timestamp is way old, estimate when it started // if the timestamp is way old, estimate when it started
if (startTime < now - 3600) { if (startTime < now - 3600) {
// more than an hour old - probably wrong // more than an hour old - probably wrong
if (lastTrack.duration && lastTrack.duration > 0) { if (lastTrack.duration && lastTrack.duration > 0) {
// guess we're about 10% in or 30 seconds, whatever's smaller // guess we're about 10% in or 30 seconds, whatever's smaller
const estimatedElapsed = Math.min(lastTrack.duration * 0.1, 30); const estimatedElapsed = Math.min(lastTrack.duration * 0.1, 30);
startTime = now - estimatedElapsed; startTime = now - estimatedElapsed;
} else { } else {
startTime = now; startTime = now;
} }
logVerbose("had to estimate start time"); logVerbose("had to estimate start time");
}
activityTimestamps = {
start: startTime * 1000, // Discord wants milliseconds
};
if (lastTrack.to) {
activityTimestamps.end = lastTrack.to * 1000;
}
}
logVerbose(
`🎯 Preparing RPC update for: ${lastTrack.artist} - ${lastTrack.name}`,
);
const activity: Activity = {
name: currentSettings.appName || Constants.DEFAULT_APP_NAME,
flags: 0,
type: currentSettings.listeningTo
? ActivityType.LISTENING
: ActivityType.PLAYING,
details: lastTrack.name,
state: `by ${lastTrack.artist}`,
status_display_type: 1,
application_id: Constants.APPLICATION_ID,
};
// replace template variables in app name if user is using them
if (activity.name.includes("{{")) {
const variables = {
artist: lastTrack.artist,
name: lastTrack.name,
album: lastTrack.album,
service: serviceName,
};
for (const [key, value] of Object.entries(variables)) {
activity.name = activity.name.replace(
new RegExp(`{{${key}}}`, "g"),
value || "",
);
}
}
// add the timestamps we figured out
if (activityTimestamps) {
activity.timestamps = activityTimestamps;
logVerbose("Timestamps set:", {
start: new Date(activityTimestamps.start).toISOString(),
end: activityTimestamps.end
? new Date(activityTimestamps.end).toISOString()
: "none",
duration: lastTrack.duration
? formatDuration(lastTrack.duration)
: "unknown",
});
}
// set up album art and tooltip text
if (lastTrack.album || lastTrack.albumArt) {
const assetUrls = lastTrack.albumArt ? [lastTrack.albumArt] : [];
const assets = await fetchAsset(assetUrls);
if (assets[0]) {
const largeText = lastTrack.album
? `on ${lastTrack.album}`
: `${lastTrack.artist} - ${lastTrack.name}`;
// include track length in the tooltip if we have it
const durationText =
currentSettings.showTimestamp && lastTrack.duration
? ` • ${formatDuration(lastTrack.duration)}`
: "";
activity.assets = {
large_image: assets[0],
large_text: largeText + durationText,
};
logVerbose("Album art set:", assets[0]);
} else if (lastTrack.album) {
// no album art but we can still show the album name
activity.assets = {
large_text: `on ${lastTrack.album}`,
};
}
}
logVerbose("Setting Discord activity:", activity);
setDebugInfo("lastActivity", activity);
await sendRequest(activity);
pluginState.lastTrackUrl = lastTrack.url;
this.currentActivity = activity;
pluginState.lastActivity = activity;
this.consecutiveFailures = 0;
this.lastUpdateTime = getCurrentTimestamp();
recordSuccessfulUpdate();
log(
`✅ RPC updated successfully: ${lastTrack.artist} - ${lastTrack.name}`,
);
} catch (error) {
logError("Update failed:", error);
recordServiceError(currentSettings.service, (error as Error).message);
this.handleError(error as Error);
}
}
private handleError(error: Error) {
this.consecutiveFailures++;
setDebugInfo("lastError", error);
logError(
`Failure ${this.consecutiveFailures}/${Constants.MAX_RETRY_ATTEMPTS}:`,
error.message,
);
if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) {
logError("Max retry attempts reached, initiating reconnection...");
this.startReconnection();
}
}
private startReconnection() {
if (this.isReconnecting) return;
this.isReconnecting = true;
this.stopUpdates();
log("Starting reconnection process...");
this.reconnectTimer = setInterval(() => {
log("Attempting to reconnect...");
this.initialize() // don't reset state
.then(() => {
log("Reconnection successful!");
this.stopReconnection();
})
.catch((error) => {
logError("Reconnection attempt failed:", error.message);
});
}, Constants.RETRY_DELAY);
}
private stopReconnection() {
try {
if (this.reconnectTimer) {
clearInterval(this.reconnectTimer);
this.reconnectTimer = undefined;
}
this.isReconnecting = false;
this.consecutiveFailures = 0;
} catch (error) {
// Silently handle reconnection cleanup errors
console.error("[Scrobble Plugin] Reconnection cleanup error:", error);
}
}
// clean up all timers
private stopUpdates() {
try {
if (this.updateTimer) {
clearInterval(this.updateTimer);
this.updateTimer = undefined;
}
} catch (error) {
// Silently handle timer cleanup errors
console.error("[Scrobble Plugin] Timer cleanup error:", error);
}
}
// start everything up
public async initialize() {
if (pluginState.pluginStopped) {
throw new Error("Plugin is stopped");
} }
const serviceName = serviceFactory.getCurrentService().getServiceName(); activityTimestamps = {
log(`Initializing with ${serviceName}...`); start: startTime * 1000, // Discord wants milliseconds
// make sure credentials work before starting
try {
const isValid = await serviceFactory.validateCurrentService();
if (!isValid) {
throw new Error(`Invalid credentials for ${serviceName}`);
}
log(`${serviceName} credentials validated successfully`);
} catch (error) {
logError(`Failed to validate ${serviceName} credentials:`, error);
throw error;
}
this.stopUpdates();
// check right away in case something is already playing
log("🎵 checking for already playing songs...");
// Don't reset track state - let updateActivity() handle whether to update
await this.updateActivity();
const interval = Math.max(
(Number(currentSettings.timeInterval) ||
Constants.DEFAULT_SETTINGS.timeInterval) * 1000,
Constants.MIN_UPDATE_INTERVAL * 1000,
);
this.updateTimer = setInterval(() => this.updateActivity(), interval);
log(
`Update timer started with interval: ${interval}ms (${interval / 1000}s)`,
);
}
// stop everything and clean up
public stop() {
if (pluginState.pluginStopped) {
return; // already stopped
}
log("Stopping plugin...");
pluginState.pluginStopped = true;
try {
this.stopUpdates();
this.stopReconnection();
clearActivity();
// Don't reset track state on stop - only when actually switching services
log("Plugin stopped successfully");
} catch (error) {
// don't let cleanup errors break things
console.error("[Scrobble Plugin] Stop error:", error);
}
}
// change to a different scrobble service
public async switchService(newService: string) {
if (pluginState.pluginStopped) {
return;
}
log(`Switching to ${newService}...`);
// stop what we're doing first
const wasRunning = !pluginState.pluginStopped;
this.stop();
try {
// clear cache so we get fresh service instances
serviceFactory.clearCache();
// Reset track state when switching services
pluginState.lastTrackUrl = undefined;
this.currentActivity = undefined;
this.lastUpdateTime = 0;
// start back up with the new service
if (wasRunning) {
pluginState.pluginStopped = false;
await this.initialize();
}
} catch (error) {
logError("Failed to switch service:", error);
}
}
// get info about what's currently happening
public getStatus() {
const serviceName = serviceFactory.getCurrentService().getServiceName();
return {
running: !pluginState.pluginStopped,
service: serviceName,
consecutiveFailures: this.consecutiveFailures,
isReconnecting: this.isReconnecting,
lastTrackUrl: pluginState.lastTrackUrl,
updateInterval: this.updateTimer ? "Active" : "Inactive",
}; };
if (lastTrack.to) {
activityTimestamps.end = lastTrack.to * 1000;
}
}
logVerbose(
`🎯 Preparing RPC update for: ${lastTrack.artist} - ${lastTrack.name}`,
);
const activity: Activity = {
name: currentSettings.appName || Constants.DEFAULT_APP_NAME,
flags: 0,
type: currentSettings.listeningTo
? ActivityType.LISTENING
: ActivityType.PLAYING,
details: lastTrack.name,
state: `by ${lastTrack.artist}`,
status_display_type: 1,
application_id: Constants.APPLICATION_ID,
};
// replace template variables in app name if user is using them
if (activity.name.includes("{{")) {
const variables = {
artist: lastTrack.artist,
name: lastTrack.name,
album: lastTrack.album,
service: serviceName,
};
for (const [key, value] of Object.entries(variables)) {
activity.name = activity.name.replace(
new RegExp(`{{${key}}}`, "g"),
value || "",
);
}
}
// add the timestamps we figured out
if (activityTimestamps) {
activity.timestamps = activityTimestamps;
logVerbose("Timestamps set:", {
start: new Date(activityTimestamps.start).toISOString(),
end: activityTimestamps.end
? new Date(activityTimestamps.end).toISOString()
: "none",
duration: lastTrack.duration
? formatDuration(lastTrack.duration)
: "unknown",
});
}
// set up album art and tooltip text
if (lastTrack.album || lastTrack.albumArt) {
const assetUrls = lastTrack.albumArt ? [lastTrack.albumArt] : [];
const assets = await fetchAsset(assetUrls);
if (assets[0]) {
const largeText = lastTrack.album
? `on ${lastTrack.album}`
: `${lastTrack.artist} - ${lastTrack.name}`;
// include track length in the tooltip if we have it
const durationText =
currentSettings.showTimestamp && lastTrack.duration
? ` • ${formatDuration(lastTrack.duration)}`
: "";
activity.assets = {
large_image: assets[0],
large_text: largeText + durationText,
};
logVerbose("Album art set:", assets[0]);
} else if (lastTrack.album) {
// no album art but we can still show the album name
activity.assets = {
large_text: `on ${lastTrack.album}`,
};
}
}
logVerbose("Setting Discord activity:", activity);
setDebugInfo("lastActivity", activity);
await sendRequest(activity);
pluginState.lastTrackUrl = lastTrack.url;
this.currentActivity = activity;
pluginState.lastActivity = activity;
this.consecutiveFailures = 0;
this.lastUpdateTime = getCurrentTimestamp();
recordSuccessfulUpdate();
log(
`✅ RPC updated successfully: ${lastTrack.artist} - ${lastTrack.name}`,
);
} catch (error) {
logError("Update failed:", error);
recordServiceError(currentSettings.service, (error as Error).message);
this.handleError(error as Error);
} }
}
private handleError(error: Error) {
this.consecutiveFailures++;
setDebugInfo("lastError", error);
logError(
`Failure ${this.consecutiveFailures}/${Constants.MAX_RETRY_ATTEMPTS}:`,
error.message,
);
if (this.consecutiveFailures >= Constants.MAX_RETRY_ATTEMPTS) {
logError("Max retry attempts reached, initiating reconnection...");
this.startReconnection();
}
}
private startReconnection() {
if (this.isReconnecting) return;
this.isReconnecting = true;
this.stopUpdates();
log("Starting reconnection process...");
this.reconnectTimer = setInterval(() => {
log("Attempting to reconnect...");
this.initialize() // don't reset state
.then(() => {
log("Reconnection successful!");
this.stopReconnection();
})
.catch((error) => {
logError("Reconnection attempt failed:", error.message);
});
}, Constants.RETRY_DELAY);
}
private stopReconnection() {
try {
if (this.reconnectTimer) {
clearInterval(this.reconnectTimer);
this.reconnectTimer = undefined;
}
this.isReconnecting = false;
this.consecutiveFailures = 0;
} catch (error) {
// Silently handle reconnection cleanup errors
console.error("[Scrobble Plugin] Reconnection cleanup error:", error);
}
}
// clean up all timers
private stopUpdates() {
try {
if (this.updateTimer) {
clearInterval(this.updateTimer);
this.updateTimer = undefined;
}
} catch (error) {
// Silently handle timer cleanup errors
console.error("[Scrobble Plugin] Timer cleanup error:", error);
}
}
// start everything up
public async initialize() {
if (pluginState.pluginStopped) {
throw new Error("Plugin is stopped");
}
const serviceName = serviceFactory.getCurrentService().getServiceName();
log(`Initializing with ${serviceName}...`);
// make sure credentials work before starting
try {
const isValid = await serviceFactory.validateCurrentService();
if (!isValid) {
throw new Error(`Invalid credentials for ${serviceName}`);
}
log(`${serviceName} credentials validated successfully`);
} catch (error) {
logError(`Failed to validate ${serviceName} credentials:`, error);
throw error;
}
this.stopUpdates();
// check right away in case something is already playing
log("🎵 checking for already playing songs...");
// Don't reset track state - let updateActivity() handle whether to update
await this.updateActivity();
const interval = Math.max(
(Number(currentSettings.timeInterval) ||
Constants.DEFAULT_SETTINGS.timeInterval) * 1000,
Constants.MIN_UPDATE_INTERVAL * 1000,
);
this.updateTimer = setInterval(() => this.updateActivity(), interval);
log(
`Update timer started with interval: ${interval}ms (${interval / 1000}s)`,
);
}
// stop everything and clean up
public stop() {
if (pluginState.pluginStopped) {
return; // already stopped
}
log("Stopping plugin...");
pluginState.pluginStopped = true;
try {
this.stopUpdates();
this.stopReconnection();
clearActivity();
// Don't reset track state on stop - only when actually switching services
log("Plugin stopped successfully");
} catch (error) {
// don't let cleanup errors break things
console.error("[Scrobble Plugin] Stop error:", error);
}
}
// change to a different scrobble service
public async switchService(newService: string) {
if (pluginState.pluginStopped) {
return;
}
log(`Switching to ${newService}...`);
// stop what we're doing first
const wasRunning = !pluginState.pluginStopped;
this.stop();
try {
// clear cache so we get fresh service instances
serviceFactory.clearCache();
// Reset track state when switching services
pluginState.lastTrackUrl = undefined;
this.currentActivity = undefined;
this.lastUpdateTime = 0;
// start back up with the new service
if (wasRunning) {
pluginState.pluginStopped = false;
await this.initialize();
}
} catch (error) {
logError("Failed to switch service:", error);
}
}
// get info about what's currently happening
public getStatus() {
const serviceName = serviceFactory.getCurrentService().getServiceName();
return {
running: !pluginState.pluginStopped,
service: serviceName,
consecutiveFailures: this.consecutiveFailures,
isReconnecting: this.isReconnecting,
lastTrackUrl: pluginState.lastTrackUrl,
updateInterval: this.updateTimer ? "Active" : "Inactive",
};
}
} }
// expose the manager functions // expose the manager functions
@ -408,5 +425,5 @@ const manager = PluginManager.getInstance();
export const initialize = () => manager.initialize(); export const initialize = () => manager.initialize();
export const stop = () => manager.stop(); export const stop = () => manager.stop();
export const switchService = (service: string) => export const switchService = (service: string) =>
manager.switchService(service); manager.switchService(service);
export const getStatus = () => manager.getStatus(); export const getStatus = () => manager.getStatus();

File diff suppressed because it is too large Load diff