UNPKG

node-csfd-api

Version:

ČSFD API in JavaScript. Amazing NPM library for scrapping csfd.cz :)

310 lines (305 loc) 10.7 kB
#!/usr/bin/env node import { c, err } from "./bin/utils.js"; //#region src/cli.ts /** * Main CLI entry point for node-csfd-api. */ const GITHUB_REPO = "bartholomej/node-csfd-api"; const GITHUB_API_LATEST = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`; const GITHUB_RELEASES_URL = `https://github.com/${GITHUB_REPO}/releases/latest`; const INSTALL_SH_URL = `https://raw.githubusercontent.com/${GITHUB_REPO}/master/install.sh`; function getCommandName() { const scriptPath = process.argv[1] ?? ""; const basename = scriptPath.split("/").pop() ?? ""; if (basename === "csfd" || basename === "node-csfd-api") return basename; if (scriptPath.includes("node-csfd-api")) return "npx node-csfd-api"; return "csfd"; } function parseNumericArg(raw, usage) { const n = Number(raw); if (!raw || isNaN(n)) { console.error(err("Please provide a valid numeric ID.")); console.log(c.dim(` Usage: ${usage}`)); process.exit(1); } return n; } function parseFormat(args) { return args.includes("--json") ? "json" : "csv"; } async function main() { const args = process.argv.slice(2); const command = args[0]; const updateHint = new Set(["update"]).has(command) ? null : checkForUpdateInBackground(); switch (command) { case "server": case "api": try { await import("./bin/server.js"); } catch (error) { console.error("Failed to start server:", error); process.exit(1); } break; case "mcp": try { await import("./bin/mcp-server.js"); } catch (error) { console.error("Failed to start MCP server:", error); process.exit(1); } break; case "export": if (args[1] === "ratings") { const userId = parseNumericArg(args[2], `${getCommandName()} export ratings <userId> [options]`); const isLetterboxd = args.includes("--letterboxd"); const format = isLetterboxd ? "letterboxd" : parseFormat(args); try { const { runRatingsExport } = await import("./bin/export-ratings.js"); await runRatingsExport(userId, { format, userRatingsOptions: { includesOnly: isLetterboxd ? ["film"] : void 0, allPages: true, allPagesDelay: 1e3 } }); } catch (error) { console.error(err("Failed to run export:"), error); process.exit(1); } } else if (args[1] === "reviews") { const userId = parseNumericArg(args[2], `${getCommandName()} export reviews <userId> [options]`); try { const { runReviewsExport } = await import("./bin/export-reviews.js"); await runReviewsExport(userId, { format: parseFormat(args), userReviewsOptions: { allPages: true, allPagesDelay: 1e3 } }); } catch (error) { console.error(err("Failed to run export:"), error); process.exit(1); } } else if (args[1] === "letterboxd") { console.warn(c.yellow(c.bold("⚠ Deprecated:")) + " \"export letterboxd\" is removed. Use \"export ratings <id> --letterboxd\" instead."); console.log(c.dim(` Usage: ${getCommandName()} export ratings <userId> --letterboxd`)); process.exit(1); } else { console.error(err(`Unknown export target: ${c.bold(String(args[1]))}`)); printUsage(); process.exit(1); } break; case "search": { const query = args.slice(1).filter((a) => !a.startsWith("--")).join(" "); if (!query) { console.error(err("Please provide a search query.")); console.log(c.dim(` Usage: ${getCommandName()} search <query> [--json]`)); process.exit(1); } try { const { runSearch } = await import("./bin/search.js"); await runSearch(query, args.includes("--json")); } catch (error) { console.error(err("Search failed:"), error); process.exit(1); } break; } case "movie": { const input = args.slice(1).filter((a) => !a.startsWith("--")).join(" "); if (!input) { console.error(err("Please provide a movie ID or title.")); console.log(c.dim(` Usage: ${getCommandName()} movie <id|title> [--json]`)); process.exit(1); } const json = args.includes("--json"); try { const { runMovieLookup } = await import("./bin/lookup-movie.js"); const numericId = /^\d+$/.test(input) ? Number(input) : null; if (numericId !== null) await runMovieLookup(numericId, json); else { const { csfd } = await import("./index.js"); const results = await csfd.search(input); const first = results.movies[0] ?? results.tvSeries[0]; if (!first) { console.error(err(`No movies found for "${input}".`)); process.exit(1); } console.log(c.dim(` → ${first.title}${first.year ? ` (${first.year})` : ""}`)); await runMovieLookup(first.id, json); } } catch (error) { console.error(err("Failed to fetch movie:"), error); process.exit(1); } break; } case "--version": case "-v": console.log(c.bold("5.10.2")); break; case "update": await runUpdate(); break; default: printUsage(); break; } if (updateHint) await updateHint; } function isRunningViaNpx() { const base = (process.execPath ?? "").split("/").pop() ?? ""; return base === "node" || base === "node.exe" || base === "bun"; } function isRunningViaHomebrew() { const exec = process.execPath ?? ""; return exec.includes("/homebrew/") || exec.includes("/Cellar/"); } function compareSemver(a, b) { const parse = (v) => { const [main, pre = ""] = v.split("-"); const [major, minor, patch] = main.split(".").map(Number); return { major, minor, patch, pre }; }; const va = parse(a); const vb = parse(b); if (va.major !== vb.major) return va.major - vb.major; if (va.minor !== vb.minor) return va.minor - vb.minor; if (va.patch !== vb.patch) return va.patch - vb.patch; if (va.pre && !vb.pre) return -1; if (!va.pre && vb.pre) return 1; return va.pre.localeCompare(vb.pre); } async function fetchLatestVersion(timeoutMs = 5e3) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { return (await (await fetch(GITHUB_API_LATEST, { signal: controller.signal })).json()).tag_name?.replace(/^v/, "") ?? ""; } finally { clearTimeout(timer); } } function printUpgradeInstructions(latest) { console.log(c.green(c.bold("↑ New version available: ")) + c.bold(latest)); if (isRunningViaNpx()) { console.log("\n" + c.bold("Run:")); console.log(" " + c.cyan("npx node-csfd-api@latest <command>")); } else if (isRunningViaHomebrew()) { console.log("\n" + c.bold("Run:")); console.log(" " + c.cyan("brew upgrade csfd")); } else if (process.platform === "win32") { console.log("\n" + c.bold("Download the latest release from:")); console.log(" " + c.cyan(GITHUB_RELEASES_URL)); } else { console.log("\n" + c.bold("Run:")); console.log(" " + c.cyan(`curl -fsSL ${INSTALL_SH_URL} | bash`)); } } const UPDATE_CACHE_TTL = 1440 * 60 * 1e3; function getUpdateCachePath() { const home = process.env["HOME"] || process.env["USERPROFILE"] || ""; return `${process.platform === "win32" ? process.env["APPDATA"] || home : process.env["XDG_CONFIG_HOME"] || `${home}/.config`}/csfd/update-check.json`; } async function checkForUpdateInBackground() { try { const { readFileSync, writeFileSync, mkdirSync } = await import("node:fs"); const { dirname } = await import("node:path"); const cachePath = getUpdateCachePath(); let cache = null; try { cache = JSON.parse(readFileSync(cachePath, "utf-8")); } catch {} const now = Date.now(); let latestVersion = cache?.latestVersion ?? ""; if (!cache || now - cache.lastCheck > UPDATE_CACHE_TTL) try { const fetched = await fetchLatestVersion(3e3); if (fetched) { latestVersion = fetched; try { mkdirSync(dirname(cachePath), { recursive: true }); writeFileSync(cachePath, JSON.stringify({ lastCheck: now, latestVersion })); } catch {} } } catch {} if (!latestVersion || compareSemver("5.10.2", latestVersion) >= 0) return; console.log(""); console.log(c.dim(" " + "─".repeat(44))); console.log(` ${c.yellow(c.bold("↑ Update available:"))} ${c.dim("5.10.2")}${c.bold(c.green(latestVersion))}`); console.log(` ${c.dim("Run")} ${c.cyan(getCommandName() + " update")} ${c.dim("for upgrade instructions.")}`); } catch {} } async function runUpdate() { console.log(c.dim("Current version: ") + c.bold("5.10.2")); console.log(c.dim("Checking for updates...")); let latest; try { latest = await fetchLatestVersion(); } catch { console.error(err("Could not reach GitHub API.")); process.exit(1); } if (!latest) { console.error(err("Could not determine latest version.")); process.exit(1); } const cmp = compareSemver("5.10.2", latest); if (cmp === 0) { console.log(c.green("✔ Already up to date.")); return; } if (cmp > 0) { console.log(c.yellow("⚠ You are running a pre-release version.") + c.dim(` Latest stable: ${latest}`)); return; } printUpgradeInstructions(latest); } function printUsage() { const cmd = getCommandName(); const header = c.bold(c.cyan("csfd")) + " " + c.dim(`v5.10.2`); const usage = c.bold("Usage:") + ` ${c.cyan(cmd)} ${c.dim("<command> [options]")}`; const section = (title) => c.bold(title); const cmd_ = (name) => " " + c.cyan(name); const flag_ = (name) => " " + c.dim(name); const desc = (text) => c.dim(text); const sub_ = (name) => " " + c.dim(name); console.log(` ${header} ${usage} ${section("Commands:")} ${cmd_("server, api")} ${desc("Start the REST API server")} ${cmd_("mcp")} ${desc("Start the MCP server for AI agents")} ${cmd_("export ratings <userId>")} ${desc("Export user ratings")} ${sub_("--csv")} ${desc("CSV format (default)")} ${sub_("--json")} ${desc("JSON format")} ${sub_("--letterboxd")} ${desc("Letterboxd-compatible CSV")} ${cmd_("export reviews <userId>")} ${desc("Export user reviews")} ${sub_("--csv")} ${desc("CSV format (default)")} ${sub_("--json")} ${desc("JSON format")} ${cmd_("search <query>")} ${desc("Search movies, series, creators and users")} ${cmd_("movie <id|title>")} ${desc("Show movie details (ID or film name)")} ${sub_("--json")} ${desc("Output raw JSON")} ${cmd_("update")} ${desc("Check for updates")} ${cmd_("help")} ${desc("Show this help")} ${section("Flags:")} ${flag_("-v, --version")} ${desc("Show version")} ${flag_("-h, --help")} ${desc("Show this help")} `); } main().catch((error) => { console.error(err("Fatal: " + String(error))); process.exit(1); }); //#endregion export { };