@rocksky/cli 0.3.0 → 0.3.1
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 +0 -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 +313 -84
- 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/src/cmd/scrobble-api.ts +8 -0
- package/src/cmd/sync.ts +345 -72
- 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);
|
|
@@ -7636,7 +7645,7 @@ const createArtists = async (artists, user) => {
|
|
|
7636
7645
|
id: createId(),
|
|
7637
7646
|
name: tag
|
|
7638
7647
|
}));
|
|
7639
|
-
const BATCH_SIZE =
|
|
7648
|
+
const BATCH_SIZE = 1e3;
|
|
7640
7649
|
for (let i = 0; i < uniqueTags.length; i += BATCH_SIZE) {
|
|
7641
7650
|
const batch = uniqueTags.slice(i, i + BATCH_SIZE);
|
|
7642
7651
|
await ctx.db.insert(schema.genres).values(batch).onConflictDoNothing({
|
|
@@ -7699,7 +7708,7 @@ const createAlbums = async (albums, user) => {
|
|
|
7699
7708
|
)
|
|
7700
7709
|
);
|
|
7701
7710
|
const validAlbumData = albums.map((album, index) => ({ album, artist: artists[index] })).filter(({ artist }) => artist);
|
|
7702
|
-
const BATCH_SIZE =
|
|
7711
|
+
const BATCH_SIZE = 1e3;
|
|
7703
7712
|
let totalAlbumsImported = 0;
|
|
7704
7713
|
for (let i = 0; i < validAlbumData.length; i += BATCH_SIZE) {
|
|
7705
7714
|
const batch = validAlbumData.slice(i, i + BATCH_SIZE);
|
|
@@ -7759,7 +7768,7 @@ const createSongs = async (songs, user) => {
|
|
|
7759
7768
|
artist: artists[index],
|
|
7760
7769
|
album: albums[index]
|
|
7761
7770
|
})).filter(({ artist, album }) => artist && album);
|
|
7762
|
-
const BATCH_SIZE =
|
|
7771
|
+
const BATCH_SIZE = 1e3;
|
|
7763
7772
|
let totalTracksImported = 0;
|
|
7764
7773
|
for (let i = 0; i < validSongData.length; i += BATCH_SIZE) {
|
|
7765
7774
|
const batch = validSongData.slice(i, i + BATCH_SIZE);
|
|
@@ -7862,7 +7871,7 @@ const createScrobbles = async (scrobbles, user) => {
|
|
|
7862
7871
|
album: albums[index],
|
|
7863
7872
|
artist: artists[index]
|
|
7864
7873
|
})).filter(({ track, album, artist }) => track && album && artist);
|
|
7865
|
-
const BATCH_SIZE =
|
|
7874
|
+
const BATCH_SIZE = 1e3;
|
|
7866
7875
|
let totalScrobblesImported = 0;
|
|
7867
7876
|
for (let i = 0; i < validScrobbleData.length; i += BATCH_SIZE) {
|
|
7868
7877
|
const batch = validScrobbleData.slice(i, i + BATCH_SIZE);
|
|
@@ -7951,7 +7960,7 @@ const subscribeToJetstream = (user) => {
|
|
|
7951
7960
|
const onNewCollection = async (record, cid, uri, user) => {
|
|
7952
7961
|
switch (record.$type) {
|
|
7953
7962
|
case "app.rocksky.song":
|
|
7954
|
-
await onNewSong(record);
|
|
7963
|
+
await onNewSong(record, cid, uri, user);
|
|
7955
7964
|
break;
|
|
7956
7965
|
case "app.rocksky.album":
|
|
7957
7966
|
await onNewAlbum(record, cid, uri, user);
|
|
@@ -7969,6 +7978,16 @@ const onNewCollection = async (record, cid, uri, user) => {
|
|
|
7969
7978
|
const onNewSong = async (record, cid, uri, user) => {
|
|
7970
7979
|
const { title, artist, album } = record;
|
|
7971
7980
|
logger.info` New song: ${title} by ${artist} from ${album}`;
|
|
7981
|
+
await createSongs(
|
|
7982
|
+
[
|
|
7983
|
+
{
|
|
7984
|
+
cid,
|
|
7985
|
+
uri,
|
|
7986
|
+
value: record
|
|
7987
|
+
}
|
|
7988
|
+
],
|
|
7989
|
+
user
|
|
7990
|
+
);
|
|
7972
7991
|
};
|
|
7973
7992
|
const onNewAlbum = async (record, cid, uri, user) => {
|
|
7974
7993
|
const { title, artist } = record;
|
|
@@ -7999,8 +8018,140 @@ const onNewArtist = async (record, cid, uri, user) => {
|
|
|
7999
8018
|
);
|
|
8000
8019
|
};
|
|
8001
8020
|
const onNewScrobble = async (record, cid, uri, user) => {
|
|
8002
|
-
const { title, createdAt } = record;
|
|
8021
|
+
const { title, createdAt, artist, album, albumArtist } = record;
|
|
8003
8022
|
logger.info` New scrobble: ${title} at ${createdAt}`;
|
|
8023
|
+
let [artistRecord] = await ctx.db.select().from(schema.artists).where(eq(schema.artists.name, record.albumArtist)).execute();
|
|
8024
|
+
if (!artistRecord) {
|
|
8025
|
+
logger.info` ⚙️ Artist not found, creating: "${albumArtist}"`;
|
|
8026
|
+
const artistUri = `at://${user.did}/app.rocksky.artist/${createId()}`;
|
|
8027
|
+
const artistCid = createId();
|
|
8028
|
+
await createArtists(
|
|
8029
|
+
[
|
|
8030
|
+
{
|
|
8031
|
+
cid: artistCid,
|
|
8032
|
+
uri: artistUri,
|
|
8033
|
+
value: {
|
|
8034
|
+
$type: "app.rocksky.artist",
|
|
8035
|
+
name: record.albumArtist,
|
|
8036
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8037
|
+
tags: record.tags || []
|
|
8038
|
+
}
|
|
8039
|
+
}
|
|
8040
|
+
],
|
|
8041
|
+
user
|
|
8042
|
+
);
|
|
8043
|
+
[artistRecord] = await ctx.db.select().from(schema.artists).where(eq(schema.artists.name, record.albumArtist)).execute();
|
|
8044
|
+
if (!artistRecord) {
|
|
8045
|
+
logger.error` ❌ Failed to create artist. Skipping scrobble.`;
|
|
8046
|
+
return;
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
let [albumRecord] = await ctx.db.select().from(schema.albums).where(
|
|
8050
|
+
and(
|
|
8051
|
+
eq(schema.albums.title, record.album),
|
|
8052
|
+
eq(schema.albums.artist, record.albumArtist)
|
|
8053
|
+
)
|
|
8054
|
+
).execute();
|
|
8055
|
+
if (!albumRecord) {
|
|
8056
|
+
logger.info` ⚙️ Album not found, creating: "${album}" by ${albumArtist}`;
|
|
8057
|
+
const albumUri = `at://${user.did}/app.rocksky.album/${createId()}`;
|
|
8058
|
+
const albumCid = createId();
|
|
8059
|
+
await createAlbums(
|
|
8060
|
+
[
|
|
8061
|
+
{
|
|
8062
|
+
cid: albumCid,
|
|
8063
|
+
uri: albumUri,
|
|
8064
|
+
value: {
|
|
8065
|
+
$type: "app.rocksky.album",
|
|
8066
|
+
title: record.album,
|
|
8067
|
+
artist: record.albumArtist,
|
|
8068
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8069
|
+
releaseDate: record.releaseDate,
|
|
8070
|
+
year: record.year,
|
|
8071
|
+
albumArt: record.albumArt,
|
|
8072
|
+
artistUri: artistRecord.uri,
|
|
8073
|
+
spotifyLink: record.spotifyLink,
|
|
8074
|
+
appleMusicLink: record.appleMusicLink,
|
|
8075
|
+
tidalLink: record.tidalLink,
|
|
8076
|
+
youtubeLink: record.youtubeLink
|
|
8077
|
+
}
|
|
8078
|
+
}
|
|
8079
|
+
],
|
|
8080
|
+
user
|
|
8081
|
+
);
|
|
8082
|
+
[albumRecord] = await ctx.db.select().from(schema.albums).where(
|
|
8083
|
+
and(
|
|
8084
|
+
eq(schema.albums.title, record.album),
|
|
8085
|
+
eq(schema.albums.artist, record.albumArtist)
|
|
8086
|
+
)
|
|
8087
|
+
).execute();
|
|
8088
|
+
if (!albumRecord) {
|
|
8089
|
+
logger.error` ❌ Failed to create album. Skipping scrobble.`;
|
|
8090
|
+
return;
|
|
8091
|
+
}
|
|
8092
|
+
}
|
|
8093
|
+
let [track] = await ctx.db.select().from(schema.tracks).where(
|
|
8094
|
+
and(
|
|
8095
|
+
eq(schema.tracks.title, record.title),
|
|
8096
|
+
eq(schema.tracks.artist, record.artist),
|
|
8097
|
+
eq(schema.tracks.album, record.album),
|
|
8098
|
+
eq(schema.tracks.albumArtist, record.albumArtist)
|
|
8099
|
+
)
|
|
8100
|
+
).execute();
|
|
8101
|
+
if (!track) {
|
|
8102
|
+
logger.info` ⚙️ Track not found, creating: "${title}" by ${artist} from ${album}`;
|
|
8103
|
+
const trackUri = `at://${user.did}/app.rocksky.song/${createId()}`;
|
|
8104
|
+
const trackCid = createId();
|
|
8105
|
+
await createSongs(
|
|
8106
|
+
[
|
|
8107
|
+
{
|
|
8108
|
+
cid: trackCid,
|
|
8109
|
+
uri: trackUri,
|
|
8110
|
+
value: {
|
|
8111
|
+
$type: "app.rocksky.song",
|
|
8112
|
+
title: record.title,
|
|
8113
|
+
artist: record.artist,
|
|
8114
|
+
albumArtist: record.albumArtist,
|
|
8115
|
+
album: record.album,
|
|
8116
|
+
duration: record.duration,
|
|
8117
|
+
trackNumber: record.trackNumber,
|
|
8118
|
+
discNumber: record.discNumber,
|
|
8119
|
+
releaseDate: record.releaseDate,
|
|
8120
|
+
year: record.year,
|
|
8121
|
+
genre: record.genre,
|
|
8122
|
+
tags: record.tags,
|
|
8123
|
+
composer: record.composer,
|
|
8124
|
+
lyrics: record.lyrics,
|
|
8125
|
+
copyrightMessage: record.copyrightMessage,
|
|
8126
|
+
albumArt: record.albumArt,
|
|
8127
|
+
youtubeLink: record.youtubeLink,
|
|
8128
|
+
spotifyLink: record.spotifyLink,
|
|
8129
|
+
tidalLink: record.tidalLink,
|
|
8130
|
+
appleMusicLink: record.appleMusicLink,
|
|
8131
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8132
|
+
mbId: record.mbid,
|
|
8133
|
+
label: record.label,
|
|
8134
|
+
albumUri: albumRecord.uri,
|
|
8135
|
+
artistUri: artistRecord.uri
|
|
8136
|
+
}
|
|
8137
|
+
}
|
|
8138
|
+
],
|
|
8139
|
+
user
|
|
8140
|
+
);
|
|
8141
|
+
[track] = await ctx.db.select().from(schema.tracks).where(
|
|
8142
|
+
and(
|
|
8143
|
+
eq(schema.tracks.title, record.title),
|
|
8144
|
+
eq(schema.tracks.artist, record.artist),
|
|
8145
|
+
eq(schema.tracks.album, record.album),
|
|
8146
|
+
eq(schema.tracks.albumArtist, record.albumArtist)
|
|
8147
|
+
)
|
|
8148
|
+
).execute();
|
|
8149
|
+
if (!track) {
|
|
8150
|
+
logger.error` ❌ Failed to create track. Skipping scrobble.`;
|
|
8151
|
+
return;
|
|
8152
|
+
}
|
|
8153
|
+
}
|
|
8154
|
+
logger.info` ✓ All required entities ready. Creating scrobble...`;
|
|
8004
8155
|
await createScrobbles(
|
|
8005
8156
|
[
|
|
8006
8157
|
{
|
|
@@ -8013,79 +8164,150 @@ const onNewScrobble = async (record, cid, uri, user) => {
|
|
|
8013
8164
|
);
|
|
8014
8165
|
};
|
|
8015
8166
|
const getRockskyUserSongs = async (agent) => {
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
const
|
|
8020
|
-
|
|
8021
|
-
collection: "app.rocksky.song",
|
|
8022
|
-
limit: PAGE_SIZE,
|
|
8023
|
-
cursor
|
|
8167
|
+
const results = [];
|
|
8168
|
+
try {
|
|
8169
|
+
logger.info(`Fetching repository CAR file for songs...`);
|
|
8170
|
+
const repoRes = await agent.com.atproto.sync.getRepo({
|
|
8171
|
+
did: agent.assertDid
|
|
8024
8172
|
});
|
|
8025
|
-
const
|
|
8026
|
-
|
|
8027
|
-
|
|
8173
|
+
const carReader = await CarReader.fromBytes(new Uint8Array(repoRes.data));
|
|
8174
|
+
const collection = "app.rocksky.song";
|
|
8175
|
+
for await (const { cid, bytes } of carReader.blocks()) {
|
|
8176
|
+
try {
|
|
8177
|
+
const decoded = cbor.decode(bytes);
|
|
8178
|
+
if (decoded && typeof decoded === "object" && "$type" in decoded) {
|
|
8179
|
+
if (decoded.$type === collection) {
|
|
8180
|
+
const value = decoded;
|
|
8181
|
+
const uri = `at://${agent.assertDid}/${collection}/${cid.toString()}`;
|
|
8182
|
+
results.push({
|
|
8183
|
+
value,
|
|
8184
|
+
uri,
|
|
8185
|
+
cid: cid.toString()
|
|
8186
|
+
});
|
|
8187
|
+
}
|
|
8188
|
+
}
|
|
8189
|
+
} catch (e) {
|
|
8190
|
+
logger.warn` Skipping block with CID ${cid.toString()} due to decode error: ${e}`;
|
|
8191
|
+
continue;
|
|
8192
|
+
}
|
|
8193
|
+
}
|
|
8028
8194
|
logger.info(
|
|
8029
8195
|
`${chalk.cyanBright(agent.assertDid)} ${chalk.greenBright(results.length)} songs`
|
|
8030
8196
|
);
|
|
8031
|
-
}
|
|
8197
|
+
} catch (error) {
|
|
8198
|
+
logger.error(`Error fetching songs from CAR: ${error}`);
|
|
8199
|
+
throw error;
|
|
8200
|
+
}
|
|
8032
8201
|
return results;
|
|
8033
8202
|
};
|
|
8034
8203
|
const getRockskyUserAlbums = async (agent) => {
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
const
|
|
8039
|
-
|
|
8040
|
-
collection: "app.rocksky.album",
|
|
8041
|
-
limit: PAGE_SIZE,
|
|
8042
|
-
cursor
|
|
8204
|
+
const results = [];
|
|
8205
|
+
try {
|
|
8206
|
+
logger.info(`Fetching repository CAR file for albums...`);
|
|
8207
|
+
const repoRes = await agent.com.atproto.sync.getRepo({
|
|
8208
|
+
did: agent.assertDid
|
|
8043
8209
|
});
|
|
8044
|
-
const
|
|
8045
|
-
|
|
8046
|
-
|
|
8210
|
+
const carReader = await CarReader.fromBytes(new Uint8Array(repoRes.data));
|
|
8211
|
+
const collection = "app.rocksky.album";
|
|
8212
|
+
for await (const { cid, bytes } of carReader.blocks()) {
|
|
8213
|
+
try {
|
|
8214
|
+
const decoded = cbor.decode(bytes);
|
|
8215
|
+
if (decoded && typeof decoded === "object" && "$type" in decoded) {
|
|
8216
|
+
if (decoded.$type === collection) {
|
|
8217
|
+
const value = decoded;
|
|
8218
|
+
const uri = `at://${agent.assertDid}/${collection}/${cid.toString()}`;
|
|
8219
|
+
results.push({
|
|
8220
|
+
value,
|
|
8221
|
+
uri,
|
|
8222
|
+
cid: cid.toString()
|
|
8223
|
+
});
|
|
8224
|
+
}
|
|
8225
|
+
}
|
|
8226
|
+
} catch (e) {
|
|
8227
|
+
logger.warn` Skipping block with CID ${cid.toString()} due to decode error: ${e}`;
|
|
8228
|
+
continue;
|
|
8229
|
+
}
|
|
8230
|
+
}
|
|
8047
8231
|
logger.info(
|
|
8048
8232
|
`${chalk.cyanBright(agent.assertDid)} ${chalk.greenBright(results.length)} albums`
|
|
8049
8233
|
);
|
|
8050
|
-
}
|
|
8234
|
+
} catch (error) {
|
|
8235
|
+
logger.error(`Error fetching albums from CAR: ${error}`);
|
|
8236
|
+
throw error;
|
|
8237
|
+
}
|
|
8051
8238
|
return results;
|
|
8052
8239
|
};
|
|
8053
8240
|
const getRockskyUserArtists = async (agent) => {
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
const
|
|
8058
|
-
|
|
8059
|
-
collection: "app.rocksky.artist",
|
|
8060
|
-
limit: PAGE_SIZE,
|
|
8061
|
-
cursor
|
|
8241
|
+
const results = [];
|
|
8242
|
+
try {
|
|
8243
|
+
logger.info(`Fetching repository CAR file for artists...`);
|
|
8244
|
+
const repoRes = await agent.com.atproto.sync.getRepo({
|
|
8245
|
+
did: agent.assertDid
|
|
8062
8246
|
});
|
|
8063
|
-
const
|
|
8064
|
-
|
|
8065
|
-
|
|
8247
|
+
const carReader = await CarReader.fromBytes(new Uint8Array(repoRes.data));
|
|
8248
|
+
const collection = "app.rocksky.artist";
|
|
8249
|
+
for await (const { cid, bytes } of carReader.blocks()) {
|
|
8250
|
+
try {
|
|
8251
|
+
const decoded = cbor.decode(bytes);
|
|
8252
|
+
if (decoded && typeof decoded === "object" && "$type" in decoded) {
|
|
8253
|
+
if (decoded.$type === collection) {
|
|
8254
|
+
const value = decoded;
|
|
8255
|
+
const uri = `at://${agent.assertDid}/${collection}/${cid.toString()}`;
|
|
8256
|
+
results.push({
|
|
8257
|
+
value,
|
|
8258
|
+
uri,
|
|
8259
|
+
cid: cid.toString()
|
|
8260
|
+
});
|
|
8261
|
+
}
|
|
8262
|
+
}
|
|
8263
|
+
} catch (e) {
|
|
8264
|
+
continue;
|
|
8265
|
+
}
|
|
8266
|
+
}
|
|
8066
8267
|
logger.info(
|
|
8067
8268
|
`${chalk.cyanBright(agent.assertDid)} ${chalk.greenBright(results.length)} artists`
|
|
8068
8269
|
);
|
|
8069
|
-
}
|
|
8270
|
+
} catch (error) {
|
|
8271
|
+
logger.error(`Error fetching artists from CAR: ${error}`);
|
|
8272
|
+
throw error;
|
|
8273
|
+
}
|
|
8070
8274
|
return results;
|
|
8071
8275
|
};
|
|
8072
8276
|
const getRockskyUserScrobbles = async (agent) => {
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
const
|
|
8077
|
-
|
|
8078
|
-
collection: "app.rocksky.scrobble",
|
|
8079
|
-
limit: PAGE_SIZE,
|
|
8080
|
-
cursor
|
|
8277
|
+
const results = [];
|
|
8278
|
+
try {
|
|
8279
|
+
logger.info(`Fetching repository CAR file for scrobbles...`);
|
|
8280
|
+
const repoRes = await agent.com.atproto.sync.getRepo({
|
|
8281
|
+
did: agent.assertDid
|
|
8081
8282
|
});
|
|
8082
|
-
const
|
|
8083
|
-
|
|
8084
|
-
|
|
8283
|
+
const carReader = await CarReader.fromBytes(new Uint8Array(repoRes.data));
|
|
8284
|
+
const collection = "app.rocksky.scrobble";
|
|
8285
|
+
for await (const { cid, bytes } of carReader.blocks()) {
|
|
8286
|
+
try {
|
|
8287
|
+
const decoded = cbor.decode(bytes);
|
|
8288
|
+
if (decoded && typeof decoded === "object" && "$type" in decoded) {
|
|
8289
|
+
if (decoded.$type === collection) {
|
|
8290
|
+
const value = decoded;
|
|
8291
|
+
const uri = `at://${agent.assertDid}/${collection}/${cid.toString()}`;
|
|
8292
|
+
results.push({
|
|
8293
|
+
value,
|
|
8294
|
+
uri,
|
|
8295
|
+
cid: cid.toString()
|
|
8296
|
+
});
|
|
8297
|
+
}
|
|
8298
|
+
}
|
|
8299
|
+
} catch (e) {
|
|
8300
|
+
logger.warn` Skipping block with CID ${cid.toString()} due to decode error: ${e}`;
|
|
8301
|
+
continue;
|
|
8302
|
+
}
|
|
8303
|
+
}
|
|
8085
8304
|
logger.info(
|
|
8086
8305
|
`${chalk.cyanBright(agent.assertDid)} ${chalk.greenBright(results.length)} scrobbles`
|
|
8087
8306
|
);
|
|
8088
|
-
}
|
|
8307
|
+
} catch (error) {
|
|
8308
|
+
logger.error(`Error fetching scrobbles from CAR: ${error}`);
|
|
8309
|
+
throw error;
|
|
8310
|
+
}
|
|
8089
8311
|
return results;
|
|
8090
8312
|
};
|
|
8091
8313
|
|
|
@@ -8571,7 +8793,7 @@ async function whoami() {
|
|
|
8571
8793
|
}
|
|
8572
8794
|
}
|
|
8573
8795
|
|
|
8574
|
-
var version = "0.3.
|
|
8796
|
+
var version = "0.3.1";
|
|
8575
8797
|
|
|
8576
8798
|
async function login(handle) {
|
|
8577
8799
|
const app = express();
|
|
@@ -9201,6 +9423,13 @@ Welcome to the lastfm compatibility API
|
|
|
9201
9423
|
return c.text("Scrobble received");
|
|
9202
9424
|
});
|
|
9203
9425
|
logger.info`lastfm/listenbrainz/webscrobbler scrobble API listening on ${"http://localhost:" + port}`;
|
|
9426
|
+
new Promise(async () => {
|
|
9427
|
+
try {
|
|
9428
|
+
await sync();
|
|
9429
|
+
} catch (err) {
|
|
9430
|
+
logger.warn`Error during initial sync: ${err}`;
|
|
9431
|
+
}
|
|
9432
|
+
});
|
|
9204
9433
|
serve({ fetch: app.fetch, port });
|
|
9205
9434
|
}
|
|
9206
9435
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CREATE INDEX `idx_title_artist_album_albumartist` ON `tracks` (`title`,`artist`,`album`,`album_artist`);
|