@rocksky/cli 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/dist/drizzle/0001_awesome_gabe_jones.sql +1 -0
- package/dist/drizzle/meta/0001_snapshot.json +1569 -0
- package/dist/drizzle/meta/_journal.json +7 -0
- package/dist/index.js +316 -96
- package/drizzle/0001_awesome_gabe_jones.sql +1 -0
- package/drizzle/meta/0001_snapshot.json +1569 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +5 -1
- package/preview.png +0 -0
- package/src/cmd/scrobble-api.ts +8 -0
- package/src/cmd/sync.ts +356 -86
- package/src/schema/tracks.ts +40 -32
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import { IdResolver } from '@atproto/identity';
|
|
|
22
22
|
import { configure, getConsoleSink, getLogger } from '@logtape/logtape';
|
|
23
23
|
import { AtpAgent } from '@atproto/api';
|
|
24
24
|
import { sql, eq, and, or, gte, lte } from 'drizzle-orm';
|
|
25
|
-
import { sqliteTable, integer, text, unique } from 'drizzle-orm/sqlite-core';
|
|
25
|
+
import { sqliteTable, integer, text, index, unique } from 'drizzle-orm/sqlite-core';
|
|
26
26
|
import dotenv from 'dotenv';
|
|
27
27
|
import { cleanEnv, str } from 'envalid';
|
|
28
28
|
import crypto from 'node:crypto';
|
|
@@ -33,6 +33,8 @@ import { Lexicons } from '@atproto/lexicon';
|
|
|
33
33
|
import { TID } from '@atproto/common';
|
|
34
34
|
import { createId } from '@paralleldrive/cuid2';
|
|
35
35
|
import _ from 'lodash';
|
|
36
|
+
import { CarReader } from '@ipld/car';
|
|
37
|
+
import * as cbor from '@ipld/dag-cbor';
|
|
36
38
|
import { table, getBorderCharacters } from 'table';
|
|
37
39
|
import { Command } from 'commander';
|
|
38
40
|
import axios from 'axios';
|
|
@@ -1369,33 +1371,41 @@ const albums = sqliteTable("albums", {
|
|
|
1369
1371
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`)
|
|
1370
1372
|
});
|
|
1371
1373
|
|
|
1372
|
-
const tracks$1 = sqliteTable(
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
})
|
|
1374
|
+
const tracks$1 = sqliteTable(
|
|
1375
|
+
"tracks",
|
|
1376
|
+
{
|
|
1377
|
+
id: text("id").primaryKey().notNull(),
|
|
1378
|
+
title: text("title").notNull(),
|
|
1379
|
+
artist: text("artist").notNull(),
|
|
1380
|
+
albumArtist: text("album_artist").notNull(),
|
|
1381
|
+
albumArt: text("album_art"),
|
|
1382
|
+
album: text("album").notNull(),
|
|
1383
|
+
trackNumber: integer("track_number"),
|
|
1384
|
+
duration: integer("duration").notNull(),
|
|
1385
|
+
mbId: text("mb_id").unique(),
|
|
1386
|
+
youtubeLink: text("youtube_link").unique(),
|
|
1387
|
+
spotifyLink: text("spotify_link").unique(),
|
|
1388
|
+
appleMusicLink: text("apple_music_link").unique(),
|
|
1389
|
+
tidalLink: text("tidal_link").unique(),
|
|
1390
|
+
discNumber: integer("disc_number"),
|
|
1391
|
+
lyrics: text("lyrics"),
|
|
1392
|
+
composer: text("composer"),
|
|
1393
|
+
genre: text("genre"),
|
|
1394
|
+
label: text("label"),
|
|
1395
|
+
copyrightMessage: text("copyright_message"),
|
|
1396
|
+
uri: text("uri").unique(),
|
|
1397
|
+
cid: text("cid").unique().notNull(),
|
|
1398
|
+
albumUri: text("album_uri"),
|
|
1399
|
+
artistUri: text("artist_uri"),
|
|
1400
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`),
|
|
1401
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(sql`(unixepoch())`)
|
|
1402
|
+
},
|
|
1403
|
+
(t) => ({
|
|
1404
|
+
idx_title_artist_album_albumartist: index(
|
|
1405
|
+
"idx_title_artist_album_albumartist"
|
|
1406
|
+
).on(t.title, t.artist, t.album, t.albumArtist)
|
|
1407
|
+
})
|
|
1408
|
+
);
|
|
1399
1409
|
|
|
1400
1410
|
const albumTracks = sqliteTable(
|
|
1401
1411
|
"album_tracks",
|
|
@@ -7566,7 +7576,6 @@ function cleanUpSyncLockOnExit(did) {
|
|
|
7566
7576
|
});
|
|
7567
7577
|
}
|
|
7568
7578
|
|
|
7569
|
-
const PAGE_SIZE = 100;
|
|
7570
7579
|
async function sync() {
|
|
7571
7580
|
const [did, handle] = await getDidAndHandle();
|
|
7572
7581
|
const agent = await createAgent(did, handle);
|
|
@@ -7574,11 +7583,12 @@ async function sync() {
|
|
|
7574
7583
|
await subscribeToJetstream(user);
|
|
7575
7584
|
logger.info` DID: ${did}`;
|
|
7576
7585
|
logger.info` Handle: ${handle}`;
|
|
7586
|
+
const carReader = await downloadCarFile(agent);
|
|
7577
7587
|
const [artists, albums, songs, scrobbles] = await Promise.all([
|
|
7578
|
-
getRockskyUserArtists(agent),
|
|
7579
|
-
getRockskyUserAlbums(agent),
|
|
7580
|
-
getRockskyUserSongs(agent),
|
|
7581
|
-
getRockskyUserScrobbles(agent)
|
|
7588
|
+
getRockskyUserArtists(agent, carReader),
|
|
7589
|
+
getRockskyUserAlbums(agent, carReader),
|
|
7590
|
+
getRockskyUserSongs(agent, carReader),
|
|
7591
|
+
getRockskyUserScrobbles(agent, carReader)
|
|
7582
7592
|
]);
|
|
7583
7593
|
logger.info` Artists: ${artists.length}`;
|
|
7584
7594
|
logger.info` Albums: ${albums.length}`;
|
|
@@ -7636,7 +7646,7 @@ const createArtists = async (artists, user) => {
|
|
|
7636
7646
|
id: createId(),
|
|
7637
7647
|
name: tag
|
|
7638
7648
|
}));
|
|
7639
|
-
const BATCH_SIZE =
|
|
7649
|
+
const BATCH_SIZE = 1e3;
|
|
7640
7650
|
for (let i = 0; i < uniqueTags.length; i += BATCH_SIZE) {
|
|
7641
7651
|
const batch = uniqueTags.slice(i, i + BATCH_SIZE);
|
|
7642
7652
|
await ctx.db.insert(schema.genres).values(batch).onConflictDoNothing({
|
|
@@ -7699,7 +7709,7 @@ const createAlbums = async (albums, user) => {
|
|
|
7699
7709
|
)
|
|
7700
7710
|
);
|
|
7701
7711
|
const validAlbumData = albums.map((album, index) => ({ album, artist: artists[index] })).filter(({ artist }) => artist);
|
|
7702
|
-
const BATCH_SIZE =
|
|
7712
|
+
const BATCH_SIZE = 1e3;
|
|
7703
7713
|
let totalAlbumsImported = 0;
|
|
7704
7714
|
for (let i = 0; i < validAlbumData.length; i += BATCH_SIZE) {
|
|
7705
7715
|
const batch = validAlbumData.slice(i, i + BATCH_SIZE);
|
|
@@ -7759,7 +7769,7 @@ const createSongs = async (songs, user) => {
|
|
|
7759
7769
|
artist: artists[index],
|
|
7760
7770
|
album: albums[index]
|
|
7761
7771
|
})).filter(({ artist, album }) => artist && album);
|
|
7762
|
-
const BATCH_SIZE =
|
|
7772
|
+
const BATCH_SIZE = 1e3;
|
|
7763
7773
|
let totalTracksImported = 0;
|
|
7764
7774
|
for (let i = 0; i < validSongData.length; i += BATCH_SIZE) {
|
|
7765
7775
|
const batch = validSongData.slice(i, i + BATCH_SIZE);
|
|
@@ -7862,7 +7872,7 @@ const createScrobbles = async (scrobbles, user) => {
|
|
|
7862
7872
|
album: albums[index],
|
|
7863
7873
|
artist: artists[index]
|
|
7864
7874
|
})).filter(({ track, album, artist }) => track && album && artist);
|
|
7865
|
-
const BATCH_SIZE =
|
|
7875
|
+
const BATCH_SIZE = 1e3;
|
|
7866
7876
|
let totalScrobblesImported = 0;
|
|
7867
7877
|
for (let i = 0; i < validScrobbleData.length; i += BATCH_SIZE) {
|
|
7868
7878
|
const batch = validScrobbleData.slice(i, i + BATCH_SIZE);
|
|
@@ -7951,7 +7961,7 @@ const subscribeToJetstream = (user) => {
|
|
|
7951
7961
|
const onNewCollection = async (record, cid, uri, user) => {
|
|
7952
7962
|
switch (record.$type) {
|
|
7953
7963
|
case "app.rocksky.song":
|
|
7954
|
-
await onNewSong(record);
|
|
7964
|
+
await onNewSong(record, cid, uri, user);
|
|
7955
7965
|
break;
|
|
7956
7966
|
case "app.rocksky.album":
|
|
7957
7967
|
await onNewAlbum(record, cid, uri, user);
|
|
@@ -7969,6 +7979,16 @@ const onNewCollection = async (record, cid, uri, user) => {
|
|
|
7969
7979
|
const onNewSong = async (record, cid, uri, user) => {
|
|
7970
7980
|
const { title, artist, album } = record;
|
|
7971
7981
|
logger.info` New song: ${title} by ${artist} from ${album}`;
|
|
7982
|
+
await createSongs(
|
|
7983
|
+
[
|
|
7984
|
+
{
|
|
7985
|
+
cid,
|
|
7986
|
+
uri,
|
|
7987
|
+
value: record
|
|
7988
|
+
}
|
|
7989
|
+
],
|
|
7990
|
+
user
|
|
7991
|
+
);
|
|
7972
7992
|
};
|
|
7973
7993
|
const onNewAlbum = async (record, cid, uri, user) => {
|
|
7974
7994
|
const { title, artist } = record;
|
|
@@ -7999,8 +8019,140 @@ const onNewArtist = async (record, cid, uri, user) => {
|
|
|
7999
8019
|
);
|
|
8000
8020
|
};
|
|
8001
8021
|
const onNewScrobble = async (record, cid, uri, user) => {
|
|
8002
|
-
const { title, createdAt } = record;
|
|
8022
|
+
const { title, createdAt, artist, album, albumArtist } = record;
|
|
8003
8023
|
logger.info` New scrobble: ${title} at ${createdAt}`;
|
|
8024
|
+
let [artistRecord] = await ctx.db.select().from(schema.artists).where(eq(schema.artists.name, record.albumArtist)).execute();
|
|
8025
|
+
if (!artistRecord) {
|
|
8026
|
+
logger.info` ⚙️ Artist not found, creating: "${albumArtist}"`;
|
|
8027
|
+
const artistUri = `at://${user.did}/app.rocksky.artist/${createId()}`;
|
|
8028
|
+
const artistCid = createId();
|
|
8029
|
+
await createArtists(
|
|
8030
|
+
[
|
|
8031
|
+
{
|
|
8032
|
+
cid: artistCid,
|
|
8033
|
+
uri: artistUri,
|
|
8034
|
+
value: {
|
|
8035
|
+
$type: "app.rocksky.artist",
|
|
8036
|
+
name: record.albumArtist,
|
|
8037
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8038
|
+
tags: record.tags || []
|
|
8039
|
+
}
|
|
8040
|
+
}
|
|
8041
|
+
],
|
|
8042
|
+
user
|
|
8043
|
+
);
|
|
8044
|
+
[artistRecord] = await ctx.db.select().from(schema.artists).where(eq(schema.artists.name, record.albumArtist)).execute();
|
|
8045
|
+
if (!artistRecord) {
|
|
8046
|
+
logger.error` ❌ Failed to create artist. Skipping scrobble.`;
|
|
8047
|
+
return;
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
let [albumRecord] = await ctx.db.select().from(schema.albums).where(
|
|
8051
|
+
and(
|
|
8052
|
+
eq(schema.albums.title, record.album),
|
|
8053
|
+
eq(schema.albums.artist, record.albumArtist)
|
|
8054
|
+
)
|
|
8055
|
+
).execute();
|
|
8056
|
+
if (!albumRecord) {
|
|
8057
|
+
logger.info` ⚙️ Album not found, creating: "${album}" by ${albumArtist}`;
|
|
8058
|
+
const albumUri = `at://${user.did}/app.rocksky.album/${createId()}`;
|
|
8059
|
+
const albumCid = createId();
|
|
8060
|
+
await createAlbums(
|
|
8061
|
+
[
|
|
8062
|
+
{
|
|
8063
|
+
cid: albumCid,
|
|
8064
|
+
uri: albumUri,
|
|
8065
|
+
value: {
|
|
8066
|
+
$type: "app.rocksky.album",
|
|
8067
|
+
title: record.album,
|
|
8068
|
+
artist: record.albumArtist,
|
|
8069
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8070
|
+
releaseDate: record.releaseDate,
|
|
8071
|
+
year: record.year,
|
|
8072
|
+
albumArt: record.albumArt,
|
|
8073
|
+
artistUri: artistRecord.uri,
|
|
8074
|
+
spotifyLink: record.spotifyLink,
|
|
8075
|
+
appleMusicLink: record.appleMusicLink,
|
|
8076
|
+
tidalLink: record.tidalLink,
|
|
8077
|
+
youtubeLink: record.youtubeLink
|
|
8078
|
+
}
|
|
8079
|
+
}
|
|
8080
|
+
],
|
|
8081
|
+
user
|
|
8082
|
+
);
|
|
8083
|
+
[albumRecord] = await ctx.db.select().from(schema.albums).where(
|
|
8084
|
+
and(
|
|
8085
|
+
eq(schema.albums.title, record.album),
|
|
8086
|
+
eq(schema.albums.artist, record.albumArtist)
|
|
8087
|
+
)
|
|
8088
|
+
).execute();
|
|
8089
|
+
if (!albumRecord) {
|
|
8090
|
+
logger.error` ❌ Failed to create album. Skipping scrobble.`;
|
|
8091
|
+
return;
|
|
8092
|
+
}
|
|
8093
|
+
}
|
|
8094
|
+
let [track] = await ctx.db.select().from(schema.tracks).where(
|
|
8095
|
+
and(
|
|
8096
|
+
eq(schema.tracks.title, record.title),
|
|
8097
|
+
eq(schema.tracks.artist, record.artist),
|
|
8098
|
+
eq(schema.tracks.album, record.album),
|
|
8099
|
+
eq(schema.tracks.albumArtist, record.albumArtist)
|
|
8100
|
+
)
|
|
8101
|
+
).execute();
|
|
8102
|
+
if (!track) {
|
|
8103
|
+
logger.info` ⚙️ Track not found, creating: "${title}" by ${artist} from ${album}`;
|
|
8104
|
+
const trackUri = `at://${user.did}/app.rocksky.song/${createId()}`;
|
|
8105
|
+
const trackCid = createId();
|
|
8106
|
+
await createSongs(
|
|
8107
|
+
[
|
|
8108
|
+
{
|
|
8109
|
+
cid: trackCid,
|
|
8110
|
+
uri: trackUri,
|
|
8111
|
+
value: {
|
|
8112
|
+
$type: "app.rocksky.song",
|
|
8113
|
+
title: record.title,
|
|
8114
|
+
artist: record.artist,
|
|
8115
|
+
albumArtist: record.albumArtist,
|
|
8116
|
+
album: record.album,
|
|
8117
|
+
duration: record.duration,
|
|
8118
|
+
trackNumber: record.trackNumber,
|
|
8119
|
+
discNumber: record.discNumber,
|
|
8120
|
+
releaseDate: record.releaseDate,
|
|
8121
|
+
year: record.year,
|
|
8122
|
+
genre: record.genre,
|
|
8123
|
+
tags: record.tags,
|
|
8124
|
+
composer: record.composer,
|
|
8125
|
+
lyrics: record.lyrics,
|
|
8126
|
+
copyrightMessage: record.copyrightMessage,
|
|
8127
|
+
albumArt: record.albumArt,
|
|
8128
|
+
youtubeLink: record.youtubeLink,
|
|
8129
|
+
spotifyLink: record.spotifyLink,
|
|
8130
|
+
tidalLink: record.tidalLink,
|
|
8131
|
+
appleMusicLink: record.appleMusicLink,
|
|
8132
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8133
|
+
mbId: record.mbid,
|
|
8134
|
+
label: record.label,
|
|
8135
|
+
albumUri: albumRecord.uri,
|
|
8136
|
+
artistUri: artistRecord.uri
|
|
8137
|
+
}
|
|
8138
|
+
}
|
|
8139
|
+
],
|
|
8140
|
+
user
|
|
8141
|
+
);
|
|
8142
|
+
[track] = await ctx.db.select().from(schema.tracks).where(
|
|
8143
|
+
and(
|
|
8144
|
+
eq(schema.tracks.title, record.title),
|
|
8145
|
+
eq(schema.tracks.artist, record.artist),
|
|
8146
|
+
eq(schema.tracks.album, record.album),
|
|
8147
|
+
eq(schema.tracks.albumArtist, record.albumArtist)
|
|
8148
|
+
)
|
|
8149
|
+
).execute();
|
|
8150
|
+
if (!track) {
|
|
8151
|
+
logger.error` ❌ Failed to create track. Skipping scrobble.`;
|
|
8152
|
+
return;
|
|
8153
|
+
}
|
|
8154
|
+
}
|
|
8155
|
+
logger.info` ✓ All required entities ready. Creating scrobble...`;
|
|
8004
8156
|
await createScrobbles(
|
|
8005
8157
|
[
|
|
8006
8158
|
{
|
|
@@ -8012,80 +8164,141 @@ const onNewScrobble = async (record, cid, uri, user) => {
|
|
|
8012
8164
|
user
|
|
8013
8165
|
);
|
|
8014
8166
|
};
|
|
8015
|
-
const
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
const
|
|
8026
|
-
|
|
8027
|
-
|
|
8167
|
+
const downloadCarFile = async (agent) => {
|
|
8168
|
+
logger.info(`Fetching repository CAR file ...`);
|
|
8169
|
+
const repoRes = await agent.com.atproto.sync.getRepo({
|
|
8170
|
+
did: agent.assertDid
|
|
8171
|
+
});
|
|
8172
|
+
return CarReader.fromBytes(new Uint8Array(repoRes.data));
|
|
8173
|
+
};
|
|
8174
|
+
const getRockskyUserSongs = async (agent, carReader) => {
|
|
8175
|
+
const results = [];
|
|
8176
|
+
try {
|
|
8177
|
+
const collection = "app.rocksky.song";
|
|
8178
|
+
for await (const { cid, bytes } of carReader.blocks()) {
|
|
8179
|
+
try {
|
|
8180
|
+
const decoded = cbor.decode(bytes);
|
|
8181
|
+
if (decoded && typeof decoded === "object" && "$type" in decoded) {
|
|
8182
|
+
if (decoded.$type === collection) {
|
|
8183
|
+
const value = decoded;
|
|
8184
|
+
const uri = `at://${agent.assertDid}/${collection}/${cid.toString()}`;
|
|
8185
|
+
results.push({
|
|
8186
|
+
value,
|
|
8187
|
+
uri,
|
|
8188
|
+
cid: cid.toString()
|
|
8189
|
+
});
|
|
8190
|
+
}
|
|
8191
|
+
}
|
|
8192
|
+
} catch (e) {
|
|
8193
|
+
logger.warn` Skipping block with CID ${cid.toString()} due to decode error: ${e}`;
|
|
8194
|
+
continue;
|
|
8195
|
+
}
|
|
8196
|
+
}
|
|
8028
8197
|
logger.info(
|
|
8029
8198
|
`${chalk.cyanBright(agent.assertDid)} ${chalk.greenBright(results.length)} songs`
|
|
8030
8199
|
);
|
|
8031
|
-
}
|
|
8200
|
+
} catch (error) {
|
|
8201
|
+
logger.error(`Error fetching songs from CAR: ${error}`);
|
|
8202
|
+
throw error;
|
|
8203
|
+
}
|
|
8032
8204
|
return results;
|
|
8033
8205
|
};
|
|
8034
|
-
const getRockskyUserAlbums = async (agent) => {
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8206
|
+
const getRockskyUserAlbums = async (agent, carReader) => {
|
|
8207
|
+
const results = [];
|
|
8208
|
+
try {
|
|
8209
|
+
const collection = "app.rocksky.album";
|
|
8210
|
+
logger.info`Extracting ${collection} records from CAR file ...`;
|
|
8211
|
+
for await (const { cid, bytes } of carReader.blocks()) {
|
|
8212
|
+
try {
|
|
8213
|
+
const decoded = cbor.decode(bytes);
|
|
8214
|
+
if (decoded && typeof decoded === "object" && "$type" in decoded) {
|
|
8215
|
+
if (decoded.$type === collection) {
|
|
8216
|
+
const value = decoded;
|
|
8217
|
+
const uri = `at://${agent.assertDid}/${collection}/${cid.toString()}`;
|
|
8218
|
+
results.push({
|
|
8219
|
+
value,
|
|
8220
|
+
uri,
|
|
8221
|
+
cid: cid.toString()
|
|
8222
|
+
});
|
|
8223
|
+
}
|
|
8224
|
+
}
|
|
8225
|
+
} catch (e) {
|
|
8226
|
+
logger.warn` Skipping block with CID ${cid.toString()} due to decode error: ${e}`;
|
|
8227
|
+
continue;
|
|
8228
|
+
}
|
|
8229
|
+
}
|
|
8047
8230
|
logger.info(
|
|
8048
8231
|
`${chalk.cyanBright(agent.assertDid)} ${chalk.greenBright(results.length)} albums`
|
|
8049
8232
|
);
|
|
8050
|
-
}
|
|
8233
|
+
} catch (error) {
|
|
8234
|
+
logger.error(`Error fetching albums from CAR: ${error}`);
|
|
8235
|
+
throw error;
|
|
8236
|
+
}
|
|
8051
8237
|
return results;
|
|
8052
8238
|
};
|
|
8053
|
-
const getRockskyUserArtists = async (agent) => {
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8239
|
+
const getRockskyUserArtists = async (agent, carReader) => {
|
|
8240
|
+
const results = [];
|
|
8241
|
+
try {
|
|
8242
|
+
const collection = "app.rocksky.artist";
|
|
8243
|
+
logger.info`Extracting ${collection} records from CAR file ...`;
|
|
8244
|
+
for await (const { cid, bytes } of carReader.blocks()) {
|
|
8245
|
+
try {
|
|
8246
|
+
const decoded = cbor.decode(bytes);
|
|
8247
|
+
if (decoded && typeof decoded === "object" && "$type" in decoded) {
|
|
8248
|
+
if (decoded.$type === collection) {
|
|
8249
|
+
const value = decoded;
|
|
8250
|
+
const uri = `at://${agent.assertDid}/${collection}/${cid.toString()}`;
|
|
8251
|
+
results.push({
|
|
8252
|
+
value,
|
|
8253
|
+
uri,
|
|
8254
|
+
cid: cid.toString()
|
|
8255
|
+
});
|
|
8256
|
+
}
|
|
8257
|
+
}
|
|
8258
|
+
} catch (e) {
|
|
8259
|
+
continue;
|
|
8260
|
+
}
|
|
8261
|
+
}
|
|
8066
8262
|
logger.info(
|
|
8067
8263
|
`${chalk.cyanBright(agent.assertDid)} ${chalk.greenBright(results.length)} artists`
|
|
8068
8264
|
);
|
|
8069
|
-
}
|
|
8265
|
+
} catch (error) {
|
|
8266
|
+
logger.error(`Error fetching artists from CAR: ${error}`);
|
|
8267
|
+
throw error;
|
|
8268
|
+
}
|
|
8070
8269
|
return results;
|
|
8071
8270
|
};
|
|
8072
|
-
const getRockskyUserScrobbles = async (agent) => {
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8271
|
+
const getRockskyUserScrobbles = async (agent, carReader) => {
|
|
8272
|
+
const results = [];
|
|
8273
|
+
try {
|
|
8274
|
+
const collection = "app.rocksky.scrobble";
|
|
8275
|
+
logger.info`Extracting ${collection} records from CAR file ...`;
|
|
8276
|
+
for await (const { cid, bytes } of carReader.blocks()) {
|
|
8277
|
+
try {
|
|
8278
|
+
const decoded = cbor.decode(bytes);
|
|
8279
|
+
if (decoded && typeof decoded === "object" && "$type" in decoded) {
|
|
8280
|
+
if (decoded.$type === collection) {
|
|
8281
|
+
const value = decoded;
|
|
8282
|
+
const uri = `at://${agent.assertDid}/${collection}/${cid.toString()}`;
|
|
8283
|
+
results.push({
|
|
8284
|
+
value,
|
|
8285
|
+
uri,
|
|
8286
|
+
cid: cid.toString()
|
|
8287
|
+
});
|
|
8288
|
+
}
|
|
8289
|
+
}
|
|
8290
|
+
} catch (e) {
|
|
8291
|
+
logger.warn` Skipping block with CID ${cid.toString()} due to decode error: ${e}`;
|
|
8292
|
+
continue;
|
|
8293
|
+
}
|
|
8294
|
+
}
|
|
8085
8295
|
logger.info(
|
|
8086
8296
|
`${chalk.cyanBright(agent.assertDid)} ${chalk.greenBright(results.length)} scrobbles`
|
|
8087
8297
|
);
|
|
8088
|
-
}
|
|
8298
|
+
} catch (error) {
|
|
8299
|
+
logger.error(`Error fetching scrobbles from CAR: ${error}`);
|
|
8300
|
+
throw error;
|
|
8301
|
+
}
|
|
8089
8302
|
return results;
|
|
8090
8303
|
};
|
|
8091
8304
|
|
|
@@ -8571,7 +8784,7 @@ async function whoami() {
|
|
|
8571
8784
|
}
|
|
8572
8785
|
}
|
|
8573
8786
|
|
|
8574
|
-
var version = "0.3.
|
|
8787
|
+
var version = "0.3.2";
|
|
8575
8788
|
|
|
8576
8789
|
async function login(handle) {
|
|
8577
8790
|
const app = express();
|
|
@@ -9201,6 +9414,13 @@ Welcome to the lastfm compatibility API
|
|
|
9201
9414
|
return c.text("Scrobble received");
|
|
9202
9415
|
});
|
|
9203
9416
|
logger.info`lastfm/listenbrainz/webscrobbler scrobble API listening on ${"http://localhost:" + port}`;
|
|
9417
|
+
new Promise(async () => {
|
|
9418
|
+
try {
|
|
9419
|
+
await sync();
|
|
9420
|
+
} catch (err) {
|
|
9421
|
+
logger.warn`Error during initial sync: ${err}`;
|
|
9422
|
+
}
|
|
9423
|
+
});
|
|
9204
9424
|
serve({ fetch: app.fetch, port });
|
|
9205
9425
|
}
|
|
9206
9426
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CREATE INDEX `idx_title_artist_album_albumartist` ON `tracks` (`title`,`artist`,`album`,`album_artist`);
|