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 +61 -29
- package/bin/export-ratings.js +5 -6
- package/bin/export-reviews.js +53 -0
- package/bin/lookup-movie.js +36 -0
- package/bin/utils.js +27 -0
- package/cli.js +203 -33
- package/dto/user-ratings.d.cts +7 -0
- package/dto/user-ratings.d.ts +7 -0
- package/dto/user-reviews.d.cts +7 -0
- package/dto/user-reviews.d.ts +7 -0
- package/fetchers/index.cjs +4 -3
- package/fetchers/index.cjs.map +1 -1
- package/fetchers/index.js +4 -3
- package/fetchers/index.js.map +1 -1
- package/package.js +1 -1
- package/package.json +1 -1
- package/services/cinema.service.cjs +1 -1
- package/services/cinema.service.js +1 -1
- package/services/creator.service.cjs +1 -1
- package/services/creator.service.js +1 -1
- package/services/movie.service.cjs +2 -2
- package/services/movie.service.cjs.map +1 -1
- package/services/movie.service.js +2 -2
- package/services/movie.service.js.map +1 -1
- package/services/search.service.cjs +1 -1
- package/services/search.service.js +1 -1
- package/services/user-ratings.service.cjs +4 -9
- package/services/user-ratings.service.cjs.map +1 -1
- package/services/user-ratings.service.js +4 -9
- package/services/user-ratings.service.js.map +1 -1
- package/services/user-reviews.service.cjs +4 -9
- package/services/user-reviews.service.cjs.map +1 -1
- package/services/user-reviews.service.js +4 -9
- package/services/user-reviews.service.js.map +1 -1
- package/vars.cjs +2 -0
- package/vars.cjs.map +1 -1
- package/vars.js +2 -1
- package/vars.js.map +1 -1
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
|
|
440
|
-
| --------------- |
|
|
441
|
-
| `includesOnly` | `CSFDFilmTypes[]`
|
|
442
|
-
| `exclude` | `CSFDFilmTypes[]`
|
|
443
|
-
| `allPages` | `boolean`
|
|
444
|
-
| `allPagesDelay` | `number`
|
|
445
|
-
| `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
|
|
528
|
+
This library ships with a CLI exposing several tools. Choose the installation method that fits your workflow.
|
|
528
529
|
|
|
529
|
-
### Installation
|
|
530
|
+
### Installation
|
|
530
531
|
|
|
531
|
-
**Option A:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
568
|
+
#### 1. Movie Details
|
|
552
569
|
|
|
553
570
|
```bash
|
|
554
|
-
|
|
555
|
-
npx node-csfd-api
|
|
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
|
-
|
|
558
|
-
npx node-csfd-api export ratings 912 --json
|
|
577
|
+
#### 2. Export Ratings (CSV, JSON & Letterboxd)
|
|
559
578
|
|
|
560
|
-
|
|
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
|
-
|
|
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
|
-
|
|
590
|
+
#### 3. Export Reviews (CSV & JSON)
|
|
567
591
|
|
|
568
592
|
```bash
|
|
569
|
-
|
|
570
|
-
#
|
|
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
|
-
|
|
599
|
+
#### 4. REST API Server
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
csfd server
|
|
603
|
+
# npx node-csfd-api server
|
|
604
|
+
```
|
|
574
605
|
|
|
575
|
-
|
|
606
|
+
#### 5. MCP Server for AI Agents
|
|
576
607
|
|
|
577
608
|
```bash
|
|
578
|
-
|
|
609
|
+
csfd mcp
|
|
610
|
+
# npx node-csfd-api mcp
|
|
579
611
|
```
|
|
580
612
|
|
|
581
613
|
## 🤖 MCP Server (Model Context Protocol)
|
package/bin/export-ratings.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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("
|
|
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.
|
|
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
|
-
|
|
245
|
+
${header}
|
|
246
|
+
|
|
247
|
+
${usage}
|
|
83
248
|
|
|
84
|
-
Commands:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
270
|
+
console.error(err("Fatal: " + String(error)));
|
|
101
271
|
process.exit(1);
|
|
102
272
|
});
|
|
103
273
|
|
package/dto/user-ratings.d.cts
CHANGED
|
@@ -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
|
package/dto/user-ratings.d.ts
CHANGED
|
@@ -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
|
package/dto/user-reviews.d.cts
CHANGED
|
@@ -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 };
|
package/dto/user-reviews.d.ts
CHANGED
|
@@ -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 };
|
package/fetchers/index.cjs
CHANGED
|
@@ -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
|
|
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
|
};
|