feat(spotify): implement milestone M3 api commands
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { createSpotifyApiClient, type SpotifyApiClient } from "./api-client.js";
|
||||
import type { CliDeps, ParsedCli } from "./cli.js";
|
||||
import { mapPlaylist, mapTrack } from "./search.js";
|
||||
import type { SpotifyTrack } from "./types.js";
|
||||
|
||||
type PlaylistClient = Pick<SpotifyApiClient, "createPlaylist" | "addItemsToPlaylist" | "removeItemsFromPlaylist" | "searchTracks">;
|
||||
|
||||
export function validateTrackUris(uris: string[]): string[] {
|
||||
if (uris.length === 0) {
|
||||
throw new Error("At least one spotify:track URI is required.");
|
||||
}
|
||||
for (const uri of uris) {
|
||||
if (!uri.startsWith("spotify:track:")) {
|
||||
throw new Error(`Invalid Spotify track URI: ${uri}`);
|
||||
}
|
||||
}
|
||||
return uris;
|
||||
}
|
||||
|
||||
function snapshotIds(results: Array<{ snapshot_id?: string }>): string[] {
|
||||
return results.map((result) => result.snapshot_id).filter((id): id is string => Boolean(id));
|
||||
}
|
||||
|
||||
export async function runCreatePlaylistCommand(
|
||||
args: ParsedCli,
|
||||
deps: CliDeps,
|
||||
client: PlaylistClient = createSpotifyApiClient()
|
||||
): Promise<number> {
|
||||
const name = args.positional.join(" ").trim();
|
||||
if (!name) {
|
||||
throw new Error("Missing playlist name.");
|
||||
}
|
||||
const playlist = mapPlaylist(await client.createPlaylist(name, { description: args.description, public: Boolean(args.public) }));
|
||||
if (args.json) {
|
||||
deps.stdout.write(`${JSON.stringify({ playlist }, null, 2)}\n`);
|
||||
} else {
|
||||
deps.stdout.write(`Created playlist ${playlist.id}: ${playlist.name}\n`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function runAddToPlaylistCommand(
|
||||
args: ParsedCli,
|
||||
deps: CliDeps,
|
||||
client: PlaylistClient = createSpotifyApiClient()
|
||||
): Promise<number> {
|
||||
const [playlistId, ...uris] = args.positional;
|
||||
if (!playlistId) {
|
||||
throw new Error("Missing playlist id.");
|
||||
}
|
||||
const validUris = validateTrackUris(uris);
|
||||
const results = await client.addItemsToPlaylist(playlistId, validUris);
|
||||
const output = { playlistId, count: validUris.length, snapshotIds: snapshotIds(results) };
|
||||
deps.stdout.write(args.json ? `${JSON.stringify(output, null, 2)}\n` : `Added ${output.count} track(s) to ${playlistId}.\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function runRemoveFromPlaylistCommand(
|
||||
args: ParsedCli,
|
||||
deps: CliDeps,
|
||||
client: PlaylistClient = createSpotifyApiClient()
|
||||
): Promise<number> {
|
||||
const [playlistId, ...uris] = args.positional;
|
||||
if (!playlistId) {
|
||||
throw new Error("Missing playlist id.");
|
||||
}
|
||||
const validUris = validateTrackUris(uris);
|
||||
const results = await client.removeItemsFromPlaylist(playlistId, validUris);
|
||||
const output = { playlistId, count: validUris.length, snapshotIds: snapshotIds(results) };
|
||||
deps.stdout.write(args.json ? `${JSON.stringify(output, null, 2)}\n` : `Removed ${output.count} track(s) from ${playlistId}.\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function searchAndAdd(
|
||||
playlistId: string,
|
||||
queries: string[],
|
||||
client: Pick<PlaylistClient, "searchTracks" | "addItemsToPlaylist">
|
||||
): Promise<{ added: Array<{ query: string; uri: string; name: string; artists: string[] }>; missed: string[]; snapshotIds: string[] }> {
|
||||
if (!playlistId) {
|
||||
throw new Error("Missing playlist id.");
|
||||
}
|
||||
if (queries.length === 0) {
|
||||
throw new Error("At least one search query is required.");
|
||||
}
|
||||
|
||||
const added: Array<{ query: string; uri: string; name: string; artists: string[] }> = [];
|
||||
const missed: string[] = [];
|
||||
for (const query of queries) {
|
||||
const tracks: SpotifyTrack[] = await client.searchTracks(query, 1);
|
||||
const first = tracks[0];
|
||||
if (!first) {
|
||||
missed.push(query);
|
||||
continue;
|
||||
}
|
||||
const output = mapTrack(first);
|
||||
added.push({ query, uri: output.uri, name: output.name, artists: output.artists });
|
||||
}
|
||||
|
||||
const mutationResults = added.length > 0
|
||||
? await client.addItemsToPlaylist(playlistId, added.map((entry) => entry.uri))
|
||||
: [];
|
||||
return { added, missed, snapshotIds: snapshotIds(mutationResults) };
|
||||
}
|
||||
|
||||
export async function runSearchAndAddCommand(
|
||||
args: ParsedCli,
|
||||
deps: CliDeps,
|
||||
client: PlaylistClient = createSpotifyApiClient()
|
||||
): Promise<number> {
|
||||
const [playlistId, ...queries] = args.positional;
|
||||
const output = await searchAndAdd(playlistId, queries, client);
|
||||
if (args.json) {
|
||||
deps.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
||||
} else {
|
||||
deps.stdout.write(`Added ${output.added.length} track(s) to ${playlistId}; missed ${output.missed.length}.\n`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user