feat(api/music): initial implementation

This commit is contained in:
mikhail "synzr" 2025-12-03 17:25:57 +05:00
parent 8231c77ead
commit 13fb29389e
5 changed files with 130 additions and 0 deletions

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
LASTFM_API_KEY=
LASTFM_USER=

View file

@ -33,5 +33,9 @@
"typescript-eslint": "^8.47.0",
"vite": "^7.2.2",
"vite-plugin-devtools-json": "^1.0.0"
},
"dependencies": {
"lastfm-ts-api": "^2.2.0",
"node-cache": "^5.1.2"
}
}

26
pnpm-lock.yaml generated
View file

@ -7,6 +7,13 @@ settings:
importers:
.:
dependencies:
lastfm-ts-api:
specifier: ^2.2.0
version: 2.2.0
node-cache:
specifier: ^5.1.2
version: 5.1.2
devDependencies:
'@eslint/compat':
specifier: ^1.4.0
@ -679,6 +686,10 @@ packages:
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
clone@2.1.2:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
engines: {node: '>=0.8'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@ -917,6 +928,9 @@ packages:
known-css-properties@0.37.0:
resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==}
lastfm-ts-api@2.2.0:
resolution: {integrity: sha512-RGceA84ImKtPtFKZaAE9FbTI9hchjxAwi3ZJAr5nlrfVjvSQvpoEFtamrgJY+JYY9PdJusHil8eki00ll4ILZg==}
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@ -1034,6 +1048,10 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-cache@5.1.2:
resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==}
engines: {node: '>= 8.0.0'}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@ -1801,6 +1819,8 @@ snapshots:
dependencies:
clsx: 2.1.1
clone@2.1.2: {}
clsx@2.1.1: {}
color-convert@2.0.1:
@ -2047,6 +2067,8 @@ snapshots:
known-css-properties@0.37.0: {}
lastfm-ts-api@2.2.0: {}
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@ -2133,6 +2155,10 @@ snapshots:
natural-compare@1.4.0: {}
node-cache@5.1.2:
dependencies:
clone: 2.1.2
optionator@0.9.4:
dependencies:
deep-is: 0.1.4

View file

@ -0,0 +1,87 @@
import { LASTFM_API_KEY, LASTFM_USER } from "$env/static/private";
import { LastFMUser } from "lastfm-ts-api";
import NodeCache from "node-cache";
/**
* API cache.
*/
const cache = new NodeCache({
// NOTE: TTL is 1 minute
stdTTL: 60,
});
/**
* Last.fm API client.
*/
const client = new LastFMUser(LASTFM_API_KEY);
/**
* Recent track.
*/
export interface RecentTrack {
/**
* Song title.
*/
title: string;
/**
* Song artist.
*/
artist: string;
/**
* Album title.
*/
album: string;
/**
* Cover URL.
*/
cover: string;
/**
* Song URL.
*/
url: string;
}
async function _getRecentTrack(): Promise<RecentTrack> {
// NOTE: fetch the recent tracks
const {
recenttracks: {
track: tracks,
},
} = await client.getRecentTracks({
user: LASTFM_USER,
});
// NOTE: get the first track and return it
const track = tracks.shift()!;
return {
title: track.name,
artist: track.artist["#text"],
album: track.album["#text"],
// NOTE: the most HQ cover is always last in an array
cover: track.image.pop()!["#text"],
url: track.url,
};
}
/**
* Get the most recent track from a listening history.
* @returns Recent track.
*/
export async function getRecentTrack(): Promise<RecentTrack> {
let result = cache.get<RecentTrack>("recentTrack");
if (result !== undefined) {
return result;
}
result = await _getRecentTrack();
cache.set("recentTrack", result);
return result;
}

View file

@ -0,0 +1,11 @@
import { getRecentTrack } from '$lib/server/clients/last-fm';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
return Response.json(
await getRecentTrack(),
{
headers: { 'cache-control': 'maxage=60' },
},
);
};