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 ( -
+
+
} > {`Thumbnail -

+

{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 ( -
+
+
} > {`Cover -

+

{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; + } }