A decentralized music tracking and discovery platform built on AT Protocol 🎵
listenbrainz spotify atproto lastfm musicbrainz scrobbling

feat(musicbrainz): add MusicBrainz service with search and hydrate endpoints

- Implemented main server logic in musicbrainz/main.go
- Added searchHandler and hydrateHandler for handling requests
- Integrated database initialization and context-based shutdown
- Updated package.json to include a script for running the MusicBrainz service

+2 -1
.dockerignore
··· 6 6 Dockerfile 7 7 *.ddb 8 8 *.ddb.wal 9 - rocksky-backup.sql 9 + rocksky-backup.sql 10 + *.db
+2 -1
.gitignore
··· 7 7 *.ddb.wal 8 8 .vscode/ 9 9 *.DS_Store 10 - rocksky-backup.sql 10 + rocksky-backup.sql 11 + *.db
+6 -1
README.md
··· 47 47 48 48 - Node.js (v22 or later) 49 49 - Rust 50 + - Go 50 51 - Turbo 51 52 - Docker 52 53 - Wasm Pack https://rustwasm.github.io/wasm-pack/installer/ ··· 92 93 ```bash 93 94 bun run dev:jetstream 94 95 ``` 95 - 9. Start the development server: 96 + 9. Start musicbrainz: 97 + ```bash 98 + bun run mb 99 + ``` 100 + 10. Start the development server: 96 101 ```bash 97 102 turbo dev --filter=@rocksky/api --filter=@rocksky/web 98 103 ```
+1 -1
apps/api/package.json
··· 5 5 "type": "module", 6 6 "module": "dist/index.js", 7 7 "scripts": { 8 - "lexgen": "lex gen-server ./src/lexicon ./lexicons/**/* ./lexicons/*", 8 + "lexgen": "lex gen-server ./src/lexicon ./src/tealfm/lexicons/teal/**/* ./lexicons/**/* ./lexicons/* ./src/tealfm/lexicons/**/*", 9 9 "dev": "concurrently 'tsx --watch ./src/index.ts' 'tsx --watch ./src/server.ts'", 10 10 "prod": "tsx ./src/index.ts", 11 11 "build": "pkgroll",
+12 -9
apps/api/src/bsky/app.ts
··· 81 81 ? Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365 * 1000 82 82 : Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, 83 83 }, 84 - env.JWT_SECRET 84 + env.JWT_SECRET, 85 85 ); 86 86 ctx.kv.set(did, token); 87 87 } catch (err) { ··· 93 93 .select() 94 94 .from(spotifyAccounts) 95 95 .where( 96 - and(eq(spotifyAccounts.userId, did), eq(spotifyAccounts.isBetaUser, true)) 96 + and( 97 + eq(spotifyAccounts.userId, did), 98 + eq(spotifyAccounts.isBetaUser, true), 99 + ), 97 100 ) 98 101 .limit(1) 99 102 .execute(); ··· 179 182 180 183 ctx.nc.publish( 181 184 "rocksky.user", 182 - Buffer.from(JSON.stringify(deepSnakeCaseKeys(user))) 185 + Buffer.from(JSON.stringify(deepSnakeCaseKeys(user))), 183 186 ); 184 187 185 188 await ctx.kv.set("lastUser", lastUser[0].id); ··· 192 195 .where( 193 196 and( 194 197 eq(spotifyAccounts.userId, did), 195 - eq(spotifyAccounts.isBetaUser, true) 196 - ) 198 + eq(spotifyAccounts.isBetaUser, true), 199 + ), 197 200 ) 198 201 .limit(1) 199 202 .execute(), ··· 209 212 .where( 210 213 and( 211 214 eq(googleDriveAccounts.userId, did), 212 - eq(googleDriveAccounts.isBetaUser, true) 213 - ) 215 + eq(googleDriveAccounts.isBetaUser, true), 216 + ), 214 217 ) 215 218 .limit(1) 216 219 .execute(), ··· 220 223 .where( 221 224 and( 222 225 eq(dropboxAccounts.userId, did), 223 - eq(dropboxAccounts.isBetaUser, true) 224 - ) 226 + eq(dropboxAccounts.isBetaUser, true), 227 + ), 225 228 ) 226 229 .limit(1) 227 230 .execute(),
+1
apps/api/src/context.ts
··· 31 31 dropbox: axios.create({ baseURL: env.DROPBOX }), 32 32 googledrive: axios.create({ baseURL: env.GOOGLE_DRIVE }), 33 33 tracklist: axios.create({ baseURL: env.TRACKLIST }), 34 + musicbrainz: axios.create({ baseURL: env.MUSICBRAINZ_URL }), 34 35 redis: await redis 35 36 .createClient({ url: env.REDIS_URL }) 36 37 .on("error", (err) => {
+6 -6
apps/api/src/dropbox/app.ts
··· 55 55 client_id: env.DROPBOX_CLIENT_ID, 56 56 client_secret: env.DROPBOX_CLIENT_SECRET, 57 57 redirect_uri: env.DROPBOX_REDIRECT_URI, 58 - } 58 + }, 59 59 ); 60 60 61 61 const { dropbox, dropbox_tokens } = await ctx.db ··· 64 64 .where(eq(tables.dropbox.userId, entries.state)) 65 65 .leftJoin( 66 66 tables.dropboxTokens, 67 - eq(tables.dropboxTokens.id, tables.dropbox.dropboxTokenId) 67 + eq(tables.dropboxTokens.id, tables.dropbox.dropboxTokenId), 68 68 ) 69 69 .limit(1) 70 70 .execute() ··· 76 76 id: dropbox_tokens?.id, 77 77 refreshToken: encrypt( 78 78 response.data.refresh_token, 79 - env.SPOTIFY_ENCRYPTION_KEY 79 + env.SPOTIFY_ENCRYPTION_KEY, 80 80 ), 81 81 }) 82 82 .onConflictDoUpdate({ ··· 84 84 set: { 85 85 refreshToken: encrypt( 86 86 response.data.refresh_token, 87 - env.SPOTIFY_ENCRYPTION_KEY 87 + env.SPOTIFY_ENCRYPTION_KEY, 88 88 ), 89 89 }, 90 90 }) ··· 377 377 378 378 c.header( 379 379 "Content-Type", 380 - response.headers["content-type"] || "application/octet-stream" 380 + response.headers["content-type"] || "application/octet-stream", 381 381 ); 382 382 c.header( 383 383 "Content-Disposition", 384 - response.headers["content-disposition"] || "attachment" 384 + response.headers["content-disposition"] || "attachment", 385 385 ); 386 386 387 387 return new Response(response.data, {
+12 -12
apps/api/src/googledrive/app.ts
··· 42 42 } 43 43 44 44 const credentials = JSON.parse( 45 - fs.readFileSync("credentials.json").toString("utf-8") 45 + fs.readFileSync("credentials.json").toString("utf-8"), 46 46 ); 47 47 const { client_id, client_secret } = credentials.installed || credentials.web; 48 48 const oAuth2Client = new google.auth.OAuth2( 49 49 client_id, 50 50 client_secret, 51 - env.GOOGLE_REDIRECT_URI 51 + env.GOOGLE_REDIRECT_URI, 52 52 ); 53 53 54 54 // Generate Auth URL ··· 70 70 const entries = Object.fromEntries(params.entries()); 71 71 72 72 const credentials = JSON.parse( 73 - fs.readFileSync("credentials.json").toString("utf-8") 73 + fs.readFileSync("credentials.json").toString("utf-8"), 74 74 ); 75 75 const { client_id, client_secret } = credentials.installed || credentials.web; 76 76 ··· 92 92 .innerJoin(users, eq(googleDrive.userId, users.id)) 93 93 .leftJoin( 94 94 googleDriveTokens, 95 - eq(googleDrive.googleDriveTokenId, googleDriveTokens.id) 95 + eq(googleDrive.googleDriveTokenId, googleDriveTokens.id), 96 96 ) 97 97 .where(eq(users.id, entries.state)) 98 98 .limit(1) ··· 105 105 .set({ 106 106 refreshToken: encrypt( 107 107 response.data.refresh_token, 108 - env.SPOTIFY_ENCRYPTION_KEY 108 + env.SPOTIFY_ENCRYPTION_KEY, 109 109 ), 110 110 }) 111 111 .where(eq(googleDriveTokens.id, existingGoogleDrive.token.id)) ··· 117 117 .values({ 118 118 refreshToken: encrypt( 119 119 response.data.refresh_token, 120 - env.SPOTIFY_ENCRYPTION_KEY 120 + env.SPOTIFY_ENCRYPTION_KEY, 121 121 ), 122 122 }) 123 123 .returning(); ··· 237 237 { 238 238 did, 239 239 parent_id, 240 - } 240 + }, 241 241 ); 242 242 return c.json(data); 243 243 } ··· 258 258 { 259 259 did, 260 260 parent_id: response.data.files[0].id, 261 - } 261 + }, 262 262 ); 263 263 return c.json(data); 264 264 } catch (error) { ··· 266 266 console.error("Axios error:", error.response?.data || error.message); 267 267 268 268 const credentials = JSON.parse( 269 - fs.readFileSync("credentials.json").toString("utf-8") 269 + fs.readFileSync("credentials.json").toString("utf-8"), 270 270 ); 271 271 const { client_id, client_secret } = 272 272 credentials.installed || credentials.web; 273 273 const oAuth2Client = new google.auth.OAuth2( 274 274 client_id, 275 275 client_secret, 276 - env.GOOGLE_REDIRECT_URI 276 + env.GOOGLE_REDIRECT_URI, 277 277 ); 278 278 279 279 // Generate Auth URL ··· 362 362 363 363 c.header( 364 364 "Content-Type", 365 - response.headers["content-type"] || "application/octet-stream" 365 + response.headers["content-type"] || "application/octet-stream", 366 366 ); 367 367 c.header( 368 368 "Content-Disposition", 369 - response.headers["content-disposition"] || "attachment" 369 + response.headers["content-disposition"] || "attachment", 370 370 ); 371 371 372 372 return new Response(response.data, {
+3 -3
apps/api/src/index.ts
··· 47 47 rateLimiter({ 48 48 limit: 1000, 49 49 window: 30, // 👈 30 seconds 50 - }) 50 + }), 51 51 ); 52 52 53 53 app.use("*", async (c, next) => { ··· 162 162 ctx.redis.get(`nowplaying:${user.did}:status`), 163 163 ]); 164 164 return c.json( 165 - nowPlaying ? { ...JSON.parse(nowPlaying), is_playing: status === "1" } : {} 165 + nowPlaying ? { ...JSON.parse(nowPlaying), is_playing: status === "1" } : {}, 166 166 ); 167 167 }); 168 168 ··· 314 314 listeners: 1, 315 315 sha256: item.track.sha256, 316 316 id: item.scrobble.id, 317 - })) 317 + })), 318 318 ); 319 319 }); 320 320
+110
apps/api/src/lexicon/index.ts
··· 9 9 type StreamAuthVerifier, 10 10 } from "@atproto/xrpc-server"; 11 11 import { schemas } from "./lexicons"; 12 + import type * as FmTealAlphaActorGetProfile from "./types/fm/teal/alpha/actor/getProfile"; 13 + import type * as FmTealAlphaActorGetProfiles from "./types/fm/teal/alpha/actor/getProfiles"; 14 + import type * as FmTealAlphaActorSearchActors from "./types/fm/teal/alpha/actor/searchActors"; 15 + import type * as FmTealAlphaFeedGetActorFeed from "./types/fm/teal/alpha/feed/getActorFeed"; 16 + import type * as FmTealAlphaFeedGetPlay from "./types/fm/teal/alpha/feed/getPlay"; 12 17 import type * as AppRockskyActorGetActorAlbums from "./types/app/rocksky/actor/getActorAlbums"; 13 18 import type * as AppRockskyActorGetActorArtists from "./types/app/rocksky/actor/getActorArtists"; 14 19 import type * as AppRockskyActorGetActorLovedSongs from "./types/app/rocksky/actor/getActorLovedSongs"; ··· 90 95 91 96 export class Server { 92 97 xrpc: XrpcServer; 98 + fm: FmNS; 93 99 app: AppNS; 94 100 com: ComNS; 95 101 96 102 constructor(options?: XrpcOptions) { 97 103 this.xrpc = createXrpcServer(schemas, options); 104 + this.fm = new FmNS(this); 98 105 this.app = new AppNS(this); 99 106 this.com = new ComNS(this); 107 + } 108 + } 109 + 110 + export class FmNS { 111 + _server: Server; 112 + teal: FmTealNS; 113 + 114 + constructor(server: Server) { 115 + this._server = server; 116 + this.teal = new FmTealNS(server); 117 + } 118 + } 119 + 120 + export class FmTealNS { 121 + _server: Server; 122 + alpha: FmTealAlphaNS; 123 + 124 + constructor(server: Server) { 125 + this._server = server; 126 + this.alpha = new FmTealAlphaNS(server); 127 + } 128 + } 129 + 130 + export class FmTealAlphaNS { 131 + _server: Server; 132 + actor: FmTealAlphaActorNS; 133 + feed: FmTealAlphaFeedNS; 134 + 135 + constructor(server: Server) { 136 + this._server = server; 137 + this.actor = new FmTealAlphaActorNS(server); 138 + this.feed = new FmTealAlphaFeedNS(server); 139 + } 140 + } 141 + 142 + export class FmTealAlphaActorNS { 143 + _server: Server; 144 + 145 + constructor(server: Server) { 146 + this._server = server; 147 + } 148 + 149 + getProfile<AV extends AuthVerifier>( 150 + cfg: ConfigOf< 151 + AV, 152 + FmTealAlphaActorGetProfile.Handler<ExtractAuth<AV>>, 153 + FmTealAlphaActorGetProfile.HandlerReqCtx<ExtractAuth<AV>> 154 + >, 155 + ) { 156 + const nsid = "fm.teal.alpha.actor.getProfile"; // @ts-ignore 157 + return this._server.xrpc.method(nsid, cfg); 158 + } 159 + 160 + getProfiles<AV extends AuthVerifier>( 161 + cfg: ConfigOf< 162 + AV, 163 + FmTealAlphaActorGetProfiles.Handler<ExtractAuth<AV>>, 164 + FmTealAlphaActorGetProfiles.HandlerReqCtx<ExtractAuth<AV>> 165 + >, 166 + ) { 167 + const nsid = "fm.teal.alpha.actor.getProfiles"; // @ts-ignore 168 + return this._server.xrpc.method(nsid, cfg); 169 + } 170 + 171 + searchActors<AV extends AuthVerifier>( 172 + cfg: ConfigOf< 173 + AV, 174 + FmTealAlphaActorSearchActors.Handler<ExtractAuth<AV>>, 175 + FmTealAlphaActorSearchActors.HandlerReqCtx<ExtractAuth<AV>> 176 + >, 177 + ) { 178 + const nsid = "fm.teal.alpha.actor.searchActors"; // @ts-ignore 179 + return this._server.xrpc.method(nsid, cfg); 180 + } 181 + } 182 + 183 + export class FmTealAlphaFeedNS { 184 + _server: Server; 185 + 186 + constructor(server: Server) { 187 + this._server = server; 188 + } 189 + 190 + getActorFeed<AV extends AuthVerifier>( 191 + cfg: ConfigOf< 192 + AV, 193 + FmTealAlphaFeedGetActorFeed.Handler<ExtractAuth<AV>>, 194 + FmTealAlphaFeedGetActorFeed.HandlerReqCtx<ExtractAuth<AV>> 195 + >, 196 + ) { 197 + const nsid = "fm.teal.alpha.feed.getActorFeed"; // @ts-ignore 198 + return this._server.xrpc.method(nsid, cfg); 199 + } 200 + 201 + getPlay<AV extends AuthVerifier>( 202 + cfg: ConfigOf< 203 + AV, 204 + FmTealAlphaFeedGetPlay.Handler<ExtractAuth<AV>>, 205 + FmTealAlphaFeedGetPlay.HandlerReqCtx<ExtractAuth<AV>> 206 + >, 207 + ) { 208 + const nsid = "fm.teal.alpha.feed.getPlay"; // @ts-ignore 209 + return this._server.xrpc.method(nsid, cfg); 100 210 } 101 211 } 102 212
+594
apps/api/src/lexicon/lexicons.ts
··· 4 4 import { type LexiconDoc, Lexicons } from "@atproto/lexicon"; 5 5 6 6 export const schemaDict = { 7 + FmTealAlphaActorDefs: { 8 + lexicon: 1, 9 + id: "fm.teal.alpha.actor.defs", 10 + defs: { 11 + profileView: { 12 + type: "object", 13 + properties: { 14 + did: { 15 + type: "string", 16 + description: "The decentralized identifier of the actor", 17 + }, 18 + displayName: { 19 + type: "string", 20 + }, 21 + description: { 22 + type: "string", 23 + description: "Free-form profile description text.", 24 + }, 25 + descriptionFacets: { 26 + type: "array", 27 + description: 28 + "Annotations of text in the profile description (mentions, URLs, hashtags, etc). May be changed to another (backwards compatible) lexicon.", 29 + items: { 30 + type: "ref", 31 + ref: "lex:app.bsky.richtext.facet", 32 + }, 33 + }, 34 + featuredItem: { 35 + type: "ref", 36 + description: 37 + "The user's most recent item featured on their profile.", 38 + ref: "lex:fm.teal.alpha.actor.profile#featuredItem", 39 + }, 40 + avatar: { 41 + type: "string", 42 + description: "IPLD of the avatar", 43 + }, 44 + banner: { 45 + type: "string", 46 + description: "IPLD of the banner image", 47 + }, 48 + createdAt: { 49 + type: "string", 50 + format: "datetime", 51 + }, 52 + }, 53 + }, 54 + miniProfileView: { 55 + type: "object", 56 + properties: { 57 + did: { 58 + type: "string", 59 + description: "The decentralized identifier of the actor", 60 + }, 61 + displayName: { 62 + type: "string", 63 + }, 64 + handle: { 65 + type: "string", 66 + }, 67 + avatar: { 68 + type: "string", 69 + description: "IPLD of the avatar", 70 + }, 71 + }, 72 + }, 73 + }, 74 + }, 75 + FmTealAlphaActorGetProfile: { 76 + lexicon: 1, 77 + id: "fm.teal.alpha.actor.getProfile", 78 + description: 79 + "This lexicon is in a not officially released state. It is subject to change. | Retrieves a play given an author DID and record key.", 80 + defs: { 81 + main: { 82 + type: "query", 83 + parameters: { 84 + type: "params", 85 + required: ["actor"], 86 + properties: { 87 + actor: { 88 + type: "string", 89 + format: "at-identifier", 90 + description: "The author's DID", 91 + }, 92 + }, 93 + }, 94 + output: { 95 + encoding: "application/json", 96 + schema: { 97 + type: "object", 98 + required: ["actor"], 99 + properties: { 100 + actor: { 101 + type: "ref", 102 + ref: "lex:fm.teal.alpha.actor.defs#profileView", 103 + }, 104 + }, 105 + }, 106 + }, 107 + }, 108 + }, 109 + }, 110 + FmTealAlphaActorGetProfiles: { 111 + lexicon: 1, 112 + id: "fm.teal.alpha.actor.getProfiles", 113 + description: 114 + "This lexicon is in a not officially released state. It is subject to change. | Retrieves the associated profile.", 115 + defs: { 116 + main: { 117 + type: "query", 118 + parameters: { 119 + type: "params", 120 + required: ["actors"], 121 + properties: { 122 + actors: { 123 + type: "array", 124 + items: { 125 + type: "string", 126 + format: "at-identifier", 127 + }, 128 + description: "Array of actor DIDs", 129 + }, 130 + }, 131 + }, 132 + output: { 133 + encoding: "application/json", 134 + schema: { 135 + type: "object", 136 + required: ["actors"], 137 + properties: { 138 + actors: { 139 + type: "array", 140 + items: { 141 + type: "ref", 142 + ref: "lex:fm.teal.alpha.actor.defs#miniProfileView", 143 + }, 144 + }, 145 + }, 146 + }, 147 + }, 148 + }, 149 + }, 150 + }, 151 + FmTealAlphaActorProfile: { 152 + lexicon: 1, 153 + id: "fm.teal.alpha.actor.profile", 154 + defs: { 155 + main: { 156 + type: "record", 157 + description: 158 + "This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm account profile.", 159 + key: "literal:self", 160 + record: { 161 + type: "object", 162 + properties: { 163 + displayName: { 164 + type: "string", 165 + maxGraphemes: 64, 166 + maxLength: 640, 167 + }, 168 + description: { 169 + type: "string", 170 + description: "Free-form profile description text.", 171 + maxGraphemes: 256, 172 + maxLength: 2560, 173 + }, 174 + descriptionFacets: { 175 + type: "array", 176 + description: 177 + "Annotations of text in the profile description (mentions, URLs, hashtags, etc).", 178 + items: { 179 + type: "ref", 180 + ref: "lex:app.bsky.richtext.facet", 181 + }, 182 + }, 183 + featuredItem: { 184 + type: "ref", 185 + description: 186 + "The user's most recent item featured on their profile.", 187 + ref: "lex:fm.teal.alpha.actor.profile#featuredItem", 188 + }, 189 + avatar: { 190 + type: "blob", 191 + description: 192 + "Small image to be displayed next to posts from account. AKA, 'profile picture'", 193 + accept: ["image/png", "image/jpeg"], 194 + maxSize: 1000000, 195 + }, 196 + banner: { 197 + type: "blob", 198 + description: 199 + "Larger horizontal image to display behind profile view.", 200 + accept: ["image/png", "image/jpeg"], 201 + maxSize: 1000000, 202 + }, 203 + createdAt: { 204 + type: "string", 205 + format: "datetime", 206 + }, 207 + }, 208 + }, 209 + }, 210 + featuredItem: { 211 + type: "object", 212 + required: ["mbid", "type"], 213 + properties: { 214 + mbid: { 215 + type: "string", 216 + description: "The Musicbrainz ID of the item", 217 + }, 218 + type: { 219 + type: "string", 220 + description: 221 + "The type of the item. Must be a valid Musicbrainz type, e.g. album, track, recording, etc.", 222 + }, 223 + }, 224 + }, 225 + }, 226 + }, 227 + FmTealAlphaActorSearchActors: { 228 + lexicon: 1, 229 + id: "fm.teal.alpha.actor.searchActors", 230 + description: 231 + "This lexicon is in a not officially released state. It is subject to change. | Searches for actors based on profile contents.", 232 + defs: { 233 + main: { 234 + type: "query", 235 + parameters: { 236 + type: "params", 237 + required: ["q"], 238 + properties: { 239 + q: { 240 + type: "string", 241 + description: "The search query", 242 + maxGraphemes: 128, 243 + maxLength: 640, 244 + }, 245 + limit: { 246 + type: "integer", 247 + description: "The maximum number of actors to return", 248 + minimum: 1, 249 + maximum: 25, 250 + }, 251 + cursor: { 252 + type: "string", 253 + description: "Cursor for pagination", 254 + }, 255 + }, 256 + }, 257 + output: { 258 + encoding: "application/json", 259 + schema: { 260 + type: "object", 261 + required: ["actors"], 262 + properties: { 263 + actors: { 264 + type: "array", 265 + items: { 266 + type: "ref", 267 + ref: "lex:fm.teal.alpha.actor.defs#miniProfileView", 268 + }, 269 + }, 270 + cursor: { 271 + type: "string", 272 + description: "Cursor for pagination", 273 + }, 274 + }, 275 + }, 276 + }, 277 + }, 278 + }, 279 + }, 280 + FmTealAlphaActorStatus: { 281 + lexicon: 1, 282 + id: "fm.teal.alpha.actor.status", 283 + defs: { 284 + main: { 285 + type: "record", 286 + description: 287 + "This lexicon is in a not officially released state. It is subject to change. | A declaration of the status of the actor. Only one can be shown at a time. If there are multiple, the latest record should be picked and earlier records should be deleted or tombstoned.", 288 + key: "literal:self", 289 + record: { 290 + type: "object", 291 + required: ["time", "item"], 292 + properties: { 293 + time: { 294 + type: "string", 295 + format: "datetime", 296 + description: "The unix timestamp of when the item was recorded", 297 + }, 298 + expiry: { 299 + type: "string", 300 + format: "datetime", 301 + description: 302 + "The unix timestamp of the expiry time of the item. If unavailable, default to 10 minutes past the start time.", 303 + }, 304 + item: { 305 + type: "ref", 306 + ref: "lex:fm.teal.alpha.feed.defs#playView", 307 + }, 308 + }, 309 + }, 310 + }, 311 + }, 312 + }, 313 + FmTealAlphaFeedDefs: { 314 + lexicon: 1, 315 + id: "fm.teal.alpha.feed.defs", 316 + description: 317 + "This lexicon is in a not officially released state. It is subject to change. | Misc. items related to feeds.", 318 + defs: { 319 + playView: { 320 + type: "object", 321 + required: ["trackName", "artists"], 322 + properties: { 323 + trackName: { 324 + type: "string", 325 + minLength: 1, 326 + maxLength: 256, 327 + maxGraphemes: 2560, 328 + description: "The name of the track", 329 + }, 330 + trackMbId: { 331 + type: "string", 332 + description: "The Musicbrainz ID of the track", 333 + }, 334 + recordingMbId: { 335 + type: "string", 336 + description: "The Musicbrainz recording ID of the track", 337 + }, 338 + duration: { 339 + type: "integer", 340 + description: "The length of the track in seconds", 341 + }, 342 + artists: { 343 + type: "array", 344 + items: { 345 + type: "ref", 346 + ref: "lex:fm.teal.alpha.feed.defs#artist", 347 + }, 348 + description: "Array of artists in order of original appearance.", 349 + }, 350 + releaseName: { 351 + type: "string", 352 + maxLength: 256, 353 + maxGraphemes: 2560, 354 + description: "The name of the release/album", 355 + }, 356 + releaseMbId: { 357 + type: "string", 358 + description: "The Musicbrainz release ID", 359 + }, 360 + isrc: { 361 + type: "string", 362 + description: "The ISRC code associated with the recording", 363 + }, 364 + originUrl: { 365 + type: "string", 366 + description: "The URL associated with this track", 367 + }, 368 + musicServiceBaseDomain: { 369 + type: "string", 370 + description: 371 + "The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if not provided.", 372 + }, 373 + submissionClientAgent: { 374 + type: "string", 375 + maxLength: 256, 376 + maxGraphemes: 2560, 377 + description: 378 + "A user-agent style string specifying the user agent. e.g. tealtracker/0.0.1b (Linux; Android 13; SM-A715F). Defaults to 'manual/unknown' if not provided.", 379 + }, 380 + playedTime: { 381 + type: "string", 382 + format: "datetime", 383 + description: "The unix timestamp of when the track was played", 384 + }, 385 + }, 386 + }, 387 + artist: { 388 + type: "object", 389 + required: ["artistName"], 390 + properties: { 391 + artistName: { 392 + type: "string", 393 + minLength: 1, 394 + maxLength: 256, 395 + maxGraphemes: 2560, 396 + description: "The name of the artist", 397 + }, 398 + artistMbId: { 399 + type: "string", 400 + description: "The Musicbrainz ID of the artist", 401 + }, 402 + }, 403 + }, 404 + }, 405 + }, 406 + FmTealAlphaFeedGetActorFeed: { 407 + lexicon: 1, 408 + id: "fm.teal.alpha.feed.getActorFeed", 409 + description: 410 + "This lexicon is in a not officially released state. It is subject to change. | Retrieves multiple plays from the index or via an author's DID.", 411 + defs: { 412 + main: { 413 + type: "query", 414 + parameters: { 415 + type: "params", 416 + required: ["authorDID"], 417 + properties: { 418 + authorDID: { 419 + type: "string", 420 + format: "at-identifier", 421 + description: "The author's DID for the play", 422 + }, 423 + cursor: { 424 + type: "string", 425 + description: "The cursor to start the query from", 426 + }, 427 + limit: { 428 + type: "integer", 429 + description: 430 + "The upper limit of tracks to get per request. Default is 20, max is 50.", 431 + }, 432 + }, 433 + }, 434 + output: { 435 + encoding: "application/json", 436 + schema: { 437 + type: "object", 438 + required: ["plays"], 439 + properties: { 440 + plays: { 441 + type: "array", 442 + items: { 443 + type: "ref", 444 + ref: "lex:fm.teal.alpha.feed.defs#playView", 445 + }, 446 + }, 447 + }, 448 + }, 449 + }, 450 + }, 451 + }, 452 + }, 453 + FmTealAlphaFeedGetPlay: { 454 + lexicon: 1, 455 + id: "fm.teal.alpha.feed.getPlay", 456 + description: 457 + "This lexicon is in a not officially released state. It is subject to change. | Retrieves a play given an author DID and record key.", 458 + defs: { 459 + main: { 460 + type: "query", 461 + parameters: { 462 + type: "params", 463 + required: ["authorDID", "rkey"], 464 + properties: { 465 + authorDID: { 466 + type: "string", 467 + format: "at-identifier", 468 + description: "The author's DID for the play", 469 + }, 470 + rkey: { 471 + type: "string", 472 + description: "The record key of the play", 473 + }, 474 + }, 475 + }, 476 + output: { 477 + encoding: "application/json", 478 + schema: { 479 + type: "object", 480 + required: ["play"], 481 + properties: { 482 + play: { 483 + type: "ref", 484 + ref: "lex:fm.teal.alpha.feed.defs#playView", 485 + }, 486 + }, 487 + }, 488 + }, 489 + }, 490 + }, 491 + }, 492 + FmTealAlphaFeedPlay: { 493 + lexicon: 1, 494 + id: "fm.teal.alpha.feed.play", 495 + description: 496 + "This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm play. Plays are submitted as a result of a user listening to a track. Plays should be marked as tracked when a user has listened to the entire track if it's under 2 minutes long, or half of the track's duration up to 4 minutes, whichever is longest.", 497 + defs: { 498 + main: { 499 + type: "record", 500 + key: "tid", 501 + record: { 502 + type: "object", 503 + required: ["trackName"], 504 + properties: { 505 + trackName: { 506 + type: "string", 507 + minLength: 1, 508 + maxLength: 256, 509 + maxGraphemes: 2560, 510 + description: "The name of the track", 511 + }, 512 + trackMbId: { 513 + type: "string", 514 + description: "The Musicbrainz ID of the track", 515 + }, 516 + recordingMbId: { 517 + type: "string", 518 + description: "The Musicbrainz recording ID of the track", 519 + }, 520 + duration: { 521 + type: "integer", 522 + description: "The length of the track in seconds", 523 + }, 524 + artistNames: { 525 + type: "array", 526 + items: { 527 + type: "string", 528 + minLength: 1, 529 + maxLength: 256, 530 + maxGraphemes: 2560, 531 + }, 532 + description: 533 + "Array of artist names in order of original appearance. Prefer using 'artists'.", 534 + }, 535 + artistMbIds: { 536 + type: "array", 537 + items: { 538 + type: "string", 539 + }, 540 + description: 541 + "Array of Musicbrainz artist IDs. Prefer using 'artists'.", 542 + }, 543 + artists: { 544 + type: "array", 545 + items: { 546 + type: "ref", 547 + ref: "lex:fm.teal.alpha.feed.defs#artist", 548 + }, 549 + description: "Array of artists in order of original appearance.", 550 + }, 551 + releaseName: { 552 + type: "string", 553 + maxLength: 256, 554 + maxGraphemes: 2560, 555 + description: "The name of the release/album", 556 + }, 557 + releaseMbId: { 558 + type: "string", 559 + description: "The Musicbrainz release ID", 560 + }, 561 + isrc: { 562 + type: "string", 563 + description: "The ISRC code associated with the recording", 564 + }, 565 + originUrl: { 566 + type: "string", 567 + description: "The URL associated with this track", 568 + }, 569 + musicServiceBaseDomain: { 570 + type: "string", 571 + description: 572 + "The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if unavailable or not provided.", 573 + }, 574 + submissionClientAgent: { 575 + type: "string", 576 + maxLength: 256, 577 + maxGraphemes: 2560, 578 + description: 579 + "A metadata string specifying the user agent where the format is `<app-identifier>/<version> (<kernel/OS-base>; <platform/OS-version>; <device-model>)`. If string is provided, only `app-identifier` and `version` are required. `app-identifier` is recommended to be in reverse dns format. Defaults to 'manual/unknown' if unavailable or not provided.", 580 + }, 581 + playedTime: { 582 + type: "string", 583 + format: "datetime", 584 + description: "The unix timestamp of when the track was played", 585 + }, 586 + }, 587 + }, 588 + }, 589 + }, 590 + }, 7 591 AppRockskyActorDefs: { 8 592 lexicon: 1, 9 593 id: "app.rocksky.actor.defs", ··· 4421 5005 export const schemas = Object.values(schemaDict); 4422 5006 export const lexicons: Lexicons = new Lexicons(schemas); 4423 5007 export const ids = { 5008 + FmTealAlphaActorDefs: "fm.teal.alpha.actor.defs", 5009 + FmTealAlphaActorGetProfile: "fm.teal.alpha.actor.getProfile", 5010 + FmTealAlphaActorGetProfiles: "fm.teal.alpha.actor.getProfiles", 5011 + FmTealAlphaActorProfile: "fm.teal.alpha.actor.profile", 5012 + FmTealAlphaActorSearchActors: "fm.teal.alpha.actor.searchActors", 5013 + FmTealAlphaActorStatus: "fm.teal.alpha.actor.status", 5014 + FmTealAlphaFeedDefs: "fm.teal.alpha.feed.defs", 5015 + FmTealAlphaFeedGetActorFeed: "fm.teal.alpha.feed.getActorFeed", 5016 + FmTealAlphaFeedGetPlay: "fm.teal.alpha.feed.getPlay", 5017 + FmTealAlphaFeedPlay: "fm.teal.alpha.feed.play", 4424 5018 AppRockskyActorDefs: "app.rocksky.actor.defs", 4425 5019 AppRockskyActorGetActorAlbums: "app.rocksky.actor.getActorAlbums", 4426 5020 AppRockskyActorGetActorArtists: "app.rocksky.actor.getActorArtists",
+60
apps/api/src/lexicon/types/fm/teal/alpha/actor/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../../../lexicons"; 6 + import { isObj, hasProp } from "../../../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + import type * as AppBskyRichtextFacet from "../../../../app/bsky/richtext/facet"; 9 + import type * as FmTealAlphaActorProfile from "./profile"; 10 + 11 + export interface ProfileView { 12 + /** The decentralized identifier of the actor */ 13 + did?: string; 14 + displayName?: string; 15 + /** Free-form profile description text. */ 16 + description?: string; 17 + /** Annotations of text in the profile description (mentions, URLs, hashtags, etc). May be changed to another (backwards compatible) lexicon. */ 18 + descriptionFacets?: AppBskyRichtextFacet.Main[]; 19 + featuredItem?: FmTealAlphaActorProfile.FeaturedItem; 20 + /** IPLD of the avatar */ 21 + avatar?: string; 22 + /** IPLD of the banner image */ 23 + banner?: string; 24 + createdAt?: string; 25 + [k: string]: unknown; 26 + } 27 + 28 + export function isProfileView(v: unknown): v is ProfileView { 29 + return ( 30 + isObj(v) && 31 + hasProp(v, "$type") && 32 + v.$type === "fm.teal.alpha.actor.defs#profileView" 33 + ); 34 + } 35 + 36 + export function validateProfileView(v: unknown): ValidationResult { 37 + return lexicons.validate("fm.teal.alpha.actor.defs#profileView", v); 38 + } 39 + 40 + export interface MiniProfileView { 41 + /** The decentralized identifier of the actor */ 42 + did?: string; 43 + displayName?: string; 44 + handle?: string; 45 + /** IPLD of the avatar */ 46 + avatar?: string; 47 + [k: string]: unknown; 48 + } 49 + 50 + export function isMiniProfileView(v: unknown): v is MiniProfileView { 51 + return ( 52 + isObj(v) && 53 + hasProp(v, "$type") && 54 + v.$type === "fm.teal.alpha.actor.defs#miniProfileView" 55 + ); 56 + } 57 + 58 + export function validateMiniProfileView(v: unknown): ValidationResult { 59 + return lexicons.validate("fm.teal.alpha.actor.defs#miniProfileView", v); 60 + }
+48
apps/api/src/lexicon/types/fm/teal/alpha/actor/getProfile.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + import type * as FmTealAlphaActorDefs from "./defs"; 11 + 12 + export interface QueryParams { 13 + /** The author's DID */ 14 + actor: string; 15 + } 16 + 17 + export type InputSchema = undefined; 18 + 19 + export interface OutputSchema { 20 + actor: FmTealAlphaActorDefs.ProfileView; 21 + [k: string]: unknown; 22 + } 23 + 24 + export type HandlerInput = undefined; 25 + 26 + export interface HandlerSuccess { 27 + encoding: "application/json"; 28 + body: OutputSchema; 29 + headers?: { [key: string]: string }; 30 + } 31 + 32 + export interface HandlerError { 33 + status: number; 34 + message?: string; 35 + } 36 + 37 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 38 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 39 + auth: HA; 40 + params: QueryParams; 41 + input: HandlerInput; 42 + req: express.Request; 43 + res: express.Response; 44 + resetRouteRateLimits: () => Promise<void>; 45 + }; 46 + export type Handler<HA extends HandlerAuth = never> = ( 47 + ctx: HandlerReqCtx<HA>, 48 + ) => Promise<HandlerOutput> | HandlerOutput;
+48
apps/api/src/lexicon/types/fm/teal/alpha/actor/getProfiles.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + import type * as FmTealAlphaActorDefs from "./defs"; 11 + 12 + export interface QueryParams { 13 + /** Array of actor DIDs */ 14 + actors: string[]; 15 + } 16 + 17 + export type InputSchema = undefined; 18 + 19 + export interface OutputSchema { 20 + actors: FmTealAlphaActorDefs.MiniProfileView[]; 21 + [k: string]: unknown; 22 + } 23 + 24 + export type HandlerInput = undefined; 25 + 26 + export interface HandlerSuccess { 27 + encoding: "application/json"; 28 + body: OutputSchema; 29 + headers?: { [key: string]: string }; 30 + } 31 + 32 + export interface HandlerError { 33 + status: number; 34 + message?: string; 35 + } 36 + 37 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 38 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 39 + auth: HA; 40 + params: QueryParams; 41 + input: HandlerInput; 42 + req: express.Request; 43 + res: express.Response; 44 + resetRouteRateLimits: () => Promise<void>; 45 + }; 46 + export type Handler<HA extends HandlerAuth = never> = ( 47 + ctx: HandlerReqCtx<HA>, 48 + ) => Promise<HandlerOutput> | HandlerOutput;
+56
apps/api/src/lexicon/types/fm/teal/alpha/actor/profile.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type { ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../../../lexicons"; 6 + import { isObj, hasProp } from "../../../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + import type * as AppBskyRichtextFacet from "../../../../app/bsky/richtext/facet"; 9 + 10 + export interface Record { 11 + displayName?: string; 12 + /** Free-form profile description text. */ 13 + description?: string; 14 + /** Annotations of text in the profile description (mentions, URLs, hashtags, etc). */ 15 + descriptionFacets?: AppBskyRichtextFacet.Main[]; 16 + featuredItem?: FeaturedItem; 17 + /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ 18 + avatar?: BlobRef; 19 + /** Larger horizontal image to display behind profile view. */ 20 + banner?: BlobRef; 21 + createdAt?: string; 22 + [k: string]: unknown; 23 + } 24 + 25 + export function isRecord(v: unknown): v is Record { 26 + return ( 27 + isObj(v) && 28 + hasProp(v, "$type") && 29 + (v.$type === "fm.teal.alpha.actor.profile#main" || 30 + v.$type === "fm.teal.alpha.actor.profile") 31 + ); 32 + } 33 + 34 + export function validateRecord(v: unknown): ValidationResult { 35 + return lexicons.validate("fm.teal.alpha.actor.profile#main", v); 36 + } 37 + 38 + export interface FeaturedItem { 39 + /** The Musicbrainz ID of the item */ 40 + mbid: string; 41 + /** The type of the item. Must be a valid Musicbrainz type, e.g. album, track, recording, etc. */ 42 + type: string; 43 + [k: string]: unknown; 44 + } 45 + 46 + export function isFeaturedItem(v: unknown): v is FeaturedItem { 47 + return ( 48 + isObj(v) && 49 + hasProp(v, "$type") && 50 + v.$type === "fm.teal.alpha.actor.profile#featuredItem" 51 + ); 52 + } 53 + 54 + export function validateFeaturedItem(v: unknown): ValidationResult { 55 + return lexicons.validate("fm.teal.alpha.actor.profile#featuredItem", v); 56 + }
+54
apps/api/src/lexicon/types/fm/teal/alpha/actor/searchActors.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + import type * as FmTealAlphaActorDefs from "./defs"; 11 + 12 + export interface QueryParams { 13 + /** The search query */ 14 + q: string; 15 + /** The maximum number of actors to return */ 16 + limit?: number; 17 + /** Cursor for pagination */ 18 + cursor?: string; 19 + } 20 + 21 + export type InputSchema = undefined; 22 + 23 + export interface OutputSchema { 24 + actors: FmTealAlphaActorDefs.MiniProfileView[]; 25 + /** Cursor for pagination */ 26 + cursor?: string; 27 + [k: string]: unknown; 28 + } 29 + 30 + export type HandlerInput = undefined; 31 + 32 + export interface HandlerSuccess { 33 + encoding: "application/json"; 34 + body: OutputSchema; 35 + headers?: { [key: string]: string }; 36 + } 37 + 38 + export interface HandlerError { 39 + status: number; 40 + message?: string; 41 + } 42 + 43 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 44 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 45 + auth: HA; 46 + params: QueryParams; 47 + input: HandlerInput; 48 + req: express.Request; 49 + res: express.Response; 50 + resetRouteRateLimits: () => Promise<void>; 51 + }; 52 + export type Handler<HA extends HandlerAuth = never> = ( 53 + ctx: HandlerReqCtx<HA>, 54 + ) => Promise<HandlerOutput> | HandlerOutput;
+30
apps/api/src/lexicon/types/fm/teal/alpha/actor/status.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../../../lexicons"; 6 + import { isObj, hasProp } from "../../../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + import type * as FmTealAlphaFeedDefs from "../feed/defs"; 9 + 10 + export interface Record { 11 + /** The unix timestamp of when the item was recorded */ 12 + time: string; 13 + /** The unix timestamp of the expiry time of the item. If unavailable, default to 10 minutes past the start time. */ 14 + expiry?: string; 15 + item: FmTealAlphaFeedDefs.PlayView; 16 + [k: string]: unknown; 17 + } 18 + 19 + export function isRecord(v: unknown): v is Record { 20 + return ( 21 + isObj(v) && 22 + hasProp(v, "$type") && 23 + (v.$type === "fm.teal.alpha.actor.status#main" || 24 + v.$type === "fm.teal.alpha.actor.status") 25 + ); 26 + } 27 + 28 + export function validateRecord(v: unknown): ValidationResult { 29 + return lexicons.validate("fm.teal.alpha.actor.status#main", v); 30 + }
+67
apps/api/src/lexicon/types/fm/teal/alpha/feed/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../../../lexicons"; 6 + import { isObj, hasProp } from "../../../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + 9 + export interface PlayView { 10 + /** The name of the track */ 11 + trackName: string; 12 + /** The Musicbrainz ID of the track */ 13 + trackMbId?: string; 14 + /** The Musicbrainz recording ID of the track */ 15 + recordingMbId?: string; 16 + /** The length of the track in seconds */ 17 + duration?: number; 18 + /** Array of artists in order of original appearance. */ 19 + artists: Artist[]; 20 + /** The name of the release/album */ 21 + releaseName?: string; 22 + /** The Musicbrainz release ID */ 23 + releaseMbId?: string; 24 + /** The ISRC code associated with the recording */ 25 + isrc?: string; 26 + /** The URL associated with this track */ 27 + originUrl?: string; 28 + /** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if not provided. */ 29 + musicServiceBaseDomain?: string; 30 + /** A user-agent style string specifying the user agent. e.g. tealtracker/0.0.1b (Linux; Android 13; SM-A715F). Defaults to 'manual/unknown' if not provided. */ 31 + submissionClientAgent?: string; 32 + /** The unix timestamp of when the track was played */ 33 + playedTime?: string; 34 + [k: string]: unknown; 35 + } 36 + 37 + export function isPlayView(v: unknown): v is PlayView { 38 + return ( 39 + isObj(v) && 40 + hasProp(v, "$type") && 41 + v.$type === "fm.teal.alpha.feed.defs#playView" 42 + ); 43 + } 44 + 45 + export function validatePlayView(v: unknown): ValidationResult { 46 + return lexicons.validate("fm.teal.alpha.feed.defs#playView", v); 47 + } 48 + 49 + export interface Artist { 50 + /** The name of the artist */ 51 + artistName: string; 52 + /** The Musicbrainz ID of the artist */ 53 + artistMbId?: string; 54 + [k: string]: unknown; 55 + } 56 + 57 + export function isArtist(v: unknown): v is Artist { 58 + return ( 59 + isObj(v) && 60 + hasProp(v, "$type") && 61 + v.$type === "fm.teal.alpha.feed.defs#artist" 62 + ); 63 + } 64 + 65 + export function validateArtist(v: unknown): ValidationResult { 66 + return lexicons.validate("fm.teal.alpha.feed.defs#artist", v); 67 + }
+52
apps/api/src/lexicon/types/fm/teal/alpha/feed/getActorFeed.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + import type * as FmTealAlphaFeedDefs from "./defs"; 11 + 12 + export interface QueryParams { 13 + /** The author's DID for the play */ 14 + authorDID: string; 15 + /** The cursor to start the query from */ 16 + cursor?: string; 17 + /** The upper limit of tracks to get per request. Default is 20, max is 50. */ 18 + limit?: number; 19 + } 20 + 21 + export type InputSchema = undefined; 22 + 23 + export interface OutputSchema { 24 + plays: FmTealAlphaFeedDefs.PlayView[]; 25 + [k: string]: unknown; 26 + } 27 + 28 + export type HandlerInput = undefined; 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+50
apps/api/src/lexicon/types/fm/teal/alpha/feed/getPlay.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + import type * as FmTealAlphaFeedDefs from "./defs"; 11 + 12 + export interface QueryParams { 13 + /** The author's DID for the play */ 14 + authorDID: string; 15 + /** The record key of the play */ 16 + rkey: string; 17 + } 18 + 19 + export type InputSchema = undefined; 20 + 21 + export interface OutputSchema { 22 + play: FmTealAlphaFeedDefs.PlayView; 23 + [k: string]: unknown; 24 + } 25 + 26 + export type HandlerInput = undefined; 27 + 28 + export interface HandlerSuccess { 29 + encoding: "application/json"; 30 + body: OutputSchema; 31 + headers?: { [key: string]: string }; 32 + } 33 + 34 + export interface HandlerError { 35 + status: number; 36 + message?: string; 37 + } 38 + 39 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 40 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 41 + auth: HA; 42 + params: QueryParams; 43 + input: HandlerInput; 44 + req: express.Request; 45 + res: express.Response; 46 + resetRouteRateLimits: () => Promise<void>; 47 + }; 48 + export type Handler<HA extends HandlerAuth = never> = ( 49 + ctx: HandlerReqCtx<HA>, 50 + ) => Promise<HandlerOutput> | HandlerOutput;
+53
apps/api/src/lexicon/types/fm/teal/alpha/feed/play.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../../../lexicons"; 6 + import { isObj, hasProp } from "../../../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + import type * as FmTealAlphaFeedDefs from "./defs"; 9 + 10 + export interface Record { 11 + /** The name of the track */ 12 + trackName: string; 13 + /** The Musicbrainz ID of the track */ 14 + trackMbId?: string; 15 + /** The Musicbrainz recording ID of the track */ 16 + recordingMbId?: string; 17 + /** The length of the track in seconds */ 18 + duration?: number; 19 + /** Array of artist names in order of original appearance. Prefer using 'artists'. */ 20 + artistNames?: string[]; 21 + /** Array of Musicbrainz artist IDs. Prefer using 'artists'. */ 22 + artistMbIds?: string[]; 23 + /** Array of artists in order of original appearance. */ 24 + artists?: FmTealAlphaFeedDefs.Artist[]; 25 + /** The name of the release/album */ 26 + releaseName?: string; 27 + /** The Musicbrainz release ID */ 28 + releaseMbId?: string; 29 + /** The ISRC code associated with the recording */ 30 + isrc?: string; 31 + /** The URL associated with this track */ 32 + originUrl?: string; 33 + /** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if unavailable or not provided. */ 34 + musicServiceBaseDomain?: string; 35 + /** A metadata string specifying the user agent where the format is `<app-identifier>/<version> (<kernel/OS-base>; <platform/OS-version>; <device-model>)`. If string is provided, only `app-identifier` and `version` are required. `app-identifier` is recommended to be in reverse dns format. Defaults to 'manual/unknown' if unavailable or not provided. */ 36 + submissionClientAgent?: string; 37 + /** The unix timestamp of when the track was played */ 38 + playedTime?: string; 39 + [k: string]: unknown; 40 + } 41 + 42 + export function isRecord(v: unknown): v is Record { 43 + return ( 44 + isObj(v) && 45 + hasProp(v, "$type") && 46 + (v.$type === "fm.teal.alpha.feed.play#main" || 47 + v.$type === "fm.teal.alpha.feed.play") 48 + ); 49 + } 50 + 51 + export function validateRecord(v: unknown): ValidationResult { 52 + return lexicons.validate("fm.teal.alpha.feed.play#main", v); 53 + }
+1
apps/api/src/lib/env.ts
··· 35 35 DROPBOX: str({ default: "http://localhost:7881" }), 36 36 TRACKLIST: str({ default: "http://localhost:7884" }), 37 37 REDIS_URL: str({ default: "redis://localhost:6379" }), 38 + MUSICBRAINZ_URL: str({ devDefault: "http://localhost:8088" }), 38 39 PRIVATE_KEY_1: str({}), 39 40 PRIVATE_KEY_2: str({}), 40 41 PRIVATE_KEY_3: str({}),
+10 -10
apps/api/src/lovedtracks/lovedtracks.service.ts
··· 18 18 ctx: Context, 19 19 track: Track, 20 20 user, 21 - agent: Agent 21 + agent: Agent, 22 22 ) { 23 23 const trackSha256 = createHash("sha256") 24 24 .update(`${track.title} - ${track.artist} - ${track.album}`.toLowerCase()) ··· 156 156 .select() 157 157 .from(albumTracks) 158 158 .where( 159 - and(eq(albumTracks.albumId, albumId), eq(albumTracks.trackId, trackId)) 159 + and(eq(albumTracks.albumId, albumId), eq(albumTracks.trackId, trackId)), 160 160 ) 161 161 .limit(1) 162 162 .then((rows) => rows[0]); ··· 175 175 .where( 176 176 and( 177 177 eq(artistTracks.artistId, artistId), 178 - eq(artistTracks.trackId, trackId) 179 - ) 178 + eq(artistTracks.trackId, trackId), 179 + ), 180 180 ) 181 181 .limit(1) 182 182 .then((rows) => rows[0]); ··· 195 195 .where( 196 196 and( 197 197 eq(artistAlbums.artistId, artistId), 198 - eq(artistAlbums.albumId, albumId) 199 - ) 198 + eq(artistAlbums.albumId, albumId), 199 + ), 200 200 ) 201 201 .limit(1) 202 202 .then((rows) => rows[0]); ··· 213 213 .select() 214 214 .from(lovedTracks) 215 215 .where( 216 - and(eq(lovedTracks.userId, user.id), eq(lovedTracks.trackId, trackId)) 216 + and(eq(lovedTracks.userId, user.id), eq(lovedTracks.trackId, trackId)), 217 217 ) 218 218 .limit(1) 219 219 .then((rows) => rows[0]); ··· 304 304 ctx: Context, 305 305 trackSha256: string, 306 306 user, 307 - agent: Agent 307 + agent: Agent, 308 308 ) { 309 309 const track = await ctx.db 310 310 .select() ··· 321 321 .select() 322 322 .from(lovedTracks) 323 323 .where( 324 - and(eq(lovedTracks.userId, user.id), eq(lovedTracks.trackId, track.id)) 324 + and(eq(lovedTracks.userId, user.id), eq(lovedTracks.trackId, track.id)), 325 325 ) 326 326 .limit(1) 327 327 .then((rows) => rows[0]); ··· 351 351 ctx: Context, 352 352 user, 353 353 size = 10, 354 - offset = 0 354 + offset = 0, 355 355 ) { 356 356 const lovedTracksData = await ctx.db 357 357 .select({
+24 -1
apps/api/src/nowplaying/nowplaying.service.ts
··· 10 10 import * as Song from "lexicon/types/app/rocksky/song"; 11 11 import { deepSnakeCaseKeys } from "lib"; 12 12 import { createHash } from "node:crypto"; 13 - import type { Track } from "types/track"; 13 + import type { MusicbrainzTrack, Track } from "types/track"; 14 14 import albumTracks from "../schema/album-tracks"; 15 15 import albums from "../schema/albums"; 16 16 import artistAlbums from "../schema/artist-albums"; ··· 22 22 import userArtists from "../schema/user-artists"; 23 23 import userTracks from "../schema/user-tracks"; 24 24 import users from "../schema/users"; 25 + import tealfm from "../tealfm"; 25 26 26 27 export async function putArtistRecord( 27 28 track: Track, ··· 822 823 if (existingTrack?.artistUri) { 823 824 console.log( 824 825 `Artist uri ready: ${chalk.cyan(existingTrack.id)} - ${track.title}, after ${chalk.magenta(tries)} tries` 826 + ); 827 + } 828 + 829 + const { data: mbTrack } = await ctx.musicbrainz.post<MusicbrainzTrack>( 830 + "/hydrate", 831 + { 832 + artist: track.artist.split(",").map((a) => ({ name: a.trim() })), 833 + name: track.title, 834 + album: track.album, 835 + } 836 + ); 837 + 838 + track.mbId = mbTrack?.trackMBID; 839 + 840 + if (userDid === "did:plc:7vdlgi2bflelz7mmuxoqjfcr" && mbTrack?.trackMBID) { 841 + mbTrack.timestamp = track.timestamp 842 + ? dayjs.unix(track.timestamp).toISOString() 843 + : new Date().toISOString(); 844 + await tealfm.publishPlayingNow( 845 + agent, 846 + mbTrack, 847 + Math.floor(track.duration / 1000) 825 848 ); 826 849 } 827 850
+1 -3
apps/api/src/schema/album-tracks.ts
··· 4 4 import tracks from "./tracks"; 5 5 6 6 const albumTracks = pgTable("album_tracks", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 albumId: text("album_id") 11 9 .notNull() 12 10 .references(() => albums.id),
+1 -3
apps/api/src/schema/albums.ts
··· 2 2 import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 4 4 const albums = pgTable("albums", { 5 - id: text("xata_id") 6 - .primaryKey() 7 - .default(sql`xata_id()`), 5 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 8 6 title: text("title").notNull(), 9 7 artist: text("artist").notNull(), 10 8 releaseDate: text("release_date"),
+1 -3
apps/api/src/schema/api-keys.ts
··· 3 3 import users from "./users"; 4 4 5 5 const apiKeys = pgTable("api_keys", { 6 - id: text("xata_id") 7 - .primaryKey() 8 - .default(sql`xata_id()`), 6 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 9 7 name: text("name").notNull(), 10 8 apiKey: text("api_key").notNull(), 11 9 sharedSecret: text("shared_secret").notNull(),
+1 -3
apps/api/src/schema/artist-albums.ts
··· 4 4 import artists from "./artists"; 5 5 6 6 const artistAlbums = pgTable("artist_albums", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 artistId: text("artist_id") 11 9 .notNull() 12 10 .references(() => artists.id),
+1 -3
apps/api/src/schema/artist-tracks.ts
··· 4 4 import tracks from "./tracks"; 5 5 6 6 const artistTracks = pgTable("artist_tracks", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 artistId: text("artist_id") 11 9 .notNull() 12 10 .references(() => artists.id),
+1 -3
apps/api/src/schema/artists.ts
··· 2 2 import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 4 4 const artists = pgTable("artists", { 5 - id: text("xata_id") 6 - .primaryKey() 7 - .default(sql`xata_id()`), 5 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 8 6 name: text("name").notNull(), 9 7 biography: text("biography"), 10 8 born: timestamp("born"),
+1 -3
apps/api/src/schema/dropbox-accounts.ts
··· 3 3 import users from "./users"; 4 4 5 5 const dropboxAccounts = pgTable("dropbox_accounts", { 6 - id: text("xata_id") 7 - .primaryKey() 8 - .default(sql`xata_id()`), 6 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 9 7 email: text("email").unique().notNull(), 10 8 isBetaUser: boolean("is_beta_user").default(false).notNull(), 11 9 userId: text("user_id")
+1 -3
apps/api/src/schema/dropbox-directories.ts
··· 2 2 import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 4 4 const dropboxDirectories = pgTable("dropbox_directories", { 5 - id: text("xata_id") 6 - .primaryKey() 7 - .default(sql`xata_id()`), 5 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 8 6 name: text("name").notNull(), 9 7 path: text("path").notNull(), 10 8 parentId: text("parent_id").references(() => dropboxDirectories.id),
+1 -3
apps/api/src/schema/dropbox-paths.ts
··· 3 3 import dropboxDirectories from "./dropbox-directories"; 4 4 5 5 const dropboxPaths = pgTable("dropbox_paths", { 6 - id: text("xata_id") 7 - .primaryKey() 8 - .default(sql`xata_id()`), 6 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 9 7 path: text("path").notNull(), 10 8 name: text("name").notNull(), 11 9 dropboxId: text("dropbox_id").notNull(),
+1 -3
apps/api/src/schema/dropbox-tokens.ts
··· 2 2 import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 4 4 const dropboxTokens = pgTable("dropbox_tokens", { 5 - id: text("xata_id") 6 - .primaryKey() 7 - .default(sql`xata_id()`), 5 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 8 6 refreshToken: text("refresh_token").notNull(), 9 7 createdAt: timestamp("xata_createdat").defaultNow().notNull(), 10 8 updatedAt: timestamp("xata_updatedat").defaultNow().notNull(),
+1 -3
apps/api/src/schema/dropbox.ts
··· 4 4 import users from "./users"; 5 5 6 6 const dropbox = pgTable("dropbox", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 userId: text("user_id") 11 9 .notNull() 12 10 .references(() => users.id),
+1 -3
apps/api/src/schema/google-drive-accounts.ts
··· 3 3 import users from "./users"; 4 4 5 5 const googleDriveAccounts = pgTable("google_drive_accounts", { 6 - id: text("xata_id") 7 - .primaryKey() 8 - .default(sql`xata_id()`), 6 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 9 7 email: text("email").unique().notNull(), 10 8 isBetaUser: boolean("is_beta_user").default(false).notNull(), 11 9 userId: text("user_id")
+1 -3
apps/api/src/schema/google-drive-directories.ts
··· 2 2 import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 4 4 const googleDriveDirectories = pgTable("google_drive_directories", { 5 - id: text("xata_id") 6 - .primaryKey() 7 - .default(sql`xata_id()`), 5 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 8 6 name: text("name").notNull(), 9 7 path: text("path").notNull(), 10 8 parentId: text("parent_id").references(() => googleDriveDirectories.id),
+1 -3
apps/api/src/schema/google-drive-paths.ts
··· 3 3 import googleDriveDirectories from "./google-drive-directories"; 4 4 5 5 const googleDrivePaths = pgTable("google_drive_paths", { 6 - id: text("xata_id") 7 - .primaryKey() 8 - .default(sql`xata_id()`), 6 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 9 7 googleDriveId: text("google_drive_id").notNull(), 10 8 trackId: text("track_id").notNull(), 11 9 name: text("name").notNull(),
+1 -3
apps/api/src/schema/google-drive-tokens.ts
··· 2 2 import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 4 4 const googleDriveTokens = pgTable("google_drive_tokens", { 5 - id: text("xata_id") 6 - .primaryKey() 7 - .default(sql`xata_id()`), 5 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 8 6 refreshToken: text("refresh_token").notNull(), 9 7 createdAt: timestamp("xata_createdat").defaultNow().notNull(), 10 8 updatedAt: timestamp("xata_updatedat").defaultNow().notNull(),
+1 -3
apps/api/src/schema/googledrive.ts
··· 4 4 import users from "./users"; 5 5 6 6 const googleDrive = pgTable("google_drive", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 googleDriveTokenId: text("google_drive_token_id") 11 9 .notNull() 12 10 .references(() => googleDriveTokens.id),
+1 -3
apps/api/src/schema/loved-tracks.ts
··· 4 4 import users from "./users"; 5 5 6 6 const lovedTracks = pgTable("loved_tracks", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 userId: text("user_id") 11 9 .notNull() 12 10 .references(() => users.id),
+1 -3
apps/api/src/schema/playlist-tracks.ts
··· 4 4 import tracks from "./tracks"; 5 5 6 6 const playlistTracks = pgTable("playlist_tracks", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 playlistId: text("playlist_id") 11 9 .notNull() 12 10 .references(() => playlists.id),
+1 -3
apps/api/src/schema/playlists.ts
··· 3 3 import users from "./users"; 4 4 5 5 const playlists = pgTable("playlists", { 6 - id: text("xata_id") 7 - .primaryKey() 8 - .default(sql`xata_id()`), 6 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 9 7 name: text("name").notNull(), 10 8 picture: text("picture"), 11 9 description: text("description"),
+1 -3
apps/api/src/schema/profile-shouts.ts
··· 4 4 import users from "./users"; 5 5 6 6 const profileShouts = pgTable("profile_shouts", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 userId: text("user_id") 11 9 .notNull() 12 10 .references(() => users.id),
+1 -3
apps/api/src/schema/queue-tracks.ts
··· 4 4 import users from "./users"; 5 5 6 6 const queueTracks = pgTable("queue_tracks", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 userId: text("user_id") 11 9 .notNull() 12 10 .references(() => users.id),
+1 -3
apps/api/src/schema/scrobbles.ts
··· 6 6 import users from "./users"; 7 7 8 8 const scrobbles = pgTable("scrobbles", { 9 - id: text("xata_id") 10 - .primaryKey() 11 - .default(sql`xata_id()`), 9 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 12 10 userId: text("user_id").references(() => users.id), 13 11 trackId: text("track_id").references(() => tracks.id), 14 12 albumId: text("album_id").references(() => albums.id),
+1 -3
apps/api/src/schema/shout-likes.ts
··· 4 4 import users from "./users"; 5 5 6 6 const shoutLikes = pgTable("shout_likes", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 userId: text("user_id") 11 9 .notNull() 12 10 .references(() => users.id),
+1 -3
apps/api/src/schema/shout-reports.ts
··· 4 4 import users from "./users"; 5 5 6 6 const shoutReports = pgTable("shout_reports", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 userId: text("user_id") 11 9 .notNull() 12 10 .references(() => users.id),
+1 -3
apps/api/src/schema/shouts.ts
··· 6 6 import users from "./users"; 7 7 8 8 const shouts = pgTable("shouts", { 9 - id: text("xata_id") 10 - .primaryKey() 11 - .default(sql`xata_id()`), 9 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 12 10 content: text("content").notNull(), 13 11 trackId: text("track_id").references(() => tracks.id), 14 12 artistId: text("artist_id").references(() => users.id),
+1 -3
apps/api/src/schema/spotify-accounts.ts
··· 9 9 import users from "./users"; 10 10 11 11 const spotifyAccounts = pgTable("spotify_accounts", { 12 - id: text("xata_id") 13 - .primaryKey() 14 - .default(sql`xata_id()`), 12 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 15 13 xataVersion: integer("xata_version"), 16 14 email: text("email").notNull(), 17 15 userId: text("user_id")
+1 -3
apps/api/src/schema/spotify-tokens.ts
··· 3 3 import users from "./users"; 4 4 5 5 const spotifyTokens = pgTable("spotify_tokens", { 6 - id: text("xata_id") 7 - .primaryKey() 8 - .default(sql`xata_id()`), 6 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 9 7 xataVersion: integer("xata_version"), 10 8 accessToken: text("access_token").notNull(), 11 9 refreshToken: text("refresh_token").notNull(),
+1 -3
apps/api/src/schema/tracks.ts
··· 2 2 import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 4 4 const tracks = pgTable("tracks", { 5 - id: text("xata_id") 6 - .primaryKey() 7 - .default(sql`xata_id()`), 5 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 8 6 title: text("title").notNull(), 9 7 artist: text("artist").notNull(), 10 8 albumArtist: text("album_artist").notNull(),
+1 -3
apps/api/src/schema/user-albums.ts
··· 4 4 import users from "./users"; 5 5 6 6 const userAlbums = pgTable("user_albums", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 userId: text("user_id") 11 9 .notNull() 12 10 .references(() => users.id),
+1 -3
apps/api/src/schema/user-artists.ts
··· 4 4 import users from "./users"; 5 5 6 6 const userArtists = pgTable("user_artists", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 userId: text("user_id") 11 9 .notNull() 12 10 .references(() => users.id),
+1 -3
apps/api/src/schema/user-playlists.ts
··· 4 4 import users from "./users"; 5 5 6 6 const userPlaylists = pgTable("user_playlists", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 userId: text("user_id") 11 9 .notNull() 12 10 .references(() => users.id),
+1 -3
apps/api/src/schema/user-tracks.ts
··· 4 4 import users from "./users"; 5 5 6 6 const userTracks = pgTable("user_tracks", { 7 - id: text("xata_id") 8 - .primaryKey() 9 - .default(sql`xata_id()`), 7 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 10 8 userId: text("user_id") 11 9 .notNull() 12 10 .references(() => users.id),
+1 -3
apps/api/src/schema/users.ts
··· 2 2 import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 3 3 4 4 const users = pgTable("users", { 5 - id: text("xata_id") 6 - .primaryKey() 7 - .default(sql`xata_id()`), 5 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 8 6 did: text("did").unique().notNull(), 9 7 displayName: text("display_name"), 10 8 handle: text("handle").unique().notNull(),
+1 -3
apps/api/src/schema/webscrobblers.ts
··· 3 3 import users from "./users"; 4 4 5 5 const webscrobblers = pgTable("webscrobblers", { 6 - id: text("xata_id") 7 - .primaryKey() 8 - .default(sql`xata_id()`), 6 + id: text("xata_id").primaryKey().default(sql`xata_id()`), 9 7 name: text("name").notNull(), 10 8 uuid: text("uuid").notNull(), 11 9 description: text("description"),
+3 -3
apps/api/src/scripts/avatar.ts
··· 19 19 } 20 20 21 21 const plc = await fetch(`https://plc.directory/${user.did}`).then((res) => 22 - res.json() 22 + res.json(), 23 23 ); 24 24 25 25 const serviceEndpoint = _.get(plc, "service.0.serviceEndpoint"); ··· 29 29 } 30 30 31 31 const profile = await fetch( 32 - `${serviceEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${user.did}&collection=app.bsky.actor.profile&rkey=self` 32 + `${serviceEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${user.did}&collection=app.bsky.actor.profile&rkey=self`, 33 33 ).then((res) => res.json()); 34 34 const ref = _.get(profile, "value.avatar.ref.$link"); 35 35 const type = _.get(profile, "value.avatar.mimeType", "").split("/")[1]; ··· 53 53 54 54 ctx.nc.publish( 55 55 "rocksky.user", 56 - Buffer.from(JSON.stringify(deepSnakeCaseKeys(u))) 56 + Buffer.from(JSON.stringify(deepSnakeCaseKeys(u))), 57 57 ); 58 58 } 59 59
+3 -3
apps/api/src/scripts/genres.ts
··· 11 11 .from(tables.spotifyTokens) 12 12 .leftJoin( 13 13 tables.spotifyAccounts, 14 - eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId) 14 + eq(tables.spotifyAccounts.userId, tables.spotifyTokens.userId), 15 15 ) 16 16 .where(eq(tables.spotifyAccounts.isBetaUser, true)) 17 17 .execute() ··· 51 51 headers: { 52 52 Authorization: `Bearer ${token}`, 53 53 }, 54 - } 54 + }, 55 55 ) 56 56 .then( 57 57 (res) => ··· 64 64 images: Array<{ url: string }>; 65 65 }>; 66 66 }; 67 - }> 67 + }>, 68 68 ) 69 69 .then(async (data) => _.get(data, "artists.items.0")); 70 70
+4 -4
apps/api/src/scripts/meili.ts
··· 32 32 for (let i = 0; i < total; i += size) { 33 33 const skip = i; 34 34 console.log( 35 - `Processing ${chalk.magentaBright("albums")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}` 35 + `Processing ${chalk.magentaBright("albums")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`, 36 36 ); 37 37 const results = await ctx.db 38 38 .select() ··· 56 56 for (let i = 0; i < total; i += size) { 57 57 const skip = i; 58 58 console.log( 59 - `Processing ${chalk.magentaBright("artists")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}` 59 + `Processing ${chalk.magentaBright("artists")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`, 60 60 ); 61 61 const results = await ctx.db 62 62 .select() ··· 80 80 for (let i = 0; i < total; i += size) { 81 81 const skip = i; 82 82 console.log( 83 - `Processing ${chalk.magentaBright("tracks")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}` 83 + `Processing ${chalk.magentaBright("tracks")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`, 84 84 ); 85 85 const results = await ctx.db 86 86 .select() ··· 105 105 for (let i = 0; i < total; i += size) { 106 106 const skip = i; 107 107 console.log( 108 - `Processing ${chalk.magentaBright("users")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}` 108 + `Processing ${chalk.magentaBright("users")}: ${chalk.magentaBright(skip)} to ${chalk.magentaBright(skip + size)}`, 109 109 ); 110 110 const results = await ctx.db 111 111 .select()
+7 -7
apps/api/src/shouts/shouts.service.ts
··· 21 21 shout: Shout, 22 22 uri: string, 23 23 user, 24 - agent: Agent 24 + agent: Agent, 25 25 ) { 26 26 let album: SelectAlbum, 27 27 artist: SelectArtist, ··· 157 157 reply: Shout, 158 158 shoutUri: string, 159 159 user, 160 - agent: Agent 160 + agent: Agent, 161 161 ) { 162 162 const shout = await ctx.db 163 163 .select({ ··· 212 212 profileRecord.uri; 213 213 214 214 let service = await fetch( 215 - `https://plc.directory/${subjectUri.split("/").slice(0, 3).join("/").split("at://")[1]}` 215 + `https://plc.directory/${subjectUri.split("/").slice(0, 3).join("/").split("at://")[1]}`, 216 216 ) 217 217 .then((res) => res.json<{ service: { seviceEndpoint: string }[] }>()) 218 218 .then((data) => data.service); ··· 236 236 } 237 237 238 238 service = await fetch( 239 - `https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}` 239 + `https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}`, 240 240 ) 241 241 .then((res) => res.json<{ service: { seviceEndpoint: string }[] }>()) 242 242 .then((data) => data.service); ··· 322 322 ctx: Context, 323 323 shoutUri: string, 324 324 user, 325 - agent: Agent 325 + agent: Agent, 326 326 ) { 327 327 const rkey = TID.nextStr(); 328 328 ··· 342 342 } 343 343 344 344 const { service } = await fetch( 345 - `https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}` 345 + `https://plc.directory/${shoutUri.split("/").slice(0, 3).join("/").split("at://")[1]}`, 346 346 ).then((res) => res.json<{ service: [{ serviceEndpoint: string }] }>()); 347 347 348 348 const atpAgent = new AtpAgent({ ··· 410 410 ctx: Context, 411 411 shoutUri: string, 412 412 user, 413 - agent: Agent 413 + agent: Agent, 414 414 ) { 415 415 const likes = await ctx.db 416 416 .select({
+13 -13
apps/api/src/spotify/app.ts
··· 22 22 limit: 10, // max Spotify API calls 23 23 window: 15, // per 10 seconds 24 24 keyPrefix: "spotify-ratelimit", 25 - }) 25 + }), 26 26 ); 27 27 28 28 app.get("/login", async (c) => { ··· 55 55 const redirectUrl = `https://accounts.spotify.com/en/authorize?client_id=${env.SPOTIFY_CLIENT_ID}&response_type=code&redirect_uri=${env.SPOTIFY_REDIRECT_URI}&scope=user-read-private%20user-read-email%20user-read-playback-state%20user-read-currently-playing%20user-modify-playback-state%20playlist-modify-public%20playlist-modify-private%20playlist-read-private%20playlist-read-collaborative&state=${state}`; 56 56 c.header( 57 57 "Set-Cookie", 58 - `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure` 58 + `session-id=${state}; Path=/; HttpOnly; SameSite=Strict; Secure`, 59 59 ); 60 60 return c.json({ redirectUrl }); 61 61 }); ··· 133 133 .where( 134 134 and( 135 135 eq(spotifyAccounts.userId, user.id), 136 - eq(spotifyAccounts.isBetaUser, true) 137 - ) 136 + eq(spotifyAccounts.isBetaUser, true), 137 + ), 138 138 ) 139 139 .limit(1) 140 140 .then((rows) => rows[0]); ··· 251 251 } 252 252 253 253 const cached = await ctx.redis.get( 254 - `${spotifyAccount.spotifyAccount.email}:current` 254 + `${spotifyAccount.spotifyAccount.email}:current`, 255 255 ); 256 256 if (!cached) { 257 257 return c.json({}); ··· 261 261 262 262 const sha256 = createHash("sha256") 263 263 .update( 264 - `${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase() 264 + `${track.item.name} - ${track.item.artists.map((x) => x.name).join(", ")} - ${track.item.album.name}`.toLowerCase(), 265 265 ) 266 266 .digest("hex"); 267 267 ··· 334 334 335 335 const refreshToken = decrypt( 336 336 spotifyToken.refreshToken, 337 - env.SPOTIFY_ENCRYPTION_KEY 337 + env.SPOTIFY_ENCRYPTION_KEY, 338 338 ); 339 339 340 340 // get new access token ··· 410 410 411 411 const refreshToken = decrypt( 412 412 spotifyToken.refreshToken, 413 - env.SPOTIFY_ENCRYPTION_KEY 413 + env.SPOTIFY_ENCRYPTION_KEY, 414 414 ); 415 415 416 416 // get new access token ··· 486 486 487 487 const refreshToken = decrypt( 488 488 spotifyToken.refreshToken, 489 - env.SPOTIFY_ENCRYPTION_KEY 489 + env.SPOTIFY_ENCRYPTION_KEY, 490 490 ); 491 491 492 492 // get new access token ··· 562 562 563 563 const refreshToken = decrypt( 564 564 spotifyToken.refreshToken, 565 - env.SPOTIFY_ENCRYPTION_KEY 565 + env.SPOTIFY_ENCRYPTION_KEY, 566 566 ); 567 567 568 568 // get new access token ··· 590 590 headers: { 591 591 Authorization: `Bearer ${access_token}`, 592 592 }, 593 - } 593 + }, 594 594 ); 595 595 596 596 if (response.status === 403) { ··· 641 641 642 642 const refreshToken = decrypt( 643 643 spotifyToken.refreshToken, 644 - env.SPOTIFY_ENCRYPTION_KEY 644 + env.SPOTIFY_ENCRYPTION_KEY, 645 645 ); 646 646 647 647 // get new access token ··· 670 670 headers: { 671 671 Authorization: `Bearer ${access_token}`, 672 672 }, 673 - } 673 + }, 674 674 ); 675 675 676 676 if (response.status === 403) {
+51
apps/api/src/tealfm/index.ts
··· 1 + import type { Agent } from "@atproto/api"; 2 + import { TID } from "@atproto/common"; 3 + import chalk from "chalk"; 4 + import * as Play from "lexicon/types/fm/teal/alpha/feed/play"; 5 + import type { MusicbrainzTrack } from "types/track"; 6 + 7 + const SUBMISSION_CLIENT_AGENT = "rocksky/v0.0.1"; 8 + 9 + async function publishPlayingNow( 10 + agent: Agent, 11 + track: MusicbrainzTrack, 12 + duration: number 13 + ) { 14 + try { 15 + const rkey = TID.nextStr(); 16 + const record: Play.Record = { 17 + $type: "fm.teal.alpha.feed.play", 18 + duration, 19 + trackName: track.name, 20 + playedTime: track.timestamp, 21 + artists: track.artist.map((artist) => ({ 22 + artistMbid: artist.mbid, 23 + artistName: artist.name, 24 + })), 25 + releaseMbid: track.releaseMBID, 26 + releaseName: track.album, 27 + recordingMbId: track.trackMBID, 28 + submissionClientAgent: SUBMISSION_CLIENT_AGENT, 29 + }; 30 + 31 + if (!Play.validateRecord(record).success) { 32 + console.log(Play.validateRecord(record)); 33 + console.log(chalk.cyan(JSON.stringify(record, null, 2))); 34 + throw new Error("Invalid record"); 35 + } 36 + 37 + const res = await agent.com.atproto.repo.putRecord({ 38 + repo: agent.assertDid, 39 + collection: "fm.teal.alpha.feed.play", 40 + rkey, 41 + record, 42 + validate: false, 43 + }); 44 + const uri = res.data.uri; 45 + console.log(`tealfm Play record created at ${uri}`); 46 + } catch (error) { 47 + console.error("Error publishing teal.fm record:", error); 48 + } 49 + } 50 + 51 + export default { publishPlayingNow };
+60
apps/api/src/tealfm/lexicons/teal/actor/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.defs", 4 + "defs": { 5 + "profileView": { 6 + "type": "object", 7 + "properties": { 8 + "did": { 9 + "type": "string", 10 + "description": "The decentralized identifier of the actor" 11 + }, 12 + "displayName": { 13 + "type": "string" 14 + }, 15 + "description": { 16 + "type": "string", 17 + "description": "Free-form profile description text." 18 + }, 19 + "descriptionFacets": { 20 + "type": "array", 21 + "description": "Annotations of text in the profile description (mentions, URLs, hashtags, etc). May be changed to another (backwards compatible) lexicon.", 22 + "items": { "type": "ref", "ref": "app.bsky.richtext.facet" } 23 + }, 24 + "featuredItem": { 25 + "type": "ref", 26 + "description": "The user's most recent item featured on their profile.", 27 + "ref": "fm.teal.alpha.actor.profile#featuredItem" 28 + }, 29 + "avatar": { 30 + "type": "string", 31 + "description": "IPLD of the avatar" 32 + }, 33 + "banner": { 34 + "type": "string", 35 + "description": "IPLD of the banner image" 36 + }, 37 + "createdAt": { "type": "string", "format": "datetime" } 38 + } 39 + }, 40 + "miniProfileView": { 41 + "type": "object", 42 + "properties": { 43 + "did": { 44 + "type": "string", 45 + "description": "The decentralized identifier of the actor" 46 + }, 47 + "displayName": { 48 + "type": "string" 49 + }, 50 + "handle": { 51 + "type": "string" 52 + }, 53 + "avatar": { 54 + "type": "string", 55 + "description": "IPLD of the avatar" 56 + } 57 + } 58 + } 59 + } 60 + }
+34
apps/api/src/tealfm/lexicons/teal/actor/getProfile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.getProfile", 4 + "description": "This lexicon is in a not officially released state. It is subject to change. | Retrieves a play given an author DID and record key.", 5 + "defs": { 6 + "main": { 7 + "type": "query", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["actor"], 11 + "properties": { 12 + "actor": { 13 + "type": "string", 14 + "format": "at-identifier", 15 + "description": "The author's DID" 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "required": ["actor"], 24 + "properties": { 25 + "actor": { 26 + "type": "ref", 27 + "ref": "fm.teal.alpha.actor.defs#profileView" 28 + } 29 + } 30 + } 31 + } 32 + } 33 + } 34 + }
+40
apps/api/src/tealfm/lexicons/teal/actor/getProfiles.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.getProfiles", 4 + "description": "This lexicon is in a not officially released state. It is subject to change. | Retrieves the associated profile.", 5 + "defs": { 6 + "main": { 7 + "type": "query", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["actors"], 11 + "properties": { 12 + "actors": { 13 + "type": "array", 14 + "items": { 15 + "type": "string", 16 + "format": "at-identifier" 17 + }, 18 + "description": "Array of actor DIDs" 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": ["actors"], 27 + "properties": { 28 + "actors": { 29 + "type": "array", 30 + "items": { 31 + "type": "ref", 32 + "ref": "fm.teal.alpha.actor.defs#miniProfileView" 33 + } 34 + } 35 + } 36 + } 37 + } 38 + } 39 + } 40 + }
+64
apps/api/src/tealfm/lexicons/teal/actor/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm account profile.", 8 + "key": "literal:self", 9 + "record": { 10 + "type": "object", 11 + "properties": { 12 + "displayName": { 13 + "type": "string", 14 + "maxGraphemes": 64, 15 + "maxLength": 640 16 + }, 17 + "description": { 18 + "type": "string", 19 + "description": "Free-form profile description text.", 20 + "maxGraphemes": 256, 21 + "maxLength": 2560 22 + }, 23 + "descriptionFacets": { 24 + "type": "array", 25 + "description": "Annotations of text in the profile description (mentions, URLs, hashtags, etc).", 26 + "items": { "type": "ref", "ref": "app.bsky.richtext.facet" } 27 + }, 28 + "featuredItem": { 29 + "type": "ref", 30 + "description": "The user's most recent item featured on their profile.", 31 + "ref": "#featuredItem" 32 + }, 33 + "avatar": { 34 + "type": "blob", 35 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'", 36 + "accept": ["image/png", "image/jpeg"], 37 + "maxSize": 1000000 38 + }, 39 + "banner": { 40 + "type": "blob", 41 + "description": "Larger horizontal image to display behind profile view.", 42 + "accept": ["image/png", "image/jpeg"], 43 + "maxSize": 1000000 44 + }, 45 + "createdAt": { "type": "string", "format": "datetime" } 46 + } 47 + } 48 + }, 49 + "featuredItem": { 50 + "type": "object", 51 + "required": ["mbid", "type"], 52 + "properties": { 53 + "mbid": { 54 + "type": "string", 55 + "description": "The Musicbrainz ID of the item" 56 + }, 57 + "type": { 58 + "type": "string", 59 + "description": "The type of the item. Must be a valid Musicbrainz type, e.g. album, track, recording, etc." 60 + } 61 + } 62 + } 63 + } 64 + }
+52
apps/api/src/tealfm/lexicons/teal/actor/searchActors.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.searchActors", 4 + "description": "This lexicon is in a not officially released state. It is subject to change. | Searches for actors based on profile contents.", 5 + "defs": { 6 + "main": { 7 + "type": "query", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["q"], 11 + "properties": { 12 + "q": { 13 + "type": "string", 14 + "description": "The search query", 15 + "maxGraphemes": 128, 16 + "maxLength": 640 17 + }, 18 + "limit": { 19 + "type": "integer", 20 + "description": "The maximum number of actors to return", 21 + "minimum": 1, 22 + "maximum": 25 23 + }, 24 + "cursor": { 25 + "type": "string", 26 + "description": "Cursor for pagination" 27 + } 28 + } 29 + }, 30 + "output": { 31 + "encoding": "application/json", 32 + "schema": { 33 + "type": "object", 34 + "required": ["actors"], 35 + "properties": { 36 + "actors": { 37 + "type": "array", 38 + "items": { 39 + "type": "ref", 40 + "ref": "fm.teal.alpha.actor.defs#miniProfileView" 41 + } 42 + }, 43 + "cursor": { 44 + "type": "string", 45 + "description": "Cursor for pagination" 46 + } 47 + } 48 + } 49 + } 50 + } 51 + } 52 + }
+31
apps/api/src/tealfm/lexicons/teal/actor/status.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.status", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "This lexicon is in a not officially released state. It is subject to change. | A declaration of the status of the actor. Only one can be shown at a time. If there are multiple, the latest record should be picked and earlier records should be deleted or tombstoned.", 8 + "key": "literal:self", 9 + "record": { 10 + "type": "object", 11 + "required": ["time", "item"], 12 + "properties": { 13 + "time": { 14 + "type": "string", 15 + "format": "datetime", 16 + "description": "The unix timestamp of when the item was recorded" 17 + }, 18 + "expiry": { 19 + "type": "string", 20 + "format": "datetime", 21 + "description": "The unix timestamp of the expiry time of the item. If unavailable, default to 10 minutes past the start time." 22 + }, 23 + "item": { 24 + "type": "ref", 25 + "ref": "fm.teal.alpha.feed.defs#playView" 26 + } 27 + } 28 + } 29 + } 30 + } 31 + }
+90
apps/api/src/tealfm/lexicons/teal/feed/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.feed.defs", 4 + "description": "This lexicon is in a not officially released state. It is subject to change. | Misc. items related to feeds.", 5 + "defs": { 6 + "playView": { 7 + "type": "object", 8 + "required": ["trackName", "artists"], 9 + "properties": { 10 + "trackName": { 11 + "type": "string", 12 + "minLength": 1, 13 + "maxLength": 256, 14 + "maxGraphemes": 2560, 15 + "description": "The name of the track" 16 + }, 17 + "trackMbId": { 18 + "type": "string", 19 + "description": "The Musicbrainz ID of the track" 20 + }, 21 + "recordingMbId": { 22 + "type": "string", 23 + "description": "The Musicbrainz recording ID of the track" 24 + }, 25 + "duration": { 26 + "type": "integer", 27 + "description": "The length of the track in seconds" 28 + }, 29 + "artists": { 30 + "type": "array", 31 + "items": { 32 + "type": "ref", 33 + "ref": "#artist" 34 + }, 35 + "description": "Array of artists in order of original appearance." 36 + }, 37 + "releaseName": { 38 + "type": "string", 39 + "maxLength": 256, 40 + "maxGraphemes": 2560, 41 + "description": "The name of the release/album" 42 + }, 43 + "releaseMbId": { 44 + "type": "string", 45 + "description": "The Musicbrainz release ID" 46 + }, 47 + "isrc": { 48 + "type": "string", 49 + "description": "The ISRC code associated with the recording" 50 + }, 51 + "originUrl": { 52 + "type": "string", 53 + "description": "The URL associated with this track" 54 + }, 55 + "musicServiceBaseDomain": { 56 + "type": "string", 57 + "description": "The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if not provided." 58 + }, 59 + "submissionClientAgent": { 60 + "type": "string", 61 + "maxLength": 256, 62 + "maxGraphemes": 2560, 63 + "description": "A user-agent style string specifying the user agent. e.g. tealtracker/0.0.1b (Linux; Android 13; SM-A715F). Defaults to 'manual/unknown' if not provided." 64 + }, 65 + "playedTime": { 66 + "type": "string", 67 + "format": "datetime", 68 + "description": "The unix timestamp of when the track was played" 69 + } 70 + } 71 + }, 72 + "artist": { 73 + "type": "object", 74 + "required": ["artistName"], 75 + "properties": { 76 + "artistName": { 77 + "type": "string", 78 + "minLength": 1, 79 + "maxLength": 256, 80 + "maxGraphemes": 2560, 81 + "description": "The name of the artist" 82 + }, 83 + "artistMbId": { 84 + "type": "string", 85 + "description": "The Musicbrainz ID of the artist" 86 + } 87 + } 88 + } 89 + } 90 + }
+45
apps/api/src/tealfm/lexicons/teal/feed/getActorFeed.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.feed.getActorFeed", 4 + "description": "This lexicon is in a not officially released state. It is subject to change. | Retrieves multiple plays from the index or via an author's DID.", 5 + "defs": { 6 + "main": { 7 + "type": "query", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["authorDID"], 11 + "properties": { 12 + "authorDID": { 13 + "type": "string", 14 + "format": "at-identifier", 15 + "description": "The author's DID for the play" 16 + }, 17 + "cursor": { 18 + "type": "string", 19 + "description": "The cursor to start the query from" 20 + }, 21 + "limit": { 22 + "type": "integer", 23 + "description": "The upper limit of tracks to get per request. Default is 20, max is 50." 24 + } 25 + } 26 + }, 27 + "output": { 28 + "encoding": "application/json", 29 + "schema": { 30 + "type": "object", 31 + "required": ["plays"], 32 + "properties": { 33 + "plays": { 34 + "type": "array", 35 + "items": { 36 + "type": "ref", 37 + "ref": "fm.teal.alpha.feed.defs#playView" 38 + } 39 + } 40 + } 41 + } 42 + } 43 + } 44 + } 45 + }
+38
apps/api/src/tealfm/lexicons/teal/feed/getPlay.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.feed.getPlay", 4 + "description": "This lexicon is in a not officially released state. It is subject to change. | Retrieves a play given an author DID and record key.", 5 + "defs": { 6 + "main": { 7 + "type": "query", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["authorDID", "rkey"], 11 + "properties": { 12 + "authorDID": { 13 + "type": "string", 14 + "format": "at-identifier", 15 + "description": "The author's DID for the play" 16 + }, 17 + "rkey": { 18 + "type": "string", 19 + "description": "The record key of the play" 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "required": ["play"], 28 + "properties": { 29 + "play": { 30 + "type": "ref", 31 + "ref": "fm.teal.alpha.feed.defs#playView" 32 + } 33 + } 34 + } 35 + } 36 + } 37 + } 38 + }
+95
apps/api/src/tealfm/lexicons/teal/feed/play.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.feed.play", 4 + "description": "This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm play. Plays are submitted as a result of a user listening to a track. Plays should be marked as tracked when a user has listened to the entire track if it's under 2 minutes long, or half of the track's duration up to 4 minutes, whichever is longest.", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["trackName"], 12 + "properties": { 13 + "trackName": { 14 + "type": "string", 15 + "minLength": 1, 16 + "maxLength": 256, 17 + "maxGraphemes": 2560, 18 + "description": "The name of the track" 19 + }, 20 + "trackMbId": { 21 + "type": "string", 22 + 23 + "description": "The Musicbrainz ID of the track" 24 + }, 25 + "recordingMbId": { 26 + "type": "string", 27 + "description": "The Musicbrainz recording ID of the track" 28 + }, 29 + "duration": { 30 + "type": "integer", 31 + "description": "The length of the track in seconds" 32 + }, 33 + "artistNames": { 34 + "type": "array", 35 + "items": { 36 + "type": "string", 37 + "minLength": 1, 38 + "maxLength": 256, 39 + "maxGraphemes": 2560 40 + }, 41 + "description": "Array of artist names in order of original appearance. Prefer using 'artists'." 42 + }, 43 + "artistMbIds": { 44 + "type": "array", 45 + "items": { 46 + "type": "string" 47 + }, 48 + "description": "Array of Musicbrainz artist IDs. Prefer using 'artists'." 49 + }, 50 + "artists": { 51 + "type": "array", 52 + "items": { 53 + "type": "ref", 54 + "ref": "fm.teal.alpha.feed.defs#artist" 55 + }, 56 + "description": "Array of artists in order of original appearance." 57 + }, 58 + "releaseName": { 59 + "type": "string", 60 + "maxLength": 256, 61 + "maxGraphemes": 2560, 62 + "description": "The name of the release/album" 63 + }, 64 + "releaseMbId": { 65 + "type": "string", 66 + "description": "The Musicbrainz release ID" 67 + }, 68 + "isrc": { 69 + "type": "string", 70 + "description": "The ISRC code associated with the recording" 71 + }, 72 + "originUrl": { 73 + "type": "string", 74 + "description": "The URL associated with this track" 75 + }, 76 + "musicServiceBaseDomain": { 77 + "type": "string", 78 + "description": "The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. Defaults to 'local' if unavailable or not provided." 79 + }, 80 + "submissionClientAgent": { 81 + "type": "string", 82 + "maxLength": 256, 83 + "maxGraphemes": 2560, 84 + "description": "A metadata string specifying the user agent where the format is `<app-identifier>/<version> (<kernel/OS-base>; <platform/OS-version>; <device-model>)`. If string is provided, only `app-identifier` and `version` are required. `app-identifier` is recommended to be in reverse dns format. Defaults to 'manual/unknown' if unavailable or not provided." 85 + }, 86 + "playedTime": { 87 + "type": "string", 88 + "format": "datetime", 89 + "description": "The unix timestamp of when the track was played" 90 + } 91 + } 92 + } 93 + } 94 + } 95 + }
+9 -9
apps/api/src/tracks/tracks.service.ts
··· 132 132 if (!track_id || !album_id || !artist_id) { 133 133 console.log( 134 134 "Track not yet saved (uri not saved), retrying...", 135 - tries + 1 135 + tries + 1, 136 136 ); 137 137 await new Promise((resolve) => setTimeout(resolve, 1000)); 138 138 tries += 1; ··· 145 145 .where( 146 146 and( 147 147 eq(albumTracks.albumId, album_id.id), 148 - eq(albumTracks.trackId, track_id.id) 149 - ) 148 + eq(albumTracks.trackId, track_id.id), 149 + ), 150 150 ) 151 151 .limit(1) 152 152 .then((results) => results[0]); ··· 157 157 .where( 158 158 and( 159 159 eq(artistTracks.artistId, artist_id.id), 160 - eq(artistTracks.trackId, track_id.id) 161 - ) 160 + eq(artistTracks.trackId, track_id.id), 161 + ), 162 162 ) 163 163 .limit(1) 164 164 .then((results) => results[0]); ··· 169 169 .where( 170 170 and( 171 171 eq(artistAlbums.artistId, artist_id.id), 172 - eq(artistAlbums.albumId, album_id.id) 173 - ) 172 + eq(artistAlbums.albumId, album_id.id), 173 + ), 174 174 ) 175 175 .limit(1) 176 176 .then((results) => results[0]); ··· 264 264 xata_createdat: artist_album.createdAt.toISOString(), 265 265 xata_updatedat: artist_album.updatedAt.toISOString(), 266 266 }, 267 - }) 267 + }), 268 268 ); 269 269 270 270 ctx.nc.publish( 271 271 "rocksky.track", 272 - Buffer.from(message.replaceAll("sha_256", "sha256")) 272 + Buffer.from(message.replaceAll("sha_256", "sha256")), 273 273 ); 274 274 break; 275 275 }
+13
apps/api/src/types/track.ts
··· 34 34 }); 35 35 36 36 export type Track = z.infer<typeof trackSchema>; 37 + 38 + export type MusicbrainzTrack = { 39 + trackMBID: string; 40 + releaseMBID: string; 41 + name: string; 42 + artist: { 43 + id: string; 44 + mbid: string; 45 + name: string; 46 + }[]; 47 + album: string; 48 + timestamp: string; 49 + };
+32 -32
apps/api/src/users/app.ts
··· 20 20 import { requestCounter } from "metrics"; 21 21 import * as R from "ramda"; 22 22 import tables from "schema"; 23 - import { SelectUser } from "schema/users"; 23 + import type { SelectUser } from "schema/users"; 24 24 import { 25 25 createShout, 26 26 likeShout, ··· 123 123 data.map((item) => ({ 124 124 ...item, 125 125 tags: [], 126 - })) 126 + })), 127 127 ); 128 128 }); 129 129 ··· 153 153 results.map((x) => ({ 154 154 ...x.playlists, 155 155 trackCount: +x.trackCount, 156 - })) 156 + })), 157 157 ); 158 158 }); 159 159 ··· 219 219 .from(tables.userArtists) 220 220 .leftJoin( 221 221 tables.artists, 222 - eq(tables.userArtists.artistId, tables.artists.id) 222 + eq(tables.userArtists.artistId, tables.artists.id), 223 223 ) 224 224 .where(or(eq(tables.userArtists.uri, uri), eq(tables.artists.uri, uri))) 225 225 .limit(1) ··· 309 309 scrobbles: scrobbles || 1, 310 310 label: tracks[0]?.tracks.label || "", 311 311 tracks: dedupeTracksKeepLyrics(tracks.map((track) => track.tracks)).sort( 312 - (a, b) => a.track_number - b.track_number 312 + (a, b) => a.track_number - b.track_number, 313 313 ), 314 314 tags: [], 315 315 }); ··· 398 398 ...R.omit(["id"], item.tracks), 399 399 id: item.tracks.id, 400 400 xata_version: item.artist_tracks.xataVersion, 401 - })) 401 + })), 402 402 ); 403 403 }); 404 404 ··· 430 430 ...R.omit(["id"], item.albums), 431 431 id: item.albums.id, 432 432 xata_version: item.artist_albums.xataVersion, 433 - })) 434 - ) 433 + })), 434 + ), 435 435 ); 436 436 }); 437 437 ··· 461 461 .from(tables.playlistTracks) 462 462 .leftJoin( 463 463 tables.playlists, 464 - eq(tables.playlistTracks.playlistId, tables.playlists.id) 464 + eq(tables.playlistTracks.playlistId, tables.playlists.id), 465 465 ) 466 466 .leftJoin( 467 467 tables.tracks, 468 - eq(tables.playlistTracks.trackId, tables.tracks.id) 468 + eq(tables.playlistTracks.trackId, tables.tracks.id), 469 469 ) 470 470 .where(eq(tables.playlists.uri, uri)) 471 471 .groupBy( ··· 509 509 tables.playlists.picture, 510 510 tables.playlists.spotifyLink, 511 511 tables.playlists.tidalLink, 512 - tables.playlists.appleMusicLink 512 + tables.playlists.appleMusicLink, 513 513 ) 514 514 .orderBy(asc(tables.playlistTracks.createdAt)) 515 515 .execute(); ··· 632 632 parsed.data, 633 633 `at://${did}/app.rocksky.artist/${rkey}`, 634 634 user, 635 - agent 635 + agent, 636 636 ); 637 637 return c.json({}); 638 638 }); ··· 681 681 parsed.data, 682 682 `at://${did}/app.rocksky.album/${rkey}`, 683 683 user, 684 - agent 684 + agent, 685 685 ); 686 686 return c.json({}); 687 687 }); ··· 730 730 parsed.data, 731 731 `at://${did}/app.rocksky.song/${rkey}`, 732 732 user, 733 - agent 733 + agent, 734 734 ); 735 735 736 736 return c.json({}); ··· 780 780 parsed.data, 781 781 `at://${did}/app.rocksky.scrobble/${rkey}`, 782 782 user, 783 - agent 783 + agent, 784 784 ); 785 785 786 786 return c.json({}); ··· 1063 1063 parsed.data, 1064 1064 `at://${did}/app.rocksky.shout/${rkey}`, 1065 1065 user, 1066 - agent 1066 + agent, 1067 1067 ); 1068 1068 return c.json({}); 1069 1069 }); ··· 1132 1132 .leftJoin(tables.artists, eq(tables.shouts.artistId, tables.artists.id)) 1133 1133 .leftJoin( 1134 1134 tables.shoutLikes, 1135 - eq(tables.shouts.id, tables.shoutLikes.shoutId) 1135 + eq(tables.shouts.id, tables.shoutLikes.shoutId), 1136 1136 ) 1137 1137 .where(eq(tables.artists.uri, `at://${did}/app.rocksky.artist/${rkey}`)) 1138 1138 .groupBy( ··· 1145 1145 tables.users.did, 1146 1146 tables.users.handle, 1147 1147 tables.users.displayName, 1148 - tables.users.avatar 1148 + tables.users.avatar, 1149 1149 ) 1150 1150 .orderBy(desc(tables.shouts.createdAt)) 1151 1151 .execute(); ··· 1217 1217 .leftJoin(tables.albums, eq(tables.shouts.albumId, tables.albums.id)) 1218 1218 .leftJoin( 1219 1219 tables.shoutLikes, 1220 - eq(tables.shouts.id, tables.shoutLikes.shoutId) 1220 + eq(tables.shouts.id, tables.shoutLikes.shoutId), 1221 1221 ) 1222 1222 .where(eq(tables.albums.uri, `at://${did}/app.rocksky.album/${rkey}`)) 1223 1223 .groupBy( ··· 1230 1230 tables.users.did, 1231 1231 tables.users.handle, 1232 1232 tables.users.displayName, 1233 - tables.users.avatar 1233 + tables.users.avatar, 1234 1234 ) 1235 1235 .orderBy(desc(tables.shouts.createdAt)) 1236 1236 .execute(); ··· 1302 1302 .leftJoin(tables.tracks, eq(tables.shouts.trackId, tables.tracks.id)) 1303 1303 .leftJoin( 1304 1304 tables.shoutLikes, 1305 - eq(tables.shouts.id, tables.shoutLikes.shoutId) 1305 + eq(tables.shouts.id, tables.shoutLikes.shoutId), 1306 1306 ) 1307 1307 .where(eq(tables.tracks.uri, `at://${did}/app.rocksky.song/${rkey}`)) 1308 1308 .groupBy( ··· 1315 1315 tables.users.did, 1316 1316 tables.users.handle, 1317 1317 tables.users.displayName, 1318 - tables.users.avatar 1318 + tables.users.avatar, 1319 1319 ) 1320 1320 .orderBy(desc(tables.shouts.createdAt)) 1321 1321 .execute(); ··· 1386 1386 .leftJoin(tables.users, eq(tables.shouts.authorId, tables.users.id)) 1387 1387 .leftJoin( 1388 1388 tables.scrobbles, 1389 - eq(tables.shouts.scrobbleId, tables.scrobbles.id) 1389 + eq(tables.shouts.scrobbleId, tables.scrobbles.id), 1390 1390 ) 1391 1391 .leftJoin( 1392 1392 tables.shoutLikes, 1393 - eq(tables.shouts.id, tables.shoutLikes.shoutId) 1393 + eq(tables.shouts.id, tables.shoutLikes.shoutId), 1394 1394 ) 1395 1395 .where(eq(tables.scrobbles.uri, `at://${did}/app.rocksky.scrobble/${rkey}`)) 1396 1396 .groupBy( ··· 1403 1403 tables.users.did, 1404 1404 tables.users.handle, 1405 1405 tables.users.displayName, 1406 - tables.users.avatar 1406 + tables.users.avatar, 1407 1407 ) 1408 1408 .orderBy(desc(tables.shouts.createdAt)) 1409 1409 .execute(); ··· 1482 1482 .leftJoin(tables.shouts, eq(tables.profileShouts.shoutId, tables.shouts.id)) 1483 1483 .leftJoin( 1484 1484 aliasedTable(tables.users, "authors"), 1485 - eq(tables.shouts.authorId, aliasedTable(tables.users, "authors").id) 1485 + eq(tables.shouts.authorId, aliasedTable(tables.users, "authors").id), 1486 1486 ) 1487 1487 .leftJoin(tables.users, eq(tables.profileShouts.userId, tables.users.id)) 1488 1488 .leftJoin( 1489 1489 tables.shoutLikes, 1490 - eq(tables.shouts.id, tables.shoutLikes.shoutId) 1490 + eq(tables.shouts.id, tables.shoutLikes.shoutId), 1491 1491 ) 1492 1492 .groupBy( 1493 1493 tables.profileShouts.id, ··· 1506 1506 aliasedTable(tables.users, "authors").did, 1507 1507 aliasedTable(tables.users, "authors").handle, 1508 1508 aliasedTable(tables.users, "authors").displayName, 1509 - aliasedTable(tables.users, "authors").avatar 1509 + aliasedTable(tables.users, "authors").avatar, 1510 1510 ) 1511 1511 .orderBy(desc(tables.profileShouts.createdAt)) 1512 1512 .execute(); ··· 1615 1615 .where( 1616 1616 and( 1617 1617 eq(tables.shoutReports.userId, user.id), 1618 - eq(tables.shoutReports.shoutId, shout.id) 1619 - ) 1618 + eq(tables.shoutReports.shoutId, shout.id), 1619 + ), 1620 1620 ) 1621 1621 .limit(1) 1622 1622 .execute() ··· 1689 1689 .where( 1690 1690 and( 1691 1691 eq(tables.shoutReports.userId, user.id), 1692 - eq(tables.shoutReports.shoutId, shout.id) 1693 - ) 1692 + eq(tables.shoutReports.shoutId, shout.id), 1693 + ), 1694 1694 ) 1695 1695 .limit(1) 1696 1696 .execute()
+9 -9
apps/api/src/xrpc/app/rocksky/actor/getProfile.ts
··· 33 33 Effect.catchAll((err) => { 34 34 console.error(err); 35 35 return Effect.succeed({}); 36 - }) 36 + }), 37 37 ); 38 38 server.app.rocksky.actor.getProfile({ 39 39 auth: ctx.authVerifier, ··· 194 194 .from(tables.spotifyAccounts) 195 195 .leftJoin( 196 196 tables.users, 197 - eq(tables.spotifyAccounts.userId, tables.users.id) 197 + eq(tables.spotifyAccounts.userId, tables.users.id), 198 198 ) 199 199 .where(eq(tables.users.did, did)) 200 200 .execute() ··· 204 204 .from(tables.spotifyTokens) 205 205 .leftJoin( 206 206 tables.users, 207 - eq(tables.spotifyTokens.userId, tables.users.id) 207 + eq(tables.spotifyTokens.userId, tables.users.id), 208 208 ) 209 209 .where(eq(tables.users.did, did)) 210 210 .execute() ··· 214 214 .from(tables.googleDriveAccounts) 215 215 .leftJoin( 216 216 tables.users, 217 - eq(tables.googleDriveAccounts.userId, tables.users.id) 217 + eq(tables.googleDriveAccounts.userId, tables.users.id), 218 218 ) 219 219 .where(eq(tables.users.did, did)) 220 220 .execute() ··· 224 224 .from(tables.dropboxAccounts) 225 225 .leftJoin( 226 226 tables.users, 227 - eq(tables.dropboxAccounts.userId, tables.users.id) 227 + eq(tables.dropboxAccounts.userId, tables.users.id), 228 228 ) 229 229 .where(eq(tables.users.did, did)) 230 230 .execute() ··· 281 281 xata_createdat: profile.user.createdAt.toISOString(), 282 282 xata_updatedat: profile.user.updatedAt.toISOString(), 283 283 xata_version: 1, 284 - }) 285 - ) 284 + }), 285 + ), 286 286 ); 287 287 } else { 288 288 // Update existing user in background if handle or avatar or displayName changed ··· 315 315 xata_createdat: profile.user.createdAt.toISOString(), 316 316 xata_updatedat: new Date().toISOString(), 317 317 xata_version: (profile.user.xataVersion || 1) + 1, 318 - }) 319 - ) 318 + }), 319 + ), 320 320 ); 321 321 } 322 322 }
+78
musicbrainz/go.mod
··· 1 + module github.com/tsirysndr/rocksky/musicbrainz 2 + 3 + go 1.25.1 4 + 5 + require ( 6 + github.com/labstack/echo/v4 v4.13.3 7 + github.com/teal-fm/piper v0.0.0-20251003185718-473cb159ecbe 8 + ) 9 + 10 + require ( 11 + github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e // indirect 12 + github.com/carlmjohnson/versioninfo v0.22.5 // indirect 13 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 14 + github.com/dlclark/regexp2 v1.11.5 // indirect 15 + github.com/felixge/httpsnoop v1.0.4 // indirect 16 + github.com/go-logr/logr v1.4.2 // indirect 17 + github.com/go-logr/stdr v1.2.2 // indirect 18 + github.com/goccy/go-json v0.10.2 // indirect 19 + github.com/gogo/protobuf v1.3.2 // indirect 20 + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 21 + github.com/google/uuid v1.6.0 // indirect 22 + github.com/haileyok/atproto-oauth-golang v0.0.2 // indirect 23 + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 24 + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 25 + github.com/hashicorp/golang-lru v1.0.2 // indirect 26 + github.com/ipfs/bbloom v0.0.4 // indirect 27 + github.com/ipfs/go-block-format v0.2.0 // indirect 28 + github.com/ipfs/go-cid v0.4.1 // indirect 29 + github.com/ipfs/go-datastore v0.6.0 // indirect 30 + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 31 + github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 32 + github.com/ipfs/go-ipfs-util v0.0.3 // indirect 33 + github.com/ipfs/go-ipld-cbor v0.1.0 // indirect 34 + github.com/ipfs/go-ipld-format v0.6.0 // indirect 35 + github.com/ipfs/go-log v1.0.5 // indirect 36 + github.com/ipfs/go-log/v2 v2.5.1 // indirect 37 + github.com/ipfs/go-metrics-interface v0.0.1 // indirect 38 + github.com/jbenet/goprocess v0.1.4 // indirect 39 + github.com/klauspost/cpuid/v2 v2.2.7 // indirect 40 + github.com/labstack/gommon v0.4.2 // indirect 41 + github.com/lestrrat-go/blackmagic v1.0.2 // indirect 42 + github.com/lestrrat-go/httpcc v1.0.1 // indirect 43 + github.com/lestrrat-go/httprc v1.0.4 // indirect 44 + github.com/lestrrat-go/iter v1.0.2 // indirect 45 + github.com/lestrrat-go/jwx/v2 v2.0.12 // indirect 46 + github.com/lestrrat-go/option v1.0.1 // indirect 47 + github.com/mattn/go-colorable v0.1.13 // indirect 48 + github.com/mattn/go-isatty v0.0.20 // indirect 49 + github.com/mattn/go-sqlite3 v1.14.27 // indirect 50 + github.com/minio/sha256-simd v1.0.1 // indirect 51 + github.com/mr-tron/base58 v1.2.0 // indirect 52 + github.com/multiformats/go-base32 v0.1.0 // indirect 53 + github.com/multiformats/go-base36 v0.2.0 // indirect 54 + github.com/multiformats/go-multibase v0.2.0 // indirect 55 + github.com/multiformats/go-multihash v0.2.3 // indirect 56 + github.com/multiformats/go-varint v0.0.7 // indirect 57 + github.com/opentracing/opentracing-go v1.2.0 // indirect 58 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 59 + github.com/segmentio/asm v1.2.0 // indirect 60 + github.com/spaolacci/murmur3 v1.1.0 // indirect 61 + github.com/valyala/bytebufferpool v1.0.0 // indirect 62 + github.com/valyala/fasttemplate v1.2.2 // indirect 63 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 64 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 65 + go.opentelemetry.io/otel v1.29.0 // indirect 66 + go.opentelemetry.io/otel/metric v1.29.0 // indirect 67 + go.opentelemetry.io/otel/trace v1.29.0 // indirect 68 + go.uber.org/atomic v1.11.0 // indirect 69 + go.uber.org/multierr v1.11.0 // indirect 70 + go.uber.org/zap v1.26.0 // indirect 71 + golang.org/x/crypto v0.32.0 // indirect 72 + golang.org/x/net v0.33.0 // indirect 73 + golang.org/x/sys v0.29.0 // indirect 74 + golang.org/x/text v0.21.0 // indirect 75 + golang.org/x/time v0.11.0 // indirect 76 + golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect 77 + lukechampine.com/blake3 v1.2.1 // indirect 78 + )
+303
musicbrainz/go.sum
··· 1 + github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 + github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 + github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e h1:yEW1njmALj7i1AjLhq6Lsxts48JUCTT+wpM9m7GNLVY= 4 + github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng= 5 + github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= 6 + github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= 7 + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 8 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 11 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 + github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 13 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 14 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 15 + github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 16 + github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 17 + github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 18 + github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 19 + github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 20 + github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 21 + github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 22 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 23 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 24 + github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 25 + github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 26 + github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 27 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 28 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 29 + github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 30 + github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 31 + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 32 + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 33 + github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 34 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 35 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 37 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 38 + github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8= 39 + github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8= 40 + github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 41 + github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 42 + github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 43 + github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 44 + github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= 45 + github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= 46 + github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 47 + github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 48 + github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 49 + github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 50 + github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 51 + github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 52 + github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 53 + github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 54 + github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 55 + github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 56 + github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 57 + github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 58 + github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 59 + github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 60 + github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 61 + github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 62 + github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 63 + github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 64 + github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 65 + github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 66 + github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 67 + github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 68 + github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 69 + github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 70 + github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 71 + github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 72 + github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 73 + github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 74 + github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 75 + github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= 76 + github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 77 + github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 78 + github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 79 + github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 80 + github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 81 + github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 82 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 83 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 84 + github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 85 + github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 86 + github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 87 + github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 88 + github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 89 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 90 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 91 + github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 92 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 93 + github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 94 + github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 95 + github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 96 + github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 97 + github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 98 + github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 99 + github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 100 + github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 101 + github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 102 + github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= 103 + github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 104 + github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 105 + github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 106 + github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= 107 + github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= 108 + github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 109 + github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 110 + github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 111 + github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 112 + github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 113 + github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 114 + github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 115 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 116 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 117 + github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU= 118 + github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 119 + github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 120 + github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 121 + github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 122 + github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 123 + github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 124 + github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 125 + github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 126 + github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 127 + github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 128 + github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 129 + github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 130 + github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 131 + github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 132 + github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 133 + github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 134 + github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 135 + github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 136 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 137 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 138 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 139 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 140 + github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 141 + github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 142 + github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 143 + github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 144 + github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 145 + github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 146 + github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 147 + github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 148 + github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 149 + github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 150 + github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 151 + github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 152 + github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 153 + github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 154 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 155 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 156 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 157 + github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 158 + github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 159 + github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 160 + github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 161 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 162 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 163 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 164 + github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 165 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 166 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 167 + github.com/teal-fm/piper v0.0.0-20251003185718-473cb159ecbe h1:tQAxIgPCFq+VNm4sCGBCIMmtPzwFzPjTUQnsYI7Dx/I= 168 + github.com/teal-fm/piper v0.0.0-20251003185718-473cb159ecbe/go.mod h1:T4s0uuV1isetJS7RMSDrPUBUpgnfmLO1Sn1WeoYOMS8= 169 + github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 170 + github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 171 + github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 172 + github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 173 + github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 174 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 175 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 176 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 177 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 178 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 179 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 180 + github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 181 + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 182 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 183 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 184 + go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 185 + go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 186 + go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 187 + go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 188 + go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 189 + go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 190 + go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 191 + go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 192 + go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 193 + go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 194 + go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 195 + go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 196 + go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= 197 + go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 198 + go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 199 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 200 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 201 + go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 202 + go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 203 + go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 204 + go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 205 + go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 206 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 207 + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 208 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 209 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 210 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 211 + golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 212 + golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 213 + golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 214 + golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 215 + golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 216 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 217 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 218 + golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 219 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 220 + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 221 + golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 222 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 223 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 224 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 225 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 226 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 227 + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 228 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 229 + golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 230 + golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 231 + golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 232 + golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 233 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 234 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 235 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 236 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 237 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 238 + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 239 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 240 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 241 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 242 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 243 + golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 244 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 245 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 246 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 247 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 248 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 249 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 250 + golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 251 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 252 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 253 + golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 254 + golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 255 + golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 256 + golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 257 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 258 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 259 + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 260 + golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 261 + golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 262 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 263 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 264 + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 265 + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 266 + golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 267 + golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 268 + golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 269 + golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 270 + golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 271 + golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 272 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 273 + golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 274 + golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 275 + golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 276 + golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 277 + golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 278 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 279 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 280 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 281 + golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 282 + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 283 + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 284 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 285 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 286 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 287 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 288 + golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= 289 + golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 290 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 291 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 292 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 293 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 294 + gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 295 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 296 + gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 297 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 298 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 299 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 300 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 301 + honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 302 + lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 303 + lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+89
musicbrainz/main.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log" 7 + "net/http" 8 + "os" 9 + "os/signal" 10 + "time" 11 + 12 + "github.com/labstack/echo/v4" 13 + "github.com/teal-fm/piper/db" 14 + "github.com/teal-fm/piper/models" 15 + "github.com/teal-fm/piper/service/musicbrainz" 16 + ) 17 + 18 + type Server struct { 19 + mb *musicbrainz.MusicBrainzService 20 + } 21 + 22 + func main() { 23 + dbPath := os.Getenv("DB_PATH") 24 + if dbPath == "" { 25 + dbPath = "./piper.db" 26 + } 27 + 28 + database, err := db.New(dbPath) 29 + if err != nil { 30 + log.Fatalf("Error connecting to database: %v", err) 31 + } 32 + 33 + if err := database.Initialize(); err != nil { 34 + log.Fatalf("Error initializing database: %v", err) 35 + } 36 + 37 + srv := &Server{ 38 + mb: musicbrainz.NewMusicBrainzService(database), 39 + } 40 + 41 + e := echo.New() 42 + 43 + e.POST("/search", srv.searchHandler) 44 + e.POST("/hydrate", srv.hydrateHandler) 45 + 46 + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) 47 + defer stop() 48 + 49 + go func() { 50 + port := os.Getenv("PORT") 51 + 52 + if port == "" { 53 + port = "8088" 54 + } 55 + 56 + if err := e.Start(fmt.Sprintf(":%s", port)); err != nil && err != http.ErrServerClosed { 57 + e.Logger.Fatal(err) 58 + } 59 + }() 60 + 61 + <-ctx.Done() // wait for Ctrl+C 62 + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 63 + defer cancel() 64 + _ = e.Shutdown(shutdownCtx) 65 + } 66 + 67 + func (s *Server) searchHandler(c echo.Context) error { 68 + var req musicbrainz.SearchParams 69 + 70 + if err := c.Bind(&req); err != nil { 71 + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) 72 + } 73 + 74 + resp, _ := s.mb.SearchMusicBrainz(c.Request().Context(), req) 75 + 76 + return c.JSON(http.StatusOK, resp) 77 + } 78 + 79 + func (s *Server) hydrateHandler(c echo.Context) error { 80 + var req models.Track 81 + 82 + if err := c.Bind(&req); err != nil { 83 + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) 84 + } 85 + 86 + resp, _ := musicbrainz.HydrateTrack(s.mb, req) 87 + 88 + return c.JSON(http.StatusOK, resp) 89 + }
+2 -1
package.json
··· 26 26 "dev:storage": "cargo run -p rocksky-storage --release -- serve", 27 27 "dev:webscrobbler": "cargo run -p rockskyd --release -- webscrobbler", 28 28 "dev:tracklist": "cargo run -p rockskyd --release -- tracklist", 29 - "db:pgpull": "cargo run -p rockskyd --release -- pull && rm -f rocksky-analytics.ddb* rocksky-feed.ddb* && curl -o rocksky-analytics.ddb https://backup.rocksky.app/rocksky-analytics.ddb && curl -o rocksky-feed.ddb https://backup.rocksky.app/rocksky-feed.ddb" 29 + "db:pgpull": "cargo run -p rockskyd --release -- pull && rm -f rocksky-analytics.ddb* rocksky-feed.ddb* && curl -o rocksky-analytics.ddb https://backup.rocksky.app/rocksky-analytics.ddb && curl -o rocksky-feed.ddb https://backup.rocksky.app/rocksky-feed.ddb", 30 + "mb": "cd musicbrainz && go run main.go" 30 31 }, 31 32 "workspaces": [ 32 33 "apps/api",