Something went wrong. Try again.
A decentralized music tracking and discovery platform built on AT Protocol ๐ต rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169// `rocksky card` โ read and write SLE and ACOS contact cards.//// The contactless equivalent is `rocksky nfc` (src/cmd/nfc.ts). These are// separate commands rather than one, because the two families need different// PC/SC clients and running both against one reader at the same time would// interleave their APDUs. A card session is opened for the operation and closed// straight after.
import { loadToken } from "lib/token";import { CardUnavailableError, openCard } from "../lib/card-reader";import { cardLabel, cardSecret, fitPayloads, readCard, writeCard } from "../lib/cards";import { nfcFavoritesPayloads, nfcPayloadsFor, parseNfcPayloads } from "../lib/nfc";import { c } from "../theme";import { getAlbum, getCreds, getDid, getPlaylist } from "../tui/navidrome";
function fail(e: unknown): never { const message = e instanceof Error ? e.message : String(e); console.error(c.error(message)); process.exit(1);}
/** Describe what a payload points at, so a dump reads as more than a URI. */async function describe(payload: string): Promise<string> { const [target] = parseNfcPayloads([payload]); if (!target) return c.link("not a Rocksky record"); // Favorites name a person rather than a record; cards don't carry them, but // the shared parser can still produce one. if (target.kind === "favorites") return `favorites of ${target.did}`; const token = loadToken(); if (!token) return target.kind; try { const creds = await getCreds(token); if (!creds) return target.kind; const key = "uri" in target ? target.uri : target.id; const portable = "uri" in target ? c.highlight(" ยท portable") : ""; if (target.kind === "album" || target.kind === "albumUri") { const { album } = await getAlbum(creds, key); return `album ยท ${album?.name ?? key}${portable}`; } const { playlist } = await getPlaylist(creds, key); return `playlist ยท ${playlist?.name ?? key}${portable}`; } catch { return `${target.kind} (not in your library)`; }}
export async function cardRead() { let session; try { session = await openCard(); } catch (e) { fail(e); } try { console.log(`${c.accent("Reader:")} ${session.reader}`); console.log(`${c.accent("Card:")} ${cardLabel(session.kind)}`); console.log(`${c.accent("ATR:")} ${session.atr.toString("hex").toUpperCase()}`);
const payloads = await readCard(session.transmit, session.kind); if (!payloads.length) { console.log(c.muted("\nThe card holds nothing we can play.")); return; } console.log(); for (const p of payloads) { console.log(` ${c.secondary(p)}\n ${await describe(p)}`); } } catch (e) { fail(e); } finally { session.close(); } // The PC/SC binding keeps the event loop alive after close(), so say when we // are done rather than leaving the command hanging on a finished read. process.exit(0);}
export async function cardWrite(opts: { album?: string; playlist?: string; favorites?: boolean; secret?: string;}) { const kind = opts.album ? "album" : opts.playlist ? "playlist" : null; const ref = opts.album ?? opts.playlist; if (!opts.favorites && (!kind || !ref)) { console.error( c.error("Pass --album <ref>, --playlist <ref> or --favorites."), ); console.error( c.muted( "A ref is the record's AT-URI (at://โฆ/app.rocksky.album/โฆ), which makes\n" + "the card work on any Rocksky player, or a library id, which does not.", ), ); process.exit(1); }
// Favorites are a query with no record and no library id, so the card holds // the user's DID. That needs a session โ there is nothing to fall back on. let payloads: string[]; if (opts.favorites) { const token = loadToken(); if (!token) fail(new Error("Sign in first: rocksky login")); payloads = nfcFavoritesPayloads(await getDid(token)); } else { // Both halves when we can get them: the record URI plays anywhere, the // library id is the fallback. Whichever was passed, look up the other. payloads = ref!.startsWith("at://") ? nfcPayloadsFor(kind!, { uri: ref! }) : nfcPayloadsFor(kind!, { id: ref! }); try { const token = loadToken(); const creds = token ? await getCreds(token) : null; if (creds) { const found = kind === "album" ? (await getAlbum(creds, ref!)).album : (await getPlaylist(creds, ref!)).playlist; if (found?.id) payloads = nfcPayloadsFor(kind!, found); } } catch { // Offline or signed out โ write the half we were handed. } }
let session; try { session = await openCard(); } catch (e) { fail(e); } try { const { label, fallback } = cardSecret(session.kind); const secret = opts.secret ?? fallback; console.log(`${c.accent("Card:")} ${cardLabel(session.kind)} on ${session.reader}`); if (!opts.secret) { console.log(c.muted(`Using the factory-default ${label} ${fallback}.`)); } if (session.kind === "sle5528") { console.log( c.muted( "A wrong PSC counts against the card's retry counter, and enough wrong\n" + "ones lock it for good.", ), ); }
const written = await writeCard(session.transmit, session.kind, payloads, secret); console.log(c.highlight(`\nWrote and verified ${written.length} record(s):`)); for (const p of written) console.log(` ${c.secondary(p)}`); const dropped = payloads.length - written.length; if (dropped > 0) { console.log( c.muted( `\nThe card had room for ${written.length} of ${payloads.length}. The record URI is\n` + "what plays; the library-id fallback was dropped.", ), ); } } catch (e) { if (e instanceof CardUnavailableError) fail(e); fail(e); } finally { session.close(); } process.exit(0);}