mirror of
https://github.com/meowarex/TidaLuna-Plugins.git
synced 2026-10-15 07:53:13 +11:00
feat: animated artwork
Animated artwork over the Now Playing artwork tile, behind a new "Animated Artwork" setting.
This commit is contained in:
1 parent
7f06969bef
commit
8ef69f57a2
9 files changed
+595
-3
No files matched your search
@@ -202,6 +202,110 @@ export async function fetchLyrics(
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface AnimatedArtwork {
|
||||
url: string;
|
||||
url_tall: string;
|
||||
}
|
||||
|
||||
function artworkSearchUrl(
|
||||
title: string,
|
||||
artist: string,
|
||||
album?: string,
|
||||
): string {
|
||||
let q = `title=${encodeURIComponent(title)}&artist=${encodeURIComponent(artist)}`;
|
||||
// Omit the album param entirely when absent (a literal "undefined" string makes the endpoint miss)
|
||||
if (album && album.trim() !== "") q += `&album=${encodeURIComponent(album.trim())}`;
|
||||
return `https://ama.trainswift.net/api/v1/artwork/search?${q}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookups keyed by track, including misses (`null`): most tracks have no animated
|
||||
* art, and re-opening the lyrics view or revisiting a track would otherwise hit
|
||||
* the network again for an answer that does not change.
|
||||
*/
|
||||
const animatedArtworkCache = new Map<string, AnimatedArtwork | null>();
|
||||
const ARTWORK_CACHE_MAX = 200;
|
||||
|
||||
export async function fetchAnimatedArtwork(
|
||||
title: string,
|
||||
artist: string,
|
||||
album?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<AnimatedArtwork | null> {
|
||||
// Keyed by album, matching artworkKey() in index.ts: the endpoint returns the
|
||||
// album's artwork, so every track on one album resolves to the same entry.
|
||||
const cacheKey =
|
||||
album && album.trim() !== ""
|
||||
? `${artist}\u0000${album.trim()}`
|
||||
: `${artist}\u0000\u0000${title}`;
|
||||
if (animatedArtworkCache.has(cacheKey)) {
|
||||
return animatedArtworkCache.get(cacheKey) ?? null;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
// Caller-driven cancellation (track changed) on top of the timeout.
|
||||
const onAbort = () => controller.abort();
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
if (signal?.aborted) controller.abort();
|
||||
const tryFetch = async (url: string): Promise<AnimatedArtwork | null> => {
|
||||
try {
|
||||
sylTrace(`Animated Artwork: fetching ${url}`);
|
||||
const res = await fetch(url, { signal: controller.signal });
|
||||
if (!res.ok) {
|
||||
trace.log(`Animated Artwork: fetch failed ${res.status} | ${url}`);
|
||||
return null;
|
||||
}
|
||||
const data = (await res.json()) as {
|
||||
url?: string;
|
||||
url_tall?: string;
|
||||
message?: string;
|
||||
};
|
||||
if (data.url && data.url_tall) {
|
||||
return { url: data.url, url_tall: data.url_tall };
|
||||
}
|
||||
trace.log(`Animated Artwork: none found | ${url}`);
|
||||
return null;
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") {
|
||||
trace.log(`Animated Artwork: request timed out | ${url}`);
|
||||
} else {
|
||||
trace.log(`Animated Artwork: request error | ${url} | ${err}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const remember = (result: AnimatedArtwork | null): AnimatedArtwork | null => {
|
||||
// Never cache a cancelled attempt — it says nothing about the track.
|
||||
if (controller.signal.aborted) return result;
|
||||
if (animatedArtworkCache.size >= ARTWORK_CACHE_MAX) {
|
||||
const oldest = animatedArtworkCache.keys().next().value;
|
||||
if (oldest !== undefined) animatedArtworkCache.delete(oldest);
|
||||
}
|
||||
animatedArtworkCache.set(cacheKey, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
try {
|
||||
// Exact album first; on a miss retry once without the album (server-side disambiguation is imperfect)
|
||||
const withAlbum = await tryFetch(
|
||||
artworkSearchUrl(title, artist, album),
|
||||
);
|
||||
if (withAlbum) return remember(withAlbum);
|
||||
if (album && album.trim() !== "") {
|
||||
const withoutAlbum = await tryFetch(
|
||||
artworkSearchUrl(title, artist),
|
||||
);
|
||||
if (withoutAlbum) return remember(withoutAlbum);
|
||||
}
|
||||
return remember(null);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
}
|
||||
}
|
||||
|
||||
export async function flushLyrics(track: {
|
||||
title: string;
|
||||
artist: string;
|
||||
|
||||
Reference in new issue
Block a user