feat(spotify): implement milestone M3 api commands
This commit is contained in:
98
skills/spotify/src/search.ts
Normal file
98
skills/spotify/src/search.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user