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