feat(spotify): implement milestone M3 api commands

This commit is contained in:
2026-04-12 01:52:18 -05:00
parent c8c0876b7c
commit d8570edcf0
8 changed files with 842 additions and 9 deletions

View File

@@ -0,0 +1,98 @@
import { createSpotifyApiClient, type SpotifyApiClient } from "./api-client.js";
import type { CliDeps, ParsedCli } from "./cli.js";
import type { SpotifyPlaylist, SpotifyTrack } from "./types.js";
export interface TrackOutput {
id: string;
uri: string;
name: string;
artists: string[];
album?: string;
externalUrl?: string;
}
export interface PlaylistOutput {
id: string;
name: string;
public: boolean | null;
owner: string;
externalUrl?: string;
}
function numberOption(value: string | undefined, fallback: number): number {
if (!value) {
return fallback;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
export function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, Math.trunc(value)));
}
export function mapTrack(track: SpotifyTrack): TrackOutput {
return {
id: track.id,
uri: track.uri,
name: track.name,
artists: track.artists.map((artist) => artist.name),
album: track.album?.name,
externalUrl: track.external_urls?.spotify
};
}
export function mapPlaylist(playlist: SpotifyPlaylist): PlaylistOutput {
return {
id: playlist.id,
name: playlist.name,
public: playlist.public,
owner: playlist.owner.display_name ?? playlist.owner.id,
externalUrl: playlist.external_urls?.spotify
};
}
export function formatTrack(track: TrackOutput): string {
const album = track.album ? ` (${track.album})` : "";
return `${track.uri} | ${track.name} | ${track.artists.join(", ")}${album}`;
}
export function formatPlaylist(playlist: PlaylistOutput): string {
const visibility = playlist.public === true ? "public" : playlist.public === false ? "private" : "unknown";
return `${playlist.id} | ${visibility} | ${playlist.owner} | ${playlist.name}`;
}
export async function runSearchCommand(
args: ParsedCli,
deps: CliDeps,
client: Pick<SpotifyApiClient, "searchTracks"> = createSpotifyApiClient()
): Promise<number> {
const query = args.positional.join(" ").trim();
if (!query) {
throw new Error("Missing search query.");
}
const limit = clamp(numberOption(args.limit, 5), 1, 10);
const tracks = (await client.searchTracks(query, limit)).map(mapTrack);
if (args.json) {
deps.stdout.write(`${JSON.stringify({ tracks }, null, 2)}\n`);
} else {
deps.stdout.write(`${tracks.map(formatTrack).join("\n")}${tracks.length ? "\n" : ""}`);
}
return 0;
}
export async function runListPlaylistsCommand(
args: ParsedCli,
deps: CliDeps,
client: Pick<SpotifyApiClient, "listPlaylists"> = createSpotifyApiClient()
): Promise<number> {
const limit = clamp(numberOption(args.limit, 50), 1, 50);
const offset = Math.max(0, numberOption(args.offset, 0));
const playlists = (await client.listPlaylists(limit, offset)).map(mapPlaylist);
if (args.json) {
deps.stdout.write(`${JSON.stringify({ playlists }, null, 2)}\n`);
} else {
deps.stdout.write(`${playlists.map(formatPlaylist).join("\n")}${playlists.length ? "\n" : ""}`);
}
return 0;
}