144 lines
4.2 KiB
JavaScript
144 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import minimist from "minimist";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { getAuthStatus, runAuthorizationFlow } from "./auth.js";
|
|
import { runImportCommand } from "./importers/index.js";
|
|
import {
|
|
runAddToPlaylistCommand,
|
|
runCreatePlaylistCommand,
|
|
runRemoveFromPlaylistCommand,
|
|
runSearchAndAddCommand
|
|
} from "./playlists.js";
|
|
import { runListPlaylistsCommand, runSearchCommand } from "./search.js";
|
|
|
|
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: async (_args, deps) => {
|
|
await runAuthorizationFlow();
|
|
deps.stdout.write("Spotify authorization complete.\n");
|
|
return 0;
|
|
},
|
|
status: async (args, deps) => {
|
|
const status = await getAuthStatus();
|
|
if (args.json) {
|
|
deps.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
|
|
} else {
|
|
deps.stdout.write(`Spotify config: ${status.configFound ? "found" : "missing"}\n`);
|
|
deps.stdout.write(`Spotify token: ${status.tokenFound ? "found" : "missing"}\n`);
|
|
if (status.tokenFound) {
|
|
deps.stdout.write(`Spotify token expired: ${status.tokenExpired ? "yes" : "no"}\n`);
|
|
}
|
|
}
|
|
return status.configFound && status.tokenFound ? 0 : 1;
|
|
},
|
|
search: runSearchCommand,
|
|
"list-playlists": runListPlaylistsCommand,
|
|
"create-playlist": runCreatePlaylistCommand,
|
|
"add-to-playlist": runAddToPlaylistCommand,
|
|
"remove-from-playlist": runRemoveFromPlaylistCommand,
|
|
"search-and-add": runSearchAndAddCommand,
|
|
import: runImportCommand
|
|
};
|
|
}
|
|
|
|
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;
|
|
});
|
|
}
|