(hopefully) fix listenbrainz

so many changes because zed thought why not use formatting
This commit is contained in:
kmmiio99o 2026-01-02 19:06:57 +01:00
parent faebdafc41
commit a5686e815c
No known key found for this signature in database
GPG key ID: 163FE39F7772D2EC

View file

@ -37,171 +37,198 @@ interface ListenBrainzPlayingNowResponse {
} }
export class ListenBrainzService extends BaseService { export class ListenBrainzService extends BaseService {
getServiceName(): string { getServiceName(): string {
return "ListenBrainz"; return "ListenBrainz";
} }
protected logVerbose(...args: any[]): void { protected logVerbose(...args: any[]): void {
if (currentSettings.verboseLogging) { if (currentSettings.verboseLogging) {
console.log(`[${this.getServiceName()}] Verbose:`, ...args); console.log(`[${this.getServiceName()}] Verbose:`, ...args);
}
}
async validateCredentials(): Promise<boolean> {
try {
const username = currentSettings.listenbrainzUsername;
const token = currentSettings.listenbrainzToken;
if (!username) {
throw new Error("Username not set for ListenBrainz");
}
// For ListenBrainz, we don't need a token to read public data
// But we can test the connection by fetching user listens
const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
}
await this.makeRequest(url, { headers });
this.log("Credentials validation successful");
return true;
} catch (error) {
this.logError("Credentials validation failed:", error);
return false;
}
}
async fetchLatestScrobble(): Promise<Track> {
try {
const username = currentSettings.listenbrainzUsername;
const token = currentSettings.listenbrainzToken;
if (!username) {
throw new Error("Username not set for ListenBrainz");
}
this.logVerbose("Fetching latest scrobble for user:", username);
// First check for currently playing track
let currentlyPlaying: ListenBrainzListen | null = null;
// Helper to normalize ListenBrainz responses which may return either
// { listens: [...] } or { payload: { listens: [...] } }
const extractListens = (resp: any): ListenBrainzListen[] | undefined => {
if (!resp) return undefined;
if (Array.isArray(resp.listens)) return resp.listens;
if (resp.payload && Array.isArray(resp.payload.listens))
return resp.payload.listens;
if (resp.data && Array.isArray(resp.data.listens))
return resp.data.listens;
return undefined;
};
try {
const playingNowUrl = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/playing-now`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
} }
}
async validateCredentials(): Promise<boolean> { // Use a permissive any type for response, then normalize with extractListens
try { const playingNowRaw: any = await this.makeRequest(playingNowUrl, {
const username = currentSettings.listenbrainzUsername; headers,
const token = currentSettings.listenbrainzToken; });
const playingNowListens = extractListens(playingNowRaw);
if (!username) { if (playingNowListens && playingNowListens.length > 0) {
throw new Error("Username not set for ListenBrainz"); currentlyPlaying = playingNowListens[0];
} currentlyPlaying.playing_now = true;
// For ListenBrainz, we don't need a token to read public data
// But we can test the connection by fetching user listens
const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
}
await this.makeRequest(url, { headers });
this.log("Credentials validation successful");
return true;
} catch (error) {
this.logError("Credentials validation failed:", error);
return false;
} }
} } catch (error) {
this.logVerbose(
"No currently playing track or failed to fetch:",
error,
);
}
async fetchLatestScrobble(): Promise<Track> { let latestListen: ListenBrainzListen;
try {
const username = currentSettings.listenbrainzUsername;
const token = currentSettings.listenbrainzToken;
if (!username) { if (currentlyPlaying) {
throw new Error("Username not set for ListenBrainz"); latestListen = currentlyPlaying;
} this.logVerbose("Using currently playing track");
} else {
// Fetch recent listens
const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
}
this.logVerbose("Fetching latest scrobble for user:", username); // Fetch recent listens and normalize different response shapes
const dataRaw: any = await this.makeRequest(url, {
headers,
});
// First check for currently playing track const recentListens = ((): ListenBrainzListen[] | undefined => {
let currentlyPlaying: ListenBrainzListen | null = null; if (!dataRaw) return undefined;
try { if (Array.isArray(dataRaw.listens)) return dataRaw.listens;
const playingNowUrl = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/playing-now`; if (dataRaw.payload && Array.isArray(dataRaw.payload.listens))
const headers: Record<string, string> = {}; return dataRaw.payload.listens;
if (token) { if (dataRaw.data && Array.isArray(dataRaw.data.listens))
headers["Authorization"] = `Token ${token}`; return dataRaw.data.listens;
} return undefined;
})();
const playingNowData: ListenBrainzPlayingNowResponse = if (!recentListens || recentListens.length === 0) {
await this.makeRequest(playingNowUrl, { headers }); throw new Error("No listens found");
if (playingNowData.listens && playingNowData.listens.length > 0) { }
currentlyPlaying = playingNowData.listens[0];
currentlyPlaying.playing_now = true;
}
} catch (error) {
this.logVerbose(
"No currently playing track or failed to fetch:",
error,
);
}
let latestListen: ListenBrainzListen; latestListen = recentListens[0];
this.logVerbose("Using latest completed listen");
}
if (currentlyPlaying) { this.logVerbose("Raw listen data:", latestListen);
latestListen = currentlyPlaying;
this.logVerbose("Using currently playing track");
} else {
// Fetch recent listens
const url = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/listens?count=1`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
}
const data: ListenBrainzResponse = await this.makeRequest(url, { const isNowPlaying = Boolean(latestListen.playing_now);
headers, const trackTimestamp =
});
if (!data.listens || data.listens.length === 0) {
throw new Error("No listens found");
}
latestListen = data.listens[0];
this.logVerbose("Using latest completed listen");
}
this.logVerbose("Raw listen data:", latestListen);
const isNowPlaying = Boolean(latestListen.playing_now);
const trackTimestamp =
latestListen.listened_at || Math.floor(Date.now() / 1000); latestListen.listened_at || Math.floor(Date.now() / 1000);
let duration: number | null = null; let duration: number | null = null;
let endTime: number | null = null; let endTime: number | null = null;
// Extract duration from additional_info if available // Extract duration from additional_info if available
if (latestListen.track_metadata.additional_info?.duration_ms) { if (latestListen.track_metadata.additional_info?.duration_ms) {
duration = Math.floor( duration = Math.floor(
latestListen.track_metadata.additional_info.duration_ms / 1000, latestListen.track_metadata.additional_info.duration_ms / 1000,
); );
if (isNowPlaying && duration > 0) { if (isNowPlaying && duration > 0) {
endTime = trackTimestamp + duration; endTime = trackTimestamp + duration;
} }
} }
// Try to get album art from MusicBrainz or other sources // Try to get album art from MusicBrainz or other sources
const albumArt: string | null = null; const albumArt: string | null = null;
if (latestListen.track_metadata.additional_info?.release_mbid) { if (latestListen.track_metadata.additional_info?.release_mbid) {
try { try {
// We could fetch cover art from Cover Art Archive, but for now we'll leave it null // We could fetch cover art from Cover Art Archive, but for now we'll leave it null
// albumArt = `https://coverartarchive.org/release/${latestListen.track_metadata.additional_info.release_mbid}/front`; // albumArt = `https://coverartarchive.org/release/${latestListen.track_metadata.additional_info.release_mbid}/front`;
} catch (error) { } catch (error) {
this.logVerbose("Failed to fetch album art:", error); this.logVerbose("Failed to fetch album art:", error);
} }
} }
const track: Track = { const track: Track = {
name: latestListen.track_metadata.track_name, name: latestListen.track_metadata.track_name,
artist: latestListen.track_metadata.artist_name, artist: latestListen.track_metadata.artist_name,
album: latestListen.track_metadata.release_name || "", album: latestListen.track_metadata.release_name || "",
albumArt, albumArt,
url: url:
latestListen.track_metadata.additional_info?.origin_url || latestListen.track_metadata.additional_info?.origin_url ||
`https://listenbrainz.org/user/${username}`, `https://listenbrainz.org/user/${username}`,
date: isNowPlaying date: isNowPlaying
? "now" ? "now"
: new Date(trackTimestamp * 1000).toISOString(), : new Date(trackTimestamp * 1000).toISOString(),
nowPlaying: isNowPlaying, nowPlaying: isNowPlaying,
loved: false, // ListenBrainz doesn't have a direct "loved" concept loved: false, // ListenBrainz doesn't have a direct "loved" concept
from: trackTimestamp, from: trackTimestamp,
to: endTime, to: endTime,
duration, duration,
}; };
this.logVerbose("Processed track:", track); this.logVerbose("Processed track:", track);
this.log( this.log(
`${isNowPlaying ? "Now playing" : "Last played"}:`, `${isNowPlaying ? "Now playing" : "Last played"}:`,
`${track.artist} - ${track.name}`, `${track.artist} - ${track.name}`,
); );
return track; return track;
} catch (error) { } catch (error) {
this.logError("Failed to fetch latest listen:", error); this.logError("Failed to fetch latest listen:", error);
throw error; throw error;
}
} }
}
protected getErrorMessage(error: any): string { protected getErrorMessage(error: any): string {
// ListenBrainz uses different error format // ListenBrainz uses different error format
if (error.error) { if (error.error) {
return error.error; return error.error;
}
if (error.message) {
return error.message;
}
return error.toString() || "Unknown error";
} }
if (error.message) {
return error.message;
}
return error.toString() || "Unknown error";
}
} }