(hopefully) fix listenbrainz
so many changes because zed thought why not use formatting
This commit is contained in:
parent
faebdafc41
commit
a5686e815c
1 changed files with 169 additions and 142 deletions
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue