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;
listeningTo: boolean;
ignoreSpotify: boolean;
ignoreYouTubeMusic: boolean;
verboseLogging: boolean;
service: ServiceType | undefined;
// Service-specific settings

View file

@ -1,79 +1,80 @@
import { ServiceConfig, ServiceType } from "../../defs";
const Constants = {
DEFAULT_APP_NAME: "Music",
DEFAULT_TIME_INTERVAL: 5,
// Discord application ID
APPLICATION_ID: "1054951789318909972",
// Don't check more than once every 3 seconds to avoid getting rate limited
MIN_UPDATE_INTERVAL: 3,
// How many times to retry failed API calls
MAX_RETRY_ATTEMPTS: 3,
// Wait 5 seconds between retries
RETRY_DELAY: 5000,
DEFAULT_APP_NAME: "Music",
DEFAULT_TIME_INTERVAL: 5,
// Discord application ID
APPLICATION_ID: "1054951789318909972",
// Don't check more than once every 3 seconds to avoid getting rate limited
MIN_UPDATE_INTERVAL: 3,
// How many times to retry failed API calls
MAX_RETRY_ATTEMPTS: 3,
// Wait 5 seconds between retries
RETRY_DELAY: 5000,
// Configuration for each supported service
SERVICES: {
lastfm: {
name: "Last.fm",
baseUrl: "http://ws.audioscrobbler.com/2.0",
requiresApiKey: true,
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: "",
// Configuration for each supported service
SERVICES: {
lastfm: {
name: "Last.fm",
baseUrl: "http://ws.audioscrobbler.com/2.0",
requiresApiKey: true,
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 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>,
// 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,
ignoreYouTubeMusic: true,
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;
export default Constants;

View file

@ -4,16 +4,16 @@ import Constants from "./constants";
import { SelfPresenceStore } from "./modules";
import { clearActivity, fetchAsset, sendRequest } from "./utils/activity";
import {
setDebugInfo,
recordSuccessfulUpdate,
recordServiceError,
incrementApiCall,
setDebugInfo,
recordSuccessfulUpdate,
recordServiceError,
incrementApiCall,
} from "./utils/debug";
import { serviceFactory } from "./services/ServiceFactory";
import {
formatDuration,
validateTimestamps,
getCurrentTimestamp,
formatDuration,
validateTimestamps,
getCurrentTimestamp,
} from "./utils/time";
enum ActivityType {
@ -25,382 +25,399 @@ enum ActivityType {
const log = (...message: any[]) => console.log("[Scrobble Plugin]", ...message);
const logError = (...message: any[]) =>
console.error("[Scrobble Plugin] Error:", ...message);
console.error("[Scrobble Plugin] Error:", ...message);
const logVerbose = (...message: any[]) =>
currentSettings.verboseLogging &&
currentSettings.verboseLogging &&
console.log("[Scrobble Plugin] Verbose:", ...message);
class PluginManager {
private static instance: PluginManager;
private updateTimer?: any;
private reconnectTimer?: any;
private consecutiveFailures = 0;
private isReconnecting = false;
private currentActivity?: Activity;
private lastUpdateTime = 0;
private static instance: PluginManager;
private updateTimer?: any;
private reconnectTimer?: any;
private consecutiveFailures = 0;
private isReconnecting = false;
private currentActivity?: Activity;
private lastUpdateTime = 0;
private constructor() {
private constructor() {
// 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 {
if (!PluginManager.instance) {
PluginManager.instance = new PluginManager();
const serviceName = serviceFactory.getCurrentService().getServiceName();
logVerbose(`Fetching latest track from ${serviceName}...`);
// 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
private async updateActivity() {
if (pluginState.pluginStopped) {
logVerbose("Plugin is stopped, skipping update");
this.stopUpdates();
return;
// skip if YouTube Music is playing and user wants to ignore it
if (currentSettings.ignoreYouTubeMusic) {
const youtubeActivity = SelfPresenceStore.findActivity(
(act) =>
act.name === "YouTube Music" ||
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();
logVerbose(`Fetching latest track from ${serviceName}...`);
incrementApiCall();
const lastTrack = await serviceFactory
.getCurrentService()
.fetchLatestScrobble();
setDebugInfo("lastTrack", lastTrack);
// keep track of whether we actually need to update Discord
let willUpdateRPC = false;
if (!lastTrack.nowPlaying) {
logVerbose("Track is not currently playing");
clearActivity();
return;
}
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;
}
setDebugInfo("isSpotifyIgnored", false);
}
// use simple URL comparison like Last.fm plugin
if (pluginState.lastTrackUrl === lastTrack.url) {
logVerbose("Track hasn't changed");
recordSuccessfulUpdate();
this.consecutiveFailures = 0;
return;
}
incrementApiCall();
const lastTrack = await serviceFactory
.getCurrentService()
.fetchLatestScrobble();
setDebugInfo("lastTrack", lastTrack);
willUpdateRPC = true;
if (!lastTrack.nowPlaying) {
logVerbose("Track is not currently playing");
clearActivity();
return;
}
log(`🎵 Track changed: ${lastTrack.artist} - ${lastTrack.name}`);
// use simple URL comparison like Last.fm plugin
if (pluginState.lastTrackUrl === lastTrack.url) {
logVerbose("Track hasn't changed");
recordSuccessfulUpdate();
this.consecutiveFailures = 0;
return;
}
willUpdateRPC = true;
log(`🎵 Track changed: ${lastTrack.artist} - ${lastTrack.name}`);
// set up timestamps for the track
let activityTimestamps;
if (
lastTrack.nowPlaying &&
// set up timestamps for the track
let activityTimestamps;
if (
lastTrack.nowPlaying &&
currentSettings.showTimestamp &&
lastTrack.from
) {
// figure out when this track actually started
const now = getCurrentTimestamp();
let startTime = lastTrack.from;
) {
// figure out when this track actually started
const now = getCurrentTimestamp();
let startTime = lastTrack.from;
// if the timestamp is way old, estimate when it started
if (startTime < now - 3600) {
// more than an hour old - probably wrong
if (lastTrack.duration && lastTrack.duration > 0) {
// guess we're about 10% in or 30 seconds, whatever's smaller
const estimatedElapsed = Math.min(lastTrack.duration * 0.1, 30);
startTime = now - estimatedElapsed;
} else {
startTime = now;
}
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");
// if the timestamp is way old, estimate when it started
if (startTime < now - 3600) {
// more than an hour old - probably wrong
if (lastTrack.duration && lastTrack.duration > 0) {
// guess we're about 10% in or 30 seconds, whatever's smaller
const estimatedElapsed = Math.min(lastTrack.duration * 0.1, 30);
startTime = now - estimatedElapsed;
} else {
startTime = now;
}
logVerbose("had to estimate start time");
}
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",
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();
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
@ -408,5 +425,5 @@ const manager = PluginManager.getInstance();
export const initialize = () => manager.initialize();
export const stop = () => manager.stop();
export const switchService = (service: string) =>
manager.switchService(service);
manager.switchService(service);
export const getStatus = () => manager.getStatus();

File diff suppressed because it is too large Load diff