a minimal CLI toolkit for working with last.fm scrobbles
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(cli): add session management and config bootstrapping

Lívia (May 17, 2026, 9:41 AM -0300) 06ad4a5b c9767357

+94 -1
+37
cli/bootstrap.ts
··· 1 + import { type Config, loadConfig, updateConfig } from "~/lib/config.ts"; 2 + import { dim, log } from "~/cli/formatter.ts"; 3 + import { AppError, describe } from "~/lib/errors.ts"; 4 + import { Ok, Result } from "~/lib/result.ts"; 5 + import { ask } from "~/utils/prompt.ts"; 6 + import { italic } from "@std/fmt/colors"; 7 + 8 + type BootstrapConfig = Omit<Required<Config>, "password" | "username">; 9 + 10 + export const SECRET_WARNING = italic(dim( 11 + "\n\nYour Last.fm credentials are used only to authenticate with Last.fm services. " + 12 + "They are sent directly to Last.fm over a secure connection and are not stored locally after authentication.", 13 + )); 14 + 15 + export async function requireBaseConfig(): Promise<Result<BootstrapConfig, AppError>> { 16 + const result = await loadConfig(); 17 + 18 + if (result.ok && result.value.apiKey && result.value.secret) { 19 + return Ok(result.value as BootstrapConfig); 20 + } 21 + 22 + if (!result.ok && result.error.tag !== "not_found") { 23 + return result; 24 + } 25 + 26 + log.warn("No API credentials found. Let's set up scrobkit :3\n"); 27 + 28 + const apiKey = ask(" What is your Last.fm API key?"); 29 + const sharedSecret = ask(SECRET_WARNING + "\n\n What about your key's shared secret, eh?", true); 30 + 31 + const saved = await updateConfig({ apiKey, secret: sharedSecret }); 32 + if (!saved.ok) { 33 + return saved; 34 + } 35 + 36 + return Ok(saved.value as BootstrapConfig); 37 + }
+14 -1
cli/formatter.ts
··· 1 - import { cyan, dim, gray, green, red } from "@std/fmt/colors"; 1 + import { cyan, dim, gray, green, red, yellow } from "@std/fmt/colors"; 2 2 3 3 export const symbols = { 4 4 retry: gray("\u21bb"), ··· 6 6 success: green("\u2714"), 7 7 forbid: red("\u2298"), 8 8 error: red("\u2716"), 9 + info: yellow("\u{1f6c8}"), 9 10 } as const; 11 + 12 + export const log = { 13 + success: (msg: string) => console.log(` ${symbols.success} ${msg}`), 14 + 15 + error: (msg: string) => console.error(`${symbols.error} ${red(msg)}`), 16 + 17 + warn: (msg: string) => console.warn(`${symbols.warn} ${msg}`), 18 + 19 + info: (msg: string) => console.warn(`${symbols.info} ${msg}`), 20 + 21 + forbid: (msg: string) => console.log(`${symbols.forbid} ${cyan(msg)}`), 22 + }; 10 23 11 24 export { dim };
+42
cli/session.ts
··· 1 + import { authenticate, verifySession } from "~/api/lastfm.ts"; 2 + import { type Config, updateConfig } from "~/lib/config.ts"; 3 + import { AppError } from "~/lib/errors.ts"; 4 + import { log } from "~/cli/formatter.ts"; 5 + import { ask } from "~/utils/prompt.ts"; 6 + import { Ok, Result } from "~/lib/result.ts"; 7 + import { SECRET_WARNING } from "~/cli/bootstrap.ts"; 8 + 9 + type SessionConfig = Omit<Required<Config>, "password">; 10 + 11 + export async function ensureSession(config: Config): Promise<Result<SessionConfig, AppError>> { 12 + if (config.apiKey && config.secret && config.sessionKey && config.username) { 13 + const check = await verifySession(config.apiKey, config.secret, config.sessionKey); 14 + if (check.ok) { 15 + return Ok(config as SessionConfig); 16 + } 17 + log.warn("Session key is invalid or expired. Re-authenticating..."); 18 + } 19 + 20 + const username = ask(" What's your Last.fm username? "); 21 + const password = ask(SECRET_WARNING + "\n\n What about your account password? ", true); 22 + 23 + log.info("Authenticating..."); 24 + const auth = await authenticate(config.apiKey, config.secret, username, password); 25 + 26 + if (!auth.ok) { 27 + return auth; 28 + } 29 + 30 + const updated = await updateConfig({ 31 + username: auth.value.name, 32 + sessionKey: auth.value.key, 33 + password: undefined, 34 + }); 35 + 36 + if (!updated.ok) { 37 + return updated; 38 + } 39 + 40 + log.success(`Authenticated as ${auth.value.name}`); 41 + return Ok(updated.value as SessionConfig); 42 + }
+1
lib/config.ts
··· 6 6 apiKey: string; 7 7 secret: string; 8 8 username?: string; 9 + sessionKey?: string; 9 10 password?: string; 10 11 } 11 12