feat(spotify): implement milestone M1 scaffold
This commit is contained in:
117
skills/spotify/src/cli.ts
Normal file
117
skills/spotify/src/cli.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import minimist from "minimist";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export interface CliDeps {
|
||||
stdout: Pick<NodeJS.WriteStream, "write">;
|
||||
stderr: Pick<NodeJS.WriteStream, "write">;
|
||||
}
|
||||
|
||||
export interface ParsedCli {
|
||||
command: string;
|
||||
positional: string[];
|
||||
json: boolean;
|
||||
public: boolean;
|
||||
limit?: string;
|
||||
offset?: string;
|
||||
description?: string;
|
||||
playlist?: string;
|
||||
playlistId?: string;
|
||||
}
|
||||
|
||||
export type CommandHandler = (args: ParsedCli, deps: CliDeps) => Promise<number> | number;
|
||||
export type CommandHandlers = Record<string, CommandHandler>;
|
||||
|
||||
function notImplemented(command: string): CommandHandler {
|
||||
return (args, deps) => {
|
||||
const payload = { ok: false, error: `Command not implemented yet: ${command}` };
|
||||
if (args.json) {
|
||||
deps.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
||||
} else {
|
||||
deps.stderr.write(`${payload.error}\n`);
|
||||
}
|
||||
return 2;
|
||||
};
|
||||
}
|
||||
|
||||
export function createDefaultHandlers(): CommandHandlers {
|
||||
return {
|
||||
auth: notImplemented("auth"),
|
||||
status: notImplemented("status"),
|
||||
search: notImplemented("search"),
|
||||
"list-playlists": notImplemented("list-playlists"),
|
||||
"create-playlist": notImplemented("create-playlist"),
|
||||
"add-to-playlist": notImplemented("add-to-playlist"),
|
||||
"remove-from-playlist": notImplemented("remove-from-playlist"),
|
||||
"search-and-add": notImplemented("search-and-add"),
|
||||
import: notImplemented("import")
|
||||
};
|
||||
}
|
||||
|
||||
export function usage(): string {
|
||||
return `spotify
|
||||
|
||||
Commands:
|
||||
auth
|
||||
status [--json]
|
||||
search <query> [--limit N] [--json]
|
||||
list-playlists [--limit N] [--offset N] [--json]
|
||||
create-playlist <name> [--description TEXT] [--public] [--json]
|
||||
add-to-playlist <playlistId> <spotify:track:...> [more uris...] [--json]
|
||||
remove-from-playlist <playlistId> <spotify:track:...> [more uris...] [--json]
|
||||
search-and-add <playlistId> <query> [more queries...] [--json]
|
||||
import <path> [--playlist NAME | --playlist-id ID] [--public] [--json]
|
||||
`;
|
||||
}
|
||||
|
||||
export async function runCli(
|
||||
argv: string[],
|
||||
deps: CliDeps = { stdout: process.stdout, stderr: process.stderr },
|
||||
handlers: CommandHandlers = createDefaultHandlers()
|
||||
): Promise<number> {
|
||||
const args = minimist(argv, {
|
||||
boolean: ["help", "json", "public"],
|
||||
string: ["limit", "offset", "description", "playlist", "playlist-id"],
|
||||
alias: {
|
||||
h: "help"
|
||||
}
|
||||
});
|
||||
const [command] = args._;
|
||||
|
||||
if (!command || args.help) {
|
||||
deps.stdout.write(usage());
|
||||
return 0;
|
||||
}
|
||||
|
||||
const handler = handlers[command];
|
||||
if (!handler) {
|
||||
deps.stderr.write(`Unknown command: ${command}\n\n${usage()}`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return handler(
|
||||
{
|
||||
command,
|
||||
positional: args._.slice(1).map(String),
|
||||
json: Boolean(args.json),
|
||||
public: Boolean(args.public),
|
||||
limit: args.limit,
|
||||
offset: args.offset,
|
||||
description: args.description,
|
||||
playlist: args.playlist,
|
||||
playlistId: args["playlist-id"]
|
||||
},
|
||||
deps
|
||||
);
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
runCli(process.argv.slice(2)).then((code) => {
|
||||
process.exitCode = code;
|
||||
}).catch((error: unknown) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
process.stderr.write(`${message}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
41
skills/spotify/src/types.ts
Normal file
41
skills/spotify/src/types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export interface SpotifyConfig {
|
||||
clientId: string;
|
||||
redirectUri: string;
|
||||
}
|
||||
|
||||
export interface SpotifyToken {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export interface SpotifyTrack {
|
||||
id: string;
|
||||
uri: string;
|
||||
name: string;
|
||||
artists: Array<{ name: string }>;
|
||||
album?: { name: string };
|
||||
external_urls?: { spotify?: string };
|
||||
}
|
||||
|
||||
export interface SpotifyPlaylist {
|
||||
id: string;
|
||||
uri: string;
|
||||
name: string;
|
||||
public: boolean | null;
|
||||
owner: { id: string; display_name?: string | null };
|
||||
external_urls?: { spotify?: string };
|
||||
}
|
||||
|
||||
export interface ParsedTrackRef {
|
||||
source: string;
|
||||
query: string;
|
||||
artist?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface ImportResult {
|
||||
found: Array<ParsedTrackRef & { uri: string; matchedName: string; matchedArtists: string[] }>;
|
||||
missed: Array<ParsedTrackRef & { reason: string }>;
|
||||
added?: { playlistId: string; count: number; snapshotIds: string[] };
|
||||
}
|
||||
Reference in New Issue
Block a user