···11import { AtpAgent, type Agent } from "@atproto/api";
22import { TID } from "@atproto/common";
33+import { consola } from "consola";
34import type { Context } from "context";
45import { and, desc, eq, type SQLWrapper } from "drizzle-orm";
56import * as LikeLexicon from "lexicon/types/app/rocksky/like";
···279280 };
280281281282 if (!LikeLexicon.validateRecord(record).success) {
282282- console.log(LikeLexicon.validateRecord(record));
283283+ consola.info(LikeLexicon.validateRecord(record));
283284 throw new Error("Invalid record");
284285 }
285286···292293 validate: false,
293294 });
294295 const uri = res.data.uri;
295295- console.log(`Like record created at: ${uri}`);
296296+ consola.info(`Like record created at: ${uri}`);
296297297298 [created] = await ctx.db
298299 .update(lovedTracks)
···300301 .where(eq(lovedTracks.id, created.id))
301302 .returning();
302303 } catch (e) {
303303- console.error(`Error creating like record: ${e.message}`);
304304+ consola.error(`Error creating like record: ${e.message}`);
304305 }
305306 }
306307
+29-28
apps/api/src/nowplaying/nowplaying.service.ts
···11import type { Agent } from "@atproto/api";
22import { TID } from "@atproto/common";
33import chalk from "chalk";
44+import { consola } from "consola";
45import type { Context } from "context";
56import dayjs from "dayjs";
67import { and, eq, gte, lte, or } from "drizzle-orm";
···3839 };
39404041 if (!Artist.validateRecord(record).success) {
4141- console.log(Artist.validateRecord(record));
4242- console.log(JSON.stringify(record, null, 2));
4242+ consola.info(Artist.validateRecord(record));
4343+ consola.info(JSON.stringify(record, null, 2));
4344 throw new Error("Invalid record");
4445 }
4546···5253 validate: false,
5354 });
5455 const uri = res.data.uri;
5555- console.log(`Artist record created at ${uri}`);
5656+ consola.info(`Artist record created at ${uri}`);
5657 return uri;
5758 } catch (e) {
5858- console.error("Error creating artist record", e);
5959+ consola.error("Error creating artist record", e);
5960 return null;
6061 }
6162}
···7980 };
80818182 if (!Album.validateRecord(record).success) {
8282- console.log(Album.validateRecord(record));
8383- console.log(JSON.stringify(record, null, 2));
8383+ consola.info(Album.validateRecord(record));
8484+ consola.info(JSON.stringify(record, null, 2));
8485 throw new Error("Invalid record");
8586 }
8687···9394 validate: false,
9495 });
9596 const uri = res.data.uri;
9696- console.log(`Album record created at ${uri}`);
9797+ consola.info(`Album record created at ${uri}`);
9798 return uri;
9899 } catch (e) {
9999- console.error("Error creating album record", e);
100100+ consola.error("Error creating album record", e);
100101 return null;
101102 }
102103}
···134135 };
135136136137 if (!Song.validateRecord(record).success) {
137137- console.log(Song.validateRecord(record));
138138- console.log(chalk.cyan(JSON.stringify(record, null, 2)));
138138+ consola.info(Song.validateRecord(record));
139139+ consola.info(chalk.cyan(JSON.stringify(record, null, 2)));
139140 throw new Error("Invalid record");
140141 }
141142···148149 validate: false,
149150 });
150151 const uri = res.data.uri;
151151- console.log(`Song record created at ${uri}`);
152152+ consola.info(`Song record created at ${uri}`);
152153 return uri;
153154 } catch (e) {
154154- console.error("Error creating song record", e);
155155+ consola.error("Error creating song record", e);
155156 return null;
156157 }
157158}
···192193 };
193194194195 if (!Scrobble.validateRecord(record).success) {
195195- console.log(Scrobble.validateRecord(record));
196196- console.log(JSON.stringify(record, null, 2));
196196+ consola.info(Scrobble.validateRecord(record));
197197+ consola.info(JSON.stringify(record, null, 2));
197198 throw new Error("Invalid record");
198199 }
199200···206207 validate: false,
207208 });
208209 const uri = res.data.uri;
209209- console.log(`Scrobble record created at ${uri}`);
210210+ consola.info(`Scrobble record created at ${uri}`);
210211 return uri;
211212 } catch (e) {
212212- console.error("Error creating scrobble record", e);
213213+ consola.error("Error creating scrobble record", e);
213214 return null;
214215 }
215216}
···531532 .then((rows) => rows[0]);
532533533534 if (existingScrobble) {
534534- console.log(
535535+ consola.info(
535536 `Scrobble already exists for ${chalk.cyan(track.title)} at ${chalk.cyan(
536537 scrobbleTime.format("YYYY-MM-DD HH:mm:ss"),
537538 )}`,
···641642 name: artist.name,
642643 }));
643644 } catch (error) {
644644- console.error("Error fetching MusicBrainz data");
645645+ consola.error("Error fetching MusicBrainz data");
645646 }
646647647648 if (!existingTrack?.uri || !userTrack?.userTrack.uri?.includes(userDid)) {
···664665665666 let tries = 0;
666667 while (!existingTrack && tries < 30) {
667667- console.log(`Song not found, trying again: ${chalk.magenta(tries + 1)}`);
668668+ consola.info(`Song not found, trying again: ${chalk.magenta(tries + 1)}`);
668669 existingTrack = await ctx.db
669670 .select()
670671 .from(tracks)
···685686 }
686687687688 if (tries === 30 && !existingTrack) {
688688- console.log(`Song not found after ${chalk.magenta("30 tries")}`);
689689+ consola.info(`Song not found after ${chalk.magenta("30 tries")}`);
689690 }
690691691692 if (existingTrack) {
692692- console.log(
693693+ consola.info(
693694 `Song found: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries`,
694695 );
695696 }
···768769 .then((rows) => rows[0]);
769770770771 while (!existingTrack?.artistUri && !existingTrack?.albumUri && tries < 30) {
771771- console.log(
772772+ consola.info(
772773 `Artist uri not ready, trying again: ${chalk.magenta(tries + 1)}`,
773774 );
774775 existingTrack = await ctx.db
···847848 }
848849849850 if (tries === 30 && !existingTrack?.artistUri) {
850850- console.log(`Artist uri not ready after ${chalk.magenta("30 tries")}`);
851851+ consola.info(`Artist uri not ready after ${chalk.magenta("30 tries")}`);
851852 }
852853853854 if (existingTrack?.artistUri) {
854854- console.log(
855855+ consola.info(
855856 `Artist uri ready: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries`,
856857 );
857858 }
···925926 scrobble.track.artistUri &&
926927 scrobble.track.albumUri
927928 ) {
928928- console.log("Scrobble found after ", chalk.magenta(tries + 1), " tries");
929929+ consola.info("Scrobble found after ", chalk.magenta(tries + 1), " tries");
929930 await publishScrobble(ctx, scrobble.scrobble.id);
930930- console.log("Scrobble published");
931931+ consola.info("Scrobble published");
931932 break;
932933 }
933934 tries += 1;
934934- console.log("Scrobble not found, trying again: ", chalk.magenta(tries));
935935+ consola.info("Scrobble not found, trying again: ", chalk.magenta(tries));
935936 await new Promise((resolve) => setTimeout(resolve, 1000));
936937 }
937938938939 if (tries === 30 && !scrobble) {
939939- console.log(`Scrobble not found after ${chalk.magenta("30 tries")}`);
940940+ consola.info(`Scrobble not found after ${chalk.magenta("30 tries")}`);
940941 }
941942942943 ctx.nc.publish("rocksky.user.scrobble.sync", Buffer.from(userDid));
+10-9
apps/api/src/scripts/avatar.ts
···11import chalk from "chalk";
22+import { consola } from "consola";
23import { ctx } from "context";
34import { eq, or } from "drizzle-orm";
45import _ from "lodash";
···15161617 const serviceEndpoint = _.get(plc, "service.0.serviceEndpoint");
1718 if (!serviceEndpoint) {
1818- console.log(`Service endpoint not found for ${user.did}`);
1919+ consola.info(`Service endpoint not found for ${user.did}`);
1920 return;
2021 }
2122···3334 .where(eq(users.did, user.did))
3435 .execute();
3536 } else {
3636- console.log(`Skipping avatar update for ${user.did}`);
3737+ consola.info(`Skipping avatar update for ${user.did}`);
3738 }
38393940 const [u] = await ctx.db
···5455 xata_version: u.xataVersion,
5556 };
56575757- console.log(userPayload);
5858+ consola.info(userPayload);
5859 ctx.nc.publish("rocksky.user", Buffer.from(JSON.stringify(userPayload)));
5960}
6061···6768 .limit(1)
6869 .execute();
6970 if (!user) {
7070- console.log(`User ${did} not found`);
7171+ consola.info(`User ${did} not found`);
7172 continue;
7273 }
7374···7778 let offset = 0;
7879 let processedCount = 0;
79808080- console.log("Processing all users...");
8181+ consola.info("Processing all users...");
81828283 while (true) {
8384 const batch = await ctx.db
···9192 break; // No more users to process
9293 }
93949494- console.log(
9595+ consola.info(
9596 `Processing batch ${Math.floor(offset / BATCH_SIZE) + 1}, users ${offset + 1}-${offset + batch.length}`,
9697 );
9798···100101 await processUser(user);
101102 processedCount++;
102103 } catch (error) {
103103- console.error(`Error processing user ${user.did}:`, error);
104104+ consola.error(`Error processing user ${user.did}:`, error);
104105 }
105106 }
106107···110111 await new Promise((resolve) => setTimeout(resolve, 100));
111112 }
112113113113- console.log(`Processed ${chalk.greenBright(processedCount)} users total`);
114114+ consola.info(`Processed ${chalk.greenBright(processedCount)} users total`);
114115}
115116116117// Ensure all messages are flushed before exiting
117118await ctx.nc.flush();
118119119119-console.log("Done");
120120+consola.info("Done");
120121121122process.exit(0);
+8-7
apps/api/src/scripts/dedup.ts
···11import chalk from "chalk";
22+import { consola } from "consola";
23import { ctx } from "context";
34import { eq } from "drizzle-orm";
45import { createAgent } from "lib/agent";
···78const args = process.argv.slice(2);
89910if (args.length === 0) {
1010- console.error("Please provide user author identifier (handle or DID).");
1111- console.log(`Usage: ${chalk.cyan("npm run feed -- <handle|did>")}`);
1111+ consola.error("Please provide user author identifier (handle or DID).");
1212+ consola.info(`Usage: ${chalk.cyan("npm run feed -- <handle|did>")}`);
1213 process.exit(1);
1314}
1415···3637 .where(eq(tables.scrobbles.uri, record.uri))
3738 .limit(1);
3839 if (result.length === 0) {
3939- console.log(`${i} Deleting record:`);
4040- console.log(record);
4040+ consola.info(`${i} Deleting record:`);
4141+ consola.info(record);
4142 const rkey = record.uri.split("/").pop();
4243 await agent.com.atproto.repo.deleteRecord({
4344 repo: agent.assertDid,
···4647 });
4748 await new Promise((resolve) => setTimeout(resolve, 1000)); // rate limit
4849 } else {
4949- console.log(chalk.greenBright(`${i} Keeping record:`));
5050- console.log(record);
5050+ consola.info(chalk.greenBright(`${i} Keeping record:`));
5151+ consola.info(record);
5152 }
5253 i += 1;
5354 }
5455 cursor = records.data.cursor;
5556} while (cursor);
56575757-console.log(chalk.greenBright("Deduplication complete."));
5858+consola.info(chalk.greenBright("Deduplication complete."));
58595960process.exit(0);
+3-2
apps/api/src/scripts/exp.ts
···11+import { consola } from "consola";
12import { ctx, db } from "context";
23import { refreshSessionsAboutToExpire, updateExpiresAt } from "db";
34import { env } from "lib/env";
45import cron from "node-cron";
5666-console.log("DB Path:", env.DB_PATH);
77+consola.info("DB Path:", env.DB_PATH);
7889await updateExpiresAt(db);
910···11121213// run every 1 minute
1314cron.schedule("* * * * *", async () => {
1414- console.log("Running session refresh job...");
1515+ consola.info("Running session refresh job...");
1516 await refreshSessionsAboutToExpire(db, ctx);
1617});
+16-15
apps/api/src/scripts/feed.ts
···11import chalk from "chalk";
22+import { consola } from "consola";
23import { ctx } from "context";
34import type * as FeedGenerator from "lexicon/types/app/rocksky/feed/generator";
45import { createAgent } from "lib/agent";
···78const args = process.argv.slice(2);
89910if (args.length === 0) {
1010- console.error("Please provide user author identifier (handle or DID).");
1111+ consola.error("Please provide user author identifier (handle or DID).");
1112 console.log(`Usage: ${chalk.cyan("npm run feed -- <handle|did>")}`);
1213 process.exit(1);
1314}
···1920});
20212122if (name.value.length < 3 || name.value.length > 240) {
2222- console.error("Feed name must be between 3 and 240 characters.");
2323+ consola.error("Feed name must be between 3 and 240 characters.");
2324 process.exit(1);
2425}
2526···3031});
31323233if (description.value.length > 3000) {
3333- console.error("Description is too long. Maximum length is 3000 characters.");
3434+ consola.error("Description is too long. Maximum length is 3000 characters.");
3435 process.exit(1);
3536}
3637···4142});
42434344if (!/^did:web:[a-zA-Z0-9_.-]{3,30}$/.test(did.value)) {
4444- console.error(
4545+ consola.error(
4546 "Invalid DID format. It should start with 'did:web:' followed by 3 to 30 alphanumeric characters, underscores, hyphens, or periods.",
4647 );
4748 process.exit(1);
···5455});
55565657if (!/^[a-zA-Z0-9_-]{3,30}$/.test(rkey.value)) {
5757- console.error(
5858+ consola.error(
5859 "Invalid record key. Only alphanumeric characters, underscores, and hyphens are allowed. Length must be between 3 and 30 characters.",
5960 );
6061 process.exit(1);
6162}
62636363-console.log("Creating feed with the following details:");
6464-6565-console.log("Feed name:", name.value);
6666-console.log("Description:", description.value);
6767-console.log("DID:", did.value);
6868-console.log("Record key (rkey):", rkey.value);
6464+consola.info("Creating feed with the following details:");
6565+consola.info("---");
6666+consola.info("Feed name:", name.value);
6767+consola.info("Description:", description.value);
6868+consola.info("DID:", did.value);
6969+consola.info("Record key (rkey):", rkey.value);
69707071const confirm = await prompts({
7172 type: "confirm",
···7576});
76777778if (!confirm.value) {
7878- console.log("Feed creation cancelled.");
7979+ consola.info("Feed creation cancelled.");
7980 process.exit(0);
8081}
8182···87888889const agent = await createAgent(ctx.oauthClient, userDid);
89909090-console.log(
9191+consola.info(
9192 `Writing ${chalk.greenBright("app.rocksky.feed.generator")} record...`,
9293);
9394···106107 rkey: rkey.value,
107108});
108109109109-console.log(chalk.greenBright("Feed created successfully!"));
110110-console.log(`Record created at: ${chalk.cyan(res.data.uri)}`);
110110+consola.info(chalk.greenBright("Feed created successfully!"));
111111+consola.info(`Record created at: ${chalk.cyan(res.data.uri)}`);
111112112113process.exit(0);
+4-3
apps/api/src/scripts/genres.ts
···11+import { consola } from "consola";
12import { ctx } from "context";
23import { eq, isNull } from "drizzle-orm";
34import { decrypt } from "lib/crypto";
···7879 .then(async (data) => _.get(data, "artists.items.0"));
79808081 if (result) {
8181- console.log(JSON.stringify(result, null, 2), "\n");
8282+ consola.info(JSON.stringify(result, null, 2), "\n");
8283 if (result.genres && result.genres.length > 0) {
8384 await ctx.db
8485 .update(tables.artists)
···9798 }
9899 break; // exit the retry loop on success
99100 } catch (error) {
100100- console.error("Error fetching genres for artist:", artist.name, error);
101101+ consola.error("Error fetching genres for artist:", artist.name, error);
101102 // wait for a while before retrying
102103 await new Promise((resolve) => setTimeout(resolve, 1000));
103104 }
···130131 await getGenresAndPicture(artists);
131132}
132133133133-console.log(`Artists without genres: ${count}`);
134134+consola.info(`Artists without genres: ${count}`);
134135135136process.exit(0);
···11import chalk from "chalk";
22+import { consola } from "consola";
23import { ctx } from "context";
34import { count } from "drizzle-orm";
45import tables from "schema";
5667async function main() {
77- console.log(chalk.cyan("Starting Meilisearch sync..."));
88+ consola.info(chalk.cyan("Starting Meilisearch sync..."));
89910 try {
1011 await Promise.all([
···1314 createTracks(),
1415 createUsers(),
1516 ]);
1616- console.log(chalk.green("Meilisearch sync completed successfully."));
1717+ consola.info(chalk.green("Meilisearch sync completed successfully."));
1718 } catch (error) {
1818- console.error(chalk.red("Error during Meilisearch sync:"), error);
1919+ consola.error(chalk.red("Error during Meilisearch sync:"), error);
1920 }
2021}
2122···3132 .then(([row]) => row.value);
3233 for (let i = 0; i < total; i += size) {
3334 const skip = i;
3434- console.log(
3535+ consola.info(
3536 `Processing ${chalk.magentaBright("albums")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`,
3637 );
3738 const results = await ctx.db
···5556 .then(([row]) => row.value);
5657 for (let i = 0; i < total; i += size) {
5758 const skip = i;
5858- console.log(
5959+ consola.info(
5960 `Processing ${chalk.magentaBright("artists")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`,
6061 );
6162 const results = await ctx.db
···7980 .then(([row]) => row.value);
8081 for (let i = 0; i < total; i += size) {
8182 const skip = i;
8282- console.log(
8383+ consola.info(
8384 `Processing ${chalk.magentaBright("tracks")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`,
8485 );
8586 const results = await ctx.db
···104105105106 for (let i = 0; i < total; i += size) {
106107 const skip = i;
107107- console.log(
108108+ consola.info(
108109 `Processing ${chalk.magentaBright("users")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`,
109110 );
110111 const results = await ctx.db
+5-4
apps/api/src/scripts/seed-feed.ts
···11import type { Agent } from "@atproto/api";
22import chalk from "chalk";
33+import { consola } from "consola";
34import { ctx } from "context";
55+import { eq } from "drizzle-orm";
46import { createAgent } from "lib/agent";
57import * as FeedGenerator from "lexicon/types/app/rocksky/feed/generator";
68import tables from "schema";
79import type { InsertFeed } from "schema/feeds";
88-import { eq } from "drizzle-orm";
9101011const args = process.argv.slice(2);
11121213if (args.length === 0) {
1313- console.error("Please provide user author identifier (handle or DID).");
1414- console.log(`Usage: ${chalk.cyan("npm run seed:feed -- <handle|did>")}`);
1414+ consola.error("Please provide user author identifier (handle or DID).");
1515+ consola.info(`Usage: ${chalk.cyan("npm run seed:feed -- <handle|did>")}`);
1516 process.exit(1);
1617}
1718···5758 } satisfies InsertFeed)
5859 .onConflictDoNothing()
5960 .execute();
6060- console.log(
6161+ consola.info(
6162 `Feed ${chalk.cyanBright(feed.value.displayName)} seeded successfully.`,
6263 );
6364}
+3-2
apps/api/src/scripts/spotify.ts
···11import chalk from "chalk";
22+import { consola } from "consola";
23import { ctx } from "context";
34import { encrypt } from "lib/crypto";
45import { env } from "lib/env";
···910const clientSecret = args[1];
10111112if (!clientId || !clientSecret) {
1212- console.error(
1313+ consola.error(
1314 "Please provide Spotify Client ID and Client Secret as command line arguments",
1415 );
1515- console.log(
1616+ consola.info(
1617 chalk.greenBright("Usage: ts-node spotify.ts <client_id> <client_secret>"),
1718 );
1819 process.exit(1);
+14-11
apps/api/src/scripts/sync-library.ts
···11import chalk from "chalk";
22+import { consola } from "consola";
23import { ctx } from "context";
34import { and, count, eq } from "drizzle-orm";
45import tables from "schema";
···1112 .execute()
1213 .then(([row]) => row.value);
13141414-console.log(`Total tracks to process: ${chalk.magentaBright(total)}`);
1515+consola.info(`Total tracks to process: ${chalk.magentaBright(total)}`);
15161617for (let i = 0; i < total; i += size) {
1718 const skip = i;
1818- console.log(
1919+ consola.info(
1920 `Processing ${chalk.magentaBright("tracks")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`,
2021 );
2122 const results = await ctx.db
···27282829 for (const track of results) {
2930 if (!track.artistUri || !track.albumUri) {
3030- console.log(
3131- `Skipping track ${chalk.cyan(track.title)} due to missing artist or album URI`,
3131+ consola.info(
3232+ `Deleting album-track relationship for track: ${chalk.redBright(track.uri)}`,
3233 );
3333- console.log("artistUri", track.artistUri);
3434- console.log("albumUri", track.albumUri);
3434+ consola.info("artistUri", track.artistUri);
3535+ consola.info("albumUri", track.albumUri);
3536 continue;
3637 }
3738···5758 .then((rows) => rows.length > 0);
58595960 if (!found) {
6060- console.log(`Creating artist-album relationship for track: ${track.uri}`);
6161+ consola.info(
6262+ `Creating artist-album relationship for track: ${track.uri}`,
6363+ );
6164 const [artist, album] = await Promise.all([
6265 ctx.db
6366 .select()
···7679 ]);
77807881 if (!artist || !album) {
7979- console.error(
8080- `Artist or album not found for track: ${track.uri}. Skipping...`,
8282+ consola.error(
8383+ `Artist-album relationship already exists for track: ${chalk.redBright(track.uri)}`,
8184 );
8282- console.log("artist", artist);
8383- console.log("album", album);
8585+ consola.info("artist", artist);
8686+ consola.info("album", album);
8487 continue;
8588 }
8689
+12-11
apps/api/src/scripts/sync.ts
···11import chalk from "chalk";
22+import { consola } from "consola";
23import { ctx } from "context";
34import { desc, eq, or } from "drizzle-orm";
45import { createHash } from "node:crypto";
···3839 .then((rows) => rows[0]);
39404041 if (existingTrack && !existingTrack.albumUri) {
4141- console.log(`Updating album uri for ${chalk.cyan(track.id)} ...`);
4242+ consola.info(`Updating album uri for ${chalk.cyan(track.id)} ...`);
42434344 const albumHash = createHash("sha256")
4445 .update(`${track.album} - ${track.albumArtist}`.toLowerCase())
···6061 }
61626263 if (existingTrack && !existingTrack.artistUri) {
6363- console.log(`Updating artist uri for ${chalk.cyan(track.id)} ...`);
6464+ consola.info(`Updating artist uri for ${chalk.cyan(track.id)} ...`);
64656566 const artistHash = createHash("sha256")
6667 .update(track.albumArtist.toLowerCase())
···9394 .then((rows) => rows[0]);
94959596 if (existingTrack && album && !album.artistUri) {
9696- console.log(`Updating artist uri for ${chalk.cyan(album.id)} ...`);
9797+ consola.info(`Updating artist uri for ${chalk.cyan(album.id)} ...`);
97989899 const artistHash = createHash("sha256")
99100 .update(track.albumArtist.toLowerCase())
···117118}
118119119120if (args.includes("--background")) {
120120- console.log("Wait for new scrobbles to sync ...");
121121+ consola.info("Wait for new scrobbles to sync ...");
121122 const sub = ctx.nc.subscribe("rocksky.user.scrobble.sync");
122123 for await (const m of sub) {
123124 const did = new TextDecoder().decode(m.data);
124125 // wait for 15 seconds to ensure the scrobble is fully created
125126 await new Promise((resolve) => setTimeout(resolve, 15000));
126126- console.log(`Syncing scrobbles ${chalk.magenta(did)} ...`);
127127+ consola.info(`Syncing scrobbles ${chalk.magenta(did)} ...`);
127128 await updateUris(did);
128129129130 const records = await ctx.db
···137138 .limit(5);
138139139140 for (const { scrobble } of records) {
140140- console.log(`Syncing scrobble ${chalk.cyan(scrobble.id)} ...`);
141141+ consola.info(`Syncing scrobble ${chalk.cyan(scrobble.id)} ...`);
141142 try {
142143 await publishScrobble(ctx, scrobble.id);
143144 } catch (err) {
144144- console.error(
145145+ consola.error(
145146 `Failed to sync scrobble ${chalk.cyan(scrobble.id)}:`,
146147 err,
147148 );
···152153}
153154154155for (const arg of args) {
155155- console.log(`Syncing scrobbles ${chalk.magenta(arg)} ...`);
156156+ consola.info(`Syncing scrobbles ${chalk.magenta(arg)} ...`);
156157 await updateUris(arg);
157158158159 const records = await ctx.db
···166167 .limit(process.env.SYNC_SIZE ? parseInt(process.env.SYNC_SIZE, 10) : 20);
167168168169 for (const { scrobble } of records) {
169169- console.log(`Syncing scrobble ${chalk.cyan(scrobble.id)} ...`);
170170+ consola.info(`Syncing scrobble ${chalk.cyan(scrobble.id)} ...`);
170171 try {
171172 await publishScrobble(ctx, scrobble.id);
172173 } catch (err) {
173173- console.error(`Failed to sync scrobble ${chalk.cyan(scrobble.id)}:`, err);
174174+ consola.error(`Failed to sync scrobble ${chalk.cyan(scrobble.id)}:`, err);
174175 }
175176 }
176176- console.log(`Synced ${chalk.greenBright(records.length)} scrobbles`);
177177+ consola.info(`Synced ${chalk.greenBright(records.length)} scrobbles`);
177178}
178179179180process.exit(0);
+2-1
apps/api/src/server.ts
···11+import { consola } from "consola";
12import { ctx } from "context";
23import cors from "cors";
34import type { Request, Response } from "express";
···3031app.use(proxyMiddleware);
31323233app.listen(process.env.ROCKSKY_XPRC_PORT || 3004, () => {
3333- console.log(
3434+ consola.info(
3435 `Rocksky XRPC API is running on port ${process.env.ROCKSKY_XRPC_PORT || 3004}`,
3536 );
3637});
+11-10
apps/api/src/shouts/shouts.service.ts
···11import { type Agent, AtpAgent } from "@atproto/api";
22+import { consola } from "consola";
23import { TID } from "@atproto/common";
34import type { Context } from "context";
45import { and, eq } from "drizzle-orm";
···9899 cid: subjectRecord.data.cid,
99100 });
100101 if (!subjectRef.success) {
101101- console.log(subjectRef);
102102+ consola.info(subjectRef);
102103 throw new Error("Invalid ref");
103104 }
104105···111112 };
112113113114 if (!ShoutLexicon.validateRecord(record).success) {
114114- console.log(ShoutLexicon.validateRecord(record));
115115+ consola.info(ShoutLexicon.validateRecord(record));
115116 throw new Error("[shout] invalid record");
116117 }
117118···125126 });
126127 const uri = res.data.uri;
127128128128- console.log(`Shout record created at: ${uri}`);
129129+ consola.info(`Shout record created at: ${uri}`);
129130130131 const createdShout = await ctx.db
131132 .insert(shouts)
···148149 });
149150 }
150151 } catch (e) {
151151- console.error(`Error creating shout record: ${e.message}`);
152152+ consola.error(`Error creating shout record: ${e.message}`);
152153 }
153154}
154155···269270 };
270271271272 if (!ShoutLexicon.validateRecord(record).success) {
272272- console.log(ShoutLexicon.validateRecord(record));
273273+ consola.info(ShoutLexicon.validateRecord(record));
273274 throw new Error("Invalid record");
274275 }
275276···283284 });
284285 const uri = res.data.uri;
285286286286- console.log(`Reply record created at: ${uri}`);
287287+ consola.info(`Reply record created at: ${uri}`);
287288288289 const createdShout = await ctx.db
289290 .insert(shouts)
···314315 });
315316 }
316317 } catch (e) {
317317- console.error(`Error creating reply record: ${e.message}`);
318318+ consola.error(`Error creating reply record: ${e.message}`);
318319 }
319320}
320321···370371 };
371372372373 if (!LikeLexicon.validateRecord(record).success) {
373373- console.log(LikeLexicon.validateRecord(record));
374374+ consola.info(LikeLexicon.validateRecord(record));
374375 throw new Error("Invalid record");
375376 }
376377···383384 validate: false,
384385 });
385386 const uri = res.data.uri;
386386- console.log(`Like record created at: ${uri}`);
387387+ consola.info(`Like record created at: ${uri}`);
387388388389 const shout = await ctx.db
389390 .select()
···402403 uri,
403404 });
404405 } catch (e) {
405405- console.error(`Error creating like record: ${e.message}`);
406406+ consola.error(`Error creating like record: ${e.message}`);
406407 }
407408}
408409
+2-1
apps/api/src/spotify/app.ts
···11+import { consola } from "consola";
12import { ctx } from "context";
23import { and, eq, or, sql } from "drizzle-orm";
34import { Hono } from "hono";
···249250 });
250251 } catch (e) {
251252 if (!e.message.includes("duplicate key value violates unique constraint")) {
252252- console.error(e.message);
253253+ consola.error(e.message);
253254 } else {
254255 throw e;
255256 }
+2
apps/api/src/subscribers/index.ts
···22import { onNewPlaylist } from "./playlist";
33import { onNewTrack } from "./track";
44import { onNewUser } from "./user";
55+import { onNewScrobble } from "./scrobble";
5667export default function subscribe(ctx: Context) {
78 onNewPlaylist(ctx);
89 onNewTrack(ctx);
910 onNewUser(ctx);
1111+ onNewScrobble(ctx);
1012}
+6-5
apps/api/src/subscribers/playlist.ts
···11import { TID } from "@atproto/common";
22+import { consola } from "consola";
23import chalk from "chalk";
34import type { Context } from "context";
45import { eq } from "drizzle-orm";
···1617 id: string;
1718 did: string;
1819 } = JSON.parse(sc.decode(m.data));
1919- console.log(
2020+ consola.info(
2021 `New playlist: ${chalk.cyan(payload.did)} - ${chalk.greenBright(payload.id)}`,
2122 );
2223 await putPlaylistRecord(ctx, payload);
···3132 const agent = await createAgent(ctx.oauthClient, payload.did);
32333334 if (!agent) {
3434- console.error(
3535+ consola.error(
3536 `Failed to create agent, skipping playlist: ${chalk.cyan(payload.id)} for ${chalk.greenBright(payload.did)}`,
3637 );
3738 return;
···6970 };
70717172 if (!Playlist.validateRecord(record)) {
7272- console.error(`Invalid record: ${chalk.redBright(JSON.stringify(record))}`);
7373+ consola.error(`Invalid record: ${chalk.redBright(JSON.stringify(record))}`);
7374 return;
7475 }
7576···8283 validate: false,
8384 });
8485 const uri = res.data.uri;
8585- console.log(`Playlist record created: ${chalk.greenBright(uri)}`);
8686+ consola.info(`Playlist record created: ${chalk.greenBright(uri)}`);
8687 await ctx.db
8788 .update(tables.playlists)
8889 .set({ uri })
8990 .where(eq(tables.playlists.id, payload.id))
9091 .execute();
9192 } catch (e) {
9292- console.error(`Failed to put record: ${chalk.redBright(e.message)}`);
9393+ consola.error(`Failed to put record: ${chalk.redBright(e.message)}`);
9394 }
94959596 const [updatedPlaylist] = await ctx.db
+209
apps/api/src/subscribers/scrobble.ts
···11+import { consola } from "consola";
22+import type { Context } from "context";
33+import { eq } from "drizzle-orm";
44+import _ from "lodash";
55+import { StringCodec } from "nats";
66+import tables from "schema";
77+88+export function onNewScrobble(ctx: Context) {
99+ const sc = StringCodec();
1010+ const sub = ctx.nc.subscribe("rocksky.scrobble.new");
1111+ (async () => {
1212+ for await (const m of sub) {
1313+ const scrobbleId = sc.decode(m.data);
1414+ const result = await ctx.db
1515+ .select()
1616+ .from(tables.scrobbles)
1717+ .where(eq(tables.scrobbles.id, scrobbleId))
1818+ .execute()
1919+ .then((rows) => rows[0]);
2020+2121+ if (!result) {
2222+ consola.info(`Scrobble with ID ${scrobbleId} not found, skipping`);
2323+ }
2424+ }
2525+ })();
2626+}
2727+2828+/*
2929+import chalk from "chalk";
3030+import { ctx } from "context";
3131+import { desc, eq, or } from "drizzle-orm";
3232+import { createHash } from "node:crypto";
3333+import { publishScrobble } from "nowplaying/nowplaying.service";
3434+import albums from "../schema/albums";
3535+import artists from "../schema/artists";
3636+import scrobbles from "../schema/scrobbles";
3737+import tracks from "../schema/tracks";
3838+import users from "../schema/users";
3939+4040+const args = process.argv.slice(2);
4141+4242+async function updateUris(did: string) {
4343+ // Get scrobbles with track and user data
4444+ const records = await ctx.db
4545+ .select({
4646+ track: tracks,
4747+ user: users,
4848+ })
4949+ .from(scrobbles)
5050+ .innerJoin(tracks, eq(scrobbles.trackId, tracks.id))
5151+ .innerJoin(users, eq(scrobbles.userId, users.id))
5252+ .where(or(eq(users.did, did), eq(users.handle, did)))
5353+ .orderBy(desc(scrobbles.createdAt))
5454+ .limit(process.env.SYNC_SIZE ? parseInt(process.env.SYNC_SIZE, 10) : 20);
5555+5656+ for (const { track } of records) {
5757+ const trackHash = createHash("sha256")
5858+ .update(`${track.title} - ${track.artist} - ${track.album}`.toLowerCase())
5959+ .digest("hex");
6060+6161+ const existingTrack = await ctx.db
6262+ .select()
6363+ .from(tracks)
6464+ .where(eq(tracks.sha256, trackHash))
6565+ .limit(1)
6666+ .then((rows) => rows[0]);
6767+6868+ if (existingTrack && !existingTrack.albumUri) {
6969+ consola.info(`Updating album uri for ${chalk.cyan(track.id)} ...`);
7070+7171+ const albumHash = createHash("sha256")
7272+ .update(`${track.album} - ${track.albumArtist}`.toLowerCase())
7373+ .digest("hex");
7474+7575+ const album = await ctx.db
7676+ .select()
7777+ .from(albums)
7878+ .where(eq(albums.sha256, albumHash))
7979+ .limit(1)
8080+ .then((rows) => rows[0]);
8181+8282+ if (album) {
8383+ await ctx.db
8484+ .update(tracks)
8585+ .set({ albumUri: album.uri })
8686+ .where(eq(tracks.id, existingTrack.id));
8787+ }
8888+ }
8989+9090+ if (existingTrack && !existingTrack.artistUri) {
9191+ consola.info(`Updating artist uri for ${chalk.cyan(track.id)} ...`);
9292+9393+ const artistHash = createHash("sha256")
9494+ .update(track.albumArtist.toLowerCase())
9595+ .digest("hex");
9696+9797+ const artist = await ctx.db
9898+ .select()
9999+ .from(artists)
100100+ .where(eq(artists.sha256, artistHash))
101101+ .limit(1)
102102+ .then((rows) => rows[0]);
103103+104104+ if (artist) {
105105+ await ctx.db
106106+ .update(tracks)
107107+ .set({ artistUri: artist.uri })
108108+ .where(eq(tracks.id, existingTrack.id));
109109+ }
110110+ }
111111+112112+ const albumHash = createHash("sha256")
113113+ .update(`${track.album} - ${track.albumArtist}`.toLowerCase())
114114+ .digest("hex");
115115+116116+ const album = await ctx.db
117117+ .select()
118118+ .from(albums)
119119+ .where(eq(albums.sha256, albumHash))
120120+ .limit(1)
121121+ .then((rows) => rows[0]);
122122+123123+ if (existingTrack && album && !album.artistUri) {
124124+ consola.info(`Updating artist uri for ${chalk.cyan(album.id)} ...`);
125125+126126+ const artistHash = createHash("sha256")
127127+ .update(track.albumArtist.toLowerCase())
128128+ .digest("hex");
129129+130130+ const artist = await ctx.db
131131+ .select()
132132+ .from(artists)
133133+ .where(eq(artists.sha256, artistHash))
134134+ .limit(1)
135135+ .then((rows) => rows[0]);
136136+137137+ if (artist) {
138138+ await ctx.db
139139+ .update(albums)
140140+ .set({ artistUri: artist.uri })
141141+ .where(eq(albums.id, album.id));
142142+ }
143143+ }
144144+ }
145145+}
146146+147147+if (args.includes("--background")) {
148148+ consola.info("Wait for new scrobbles to sync ...");
149149+ const sub = ctx.nc.subscribe("rocksky.user.scrobble.sync");
150150+ for await (const m of sub) {
151151+ const did = new TextDecoder().decode(m.data);
152152+ // wait for 15 seconds to ensure the scrobble is fully created
153153+ await new Promise((resolve) => setTimeout(resolve, 15000));
154154+ consola.info(`Syncing scrobbles ${chalk.magenta(did)} ...`);
155155+ await updateUris(did);
156156+157157+ const records = await ctx.db
158158+ .select({
159159+ scrobble: scrobbles,
160160+ })
161161+ .from(scrobbles)
162162+ .innerJoin(users, eq(scrobbles.userId, users.id))
163163+ .where(or(eq(users.did, did), eq(users.handle, did)))
164164+ .orderBy(desc(scrobbles.createdAt))
165165+ .limit(5);
166166+167167+ for (const { scrobble } of records) {
168168+ consola.info(`Syncing scrobble ${chalk.cyan(scrobble.id)} ...`);
169169+ try {
170170+ await publishScrobble(ctx, scrobble.id);
171171+ } catch (err) {
172172+ consola.error(
173173+ `Failed to sync scrobble ${chalk.cyan(scrobble.id)}:`,
174174+ err,
175175+ );
176176+ }
177177+ }
178178+ }
179179+ process.exit(0);
180180+}
181181+182182+for (const arg of args) {
183183+ consola.info(`Syncing scrobbles ${chalk.magenta(arg)} ...`);
184184+ await updateUris(arg);
185185+186186+ const records = await ctx.db
187187+ .select({
188188+ scrobble: scrobbles,
189189+ })
190190+ .from(scrobbles)
191191+ .innerJoin(users, eq(scrobbles.userId, users.id))
192192+ .where(or(eq(users.did, arg), eq(users.handle, arg)))
193193+ .orderBy(desc(scrobbles.createdAt))
194194+ .limit(process.env.SYNC_SIZE ? parseInt(process.env.SYNC_SIZE, 10) : 20);
195195+196196+ for (const { scrobble } of records) {
197197+ consola.info(`Syncing scrobble ${chalk.cyan(scrobble.id)} ...`);
198198+ try {
199199+ await publishScrobble(ctx, scrobble.id);
200200+ } catch (err) {
201201+ consola.error(`Failed to sync scrobble ${chalk.cyan(scrobble.id)}:`, err);
202202+ }
203203+ }
204204+ consola.info(`Synced ${chalk.greenBright(records.length)} scrobbles`);
205205+}
206206+207207+process.exit(0);
208208+209209+*/
+2-1
apps/api/src/subscribers/track.ts
···11import chalk from "chalk";
22+import { consola } from "consola";
23import type { Context } from "context";
34import { eq } from "drizzle-orm";
45import _ from "lodash";
···3637 .execute(),
3738 ]);
38393939- console.log(`New track: ${chalk.cyan(_.get(tracks, "0.title"))}`);
4040+ consola.info(`New track: ${chalk.cyan(_.get(tracks, "0.title"))}`);
40414142 await Promise.all([
4243 ctx.meilisearch.post(`indexes/albums/documents?primaryKey=id`, albums),
+2-1
apps/api/src/subscribers/user.ts
···11import chalk from "chalk";
22+import { consola } from "consola";
23import type { Context } from "context";
34import { eq } from "drizzle-orm";
45import _ from "lodash";
···1920 .where(eq(tables.users.id, payload.xata_id))
2021 .execute();
21222222- console.log(`New user: ${chalk.cyan(_.get(results, "0.handle"))}`);
2323+ consola.info(`New user: ${chalk.cyan(_.get(results, "0.handle"))}`);
23242425 await ctx.meilisearch.post(
2526 `/indexes/users/documents?primaryKey=id`,
+10-7
apps/api/src/tealfm/index.ts
···11import type { Agent } from "@atproto/api";
22import { TID } from "@atproto/common";
33import chalk from "chalk";
44+import { consola } from "consola";
45import type * as Status from "lexicon/types/fm/teal/alpha/actor/status";
56import type { PlayView } from "lexicon/types/fm/teal/alpha/feed/defs";
67import * as Play from "lexicon/types/fm/teal/alpha/feed/play";
···2425 duration: number,
2526) {
2627 if (env.DISABLED_TEALFM.includes(agent.assertDid)) {
2727- console.log(`teal.fm is disabled for ${chalk.cyanBright(agent.assertDid)}`);
2828+ consola.info(
2929+ `teal.fm is disabled for ${chalk.cyanBright(agent.assertDid)}`,
3030+ );
2831 return;
2932 }
3033···4750 );
4851 });
4952 if (alreadyPlayed) {
5050- console.log(
5353+ consola.info(
5154 `Track ${chalk.cyan(track.name)} by ${chalk.cyan(
5255 track.artist.map((a) => a.name).join(", "),
5356 )} already played recently. Skipping...`,
···7275 };
73767477 if (!Play.validateRecord(record).success) {
7575- console.log(Play.validateRecord(record));
7676- console.log(chalk.cyan(JSON.stringify(record, null, 2)));
7878+ consola.info(Play.validateRecord(record));
7979+ consola.info(chalk.cyan(JSON.stringify(record, null, 2)));
7780 throw new Error("Invalid record");
7881 }
7982···8588 validate: false,
8689 });
8790 const uri = res.data.uri;
8888- console.log(`tealfm Play record created at ${uri}`);
9191+ consola.info(`tealfm Play record created at ${uri}`);
89929093 await publishStatus(agent, track, duration);
9194 } catch (error) {
9292- console.error("Error publishing teal.fm record:", error);
9595+ consola.error("Error publishing teal.fm record:", error);
9396 }
9497}
9598···127130 record,
128131 swapRecord,
129132 });
130130- console.log(`tealfm Status record published at ${res.data.uri}`);
133133+ consola.info(`tealfm Status record published at ${res.data.uri}`);
131134}
132135133136async function getStatusSwapRecord(agent: Agent): Promise<string | undefined> {
+15-14
apps/api/src/tracks/tracks.service.ts
···11import type { Agent } from "@atproto/api";
22+import { consola } from "consola";
23import type { Context } from "context";
34import { and, eq } from "drizzle-orm";
45import { deepSnakeCaseKeys } from "lib";
···130131 .then((results) => results[0]);
131132132133 if (!track_id || !album_id || !artist_id) {
133133- console.log(
134134+ consola.info(
134135 "Track not yet saved (uri not saved), retrying...",
135136 tries + 1,
136137 );
···218219 track_id.albumUri &&
219220 track_id.artistUri
220221 ) {
221221- console.log("Track saved successfully after", tries + 1, "tries");
222222+ consola.info("Track saved successfully after", tries + 1, "tries");
222223223224 const message = JSON.stringify(
224225 deepSnakeCaseKeys({
···275276 }
276277277278 tries += 1;
278278- console.log("Track not yet saved, retrying...", tries + 1);
279279+ consola.info("Track not yet saved, retrying...", tries + 1);
279280 if (tries === 15) {
280280- console.log(">>>");
281281- console.log(album_track);
282282- console.log(artist_track);
283283- console.log(artist_album);
284284- console.log(artist_id);
285285- console.log(album_id);
286286- console.log(track_id);
287287- console.log(track_id.albumUri);
288288- console.log(track_id.artistUri);
289289- console.log("<<<");
281281+ consola.info(">>>");
282282+ consola.info(album_track);
283283+ consola.info(artist_track);
284284+ consola.info(artist_album);
285285+ consola.info(artist_id);
286286+ consola.info(album_id);
287287+ consola.info(track_id);
288288+ consola.info(track_id.albumUri);
289289+ consola.info(track_id.artistUri);
290290+ consola.info("<<<");
290291 }
291292 await new Promise((resolve) => setTimeout(resolve, 1000));
292293 }
293294294295 if (tries === 15) {
295295- console.log("Failed to save track after 15 tries");
296296+ consola.info("Failed to save track after 15 tries");
296297 }
297298}