(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 {
getServiceName(): string {
return "ListenBrainz";
}
getServiceName(): string {
return "ListenBrainz";
}
protected logVerbose(...args: any[]): void {
if (currentSettings.verboseLogging) {
console.log(`[${this.getServiceName()}] Verbose:`, ...args);
protected logVerbose(...args: any[]): void {
if (currentSettings.verboseLogging) {
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> {
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;
// Use a permissive any type for response, then normalize with extractListens
const playingNowRaw: any = await this.makeRequest(playingNowUrl, {
headers,
});
const playingNowListens = extractListens(playingNowRaw);
if (playingNowListens && playingNowListens.length > 0) {
currentlyPlaying = playingNowListens[0];
currentlyPlaying.playing_now = true;
}
}
} catch (error) {
this.logVerbose(
"No currently playing track or failed to fetch:",
error,
);
}
async fetchLatestScrobble(): Promise<Track> {
try {
const username = currentSettings.listenbrainzUsername;
const token = currentSettings.listenbrainzToken;
let latestListen: ListenBrainzListen;
if (!username) {
throw new Error("Username not set for ListenBrainz");
}
if (currentlyPlaying) {
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
let currentlyPlaying: ListenBrainzListen | null = null;
try {
const playingNowUrl = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/playing-now`;
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Token ${token}`;
}
const recentListens = ((): ListenBrainzListen[] | undefined => {
if (!dataRaw) return undefined;
if (Array.isArray(dataRaw.listens)) return dataRaw.listens;
if (dataRaw.payload && Array.isArray(dataRaw.payload.listens))
return dataRaw.payload.listens;
if (dataRaw.data && Array.isArray(dataRaw.data.listens))
return dataRaw.data.listens;
return undefined;
})();
const playingNowData: ListenBrainzPlayingNowResponse =
await this.makeRequest(playingNowUrl, { headers });
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,
);
}
if (!recentListens || recentListens.length === 0) {
throw new Error("No listens found");
}
let latestListen: ListenBrainzListen;
latestListen = recentListens[0];
this.logVerbose("Using latest completed listen");
}
if (currentlyPlaying) {
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("Raw listen data:", latestListen);
const data: ListenBrainzResponse = await this.makeRequest(url, {
headers,
});
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 =
const isNowPlaying = Boolean(latestListen.playing_now);
const trackTimestamp =
latestListen.listened_at || Math.floor(Date.now() / 1000);
let duration: number | null = null;
let endTime: number | null = null;
let duration: number | null = null;
let endTime: number | null = null;
// Extract duration from additional_info if available
if (latestListen.track_metadata.additional_info?.duration_ms) {
duration = Math.floor(
latestListen.track_metadata.additional_info.duration_ms / 1000,
);
if (isNowPlaying && duration > 0) {
endTime = trackTimestamp + duration;
}
}
// Extract duration from additional_info if available
if (latestListen.track_metadata.additional_info?.duration_ms) {
duration = Math.floor(
latestListen.track_metadata.additional_info.duration_ms / 1000,
);
if (isNowPlaying && duration > 0) {
endTime = trackTimestamp + duration;
}
}
// Try to get album art from MusicBrainz or other sources
const albumArt: string | null = null;
if (latestListen.track_metadata.additional_info?.release_mbid) {
try {
// 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`;
} catch (error) {
this.logVerbose("Failed to fetch album art:", error);
}
}
// Try to get album art from MusicBrainz or other sources
const albumArt: string | null = null;
if (latestListen.track_metadata.additional_info?.release_mbid) {
try {
// 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`;
} catch (error) {
this.logVerbose("Failed to fetch album art:", error);
}
}
const track: Track = {
name: latestListen.track_metadata.track_name,
artist: latestListen.track_metadata.artist_name,
album: latestListen.track_metadata.release_name || "",
albumArt,
url:
const track: Track = {
name: latestListen.track_metadata.track_name,
artist: latestListen.track_metadata.artist_name,
album: latestListen.track_metadata.release_name || "",
albumArt,
url:
latestListen.track_metadata.additional_info?.origin_url ||
`https://listenbrainz.org/user/${username}`,
date: isNowPlaying
? "now"
: new Date(trackTimestamp * 1000).toISOString(),
nowPlaying: isNowPlaying,
loved: false, // ListenBrainz doesn't have a direct "loved" concept
from: trackTimestamp,
to: endTime,
duration,
};
date: isNowPlaying
? "now"
: new Date(trackTimestamp * 1000).toISOString(),
nowPlaying: isNowPlaying,
loved: false, // ListenBrainz doesn't have a direct "loved" concept
from: trackTimestamp,
to: endTime,
duration,
};
this.logVerbose("Processed track:", track);
this.log(
`${isNowPlaying ? "Now playing" : "Last played"}:`,
`${track.artist} - ${track.name}`,
);
this.logVerbose("Processed track:", track);
this.log(
`${isNowPlaying ? "Now playing" : "Last played"}:`,
`${track.artist} - ${track.name}`,
);
return track;
} catch (error) {
this.logError("Failed to fetch latest listen:", error);
throw error;
}
return track;
} catch (error) {
this.logError("Failed to fetch latest listen:", error);
throw error;
}
}
protected getErrorMessage(error: any): string {
protected getErrorMessage(error: any): string {
// ListenBrainz uses different error format
if (error.error) {
return error.error;
}
if (error.message) {
return error.message;
}
return error.toString() || "Unknown error";
if (error.error) {
return error.error;
}
if (error.message) {
return error.message;
}
return error.toString() || "Unknown error";
}
}