Remove changelog and changelog UI, require explicit service selection
- Delete changelog.ts and ChangelogView.tsx - Make service selection explicit; default to undefined instead of "lastfm" - Update settings and service logic to handle undefined service - Refactor credential checks to use only service-specific fields - Improve error handling for missing or unknown service types
This commit is contained in:
parent
a5d2b77f40
commit
511d9f8e93
9 changed files with 331 additions and 431 deletions
2
plugins/defs.d.ts
vendored
2
plugins/defs.d.ts
vendored
|
|
@ -38,7 +38,7 @@ export type LFMSettings = {
|
|||
listeningTo: boolean;
|
||||
ignoreSpotify: boolean;
|
||||
verboseLogging: boolean;
|
||||
service: ServiceType;
|
||||
service: ServiceType | undefined;
|
||||
// Service-specific settings
|
||||
librefmUsername?: string;
|
||||
librefmApiKey?: string;
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
interface ChangelogEntry {
|
||||
version: string;
|
||||
date: string;
|
||||
changes: string[];
|
||||
}
|
||||
|
||||
export const changelog: ChangelogEntry[] = [
|
||||
{
|
||||
version: "3.0.0",
|
||||
date: "2025-01-21",
|
||||
changes: [
|
||||
"Complete plugin rewrite with multi-service support",
|
||||
"Added support for Last.fm, Libre.fm, and ListenBrainz",
|
||||
"Replaced toast notifications with comprehensive console logging",
|
||||
"Enhanced timestamp handling with proper start/end times",
|
||||
"Improved error handling and retry logic",
|
||||
"Added service validation and connection testing",
|
||||
"New settings UI with per-service configuration",
|
||||
"Better credential management for multiple services",
|
||||
"Enhanced verbose logging for debugging",
|
||||
"Improved activity updates with service detection",
|
||||
"Added dynamic app name with track variables",
|
||||
"Better album art handling and fallbacks",
|
||||
"Comprehensive status monitoring and reporting",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "2.3.0",
|
||||
date: "2025-09-19",
|
||||
changes: [
|
||||
"1. I TRIED SO HARD 2. remove backend error 3. factor settings tab 4. factor V2 5. factor V3 6. factor V4 7. factor V5 8. factor V6 9. Full settings refactor :3",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "2.2.0",
|
||||
date: "2025-09-13",
|
||||
changes: ["Remember kids! Do not sniff crack!"],
|
||||
},
|
||||
{
|
||||
version: "2.0.0",
|
||||
date: "2025-09-13",
|
||||
changes: ["Fix settings scrolling"],
|
||||
},
|
||||
{
|
||||
version: "1.3.0",
|
||||
date: "2025-09-13",
|
||||
changes: ["fix reanimated"],
|
||||
},
|
||||
{
|
||||
version: "1.2.0",
|
||||
date: "2025-09-13",
|
||||
changes: ["test"],
|
||||
},
|
||||
{
|
||||
version: "1.1.0",
|
||||
date: "2025-09-12",
|
||||
changes: ["fixed reanimated issue??"],
|
||||
},
|
||||
];
|
||||
|
||||
export const currentVersion = "3.0.0";
|
||||
|
|
@ -50,7 +50,7 @@ const Constants = {
|
|||
listeningTo: true,
|
||||
ignoreSpotify: true,
|
||||
verboseLogging: false,
|
||||
service: "lastfm" as ServiceType,
|
||||
service: undefined as ServiceType | undefined,
|
||||
librefmUsername: "",
|
||||
librefmApiKey: "",
|
||||
listenbrainzUsername: "",
|
||||
|
|
|
|||
|
|
@ -74,10 +74,16 @@ async function tryInitialize() {
|
|||
}
|
||||
|
||||
async function validateAndInitialize() {
|
||||
// Check if user has selected a service first
|
||||
if (!currentSettings.service) {
|
||||
log("No service selected. Please configure a service in plugin settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceName = serviceFactory.getCurrentService().getServiceName();
|
||||
|
||||
// Make sure we have credentials for whatever service is selected
|
||||
const service = currentSettings.service || "lastfm";
|
||||
const service = currentSettings.service;
|
||||
let hasCredentials = false;
|
||||
|
||||
switch (service) {
|
||||
|
|
@ -86,14 +92,11 @@ async function validateAndInitialize() {
|
|||
break;
|
||||
case "librefm":
|
||||
hasCredentials = !!(
|
||||
(currentSettings.librefmUsername || currentSettings.username) &&
|
||||
(currentSettings.librefmApiKey || currentSettings.apiKey)
|
||||
currentSettings.librefmUsername && currentSettings.librefmApiKey
|
||||
);
|
||||
break;
|
||||
case "listenbrainz":
|
||||
hasCredentials = !!(
|
||||
currentSettings.listenbrainzUsername || currentSettings.username
|
||||
);
|
||||
hasCredentials = !!currentSettings.listenbrainzUsername;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -128,11 +131,16 @@ export default {
|
|||
log("Plugin loading...");
|
||||
pluginState.pluginStopped = false;
|
||||
|
||||
// Show what we're starting with
|
||||
const serviceName = serviceFactory.getCurrentService().getServiceName();
|
||||
log(
|
||||
`Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`,
|
||||
);
|
||||
// Check if service is configured
|
||||
if (!currentSettings.service) {
|
||||
log("No service configured. Please select a service in plugin settings.");
|
||||
} else {
|
||||
// Show what we're starting with
|
||||
const serviceName = serviceFactory.getCurrentService().getServiceName();
|
||||
log(
|
||||
`Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Add to sidebar if user wants it
|
||||
try {
|
||||
|
|
@ -200,12 +208,16 @@ export default {
|
|||
|
||||
// Switch services if needed
|
||||
if (oldService !== newService && newService) {
|
||||
log(`Service changed from ${oldService} to ${newService}`);
|
||||
log(`Service changed from ${oldService || "none"} to ${newService}`);
|
||||
await switchService(newService);
|
||||
} else if (!pluginState.pluginStopped) {
|
||||
// Just restart with the updated settings
|
||||
} else if (!pluginState.pluginStopped && currentSettings.service) {
|
||||
// Just restart with the updated settings (only if service is selected)
|
||||
log("Restarting with updated settings...");
|
||||
await tryInitialize();
|
||||
} else if (!currentSettings.service) {
|
||||
// Service was unselected, stop the plugin
|
||||
log("Service unselected, stopping plugin...");
|
||||
stop();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -38,131 +38,137 @@ interface LibreFMTrack {
|
|||
}
|
||||
|
||||
export class LibreFmService extends BaseService {
|
||||
getServiceName(): string {
|
||||
return "Libre.fm";
|
||||
}
|
||||
getServiceName(): string {
|
||||
return "Libre.fm";
|
||||
}
|
||||
|
||||
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> {
|
||||
async validateCredentials(): Promise<boolean> {
|
||||
try {
|
||||
const username = currentSettings.librefmUsername;
|
||||
const apiKey = currentSettings.librefmApiKey;
|
||||
|
||||
if (!username || !apiKey) {
|
||||
throw new Error("Username or API key not set for Libre.fm");
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
method: "user.getinfo",
|
||||
user: username,
|
||||
api_key: apiKey,
|
||||
format: "json",
|
||||
});
|
||||
|
||||
const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`;
|
||||
await this.makeRequest(url);
|
||||
|
||||
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.librefmUsername;
|
||||
const apiKey = currentSettings.librefmApiKey;
|
||||
|
||||
if (!username || !apiKey) {
|
||||
throw new Error("Username or API key not set for Libre.fm");
|
||||
}
|
||||
|
||||
this.logVerbose("Fetching latest scrobble for user:", username);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
method: "user.getrecenttracks",
|
||||
user: username,
|
||||
api_key: apiKey,
|
||||
limit: "1",
|
||||
extended: "1",
|
||||
format: "json",
|
||||
});
|
||||
|
||||
const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`;
|
||||
const data: LibreFMResponse = await this.makeRequest(url);
|
||||
|
||||
const lastTrack = data?.recenttracks?.track?.[0];
|
||||
if (!lastTrack) {
|
||||
throw new Error("No tracks found");
|
||||
}
|
||||
|
||||
this.logVerbose("Raw track data:", lastTrack);
|
||||
|
||||
const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying);
|
||||
const trackTimestamp = lastTrack.date?.uts
|
||||
? parseInt(lastTrack.date.uts)
|
||||
: Math.floor(Date.now() / 1000);
|
||||
|
||||
let duration: number | null = null;
|
||||
let endTime: number | null = null;
|
||||
|
||||
// Fetch track duration if it's currently playing
|
||||
if (isNowPlaying) {
|
||||
try {
|
||||
const username = currentSettings.librefmUsername || currentSettings.username;
|
||||
const apiKey = currentSettings.librefmApiKey || currentSettings.apiKey;
|
||||
const trackInfoParams = new URLSearchParams({
|
||||
method: "track.getInfo",
|
||||
track: lastTrack.name,
|
||||
artist: lastTrack.artist.name,
|
||||
api_key: apiKey,
|
||||
format: "json",
|
||||
});
|
||||
|
||||
if (!username || !apiKey) {
|
||||
throw new Error("Username or API key not set for Libre.fm");
|
||||
const trackInfoUrl = `${Constants.SERVICES.librefm.baseUrl}?${trackInfoParams}`;
|
||||
const trackInfo: LibreFMResponse =
|
||||
await this.makeRequest(trackInfoUrl);
|
||||
|
||||
if (trackInfo?.track?.duration) {
|
||||
duration = parseInt(trackInfo.track.duration);
|
||||
if (duration > 0) {
|
||||
// Duration is in milliseconds, convert to seconds
|
||||
duration = Math.floor(duration / 1000);
|
||||
endTime = trackTimestamp + duration;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
method: "user.getinfo",
|
||||
user: username,
|
||||
api_key: apiKey,
|
||||
format: "json",
|
||||
});
|
||||
|
||||
const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`;
|
||||
await this.makeRequest(url);
|
||||
|
||||
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.librefmUsername || currentSettings.username;
|
||||
const apiKey = currentSettings.librefmApiKey || currentSettings.apiKey;
|
||||
|
||||
if (!username || !apiKey) {
|
||||
throw new Error("Username or API key not set for Libre.fm");
|
||||
}
|
||||
|
||||
this.logVerbose("Fetching latest scrobble for user:", username);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
method: "user.getrecenttracks",
|
||||
user: username,
|
||||
api_key: apiKey,
|
||||
limit: "1",
|
||||
extended: "1",
|
||||
format: "json",
|
||||
});
|
||||
|
||||
const url = `${Constants.SERVICES.librefm.baseUrl}?${params}`;
|
||||
const data: LibreFMResponse = await this.makeRequest(url);
|
||||
|
||||
const lastTrack = data?.recenttracks?.track?.[0];
|
||||
if (!lastTrack) {
|
||||
throw new Error("No tracks found");
|
||||
}
|
||||
|
||||
this.logVerbose("Raw track data:", lastTrack);
|
||||
|
||||
const isNowPlaying = Boolean(lastTrack["@attr"]?.nowplaying);
|
||||
const trackTimestamp = lastTrack.date?.uts ? parseInt(lastTrack.date.uts) : Math.floor(Date.now() / 1000);
|
||||
|
||||
let duration: number | null = null;
|
||||
let endTime: number | null = null;
|
||||
|
||||
// Fetch track duration if it's currently playing
|
||||
if (isNowPlaying) {
|
||||
try {
|
||||
const trackInfoParams = new URLSearchParams({
|
||||
method: "track.getInfo",
|
||||
track: lastTrack.name,
|
||||
artist: lastTrack.artist.name,
|
||||
api_key: apiKey,
|
||||
format: "json",
|
||||
});
|
||||
|
||||
const trackInfoUrl = `${Constants.SERVICES.librefm.baseUrl}?${trackInfoParams}`;
|
||||
const trackInfo: LibreFMResponse = await this.makeRequest(trackInfoUrl);
|
||||
|
||||
if (trackInfo?.track?.duration) {
|
||||
duration = parseInt(trackInfo.track.duration);
|
||||
if (duration > 0) {
|
||||
// Duration is in milliseconds, convert to seconds
|
||||
duration = Math.floor(duration / 1000);
|
||||
endTime = trackTimestamp + duration;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logVerbose("Failed to fetch track duration:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const albumArt = this.processAlbumArt(
|
||||
lastTrack.image?.find(img => img.size === "large")?.["#text"]
|
||||
);
|
||||
|
||||
const track: Track = {
|
||||
name: lastTrack.name,
|
||||
artist: lastTrack.artist.name,
|
||||
album: lastTrack.album["#text"],
|
||||
albumArt,
|
||||
url: lastTrack.url,
|
||||
date: lastTrack.date?.["#text"] ?? "now",
|
||||
nowPlaying: isNowPlaying,
|
||||
loved: lastTrack.loved === "1",
|
||||
from: trackTimestamp,
|
||||
to: endTime,
|
||||
duration,
|
||||
};
|
||||
|
||||
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 scrobble:", error);
|
||||
throw error;
|
||||
this.logVerbose("Failed to fetch track duration:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const albumArt = this.processAlbumArt(
|
||||
lastTrack.image?.find((img) => img.size === "large")?.["#text"],
|
||||
);
|
||||
|
||||
const track: Track = {
|
||||
name: lastTrack.name,
|
||||
artist: lastTrack.artist.name,
|
||||
album: lastTrack.album["#text"],
|
||||
albumArt,
|
||||
url: lastTrack.url,
|
||||
date: lastTrack.date?.["#text"] ?? "now",
|
||||
nowPlaying: isNowPlaying,
|
||||
loved: lastTrack.loved === "1",
|
||||
from: trackTimestamp,
|
||||
to: endTime,
|
||||
duration,
|
||||
};
|
||||
|
||||
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 scrobble:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,156 +37,171 @@ 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;
|
||||
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> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
let latestListen: ListenBrainzListen;
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
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 =
|
||||
latestListen.listened_at || Math.floor(Date.now() / 1000);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get album art from MusicBrainz or other sources
|
||||
const albumArt: string | null = null;
|
||||
if (latestListen.track_metadata.additional_info?.release_mbid) {
|
||||
try {
|
||||
const username = currentSettings.listenbrainzUsername || currentSettings.username;
|
||||
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;
|
||||
// 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.logError("Credentials validation failed:", error);
|
||||
return false;
|
||||
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:
|
||||
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,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLatestScrobble(): Promise<Track> {
|
||||
try {
|
||||
const username = currentSettings.listenbrainzUsername || currentSettings.username;
|
||||
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;
|
||||
try {
|
||||
const playingNowUrl = `${Constants.SERVICES.listenbrainz.baseUrl}/user/${encodeURIComponent(username)}/playing-now`;
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) {
|
||||
headers["Authorization"] = `Token ${token}`;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
let latestListen: ListenBrainzListen;
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
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 = latestListen.listened_at || Math.floor(Date.now() / 1000);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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: 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,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,13 @@ export class ServiceFactory {
|
|||
this.serviceInstances = new Map();
|
||||
}
|
||||
|
||||
const type = serviceType || currentSettings.service || "lastfm";
|
||||
const type = serviceType || currentSettings.service;
|
||||
|
||||
if (!type) {
|
||||
throw new Error(
|
||||
"[ServiceFactory] No service type specified and no default service configured",
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.serviceInstances.has(type)) {
|
||||
this.serviceInstances.set(type, this.createService(type));
|
||||
|
|
@ -48,10 +54,9 @@ export class ServiceFactory {
|
|||
case "listenbrainz":
|
||||
return new ListenBrainzService();
|
||||
default:
|
||||
console.warn(
|
||||
`[ServiceFactory] Unknown service type: ${serviceType}, falling back to Last.fm`,
|
||||
throw new Error(
|
||||
`[ServiceFactory] Unknown service type: ${serviceType}`,
|
||||
);
|
||||
return new LastFmService();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
import { clipboard } from "@vendetta/metro/common";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { Forms, General } from "@vendetta/ui/components";
|
||||
import { showToast } from "@vendetta/ui/toasts";
|
||||
import { changelog, currentVersion } from "../changelog";
|
||||
import { React } from "@vendetta/metro/common";
|
||||
import { findByProps } from "@vendetta/metro";
|
||||
|
||||
const { TableRowGroup, TableRow, Stack } = findByProps(
|
||||
"TableSwitchRow",
|
||||
"TableRowGroup",
|
||||
"Stack",
|
||||
"TableRow",
|
||||
);
|
||||
const { ScrollView } = General;
|
||||
|
||||
export default function ChangelogView() {
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 10 }}>
|
||||
<Stack spacing={8}>
|
||||
<TableRowGroup title="CURRENT VERSION">
|
||||
<TableRow
|
||||
label={currentVersion}
|
||||
trailing={
|
||||
<General.TouchableOpacity
|
||||
onPress={() => {
|
||||
clipboard.setString(currentVersion);
|
||||
showToast(
|
||||
"Version copied to clipboard",
|
||||
getAssetIDByName("toast_copy_link"),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<General.Text style={{ color: "#72767d", fontSize: 16 }}>
|
||||
Copy
|
||||
</General.Text>
|
||||
</General.TouchableOpacity>
|
||||
}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
|
||||
<TableRowGroup title="CHANGELOG">
|
||||
{changelog.map((entry, index) => (
|
||||
<Stack
|
||||
key={index}
|
||||
spacing={4}
|
||||
style={{
|
||||
padding: 12,
|
||||
borderBottomWidth: index === changelog.length - 1 ? 0 : 0.5,
|
||||
borderBottomColor: "#292929",
|
||||
}}
|
||||
>
|
||||
<General.View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<General.Text style={{ color: "#fff", fontWeight: "bold" }}>
|
||||
Version {entry.version}
|
||||
</General.Text>
|
||||
<General.Text style={{ color: "#72767d" }}>
|
||||
{entry.date}
|
||||
</General.Text>
|
||||
</General.View>
|
||||
{entry.changes.map((change, changeIndex) => (
|
||||
<General.Text
|
||||
key={changeIndex}
|
||||
style={{ color: "#dcddde", marginLeft: 8, marginTop: 2 }}
|
||||
>
|
||||
• {change}
|
||||
</General.Text>
|
||||
))}
|
||||
</Stack>
|
||||
))}
|
||||
</TableRowGroup>
|
||||
</Stack>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ plugin.storage.listenbrainzUsername ??= "";
|
|||
plugin.storage.listenbrainzToken ??= "";
|
||||
plugin.storage.addToSidebar ??= true;
|
||||
|
||||
const get = (k: string, fallback: any = "") => plugin.storage[k] ?? fallback;
|
||||
const get = (k: string, fallback?: any) => plugin.storage[k] ?? fallback;
|
||||
const set = (k: string, v: any) => (plugin.storage[k] = v);
|
||||
|
||||
// Last.fm Settings Page
|
||||
|
|
@ -145,7 +145,7 @@ function LibreFmSettingsPage() {
|
|||
<TableRowGroup title="Credentials">
|
||||
<Stack spacing={4}>
|
||||
<TextInput
|
||||
placeholder="Libre.fm Username (optional, uses Last.fm if empty)"
|
||||
placeholder="Libre.fm Username"
|
||||
value={get("librefmUsername")}
|
||||
onChange={(v: string) => {
|
||||
set("librefmUsername", v);
|
||||
|
|
@ -154,7 +154,7 @@ function LibreFmSettingsPage() {
|
|||
isClearable
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Libre.fm API Key (optional, uses Last.fm if empty)"
|
||||
placeholder="Libre.fm API Key"
|
||||
value={get("librefmApiKey")}
|
||||
onChange={(v: string) => {
|
||||
set("librefmApiKey", v);
|
||||
|
|
@ -422,7 +422,7 @@ export default function Settings() {
|
|||
const navigation = NavigationNative.useNavigation();
|
||||
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
||||
|
||||
const currentService = get("service", "lastfm") as ServiceType;
|
||||
const currentService = get("service") as ServiceType;
|
||||
|
||||
const getCredentialStatus = (service: ServiceType) => {
|
||||
switch (service) {
|
||||
|
|
@ -432,15 +432,14 @@ export default function Settings() {
|
|||
: "❌ Missing credentials";
|
||||
}
|
||||
case "librefm": {
|
||||
const librefmUser = get("librefmUsername") || get("username");
|
||||
const librefmKey = get("librefmApiKey") || get("apiKey");
|
||||
return librefmUser && librefmKey
|
||||
return get("librefmUsername") && get("librefmApiKey")
|
||||
? "✅ Configured"
|
||||
: "❌ Missing credentials";
|
||||
}
|
||||
case "listenbrainz": {
|
||||
const lbUser = get("listenbrainzUsername") || get("username");
|
||||
return lbUser ? "✅ Configured" : "❌ Missing username";
|
||||
return get("listenbrainzUsername")
|
||||
? "✅ Configured"
|
||||
: "❌ Missing username";
|
||||
}
|
||||
default:
|
||||
return "❓ Unknown";
|
||||
|
|
@ -454,7 +453,11 @@ export default function Settings() {
|
|||
<TableRowGroup title="Active Service">
|
||||
<TableRow
|
||||
label="Current Service"
|
||||
subLabel={`Using: ${serviceFactory.getServiceDisplayName(currentService)}`}
|
||||
subLabel={
|
||||
currentService
|
||||
? `Using: ${serviceFactory.getServiceDisplayName(currentService)}`
|
||||
: "No service selected"
|
||||
}
|
||||
/>
|
||||
{(["lastfm", "librefm", "listenbrainz"] as ServiceType[]).map(
|
||||
(service) => (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue