From 7d679aa1fffe1397bb921c08bf72374a2701a28f Mon Sep 17 00:00:00 2001
From: karitham
Date: Fri, 12 Sep 2025 18:25:18 +0000
Subject: [PATCH] get artist covers working
---
src/App.tsx | 180 ++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 152 insertions(+), 28 deletions(-)
diff --git a/src/App.tsx b/src/App.tsx
index 7badb7e..5bb26b3 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,22 +1,76 @@
-import { type Component, createResource, For, Suspense, Show } from "solid-js";
+import {
+ type Component,
+ createResource,
+ createSignal,
+ For,
+ Show,
+ Suspense,
+} from "solid-js";
+
+export type Settings = {
+ username: string;
+ range: Range;
+};
export default () => {
- const [artistRes] = createResource(() => artists("karitham"));
- const [groupsRes] = createResource(() =>
- release_groups("karitham"),
+ const [settings, setSettings] = createSignal({
+ username: "karitham",
+ range: "all_time",
+ });
+ const [artistRes] = createResource(settings, (set) =>
+ artists(set.username, undefined, set.range),
+ );
+ const [groupsRes] = createResource(
+ settings,
+ (set) => releaseGroups(set.username, undefined, set.range),
);
return (
- Loading...
}>
-
-
-
+
+
+ Your ListenBrainz Stats
+
+
+ {
+ setSettings(() => ({
+ ...settings(),
+ username: e.target.value,
+ }));
+ }}
+ placeholder="Enter username"
+ class="p-4 rounded-lg bg-gray-800 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500 w-full sm:w-auto"
+ />
+
+
+
Loading...}>
+ Top Artists
+
+ Top Albums
+
+
+
);
};
const Artists: Component<{ artists: ArtistStatsArtist[] }> = (props) => {
return (
-
+
{(artist) => }
@@ -30,34 +84,41 @@ const ArtistsItem: Component<{ artist: ArtistStatsArtist }> = (props) => {
return null;
}
- return await getArtistImageURL(props.artist.artist_mbid);
+ return await getWikidataURL(props.artist.artist_mbid).then((s) =>
+ s ? getWikidataThumbnail(s) : null,
+ );
});
return (
-
+
+
}
>
)
-
+
{props.artist.artist_name}
-
{props.artist.listen_count} listens
+
+ {props.artist.listen_count} listens
+
);
};
const ReleaseGroups: Component<{ groups: ReleaseGroupsGroup[] }> = (props) => {
return (
-
+
{(group) => }
@@ -74,27 +135,32 @@ const ReleaseGroupItem: Component<{ group: ReleaseGroupsGroup }> = (props) => {
"release",
props.group.caa_release_mbid,
);
- return result[0]?.image;
+ return result[0]?.image.replace("http://", "https://");
});
return (
-
+
+
}
>
)
-
+
{props.group.release_group_name}
-
{props.group.listen_count} listens
+
+ {props.group.listen_count} listens
+
);
};
@@ -103,7 +169,8 @@ const LB_API_URL = `https://api.listenbrainz.org`;
const COVERARTARCHIVE_URL = "https://coverartarchive.org";
const MB_API_URL = "https://musicbrainz.org";
-export type Range = "this_week" | "this_month" | "this_year" | "all_time";
+const ranges = ["this_week", "this_month", "this_year", "all_time"] as const;
+export type Range = (typeof ranges)[number];
export type ArtistStats = {
artists: ArtistStatsArtist[];
@@ -167,7 +234,7 @@ async function artists(
return getPayload(url);
}
-async function release_groups(
+async function releaseGroups(
user: string,
offset: number = 0,
range: Range = "this_week",
@@ -214,6 +281,10 @@ export interface Thumbnails {
small: string;
}
+const defaultHeaders = {
+ "User-Agent": "listenframe/0.1",
+};
+
async function getReleaseImageURL(
kind: "release" | "release-group",
mbid: string,
@@ -231,7 +302,7 @@ async function getReleaseImageURL(
return (data as { images: Image[] }).images;
}
-async function getArtistImageURL(mbid: string): Promise
{
+async function getWikidataURL(mbid: string): Promise {
const url = new URL(`${MB_API_URL}/ws/2/artist/${mbid}`);
url.searchParams.set("inc", "url-rels");
@@ -246,5 +317,58 @@ async function getArtistImageURL(mbid: string): Promise {
const doc = parser.parseFromString(await response.text(), "text/xml");
- return doc?.querySelector("type=image")?.textContent || null;
+ return doc?.querySelector("[type=wikidata] > target")?.textContent || null;
+}
+
+async function getWikidataThumbnail(
+ wikidataUrl: string,
+): Promise {
+ try {
+ const urlParts = wikidataUrl.split("/");
+ const wikidataId = urlParts[urlParts.length - 1];
+ if (!wikidataId || !wikidataId.startsWith("Q")) {
+ console.error("Invalid Wikidata URL.");
+ return null;
+ }
+
+ const wikidataApiUrl = `https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${wikidataId}&props=claims&format=json&origin=*`;
+ const wikidataResponse = await fetch(wikidataApiUrl, {
+ headers: defaultHeaders,
+ });
+ const wikidataData = await wikidataResponse.json();
+
+ const claims = wikidataData.entities[wikidataId]?.claims;
+ const imageClaim = claims?.P18?.[0]; // P18 is the property ID for 'image'
+
+ if (!imageClaim) {
+ console.log("No image found for this Wikidata item.");
+ return null;
+ }
+
+ const imageFilename = imageClaim.mainsnak.datavalue.value;
+ if (!imageFilename) {
+ console.error("Could not extract image filename.");
+ return null;
+ }
+
+ const commonsApiUrl = `https://commons.wikimedia.org/w/api.php?action=query&titles=File:${imageFilename}&prop=imageinfo&iiprop=url&iiurlwidth=300&format=json&origin=*`;
+ const commonsResponse = await fetch(commonsApiUrl, {
+ headers: defaultHeaders,
+ });
+ const commonsData = await commonsResponse.json();
+
+ const pages = commonsData.query.pages;
+ const pageId = Object.keys(pages)[0];
+ const imageUrl = pages[pageId]?.imageinfo?.[0]?.thumburl;
+
+ if (!imageUrl) {
+ console.error("Could not find image URL.");
+ return null;
+ }
+
+ return imageUrl;
+ } catch (error) {
+ console.error("An error occurred:", error);
+ return null;
+ }
}
--
2.51.2