A decentralized music tracking and discovery platform built on AT Protocol 🎵

Add song matching with DB joins and API fallback

Add RockskyClient.matchSong to query remote API as fallback. Extend
track lookup with album/artist left joins and include genres, artist
picture, release date and year in the match result. Log the resolved
match instead of the raw DB row. Update CLI description/help styling
(ASCII banner and minor command text tweaks)

+60 -7
+19
apps/cli/src/client.ts
··· 321 322 return response.json(); 323 } 324 }
··· 321 322 return response.json(); 323 } 324 + 325 + async matchSong(title: string, artist: string) { 326 + const q = new URLSearchParams({ 327 + title, 328 + artist, 329 + }); 330 + console.log(q); 331 + const response = await fetch( 332 + `${ROCKSKY_API_URL}/xrpc/app.rocksky.song.matchSong?${q.toString()}`, 333 + ); 334 + 335 + if (!response.ok) { 336 + throw new Error( 337 + `Failed to match song: ${response.statusText} ${await response.text()}`, 338 + ); 339 + } 340 + 341 + return response.json(); 342 + } 343 }
+11 -6
apps/cli/src/index.ts
··· 22 program 23 .name("rocksky") 24 .description( 25 - `Command-line interface for Rocksky (${chalk.underline( 26 - "https://rocksky.app", 27 - )}) – scrobble tracks, view stats, and manage your listening history.`, 28 ) 29 .version(version); 30 31 program.configureHelp({ 32 styleTitle: (str) => chalk.bold.cyan(str), 33 styleCommandText: (str) => chalk.yellow(str), 34 - styleCommandDescription: (str) => chalk.gray(str), 35 styleDescriptionText: (str) => chalk.white(str), 36 styleOptionText: (str) => chalk.green(str), 37 styleArgumentText: (str) => chalk.magenta(str), ··· 134 135 program 136 .command("mcp") 137 - .description("Starts an MCP server to use with Claude or other LLMs.") 138 .action(mcp); 139 140 program 141 .command("sync") 142 - .description("Sync your local Rocksky data from AT Protocol.") 143 .action(sync); 144 145 program.parse(process.argv);
··· 22 program 23 .name("rocksky") 24 .description( 25 + ` 26 + ___ __ __ _______ ____ 27 + / _ \\___ ____/ /__ ___ / /____ __ / ___/ / / _/ 28 + / , _/ _ \\/ __/ '_/(_-</ '_/ // / / /__/ /___/ / 29 + /_/|_|\\___/\\__/_/\\_\\/___/_/\\_\\\\_, / \\___/____/___/ 30 + /___/ 31 + ${chalk.gray("Command-line interface for Rocksky (")}${chalk.gray.underline( 32 + "https://rocksky.app", 33 + )}${chalk.gray(")")} ${chalk.gray("– scrobble tracks, view stats, and manage your listening history.")}`, 34 ) 35 .version(version); 36 37 program.configureHelp({ 38 styleTitle: (str) => chalk.bold.cyan(str), 39 styleCommandText: (str) => chalk.yellow(str), 40 styleDescriptionText: (str) => chalk.white(str), 41 styleOptionText: (str) => chalk.green(str), 42 styleArgumentText: (str) => chalk.magenta(str), ··· 139 140 program 141 .command("mcp") 142 + .description("starts an MCP server to use with Claude or other LLMs.") 143 .action(mcp); 144 145 program 146 .command("sync") 147 + .description("sync your local Rocksky data from AT Protocol.") 148 .action(sync); 149 150 program.parse(process.argv);
+30 -1
apps/cli/src/lib/matchTrack.ts
··· 1 import { ctx } from "context"; 2 import { eq, and, or } from "drizzle-orm"; 3 import { logger } from "logger"; ··· 7 const [result] = await ctx.db 8 .select() 9 .from(schema.tracks) 10 .where( 11 or( 12 and(eq(schema.tracks.title, track), eq(schema.tracks.artist, artist)), ··· 17 ), 18 ) 19 .execute(); 20 logger.info`>> matchTrack ${track}, ${artist}`; 21 - logger.info`>> matchTrack result \n ${result}`; 22 }
··· 1 + import { RockskyClient } from "client"; 2 import { ctx } from "context"; 3 import { eq, and, or } from "drizzle-orm"; 4 import { logger } from "logger"; ··· 8 const [result] = await ctx.db 9 .select() 10 .from(schema.tracks) 11 + .leftJoin( 12 + schema.albumTracks, 13 + eq(schema.albumTracks.trackId, schema.tracks.id), 14 + ) 15 + .leftJoin(schema.albums, eq(schema.albumTracks.albumId, schema.albums.id)) 16 + .leftJoin( 17 + schema.artistAlbums, 18 + eq(schema.artistAlbums.albumId, schema.albums.id), 19 + ) 20 + .leftJoin( 21 + schema.artists, 22 + eq(schema.artistAlbums.artistId, schema.artists.id), 23 + ) 24 .where( 25 or( 26 and(eq(schema.tracks.title, track), eq(schema.tracks.artist, artist)), ··· 31 ), 32 ) 33 .execute(); 34 + 35 + let match = null; 36 + 37 + if (result) { 38 + match = { 39 + ...result.tracks, 40 + genres: result.artists?.genres, 41 + artistPicture: result.artists?.picture, 42 + releaseDate: result.albums?.releaseDate, 43 + year: result.albums?.year, 44 + }; 45 + } else { 46 + const client = new RockskyClient(); 47 + match = await client.matchSong(track, artist); 48 + } 49 logger.info`>> matchTrack ${track}, ${artist}`; 50 + logger.info`${match}`; 51 }