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 321 322 322 return response.json(); 323 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 + } 324 343 }
+11 -6
apps/cli/src/index.ts
··· 22 22 program 23 23 .name("rocksky") 24 24 .description( 25 - `Command-line interface for Rocksky (${chalk.underline( 26 - "https://rocksky.app", 27 - )}) – scrobble tracks, view stats, and manage your listening history.`, 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.")}`, 28 34 ) 29 35 .version(version); 30 36 31 37 program.configureHelp({ 32 38 styleTitle: (str) => chalk.bold.cyan(str), 33 39 styleCommandText: (str) => chalk.yellow(str), 34 - styleCommandDescription: (str) => chalk.gray(str), 35 40 styleDescriptionText: (str) => chalk.white(str), 36 41 styleOptionText: (str) => chalk.green(str), 37 42 styleArgumentText: (str) => chalk.magenta(str), ··· 134 139 135 140 program 136 141 .command("mcp") 137 - .description("Starts an MCP server to use with Claude or other LLMs.") 142 + .description("starts an MCP server to use with Claude or other LLMs.") 138 143 .action(mcp); 139 144 140 145 program 141 146 .command("sync") 142 - .description("Sync your local Rocksky data from AT Protocol.") 147 + .description("sync your local Rocksky data from AT Protocol.") 143 148 .action(sync); 144 149 145 150 program.parse(process.argv);
+30 -1
apps/cli/src/lib/matchTrack.ts
··· 1 + import { RockskyClient } from "client"; 1 2 import { ctx } from "context"; 2 3 import { eq, and, or } from "drizzle-orm"; 3 4 import { logger } from "logger"; ··· 7 8 const [result] = await ctx.db 8 9 .select() 9 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 + ) 10 24 .where( 11 25 or( 12 26 and(eq(schema.tracks.title, track), eq(schema.tracks.artist, artist)), ··· 17 31 ), 18 32 ) 19 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 + } 20 49 logger.info`>> matchTrack ${track}, ${artist}`; 21 - logger.info`>> matchTrack result \n ${result}`; 50 + logger.info`${match}`; 22 51 }