From 4feec1795d7addbc3671b7975a26056f391b43ed Mon Sep 17 00:00:00 2001 From: Cory Dransfeldt Date: Tue, 5 Dec 2023 11:48:30 -0800 Subject: [PATCH] chore: listenbrainz post --- ...r-lastfm-listening-data-to-listenbrainz.md | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/posts/2023/programmatically-importing-your-lastfm-listening-data-to-listenbrainz.md diff --git a/src/posts/2023/programmatically-importing-your-lastfm-listening-data-to-listenbrainz.md b/src/posts/2023/programmatically-importing-your-lastfm-listening-data-to-listenbrainz.md new file mode 100644 index 00000000..3ea4ad3a --- /dev/null +++ b/src/posts/2023/programmatically-importing-your-lastfm-listening-data-to-listenbrainz.md @@ -0,0 +1,89 @@ +--- +title: 'Programmatically importing your Last.fm listening data to ListenBrainz' +date: '2023-12-05' +draft: false +tags: +- music +- Eleventy +- development +--- +I love Last.fm, but in the interest of redundancy, Ive started programmatically importing my listening data from Last.fm into ListenBrainz. + +ListenBrainz offers a handy importer to accomplish this task but it's a manual affair that requires you enter your username and trigger the client-side process on their site. + +In my ongoing quest to automate things that don't *really* need to be automated, I went ahead and took a peek at the network traffic on ListenBrainz's import page while the task run. It works by calling Last.fm's API, transforming the data it receives and submitting the listen data to a `submit-listens` endpoint and the timestamp and source of the data to a `latest-import` endpoint. + +To faithfully recreate this process I've implemented a similar set of calls in [Eleventy](https://www.11ty.dev/), fetching the plays from Last.fm and then submitting them to ListenBrainz using the exact same calls their importer uses[^1]. + +```javascript +const EleventyFetch = require('@11ty/eleventy-fetch') +const mbidPatches = require('./json/mbid-patches.json') + +const mbidMap = (artist) => { + return mbidPatches[artist.toLowerCase()] || '' +} + +module.exports = async function () { + const MUSIC_KEY = process.env.API_KEY_LASTFM + const LISTENBRAINZ_TOKEN = process.env.LISTENBRAINZ_TOKEN + const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=coryd_&api_key=${MUSIC_KEY}&format=json&limit=200` + const res = EleventyFetch(url, { + duration: '1h', + type: 'json', + }).catch() + const data = await res + const submission = data['recenttracks']['track'].map((track) => { + let artistMbid = track['artist']['mbid']['mbid'] + + // mbid mismatches + if (mbidMap(track['artist']['#text']) !== '') artistMbid = mbidMap(track['artist']['#text']) + + return { + track_metadata: { + track_name: track['name'], + artist_name: track['artist']['#text'], + release_name: track['album']['#text'], + additional_info: { + submission_client: 'coryd.dev last.fm importer', + lastfm_track_mbid: track['mbid'], + lastfm_release_mbid: track['album']['mbid'], + lastfm_artist_mbid: artistMbid, + }, + }, + listened_at: track['date']['uts'], + } + }) + + await fetch('https://api.listenbrainz.org/1/submit-listens', { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Token ${LISTENBRAINZ_TOKEN}`, + }, + body: JSON.stringify({ + listen_type: 'import', + payload: submission, + }), + }) + + await fetch('https://api.listenbrainz.org/1/latest-import', { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Token ${LISTENBRAINZ_TOKEN}`, + }, + body: JSON.stringify({ + service: 'lastfm', + ts: submission[0]['listened_at'], + }), + }) + + return { + listenbrainz_submission: submission, + } +} +``` + +Now, every time my site is rebuilt, it'll submit my most recent listening data to ListenBrainz, ensuring that it's stored in more than one place. + +[^1]: The "gotcha" here is that you'll need to log in, perform an import, look at the network call and store the token used to authenticate you (e.g. `Authorization: Token `).