Monorepo for @ducky.ws's experiments and scripts ducky.ws

lastfm-to-tealfm: various

ducky.ws 0778871e

+199
+145
deno/lastfm-to-tealfm.ts
··· 1 + 2 + import { getCurrentGitHash, getEnvvar } from "./shared/util.ts"; 3 + import { BskyAgent } from 'npm:@atproto/api'; 4 + 5 + interface Scrobble { 6 + artist: { "#text": string }; 7 + name: string; 8 + date: { "#text": string, uts: string }; 9 + } 10 + 11 + interface RecentTracksResponse { 12 + recenttracks: { 13 + track: Scrobble[]; 14 + "@attr": { 15 + page: string; 16 + totalPages: string; 17 + }; 18 + }; 19 + } 20 + 21 + let ATPROTO_DID = getEnvvar("atproto_did"); 22 + let ATPROTO_PDS = getEnvvar("atproto_pds"); 23 + let ATPROTO_PASSWORD = getEnvvar("atproto_password"); 24 + let LASTFM_USERNAME = getEnvvar("lastfm_username"); 25 + let LASTFM_APIKEY = getEnvvar("lastfm_apiKey"); 26 + 27 + // Initialize ATProto agent 28 + const agent = new BskyAgent({ 29 + service: ATPROTO_PDS, 30 + }); 31 + 32 + // Function to process each scrobble and create ATProto record 33 + async function processScrobble(scrobble: Scrobble): Promise<void> { 34 + try { 35 + // Extract and format data 36 + const trackData = { 37 + title: scrobble.name, 38 + artist: scrobble.artist["#text"], 39 + artistMBID: scrobble.artist.mbid || null, 40 + album: scrobble.album["#text"] || null, 41 + albumMBID: scrobble.album.mbid || null, 42 + date: scrobble.date ? new Date(parseInt(scrobble.date.uts) * 1000).toISOString() : null, 43 + url: scrobble.url 44 + }; 45 + 46 + // Login to ATProto (replace with your credentials) 47 + await agent.login({ 48 + identifier: ATPROTO_DID, 49 + password: ATPROTO_PASSWORD 50 + }); 51 + 52 + // Create the record 53 + /*const record = { 54 + $type: 'temp.test.scrobble', 55 + text: `Now playing: ${trackData.title} by ${trackData.artist}${trackData.album ? ` from album: ${trackData.album}` : ''}`, 56 + createdAt: trackData.date || new Date().toISOString(), 57 + metadata: { 58 + title: trackData.title, 59 + artist: trackData.artist, 60 + artistMBID: trackData.artistMBID, 61 + album: trackData.album, 62 + albumMBID: trackData.albumMBID, 63 + url: trackData.url 64 + } 65 + };*/ 66 + 67 + const record = { 68 + $type: 'temp.test.scrobble', 69 + artists: [ 70 + { 71 + artistMbId: trackData.artistMBID, 72 + artistName: trackData.artist 73 + } 74 + ], 75 + originUrl: trackData.url, 76 + trackName: trackData.title, 77 + playedTime: trackData.date || new Date().toISOString(), 78 + releaseMbId: trackData.albumMBID, 79 + releaseName: trackData.album, 80 + musicServiceBaseDomain: "last.fm", 81 + submissionClientAgent: `dpg.lastfm-to-tealfm/${getCurrentGitHash()}` 82 + } 83 + 84 + // Post to the did:web:didd.uk repository 85 + const response = await agent.api.com.atproto.repo.createRecord({ 86 + repo: ATPROTO_DID, 87 + collection: 'temp.test.scrobble', 88 + record: record, 89 + }); 90 + 91 + console.log('Record created:', response.data.uri); 92 + 93 + } catch (error) { 94 + console.error('Error creating ATProto record:', error); 95 + } 96 + } 97 + 98 + async function fetchScrobbles(): Promise<void> { 99 + let page = 1; 100 + let totalPages = 1; 101 + 102 + // First, get the total number of pages 103 + const firstPageUrl = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${LASTFM_USERNAME}&api_key=${LASTFM_APIKEY}&format=json&limit=200&page=1`; 104 + 105 + try { 106 + const response = await fetch(firstPageUrl); 107 + const data: RecentTracksResponse = await response.json(); 108 + totalPages = parseInt(data.recenttracks["@attr"].totalPages); 109 + 110 + // Process pages in reverse order (from oldest to newest) 111 + for (let pageNum = totalPages; pageNum >= 1; pageNum--) { 112 + console.log(`Processing ${pageNum}...`); 113 + 114 + const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${LASTFM_USERNAME}&api_key=${LASTFM_APIKEY}&format=json&limit=200&page=${pageNum}`; 115 + 116 + try { 117 + const response = await fetch(url); 118 + const data: RecentTracksResponse = await response.json(); 119 + 120 + // Reverse the track order within each page to get oldest first 121 + const reversedTracks = [...data.recenttracks.track].reverse(); 122 + 123 + // Process each scrobble in reverse order 124 + if(pageNum < Number(getEnvvar("lastFm_start_from"))) { 125 + for (const scrobble of reversedTracks) { 126 + await processScrobble(scrobble); 127 + // Add delay to avoid rate limiting 128 + await new Promise(resolve => setTimeout(resolve, 1000)); 129 + } 130 + } 131 + 132 + console.log(`Processed page ${pageNum} of ${totalPages}`); 133 + } catch (error) { 134 + console.error(`Error fetching page ${pageNum}:`, error); 135 + break; 136 + } 137 + } 138 + } catch (error) { 139 + console.error('Error fetching first page:', error); 140 + } 141 + 142 + console.log("Finished processing all scrobbles"); 143 + } 144 + 145 + await fetchScrobbles();
+39
deno/shared/util.ts
··· 1 + 2 + export async function getCurrentGitHash(): Promise<string | null> { 3 + try { 4 + const process = Deno.run({ 5 + cmd: ["git", "rev-parse", "HEAD"], 6 + stdout: "piped", 7 + stderr: "piped", 8 + }); 9 + 10 + const output = await process.output(); 11 + const decoder = new TextDecoder(); 12 + const hash = decoder.decode(output).trim(); 13 + 14 + await process.status(); 15 + Deno.close(process.rid); 16 + 17 + return hash; 18 + } catch (error) { 19 + console.error("Error getting git hash:", error); 20 + return null; 21 + } 22 + } 23 + 24 + // Usage 25 + const gitHash = await getCurrentGitHash(); 26 + console.log("Current Git hash:", gitHash); 27 + 28 + export function getEnvvar(value: string, defaultValue: string | undefined = undefined): string | null { 29 + const prefix = "DPG_"; 30 + let envvarValue = Deno.env.get(`${prefix}${value.toUpperCase()}`); 31 + 32 + if(envvarValue !== undefined) 33 + return envvarValue 34 + else 35 + if(defaultValue !== undefined) 36 + return defaultValue 37 + else 38 + return null 39 + }
+15
run.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + _me_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 4 + _platform="$1" 5 + _script="$2" 6 + 7 + function run_deno() { 8 + script_path="$1" 9 + 10 + deno run -A "$script_path" 11 + } 12 + 13 + case "$_platform" in 14 + "deno") run_deno "$_me_dir/deno/$_script.ts" 15 + esac