node-csfd-api 5.6.0-next.2 → 5.6.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -436,13 +436,14 @@ const excludeEpisodes = await csfd.userRatings('912-bart', {
436
436
 
437
437
  #### UserRatingsOptions
438
438
 
439
- | Option | Type | Default | Description |
440
- | --------------- | ----------------- | ------- | ---------------------------------------------------------------- |
441
- | `includesOnly` | `CSFDFilmTypes[]` | `null` | Include only specific content types (e.g., `['film', 'series']`) |
442
- | `exclude` | `CSFDFilmTypes[]` | `null` | Exclude specific content types (e.g., `['episode']`) |
443
- | `allPages` | `boolean` | `false` | Fetch all pages of ratings |
444
- | `allPagesDelay` | `number` | `0` | Delay between page requests in milliseconds |
445
- | `page` | `number` | `1` | Fetch specific page number |
439
+ | Option | Type | Default | Description |
440
+ | --------------- | --------------------------------------- | ----------- | ---------------------------------------------------------------- |
441
+ | `includesOnly` | `CSFDFilmTypes[]` | `null` | Include only specific content types (e.g., `['film', 'series']`) |
442
+ | `exclude` | `CSFDFilmTypes[]` | `null` | Exclude specific content types (e.g., `['episode']`) |
443
+ | `allPages` | `boolean` | `false` | Fetch all pages of ratings |
444
+ | `allPagesDelay` | `number` | `0` | Delay between page requests in milliseconds |
445
+ | `page` | `number` | `1` | Fetch specific page number |
446
+ | `onProgress` | `(page: number, total: number) => void` | `undefined` | Called on each page fetch — use for progress bars or logging |
446
447
 
447
448
  > 📝 **Note**: `includesOnly` and `exclude` are mutually exclusive. If both are provided, `includesOnly` takes precedence.
448
449
  >
@@ -524,58 +525,89 @@ Same options as [UserRatingsOptions](#userrationsoptions).
524
525
 
525
526
  ## 💻 CLI Tools
526
527
 
527
- This library comes with a powerful CLI that exposes several tools. You can run the CLI either via `npx` (without installation) or by installing it globally via Homebrew (macOS/Linux).
528
+ This library ships with a CLI exposing several tools. Choose the installation method that fits your workflow.
528
529
 
529
- ### Installation / Usage
530
+ ### Installation
530
531
 
531
- **Option A: NPX (No installation required)**
532
+ **Option A: npx** _(no installation required)_
533
+
534
+ > Runs directly via Node.js
532
535
 
533
536
  ```bash
534
537
  npx node-csfd-api <command>
535
538
  ```
536
539
 
537
- **Option B: Homebrew for macOS/Linux (Global installation)**
540
+ **Option B: Homebrew** _(macOS)_
538
541
 
539
542
  ```bash
540
543
  brew install bartholomej/tap/csfd
544
+ ```
545
+
546
+ **Option C: Install script** _(macOS & Linux)_
547
+
548
+ > Installs the latest stable release as a standalone binary to `~/.local/bin/csfd`.
541
549
 
542
- csfd <command>
550
+ ```bash
551
+ curl -fsSL https://raw.githubusercontent.com/bartholomej/node-csfd-api/master/install.sh | bash
552
+ ## Install specific version
553
+ # curl -fsSL https://raw.githubusercontent.com/bartholomej/node-csfd-api/master/install.sh | CSFD_VERSION=5.5.0 bash
543
554
  ```
544
555
 
545
- > 💡 _Note: The examples below use `npx node-csfd-api`, but if you installed via Homebrew, simply replace it with `csfd` (e.g., `csfd export ratings 912`)._
556
+ **Option D: Windows** _(manual download)_
557
+
558
+ Download `csfd-windows-x64.zip` from the [latest release](https://github.com/bartholomej/node-csfd-api/releases/latest), extract `csfd.exe`, and add it to your `PATH`.
559
+
560
+ > ⚠️ Windows may show a SmartScreen warning ("Windows protected your PC") because the binary is not code-signed. To proceed: click **More info** → **Run anyway**. Alternatively, right-click the `.exe` → Properties → check **Unblock**.
546
561
 
547
562
  ---
548
563
 
549
- ### 1. Export Ratings (CSV, JSON & Letterboxd)
564
+ ### CLI Examples
565
+
566
+ > 💡 The examples below use `csfd` (Options B, C & D). If you use npx, replace it with `npx node-csfd-api` — e.g. `npx node-csfd-api export ratings 912`.
550
567
 
551
- > Backup your personal user ratings to CSV (default), JSON, or Letterboxd format. Use this tool just to keep a local copy of your data.
568
+ #### 1. Movie Details
552
569
 
553
570
  ```bash
554
- # Export to CSV (default) -> saves as <userId>-ratings.csv
555
- npx node-csfd-api export ratings 912
571
+ csfd movie 535121
572
+ # npx node-csfd-api movie 535121
573
+ csfd movie 535121 --json # raw JSON output, pipe-friendly
574
+ # npx node-csfd-api movie 535121 --json
575
+ ```
556
576
 
557
- # Export to JSON -> saves as <userId>-ratings.json
558
- npx node-csfd-api export ratings 912 --json
577
+ #### 2. Export Ratings (CSV, JSON & Letterboxd)
559
578
 
560
- # Export to Letterboxd CSV -> saves as <userId>-for-letterboxd.csv
561
- npx node-csfd-api export ratings 912 --letterboxd
562
- ```
579
+ > Backup your personal user ratings. _Use this tool just to keep a local copy of your data._
563
580
 
564
- ### 2. REST API Server
581
+ ```bash
582
+ csfd export ratings 912 # CSV (default) -> <userId>-ratings.csv
583
+ # npx node-csfd-api export ratings 912
584
+ csfd export ratings 912 --json # JSON -> <userId>-ratings.json
585
+ # npx node-csfd-api export ratings 912 --json
586
+ csfd export ratings 912 --letterboxd # Letterboxd CSV -> <userId>-for-letterboxd.csv
587
+ # npx node-csfd-api export ratings 912 --letterboxd
588
+ ```
565
589
 
566
- Run a standalone REST API server.
590
+ #### 3. Export Reviews (CSV & JSON)
567
591
 
568
592
  ```bash
569
- npx node-csfd-api server
570
- # Server listening on port 3000
593
+ csfd export reviews 912 # CSV (default) -> <userId>-reviews.csv
594
+ # npx node-csfd-api export reviews 912
595
+ csfd export reviews 912 --json # JSON -> <userId>-reviews.json
596
+ # npx node-csfd-api export reviews 912 --json
571
597
  ```
572
598
 
573
- ### 3. MCP Server for AI Agents
599
+ #### 4. REST API Server
600
+
601
+ ```bash
602
+ csfd server
603
+ # npx node-csfd-api server
604
+ ```
574
605
 
575
- Run the Model Context Protocol server for AI agents.
606
+ #### 5. MCP Server for AI Agents
576
607
 
577
608
  ```bash
578
- npx node-csfd-api mcp
609
+ csfd mcp
610
+ # npx node-csfd-api mcp
579
611
  ```
580
612
 
581
613
  ## 🤖 MCP Server (Model Context Protocol)
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { csfd } from "../index.js";
3
+ import { escapeCsvField, renderProgress } from "./utils.js";
3
4
  import { writeFile } from "node:fs/promises";
4
5
 
5
6
  //#region src/bin/export-ratings.ts
@@ -12,15 +13,13 @@ async function runRatingsExport(userId, options) {
12
13
  try {
13
14
  if (options.format === "letterboxd") csfd.setOptions({ language: "en" });
14
15
  console.log(`Fetching ratings for user ${userId} (${options.format.toUpperCase()})...`);
15
- const ratings = await csfd.userRatings(userId, options.userRatingsOptions);
16
+ const ratings = await csfd.userRatings(userId, {
17
+ ...options.userRatingsOptions,
18
+ onProgress: renderProgress
19
+ });
16
20
  console.log(`Fetched ${ratings.length} ratings.`);
17
21
  let content = "";
18
22
  let fileName = "";
19
- const escapeCsvField = (value) => {
20
- const needsQuotes = /[",\n\r]/.test(value);
21
- const escaped = value.replaceAll("\"", "\"\"");
22
- return needsQuotes ? `"${escaped}"` : escaped;
23
- };
24
23
  if (options.format === "letterboxd") {
25
24
  content = ["Title,Year,Rating,WatchedDate", ...ratings.map((r) => {
26
25
  return `${escapeCsvField(r.title ?? "")},${r.year ?? ""},${r.userRating ?? ""},${escapeCsvField(r.userDate ?? "")}`;
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import { csfd } from "../index.js";
3
+ import { escapeCsvField, renderProgress } from "./utils.js";
4
+ import { writeFile } from "node:fs/promises";
5
+
6
+ //#region src/bin/export-reviews.ts
7
+ async function runReviewsExport(userId, options) {
8
+ try {
9
+ console.log(`Fetching reviews for user ${userId} (${options.format.toUpperCase()})...`);
10
+ const reviews = await csfd.userReviews(userId, {
11
+ ...options.userReviewsOptions,
12
+ onProgress: renderProgress
13
+ });
14
+ console.log(`Fetched ${reviews.length} reviews.`);
15
+ let content = "";
16
+ let fileName = "";
17
+ if (options.format === "json") {
18
+ content = JSON.stringify(reviews, null, 2);
19
+ fileName = `${userId}-reviews.json`;
20
+ } else {
21
+ content = [[
22
+ "id",
23
+ "title",
24
+ "year",
25
+ "type",
26
+ "colorRating",
27
+ "userRating",
28
+ "date",
29
+ "url",
30
+ "text"
31
+ ].join(","), ...reviews.map((r) => [
32
+ r.id,
33
+ escapeCsvField(r.title ?? ""),
34
+ r.year ?? "",
35
+ escapeCsvField(r.type ?? ""),
36
+ escapeCsvField(r.colorRating ?? ""),
37
+ r.userRating ?? "",
38
+ escapeCsvField(r.userDate ?? ""),
39
+ escapeCsvField(r.url ?? ""),
40
+ escapeCsvField(r.text ?? "")
41
+ ].join(","))].join("\n");
42
+ fileName = `${userId}-reviews.csv`;
43
+ }
44
+ await writeFile(fileName, content);
45
+ console.log("Saved in file:", `./${fileName}`);
46
+ } catch (error) {
47
+ console.error("Error exporting reviews:", error);
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ //#endregion
53
+ export { runReviewsExport };
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { csfd } from "../index.js";
3
+ import { c } from "./utils.js";
4
+
5
+ //#region src/bin/lookup-movie.ts
6
+ async function runMovieLookup(movieId, json) {
7
+ const movie = await csfd.movie(movieId);
8
+ if (json) console.log(JSON.stringify(movie, null, 2));
9
+ else printMovie(movie);
10
+ }
11
+ function printMovie(movie) {
12
+ const ratingColor = movie.colorRating === "good" ? c.green : movie.colorRating === "average" ? c.yellow : movie.colorRating === "bad" ? c.red : c.dim;
13
+ const row = (label, value) => value ? ` ${c.dim(label.padEnd(11))} ${value}` : "";
14
+ const names = (arr, max = 5) => arr.slice(0, max).map((x) => x.name).join(", ");
15
+ const description = movie.descriptions?.[0] ? movie.descriptions[0].length > 160 ? movie.descriptions[0].slice(0, 157) + "..." : movie.descriptions[0] : "";
16
+ const vod = movie.vod?.map((v) => v.title).join(", ") ?? "";
17
+ const lines = [
18
+ "",
19
+ c.bold(movie.title) + c.dim(` (${movie.year ?? "?"})`) + " · " + c.dim(movie.type ?? ""),
20
+ c.dim("─".repeat(52)),
21
+ row("Rating", movie.rating != null ? ratingColor(c.bold(movie.rating + "%")) + c.dim(` (${movie.ratingCount?.toLocaleString()} ratings)`) : c.dim("no rating")),
22
+ row("Genres", movie.genres?.join(", ") ?? ""),
23
+ row("Origins", movie.origins?.join(", ") ?? ""),
24
+ row("Duration", movie.duration ? movie.duration + " min" : ""),
25
+ row("Directors", names(movie.creators?.directors ?? [])),
26
+ row("Cast", names(movie.creators?.actors ?? [])),
27
+ description ? "\n " + c.dim(description) : "",
28
+ vod ? "\n" + row("VOD", vod) : "",
29
+ row("URL", c.dim(movie.url ?? "")),
30
+ ""
31
+ ].filter(Boolean);
32
+ console.log(lines.join("\n"));
33
+ }
34
+
35
+ //#endregion
36
+ export { runMovieLookup };
package/bin/utils.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ //#region src/bin/utils.ts
3
+ const useColor = process.stdout.isTTY && !process.env["NO_COLOR"];
4
+ const c = {
5
+ bold: (s) => useColor ? `\x1b[1m${s}\x1b[22m` : s,
6
+ dim: (s) => useColor ? `\x1b[2m${s}\x1b[22m` : s,
7
+ cyan: (s) => useColor ? `\x1b[36m${s}\x1b[39m` : s,
8
+ green: (s) => useColor ? `\x1b[32m${s}\x1b[39m` : s,
9
+ yellow: (s) => useColor ? `\x1b[33m${s}\x1b[39m` : s,
10
+ red: (s) => useColor ? `\x1b[31m${s}\x1b[39m` : s
11
+ };
12
+ const err = (msg) => c.red(c.bold("✖ Error:")) + " " + msg;
13
+ const escapeCsvField = (value) => {
14
+ const needsQuotes = /[",\n\r]/.test(value);
15
+ const escaped = value.replaceAll("\"", "\"\"");
16
+ return needsQuotes ? `"${escaped}"` : escaped;
17
+ };
18
+ const renderProgress = (page, total) => {
19
+ const pct = Math.round(page / total * 100);
20
+ const filled = Math.round(page / total * 20);
21
+ const bar = "█".repeat(filled) + "░".repeat(20 - filled);
22
+ process.stdout.write(`\r [${bar}] ${page}/${total} pages ${pct}%`);
23
+ if (page === total) process.stdout.write("\n");
24
+ };
25
+
26
+ //#endregion
27
+ export { c, err, escapeCsvField, renderProgress };
package/cli.js CHANGED
@@ -1,5 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import { c, err } from "./bin/utils.js";
3
+
2
4
  //#region src/cli.ts
5
+ /**
6
+ * Main CLI entry point for node-csfd-api.
7
+ */
8
+ const GITHUB_REPO = "bartholomej/node-csfd-api";
9
+ const GITHUB_API_LATEST = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
10
+ const GITHUB_RELEASES_URL = `https://github.com/${GITHUB_REPO}/releases/latest`;
11
+ const INSTALL_SH_URL = `https://raw.githubusercontent.com/${GITHUB_REPO}/master/install.sh`;
3
12
  function getCommandName() {
4
13
  const scriptPath = process.argv[1] ?? "";
5
14
  const basename = scriptPath.split("/").pop() ?? "";
@@ -7,9 +16,23 @@ function getCommandName() {
7
16
  if (scriptPath.includes("node-csfd-api")) return "npx node-csfd-api";
8
17
  return "csfd";
9
18
  }
19
+ function parseNumericArg(raw, usage) {
20
+ const n = Number(raw);
21
+ if (!raw || isNaN(n)) {
22
+ console.error(err("Please provide a valid numeric ID."));
23
+ console.log(c.dim(` Usage: ${usage}`));
24
+ process.exit(1);
25
+ }
26
+ return n;
27
+ }
28
+ function parseFormat(args) {
29
+ return args.includes("--json") ? "json" : "csv";
30
+ }
10
31
  async function main() {
11
32
  const args = process.argv.slice(2);
12
- switch (args[0]) {
33
+ const command = args[0];
34
+ const updateHint = new Set(["update"]).has(command) ? null : checkForUpdateInBackground();
35
+ switch (command) {
13
36
  case "server":
14
37
  case "api":
15
38
  try {
@@ -29,20 +52,9 @@ async function main() {
29
52
  break;
30
53
  case "export":
31
54
  if (args[1] === "ratings") {
32
- const userIdRaw = args[2];
33
- const userId = Number(userIdRaw);
34
- if (!userIdRaw || isNaN(userId)) {
35
- console.error("Error: Please provide a valid numeric User ID.");
36
- console.log(`Usage: ${getCommandName()} export ratings <userId> [options]`);
37
- process.exit(1);
38
- }
55
+ const userId = parseNumericArg(args[2], `${getCommandName()} export ratings <userId> [options]`);
39
56
  const isLetterboxd = args.includes("--letterboxd");
40
- const isJson = args.includes("--json");
41
- const isCsv = args.includes("--csv");
42
- let format = "csv";
43
- if (isLetterboxd) format = "letterboxd";
44
- else if (isJson) format = "json";
45
- else if (isCsv) format = "csv";
57
+ const format = isLetterboxd ? "letterboxd" : parseFormat(args);
46
58
  try {
47
59
  const { runRatingsExport } = await import("./bin/export-ratings.js");
48
60
  await runRatingsExport(userId, {
@@ -54,50 +66,208 @@ async function main() {
54
66
  }
55
67
  });
56
68
  } catch (error) {
57
- console.error("Failed to run export:", error);
69
+ console.error(err("Failed to run export:"), error);
70
+ process.exit(1);
71
+ }
72
+ } else if (args[1] === "reviews") {
73
+ const userId = parseNumericArg(args[2], `${getCommandName()} export reviews <userId> [options]`);
74
+ try {
75
+ const { runReviewsExport } = await import("./bin/export-reviews.js");
76
+ await runReviewsExport(userId, {
77
+ format: parseFormat(args),
78
+ userReviewsOptions: {
79
+ allPages: true,
80
+ allPagesDelay: 1e3
81
+ }
82
+ });
83
+ } catch (error) {
84
+ console.error(err("Failed to run export:"), error);
58
85
  process.exit(1);
59
86
  }
60
87
  } else if (args[1] === "letterboxd") {
61
- console.warn("Deprecation Warning: \"export letterboxd\" is deprecated. Please use \"export ratings <id> --letterboxd\" instead.");
62
- console.log(`Usage: ${getCommandName()} export ratings <userId> --letterboxd`);
88
+ console.warn(c.yellow(c.bold(" Deprecated:")) + " \"export letterboxd\" is removed. Use \"export ratings <id> --letterboxd\" instead.");
89
+ console.log(c.dim(` Usage: ${getCommandName()} export ratings <userId> --letterboxd`));
63
90
  process.exit(1);
64
91
  } else {
65
- console.error(`Unknown export target: ${args[1]}`);
92
+ console.error(err(`Unknown export target: ${c.bold(String(args[1]))}`));
66
93
  printUsage();
67
94
  process.exit(1);
68
95
  }
69
96
  break;
97
+ case "movie": {
98
+ const movieId = parseNumericArg(args[1], `${getCommandName()} movie <id> [--json]`);
99
+ try {
100
+ const { runMovieLookup } = await import("./bin/lookup-movie.js");
101
+ await runMovieLookup(movieId, args.includes("--json"));
102
+ } catch (error) {
103
+ console.error(err("Failed to fetch movie:"), error);
104
+ process.exit(1);
105
+ }
106
+ break;
107
+ }
70
108
  case "--version":
71
109
  case "-v":
72
- console.log("5.6.0-next.2");
110
+ console.log(c.bold("5.6.0-next.3"));
111
+ break;
112
+ case "update":
113
+ await runUpdate();
73
114
  break;
74
115
  default:
75
116
  printUsage();
76
117
  break;
77
118
  }
119
+ if (updateHint) await updateHint;
120
+ }
121
+ function isRunningViaNpx() {
122
+ const base = (process.execPath ?? "").split("/").pop() ?? "";
123
+ return base === "node" || base === "node.exe" || base === "bun";
124
+ }
125
+ function isRunningViaHomebrew() {
126
+ const exec = process.execPath ?? "";
127
+ return exec.includes("/homebrew/") || exec.includes("/Cellar/");
128
+ }
129
+ function compareSemver(a, b) {
130
+ const parse = (v) => {
131
+ const [main, pre = ""] = v.split("-");
132
+ const [major, minor, patch] = main.split(".").map(Number);
133
+ return {
134
+ major,
135
+ minor,
136
+ patch,
137
+ pre
138
+ };
139
+ };
140
+ const va = parse(a);
141
+ const vb = parse(b);
142
+ if (va.major !== vb.major) return va.major - vb.major;
143
+ if (va.minor !== vb.minor) return va.minor - vb.minor;
144
+ if (va.patch !== vb.patch) return va.patch - vb.patch;
145
+ if (va.pre && !vb.pre) return -1;
146
+ if (!va.pre && vb.pre) return 1;
147
+ return va.pre.localeCompare(vb.pre);
148
+ }
149
+ async function fetchLatestVersion(timeoutMs = 5e3) {
150
+ const controller = new AbortController();
151
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
152
+ try {
153
+ return (await (await fetch(GITHUB_API_LATEST, { signal: controller.signal })).json()).tag_name?.replace(/^v/, "") ?? "";
154
+ } finally {
155
+ clearTimeout(timer);
156
+ }
157
+ }
158
+ function printUpgradeInstructions(latest) {
159
+ console.log(c.green(c.bold("↑ New version available: ")) + c.bold(latest));
160
+ if (isRunningViaNpx()) {
161
+ console.log("\n" + c.bold("Run:"));
162
+ console.log(" " + c.cyan("npx node-csfd-api@latest <command>"));
163
+ } else if (isRunningViaHomebrew()) {
164
+ console.log("\n" + c.bold("Run:"));
165
+ console.log(" " + c.cyan("brew upgrade csfd"));
166
+ } else if (process.platform === "win32") {
167
+ console.log("\n" + c.bold("Download the latest release from:"));
168
+ console.log(" " + c.cyan(GITHUB_RELEASES_URL));
169
+ } else {
170
+ console.log("\n" + c.bold("Run:"));
171
+ console.log(" " + c.cyan(`curl -fsSL ${INSTALL_SH_URL} | bash`));
172
+ }
173
+ }
174
+ const UPDATE_CACHE_TTL = 1440 * 60 * 1e3;
175
+ function getUpdateCachePath() {
176
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || "";
177
+ return `${process.platform === "win32" ? process.env["APPDATA"] || home : process.env["XDG_CONFIG_HOME"] || `${home}/.config`}/csfd/update-check.json`;
178
+ }
179
+ async function checkForUpdateInBackground() {
180
+ try {
181
+ const { readFileSync, writeFileSync, mkdirSync } = await import("node:fs");
182
+ const { dirname } = await import("node:path");
183
+ const cachePath = getUpdateCachePath();
184
+ let cache = null;
185
+ try {
186
+ cache = JSON.parse(readFileSync(cachePath, "utf-8"));
187
+ } catch {}
188
+ const now = Date.now();
189
+ let latestVersion = cache?.latestVersion ?? "";
190
+ if (!cache || now - cache.lastCheck > UPDATE_CACHE_TTL) try {
191
+ const fetched = await fetchLatestVersion(3e3);
192
+ if (fetched) {
193
+ latestVersion = fetched;
194
+ try {
195
+ mkdirSync(dirname(cachePath), { recursive: true });
196
+ writeFileSync(cachePath, JSON.stringify({
197
+ lastCheck: now,
198
+ latestVersion
199
+ }));
200
+ } catch {}
201
+ }
202
+ } catch {}
203
+ if (!latestVersion || compareSemver("5.6.0-next.3", latestVersion) >= 0) return;
204
+ console.log("");
205
+ console.log(c.dim(" " + "─".repeat(44)));
206
+ console.log(` ${c.yellow(c.bold("↑ Update available:"))} ${c.dim("5.6.0-next.3")} → ${c.bold(c.green(latestVersion))}`);
207
+ console.log(` ${c.dim("Run")} ${c.cyan(getCommandName() + " update")} ${c.dim("for upgrade instructions.")}`);
208
+ } catch {}
209
+ }
210
+ async function runUpdate() {
211
+ console.log(c.dim("Current version: ") + c.bold("5.6.0-next.3"));
212
+ console.log(c.dim("Checking for updates..."));
213
+ let latest;
214
+ try {
215
+ latest = await fetchLatestVersion();
216
+ } catch {
217
+ console.error(err("Could not reach GitHub API."));
218
+ process.exit(1);
219
+ }
220
+ if (!latest) {
221
+ console.error(err("Could not determine latest version."));
222
+ process.exit(1);
223
+ }
224
+ const cmp = compareSemver("5.6.0-next.3", latest);
225
+ if (cmp === 0) {
226
+ console.log(c.green("✔ Already up to date."));
227
+ return;
228
+ }
229
+ if (cmp > 0) {
230
+ console.log(c.yellow("⚠ You are running a pre-release version.") + c.dim(` Latest stable: ${latest}`));
231
+ return;
232
+ }
233
+ printUpgradeInstructions(latest);
78
234
  }
79
235
  function printUsage() {
80
236
  const cmd = getCommandName();
237
+ const header = c.bold(c.cyan("csfd")) + " " + c.dim(`v5.6.0-next.3`);
238
+ const usage = c.bold("Usage:") + ` ${c.cyan(cmd)} ${c.dim("<command> [options]")}`;
239
+ const section = (title) => c.bold(title);
240
+ const cmd_ = (name) => " " + c.cyan(name);
241
+ const flag_ = (name) => " " + c.dim(name);
242
+ const desc = (text) => c.dim(text);
243
+ const sub_ = (name) => " " + c.dim(name);
81
244
  console.log(`
82
- Usage: ${cmd} <command> [options]
245
+ ${header}
246
+
247
+ ${usage}
83
248
 
84
- Commands:
85
- server, api Start the REST API server
86
- mcp Start the MCP (Model Context Protocol) server for AI agents
87
- export ratings <userId> Export user ratings to CSV (default), JSON, or Letterboxd format
88
- Options:
89
- --csv Export to CSV format (default)
90
- --json Export to JSON format
91
- --letterboxd Export to Letterboxd-compatible CSV format
92
- help Show this help message
249
+ ${section("Commands:")}
250
+ ${cmd_("server, api")} ${desc("Start the REST API server")}
251
+ ${cmd_("mcp")} ${desc("Start the MCP server for AI agents")}
252
+ ${cmd_("export ratings <userId>")} ${desc("Export user ratings")}
253
+ ${sub_("--csv")} ${desc("CSV format (default)")}
254
+ ${sub_("--json")} ${desc("JSON format")}
255
+ ${sub_("--letterboxd")} ${desc("Letterboxd-compatible CSV")}
256
+ ${cmd_("export reviews <userId>")} ${desc("Export user reviews")}
257
+ ${sub_("--csv")} ${desc("CSV format (default)")}
258
+ ${sub_("--json")} ${desc("JSON format")}
259
+ ${cmd_("movie <id>")} ${desc("Show movie details")}
260
+ ${sub_("--json")} ${desc("Output raw JSON")}
261
+ ${cmd_("update")} ${desc("Check for updates")}
262
+ ${cmd_("help")} ${desc("Show this help")}
93
263
 
94
- Flags:
95
- --version, -v Show version
96
- --help, -h Show this help message
264
+ ${section("Flags:")}
265
+ ${flag_("-v, --version")} ${desc("Show version")}
266
+ ${flag_("-h, --help")} ${desc("Show this help")}
97
267
  `);
98
268
  }
99
269
  main().catch((error) => {
100
- console.error("Fatal error:", error);
270
+ console.error(err("Fatal: " + String(error)));
101
271
  process.exit(1);
102
272
  });
103
273
 
@@ -20,6 +20,13 @@ interface CSFDUserRatingConfig {
20
20
  * Specific page number to fetch (e.g., 2 for second page)
21
21
  */
22
22
  page?: number;
23
+ /**
24
+ * Called on each page fetch when `allPages` is enabled.
25
+ * Useful for rendering progress in CLI tools or custom logging.
26
+ * @param page - Current page number
27
+ * @param total - Total number of pages
28
+ */
29
+ onProgress?: (page: number, total: number) => void;
23
30
  }
24
31
  type CSFDColors = 'lightgrey' | 'blue' | 'red' | 'grey';
25
32
  //#endregion
@@ -20,6 +20,13 @@ interface CSFDUserRatingConfig {
20
20
  * Specific page number to fetch (e.g., 2 for second page)
21
21
  */
22
22
  page?: number;
23
+ /**
24
+ * Called on each page fetch when `allPages` is enabled.
25
+ * Useful for rendering progress in CLI tools or custom logging.
26
+ * @param page - Current page number
27
+ * @param total - Total number of pages
28
+ */
29
+ onProgress?: (page: number, total: number) => void;
23
30
  }
24
31
  type CSFDColors = 'lightgrey' | 'blue' | 'red' | 'grey';
25
32
  //#endregion
@@ -22,6 +22,13 @@ interface CSFDUserReviewsConfig {
22
22
  * Specific page number to fetch (e.g., 2 for second page)
23
23
  */
24
24
  page?: number;
25
+ /**
26
+ * Called on each page fetch when `allPages` is enabled.
27
+ * Useful for rendering progress in CLI tools or custom logging.
28
+ * @param page - Current page number
29
+ * @param total - Total number of pages
30
+ */
31
+ onProgress?: (page: number, total: number) => void;
25
32
  }
26
33
  //#endregion
27
34
  export { CSFDUserReviews, CSFDUserReviewsConfig };
@@ -22,6 +22,13 @@ interface CSFDUserReviewsConfig {
22
22
  * Specific page number to fetch (e.g., 2 for second page)
23
23
  */
24
24
  page?: number;
25
+ /**
26
+ * Called on each page fetch when `allPages` is enabled.
27
+ * Useful for rendering progress in CLI tools or custom logging.
28
+ * @param page - Current page number
29
+ * @param total - Total number of pages
30
+ */
31
+ onProgress?: (page: number, total: number) => void;
25
32
  }
26
33
  //#endregion
27
34
  export { CSFDUserReviews, CSFDUserReviewsConfig };
@@ -1,4 +1,5 @@
1
1
  const require_fetch_polyfill = require("./fetch.polyfill.cjs");
2
+ const require_vars = require("../vars.cjs");
2
3
  //#region src/fetchers/index.ts
3
4
  const browserProfiles = [
4
5
  {
@@ -61,11 +62,11 @@ const fetchPage = async (url, optionsRequest) => {
61
62
  });
62
63
  if (!response.ok) throw new Error(`node-csfd-api: Bad response ${response.status} for url: ${url}`);
63
64
  const html = await response.text();
64
- if (html.includes("Making sure you're not a bot!")) console.warn("node-csfd-api: Trap detected.");
65
+ if (html.includes("Making sure you're not a bot!")) console.warn("[node-csfd-api] Trap detected. You may be rate-limited or blocked by ČSFD.");
65
66
  return html;
66
67
  } catch (e) {
67
- if (e instanceof Error) console.error(e.message);
68
- else console.error(String(e));
68
+ if (e instanceof Error) console.error(require_vars.LIB_PREFIX, e.message);
69
+ else console.error(require_vars.LIB_PREFIX, String(e));
69
70
  return "Error";
70
71
  }
71
72
  };