From 8c4694714409d48ff0c10ff0c28dee17964c8249 Mon Sep 17 00:00:00 2001 From: alina sireneva Date: Sat, 25 Jan 2025 09:35:56 +0300 Subject: [PATCH] chore: lastfm -> listenbrainz --- src/backend/env.ts | 1 - src/backend/service/last-seen/index.ts | 2 +- src/backend/service/last-seen/lastfm.ts | 71 ------------------- src/backend/service/last-seen/listenbrainz.ts | 71 +++++++++++++++++++ 4 files changed, 72 insertions(+), 73 deletions(-) delete mode 100644 src/backend/service/last-seen/lastfm.ts create mode 100644 src/backend/service/last-seen/listenbrainz.ts diff --git a/src/backend/env.ts b/src/backend/env.ts index f988b6b..56450e2 100644 --- a/src/backend/env.ts +++ b/src/backend/env.ts @@ -9,7 +9,6 @@ export const env = zodValidateSync( UMAMI_HOST: z.string().url(), UMAMI_TOKEN: z.string(), UMAMI_SITE_ID: z.string().uuid(), - LASTFM_TOKEN: z.string(), TG_API_ID: z.coerce.number(), TG_API_HASH: z.string(), TG_BOT_TOKEN: z.string(), diff --git a/src/backend/service/last-seen/index.ts b/src/backend/service/last-seen/index.ts index fdc74e7..39944c5 100644 --- a/src/backend/service/last-seen/index.ts +++ b/src/backend/service/last-seen/index.ts @@ -2,7 +2,7 @@ import { assertMatches } from '@fuman/utils' import { bskyLastSeen } from './bsky.ts' import { githubLastSeen } from './github' -import { lastfm } from './lastfm' +import { lastfm } from './listenbrainz.ts' import { shikimoriLastSeen } from './shikimori' import { forgejoLastSeen } from './forgejo.ts' diff --git a/src/backend/service/last-seen/lastfm.ts b/src/backend/service/last-seen/lastfm.ts deleted file mode 100644 index c253d1b..0000000 --- a/src/backend/service/last-seen/lastfm.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { z } from 'zod' -import { AsyncResource } from '@fuman/utils' - -import { zodValidate } from '~/utils/zod' -import { env } from '~/backend/env' -import { ffetch } from '../../utils/fetch.ts' - -import type { LastSeenItem } from './index.ts' - -const LASTFM_TTL = 1000 * 60 * 5 // 5 minutes -const LASTFM_STALE_TTL = 1000 * 60 * 60 // 1 hour -const LASTFM_USERNAME = 'teidesu' -const LASTFM_TOKEN = env.LASTFM_TOKEN - -const LastfmTrack = z.object({ - 'artist': z.object({ 'mbid': z.string(), '#text': z.string() }), - 'name': z.string(), - 'url': z.string(), - 'date': z.object({ uts: z.string() }).optional(), - '@attr': z.object({ - nowplaying: z.literal('true'), - }).partial().optional(), -}) - -const ResponseSchema = z.object({ - recenttracks: z.object({ - 'track': z.array(LastfmTrack), - '@attr': z.object({ - user: z.string(), - totalPages: z.string(), - page: z.string(), - perPage: z.string(), - total: z.string(), - }), - }), -}) - -export const lastfm = new AsyncResource({ - async fetcher({ current }) { - const res = await ffetch('https://ws.audioscrobbler.com/2.0/', { - query: { - method: 'user.getrecenttracks', - user: LASTFM_USERNAME, - api_key: LASTFM_TOKEN, - format: 'json', - limit: '1', - from: current ? Math.floor(current.time / 1000) : undefined, - }, - }).parsedJson(ResponseSchema) - - const track = res.recenttracks.track[0] - if (!track.date && track['@attr']?.nowplaying) { - track.date = { uts: Math.floor(Date.now() / 1000).toString() } - } else if (!track.date) { - throw new Error('no track found') - } - - return { - data: { - source: 'last.fm', - sourceLink: 'https://last.fm/user/teidesu', - time: Number(track.date!.uts) * 1000, - text: `${track.name} – ${track.artist['#text']}`, - link: track.url, - }, - expiresIn: LASTFM_TTL, - } - }, - swr: true, - swrValidator: ({ currentExpiresAt }) => Date.now() - currentExpiresAt < LASTFM_STALE_TTL, -}) diff --git a/src/backend/service/last-seen/listenbrainz.ts b/src/backend/service/last-seen/listenbrainz.ts new file mode 100644 index 0000000..25456ce --- /dev/null +++ b/src/backend/service/last-seen/listenbrainz.ts @@ -0,0 +1,71 @@ +import { z } from 'zod' +import { AsyncResource } from '@fuman/utils' + +import { ffetch } from '../../utils/fetch.ts' + +import type { LastSeenItem } from './index.ts' + +const LB_TTL = 1000 * 60 * 5 // 5 minutes +const LB_STALE_TTL = 1000 * 60 * 60 // 1 hour +const LB_USERNAME = 'teidumb' + +const LbListen = z.object({ + listened_at: z.number(), + track_metadata: z.object({ + artist_name: z.string(), + track_name: z.string(), + additional_info: z.object({ + origin_url: z.string().optional(), + }).optional(), + mbid_mapping: z.object({ + recording_mbid: z.string().optional(), + }).optional(), + }), +}) + +const ResponseSchema = z.object({ + payload: z.object({ + listens: z.array(LbListen), + }), +}) + +export const lastfm = new AsyncResource({ + async fetcher({ current }) { + const res = await ffetch(`https://api.listenbrainz.org/1/user/${LB_USERNAME}/listens`, { + query: { + count: 1, + min_ts: current ? Math.floor(current.time / 1000) : '', + }, + }).parsedJson(ResponseSchema) + + if (!res.payload.listens.length) { + return { + data: current, + expiresIn: 0, + } + } + + const listen = res.payload.listens[0] + + let url: string | undefined + if (listen.track_metadata.mbid_mapping?.recording_mbid) { + url = `https://musicbrainz.org/recording/${listen.track_metadata.mbid_mapping.recording_mbid}` + } else if (listen.track_metadata.additional_info?.origin_url) { + url = listen.track_metadata.additional_info.origin_url + } else { + url = 'https://listenbrainz.org/user/teidumb/' + } + return { + data: { + source: 'listenbrainz', + sourceLink: 'https://listenbrainz.org/user/teidumb/', + time: listen.listened_at * 1000, + text: `${listen.track_metadata.track_name} – ${listen.track_metadata.artist_name}`, + link: url, + }, + expiresIn: LB_TTL, + } + }, + swr: true, + swrValidator: ({ currentExpiresAt }) => Date.now() - currentExpiresAt < LB_STALE_TTL, +})