(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 {
|
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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue