tmdb-kit 1.0.0 → 1.2.0
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 +455 -196
- package/dist/cli.js +3823 -0
- package/dist/client.d.cts +71 -0
- package/dist/client.d.ts +71 -0
- package/dist/errors.d.cts +86 -0
- package/dist/errors.d.ts +86 -0
- package/dist/helpers/cache.d.cts +28 -0
- package/dist/helpers/cache.d.ts +28 -0
- package/dist/helpers/pagination.d.cts +35 -0
- package/dist/helpers/pagination.d.ts +35 -0
- package/dist/helpers/retry.d.cts +47 -0
- package/dist/helpers/retry.d.ts +47 -0
- package/dist/helpers/timeout.d.cts +20 -0
- package/dist/helpers/timeout.d.ts +20 -0
- package/dist/http.d.cts +80 -0
- package/dist/http.d.ts +80 -0
- package/dist/index.cjs +1115 -768
- package/dist/index.cjs.map +27 -1
- package/dist/index.d.cts +13 -783
- package/dist/index.d.ts +13 -783
- package/dist/index.js +1074 -744
- package/dist/index.js.map +27 -1
- package/dist/query.d.cts +91 -0
- package/dist/query.d.ts +91 -0
- package/dist/resources/configuration.d.cts +17 -0
- package/dist/resources/configuration.d.ts +17 -0
- package/dist/resources/genres.d.cts +13 -0
- package/dist/resources/genres.d.ts +13 -0
- package/dist/resources/images.d.cts +17 -0
- package/dist/resources/images.d.ts +17 -0
- package/dist/resources/movies.d.cts +21 -0
- package/dist/resources/movies.d.ts +21 -0
- package/dist/resources/people.d.cts +14 -0
- package/dist/resources/people.d.ts +14 -0
- package/dist/resources/search.d.cts +15 -0
- package/dist/resources/search.d.ts +15 -0
- package/dist/resources/trending.d.cts +23 -0
- package/dist/resources/trending.d.ts +23 -0
- package/dist/resources/tv.d.cts +30 -0
- package/dist/resources/tv.d.ts +30 -0
- package/dist/search-guards.d.cts +32 -0
- package/dist/search-guards.d.ts +32 -0
- package/dist/types.d.cts +550 -0
- package/dist/types.d.ts +550 -0
- package/package.json +26 -19
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3823 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// node_modules/citty/dist/_chunks/libs/scule.mjs
|
|
3
|
+
var NUMBER_CHAR_RE = /\d/;
|
|
4
|
+
var STR_SPLITTERS = [
|
|
5
|
+
"-",
|
|
6
|
+
"_",
|
|
7
|
+
"/",
|
|
8
|
+
"."
|
|
9
|
+
];
|
|
10
|
+
function isUppercase(char = "") {
|
|
11
|
+
if (NUMBER_CHAR_RE.test(char))
|
|
12
|
+
return;
|
|
13
|
+
return char !== char.toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
function splitByCase(str, separators) {
|
|
16
|
+
const splitters = separators ?? STR_SPLITTERS;
|
|
17
|
+
const parts = [];
|
|
18
|
+
if (!str || typeof str !== "string")
|
|
19
|
+
return parts;
|
|
20
|
+
let buff = "";
|
|
21
|
+
let previousUpper;
|
|
22
|
+
let previousSplitter;
|
|
23
|
+
for (const char of str) {
|
|
24
|
+
const isSplitter = splitters.includes(char);
|
|
25
|
+
if (isSplitter === true) {
|
|
26
|
+
parts.push(buff);
|
|
27
|
+
buff = "";
|
|
28
|
+
previousUpper = undefined;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const isUpper = isUppercase(char);
|
|
32
|
+
if (previousSplitter === false) {
|
|
33
|
+
if (previousUpper === false && isUpper === true) {
|
|
34
|
+
parts.push(buff);
|
|
35
|
+
buff = char;
|
|
36
|
+
previousUpper = isUpper;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (previousUpper === true && isUpper === false && buff.length > 1) {
|
|
40
|
+
const lastChar = buff.at(-1);
|
|
41
|
+
parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
|
|
42
|
+
buff = lastChar + char;
|
|
43
|
+
previousUpper = isUpper;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
buff += char;
|
|
48
|
+
previousUpper = isUpper;
|
|
49
|
+
previousSplitter = isSplitter;
|
|
50
|
+
}
|
|
51
|
+
parts.push(buff);
|
|
52
|
+
return parts;
|
|
53
|
+
}
|
|
54
|
+
function upperFirst(str) {
|
|
55
|
+
return str ? str[0].toUpperCase() + str.slice(1) : "";
|
|
56
|
+
}
|
|
57
|
+
function lowerFirst(str) {
|
|
58
|
+
return str ? str[0].toLowerCase() + str.slice(1) : "";
|
|
59
|
+
}
|
|
60
|
+
function pascalCase(str, opts) {
|
|
61
|
+
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
|
|
62
|
+
}
|
|
63
|
+
function camelCase(str, opts) {
|
|
64
|
+
return lowerFirst(pascalCase(str || "", opts));
|
|
65
|
+
}
|
|
66
|
+
function kebabCase(str, joiner) {
|
|
67
|
+
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
|
|
68
|
+
}
|
|
69
|
+
function snakeCase(str) {
|
|
70
|
+
return kebabCase(str || "", "_");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// node_modules/citty/dist/index.mjs
|
|
74
|
+
import { parseArgs as parseArgs$1 } from "node:util";
|
|
75
|
+
function toArray(val) {
|
|
76
|
+
if (Array.isArray(val))
|
|
77
|
+
return val;
|
|
78
|
+
return val === undefined ? [] : [val];
|
|
79
|
+
}
|
|
80
|
+
function formatLineColumns(lines, linePrefix = "") {
|
|
81
|
+
const maxLength = [];
|
|
82
|
+
for (const line of lines)
|
|
83
|
+
for (const [i, element] of line.entries())
|
|
84
|
+
maxLength[i] = Math.max(maxLength[i] || 0, element.length);
|
|
85
|
+
return lines.map((l) => l.map((c, i) => linePrefix + c[i === 0 ? "padStart" : "padEnd"](maxLength[i])).join(" ")).join(`
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
function resolveValue(input) {
|
|
89
|
+
return typeof input === "function" ? input() : input;
|
|
90
|
+
}
|
|
91
|
+
var CLIError = class extends Error {
|
|
92
|
+
code;
|
|
93
|
+
constructor(message, code) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.name = "CLIError";
|
|
96
|
+
this.code = code;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function parseRawArgs(args = [], opts = {}) {
|
|
100
|
+
const booleans = new Set(opts.boolean || []);
|
|
101
|
+
const strings = new Set(opts.string || []);
|
|
102
|
+
const aliasMap = opts.alias || {};
|
|
103
|
+
const defaults = opts.default || {};
|
|
104
|
+
const aliasToMain = /* @__PURE__ */ new Map;
|
|
105
|
+
const mainToAliases = /* @__PURE__ */ new Map;
|
|
106
|
+
for (const [key, value] of Object.entries(aliasMap)) {
|
|
107
|
+
const targets = value;
|
|
108
|
+
for (const target of targets) {
|
|
109
|
+
aliasToMain.set(key, target);
|
|
110
|
+
if (!mainToAliases.has(target))
|
|
111
|
+
mainToAliases.set(target, []);
|
|
112
|
+
mainToAliases.get(target).push(key);
|
|
113
|
+
aliasToMain.set(target, key);
|
|
114
|
+
if (!mainToAliases.has(key))
|
|
115
|
+
mainToAliases.set(key, []);
|
|
116
|
+
mainToAliases.get(key).push(target);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const options = {};
|
|
120
|
+
function getType(name) {
|
|
121
|
+
if (booleans.has(name))
|
|
122
|
+
return "boolean";
|
|
123
|
+
const aliases = mainToAliases.get(name) || [];
|
|
124
|
+
for (const alias of aliases)
|
|
125
|
+
if (booleans.has(alias))
|
|
126
|
+
return "boolean";
|
|
127
|
+
return "string";
|
|
128
|
+
}
|
|
129
|
+
function isStringType(name) {
|
|
130
|
+
if (strings.has(name))
|
|
131
|
+
return true;
|
|
132
|
+
const aliases = mainToAliases.get(name) || [];
|
|
133
|
+
for (const alias of aliases)
|
|
134
|
+
if (strings.has(alias))
|
|
135
|
+
return true;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const allOptions = new Set([
|
|
139
|
+
...booleans,
|
|
140
|
+
...strings,
|
|
141
|
+
...Object.keys(aliasMap),
|
|
142
|
+
...Object.values(aliasMap).flat(),
|
|
143
|
+
...Object.keys(defaults)
|
|
144
|
+
]);
|
|
145
|
+
for (const name of allOptions)
|
|
146
|
+
if (!options[name])
|
|
147
|
+
options[name] = {
|
|
148
|
+
type: getType(name),
|
|
149
|
+
default: defaults[name]
|
|
150
|
+
};
|
|
151
|
+
for (const [alias, main] of aliasToMain.entries())
|
|
152
|
+
if (alias.length === 1 && options[main] && !options[main].short)
|
|
153
|
+
options[main].short = alias;
|
|
154
|
+
const processedArgs = [];
|
|
155
|
+
const negatedFlags = {};
|
|
156
|
+
for (let i = 0;i < args.length; i++) {
|
|
157
|
+
const arg = args[i];
|
|
158
|
+
if (arg === "--") {
|
|
159
|
+
processedArgs.push(...args.slice(i));
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
if (arg.startsWith("--no-")) {
|
|
163
|
+
const flagName = arg.slice(5);
|
|
164
|
+
negatedFlags[flagName] = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
processedArgs.push(arg);
|
|
168
|
+
}
|
|
169
|
+
let parsed;
|
|
170
|
+
try {
|
|
171
|
+
parsed = parseArgs$1({
|
|
172
|
+
args: processedArgs,
|
|
173
|
+
options: Object.keys(options).length > 0 ? options : undefined,
|
|
174
|
+
allowPositionals: true,
|
|
175
|
+
strict: false
|
|
176
|
+
});
|
|
177
|
+
} catch {
|
|
178
|
+
parsed = {
|
|
179
|
+
values: {},
|
|
180
|
+
positionals: processedArgs
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const out = { _: [] };
|
|
184
|
+
out._ = parsed.positionals;
|
|
185
|
+
for (const [key, value] of Object.entries(parsed.values)) {
|
|
186
|
+
let coerced = value;
|
|
187
|
+
if (getType(key) === "boolean" && typeof value === "string")
|
|
188
|
+
coerced = value !== "false";
|
|
189
|
+
else if (isStringType(key) && typeof value === "boolean")
|
|
190
|
+
coerced = "";
|
|
191
|
+
out[key] = coerced;
|
|
192
|
+
}
|
|
193
|
+
for (const [name] of Object.entries(negatedFlags)) {
|
|
194
|
+
out[name] = false;
|
|
195
|
+
const mainName = aliasToMain.get(name);
|
|
196
|
+
if (mainName)
|
|
197
|
+
out[mainName] = false;
|
|
198
|
+
const aliases = mainToAliases.get(name);
|
|
199
|
+
if (aliases)
|
|
200
|
+
for (const alias of aliases)
|
|
201
|
+
out[alias] = false;
|
|
202
|
+
}
|
|
203
|
+
for (const [alias, main] of aliasToMain.entries()) {
|
|
204
|
+
if (out[alias] !== undefined && out[main] === undefined)
|
|
205
|
+
out[main] = out[alias];
|
|
206
|
+
if (out[main] !== undefined && out[alias] === undefined)
|
|
207
|
+
out[alias] = out[main];
|
|
208
|
+
if (out[alias] !== out[main] && defaults[main] === out[main])
|
|
209
|
+
out[main] = out[alias];
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
var noColor = /* @__PURE__ */ (() => {
|
|
214
|
+
const env = globalThis.process?.env ?? {};
|
|
215
|
+
return env.NO_COLOR === "1" || env.TERM === "dumb" || env.TEST || env.CI;
|
|
216
|
+
})();
|
|
217
|
+
var _c = (c, r = 39) => (t) => noColor ? t : `\x1B[${c}m${t}\x1B[${r}m`;
|
|
218
|
+
var bold = /* @__PURE__ */ _c(1, 22);
|
|
219
|
+
var cyan = /* @__PURE__ */ _c(36);
|
|
220
|
+
var gray = /* @__PURE__ */ _c(90);
|
|
221
|
+
var underline = /* @__PURE__ */ _c(4, 24);
|
|
222
|
+
function parseArgs(rawArgs, argsDef) {
|
|
223
|
+
const parseOptions = {
|
|
224
|
+
boolean: [],
|
|
225
|
+
string: [],
|
|
226
|
+
alias: {},
|
|
227
|
+
default: {}
|
|
228
|
+
};
|
|
229
|
+
const args = resolveArgs(argsDef);
|
|
230
|
+
for (const arg of args) {
|
|
231
|
+
if (arg.type === "positional")
|
|
232
|
+
continue;
|
|
233
|
+
if (arg.type === "string" || arg.type === "enum")
|
|
234
|
+
parseOptions.string.push(arg.name);
|
|
235
|
+
else if (arg.type === "boolean")
|
|
236
|
+
parseOptions.boolean.push(arg.name);
|
|
237
|
+
if (arg.default !== undefined)
|
|
238
|
+
parseOptions.default[arg.name] = arg.default;
|
|
239
|
+
if (arg.alias)
|
|
240
|
+
parseOptions.alias[arg.name] = arg.alias;
|
|
241
|
+
const camelName = camelCase(arg.name);
|
|
242
|
+
const kebabName = kebabCase(arg.name);
|
|
243
|
+
if (camelName !== arg.name || kebabName !== arg.name) {
|
|
244
|
+
const existingAliases = toArray(parseOptions.alias[arg.name] || []);
|
|
245
|
+
if (camelName !== arg.name && !existingAliases.includes(camelName))
|
|
246
|
+
existingAliases.push(camelName);
|
|
247
|
+
if (kebabName !== arg.name && !existingAliases.includes(kebabName))
|
|
248
|
+
existingAliases.push(kebabName);
|
|
249
|
+
if (existingAliases.length > 0)
|
|
250
|
+
parseOptions.alias[arg.name] = existingAliases;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const parsed = parseRawArgs(rawArgs, parseOptions);
|
|
254
|
+
const [...positionalArguments] = parsed._;
|
|
255
|
+
const parsedArgsProxy = new Proxy(parsed, { get(target, prop) {
|
|
256
|
+
return target[prop] ?? target[camelCase(prop)] ?? target[kebabCase(prop)];
|
|
257
|
+
} });
|
|
258
|
+
for (const [, arg] of args.entries())
|
|
259
|
+
if (arg.type === "positional") {
|
|
260
|
+
const nextPositionalArgument = positionalArguments.shift();
|
|
261
|
+
if (nextPositionalArgument !== undefined)
|
|
262
|
+
parsedArgsProxy[arg.name] = nextPositionalArgument;
|
|
263
|
+
else if (arg.default === undefined && arg.required !== false)
|
|
264
|
+
throw new CLIError(`Missing required positional argument: ${arg.name.toUpperCase()}`, "EARG");
|
|
265
|
+
else
|
|
266
|
+
parsedArgsProxy[arg.name] = arg.default;
|
|
267
|
+
} else if (arg.type === "enum") {
|
|
268
|
+
const argument = parsedArgsProxy[arg.name];
|
|
269
|
+
const options = arg.options || [];
|
|
270
|
+
if (argument !== undefined && options.length > 0 && !options.includes(argument))
|
|
271
|
+
throw new CLIError(`Invalid value for argument: ${cyan(`--${arg.name}`)} (${cyan(argument)}). Expected one of: ${options.map((o) => cyan(o)).join(", ")}.`, "EARG");
|
|
272
|
+
} else if (arg.required && parsedArgsProxy[arg.name] === undefined)
|
|
273
|
+
throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
|
|
274
|
+
return parsedArgsProxy;
|
|
275
|
+
}
|
|
276
|
+
function resolveArgs(argsDef) {
|
|
277
|
+
const args = [];
|
|
278
|
+
for (const [name, argDef] of Object.entries(argsDef || {}))
|
|
279
|
+
args.push({
|
|
280
|
+
...argDef,
|
|
281
|
+
name,
|
|
282
|
+
alias: toArray(argDef.alias)
|
|
283
|
+
});
|
|
284
|
+
return args;
|
|
285
|
+
}
|
|
286
|
+
async function resolvePlugins(plugins) {
|
|
287
|
+
return Promise.all(plugins.map((p) => resolveValue(p)));
|
|
288
|
+
}
|
|
289
|
+
function defineCommand(def) {
|
|
290
|
+
return def;
|
|
291
|
+
}
|
|
292
|
+
async function runCommand(cmd, opts) {
|
|
293
|
+
const cmdArgs = await resolveValue(cmd.args || {});
|
|
294
|
+
const parsedArgs = parseArgs(opts.rawArgs, cmdArgs);
|
|
295
|
+
const context = {
|
|
296
|
+
rawArgs: opts.rawArgs,
|
|
297
|
+
args: parsedArgs,
|
|
298
|
+
data: opts.data,
|
|
299
|
+
cmd
|
|
300
|
+
};
|
|
301
|
+
const plugins = await resolvePlugins(cmd.plugins ?? []);
|
|
302
|
+
let result;
|
|
303
|
+
let runError;
|
|
304
|
+
try {
|
|
305
|
+
for (const plugin of plugins)
|
|
306
|
+
await plugin.setup?.(context);
|
|
307
|
+
if (typeof cmd.setup === "function")
|
|
308
|
+
await cmd.setup(context);
|
|
309
|
+
const subCommands = await resolveValue(cmd.subCommands);
|
|
310
|
+
if (subCommands && Object.keys(subCommands).length > 0) {
|
|
311
|
+
const subCommandArgIndex = findSubCommandIndex(opts.rawArgs, cmdArgs);
|
|
312
|
+
const explicitName = opts.rawArgs[subCommandArgIndex];
|
|
313
|
+
if (explicitName) {
|
|
314
|
+
const subCommand = await _findSubCommand(subCommands, explicitName);
|
|
315
|
+
if (!subCommand)
|
|
316
|
+
throw new CLIError(`Unknown command ${cyan(explicitName)}`, "E_UNKNOWN_COMMAND");
|
|
317
|
+
await runCommand(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
|
|
318
|
+
} else {
|
|
319
|
+
const defaultSubCommand = await resolveValue(cmd.default);
|
|
320
|
+
if (defaultSubCommand) {
|
|
321
|
+
if (cmd.run)
|
|
322
|
+
throw new CLIError(`Cannot specify both 'run' and 'default' on the same command.`, "E_DEFAULT_CONFLICT");
|
|
323
|
+
const subCommand = await _findSubCommand(subCommands, defaultSubCommand);
|
|
324
|
+
if (!subCommand)
|
|
325
|
+
throw new CLIError(`Default sub command ${cyan(defaultSubCommand)} not found in subCommands.`, "E_UNKNOWN_COMMAND");
|
|
326
|
+
await runCommand(subCommand, { rawArgs: opts.rawArgs });
|
|
327
|
+
} else if (!cmd.run)
|
|
328
|
+
throw new CLIError(`No command specified.`, "E_NO_COMMAND");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (typeof cmd.run === "function")
|
|
332
|
+
result = await cmd.run(context);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
runError = error;
|
|
335
|
+
}
|
|
336
|
+
const cleanupErrors = [];
|
|
337
|
+
if (typeof cmd.cleanup === "function")
|
|
338
|
+
try {
|
|
339
|
+
await cmd.cleanup(context);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
cleanupErrors.push(error);
|
|
342
|
+
}
|
|
343
|
+
for (const plugin of [...plugins].reverse())
|
|
344
|
+
try {
|
|
345
|
+
await plugin.cleanup?.(context);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
cleanupErrors.push(error);
|
|
348
|
+
}
|
|
349
|
+
if (runError)
|
|
350
|
+
throw runError;
|
|
351
|
+
if (cleanupErrors.length === 1)
|
|
352
|
+
throw cleanupErrors[0];
|
|
353
|
+
if (cleanupErrors.length > 1)
|
|
354
|
+
throw new Error("Multiple cleanup errors", { cause: cleanupErrors });
|
|
355
|
+
return { result };
|
|
356
|
+
}
|
|
357
|
+
async function _findSubCommand(subCommands, name) {
|
|
358
|
+
if (name in subCommands)
|
|
359
|
+
return resolveValue(subCommands[name]);
|
|
360
|
+
for (const sub of Object.values(subCommands)) {
|
|
361
|
+
const resolved = await resolveValue(sub);
|
|
362
|
+
const meta = await resolveValue(resolved?.meta);
|
|
363
|
+
if (meta?.alias) {
|
|
364
|
+
if (toArray(meta.alias).includes(name))
|
|
365
|
+
return resolved;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function findSubCommandIndex(rawArgs, argsDef) {
|
|
370
|
+
for (let i = 0;i < rawArgs.length; i++) {
|
|
371
|
+
const arg = rawArgs[i];
|
|
372
|
+
if (arg === "--")
|
|
373
|
+
return -1;
|
|
374
|
+
if (arg.startsWith("-")) {
|
|
375
|
+
if (!arg.includes("=") && _isValueFlag(arg, argsDef))
|
|
376
|
+
i++;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
return i;
|
|
380
|
+
}
|
|
381
|
+
return -1;
|
|
382
|
+
}
|
|
383
|
+
function _isValueFlag(flag, argsDef) {
|
|
384
|
+
const name = flag.replace(/^-{1,2}/, "");
|
|
385
|
+
const normalized = camelCase(name);
|
|
386
|
+
for (const [key, def] of Object.entries(argsDef)) {
|
|
387
|
+
if (def.type !== "string" && def.type !== "enum")
|
|
388
|
+
continue;
|
|
389
|
+
if (normalized === camelCase(key))
|
|
390
|
+
return true;
|
|
391
|
+
if ((Array.isArray(def.alias) ? def.alias : def.alias ? [def.alias] : []).includes(name))
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
var negativePrefixRe = /^no[-A-Z]/;
|
|
397
|
+
async function renderUsage(cmd, parent) {
|
|
398
|
+
const cmdMeta = await resolveValue(cmd.meta || {});
|
|
399
|
+
const cmdArgs = resolveArgs(await resolveValue(cmd.args || {}));
|
|
400
|
+
const parentMeta = await resolveValue(parent?.meta || {});
|
|
401
|
+
const commandName = `${parentMeta.name ? `${parentMeta.name} ` : ""}` + (cmdMeta.name || process.argv[1]);
|
|
402
|
+
const argLines = [];
|
|
403
|
+
const posLines = [];
|
|
404
|
+
const commandsLines = [];
|
|
405
|
+
const usageLine = [];
|
|
406
|
+
for (const arg of cmdArgs)
|
|
407
|
+
if (arg.type === "positional") {
|
|
408
|
+
const name = arg.name.toUpperCase();
|
|
409
|
+
const isRequired = arg.required !== false && arg.default === undefined;
|
|
410
|
+
posLines.push([cyan(name + renderValueHint(arg)), renderDescription(arg, isRequired)]);
|
|
411
|
+
usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
|
|
412
|
+
} else {
|
|
413
|
+
const isRequired = arg.required === true && arg.default === undefined;
|
|
414
|
+
const argStr = [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(", ") + renderValueHint(arg);
|
|
415
|
+
argLines.push([cyan(argStr), renderDescription(arg, isRequired)]);
|
|
416
|
+
if (arg.type === "boolean" && (arg.default === true || arg.negativeDescription) && !negativePrefixRe.test(arg.name)) {
|
|
417
|
+
const negativeArgStr = [...(arg.alias || []).map((a) => `--no-${a}`), `--no-${arg.name}`].join(", ");
|
|
418
|
+
argLines.push([cyan(negativeArgStr), [arg.negativeDescription, isRequired ? gray("(Required)") : ""].filter(Boolean).join(" ")]);
|
|
419
|
+
}
|
|
420
|
+
if (isRequired)
|
|
421
|
+
usageLine.push(`--${arg.name}` + renderValueHint(arg));
|
|
422
|
+
}
|
|
423
|
+
if (cmd.subCommands) {
|
|
424
|
+
const commandNames = [];
|
|
425
|
+
const subCommands = await resolveValue(cmd.subCommands);
|
|
426
|
+
for (const [name, sub] of Object.entries(subCommands)) {
|
|
427
|
+
const meta = await resolveValue((await resolveValue(sub))?.meta);
|
|
428
|
+
if (meta?.hidden)
|
|
429
|
+
continue;
|
|
430
|
+
const aliases = toArray(meta?.alias);
|
|
431
|
+
const label = [name, ...aliases].join(", ");
|
|
432
|
+
commandsLines.push([cyan(label), meta?.description || ""]);
|
|
433
|
+
commandNames.push(name, ...aliases);
|
|
434
|
+
}
|
|
435
|
+
usageLine.push(commandNames.join("|"));
|
|
436
|
+
}
|
|
437
|
+
const usageLines = [];
|
|
438
|
+
const version = cmdMeta.version || parentMeta.version;
|
|
439
|
+
usageLines.push(gray(`${cmdMeta.description} (${commandName + (version ? ` v${version}` : "")})`), "");
|
|
440
|
+
const hasOptions = argLines.length > 0 || posLines.length > 0;
|
|
441
|
+
usageLines.push(`${underline(bold("USAGE"))} ${cyan(`${commandName}${hasOptions ? " [OPTIONS]" : ""} ${usageLine.join(" ")}`)}`, "");
|
|
442
|
+
if (posLines.length > 0) {
|
|
443
|
+
usageLines.push(underline(bold("ARGUMENTS")), "");
|
|
444
|
+
usageLines.push(formatLineColumns(posLines, " "));
|
|
445
|
+
usageLines.push("");
|
|
446
|
+
}
|
|
447
|
+
if (argLines.length > 0) {
|
|
448
|
+
usageLines.push(underline(bold("OPTIONS")), "");
|
|
449
|
+
usageLines.push(formatLineColumns(argLines, " "));
|
|
450
|
+
usageLines.push("");
|
|
451
|
+
}
|
|
452
|
+
if (commandsLines.length > 0) {
|
|
453
|
+
usageLines.push(underline(bold("COMMANDS")), "");
|
|
454
|
+
usageLines.push(formatLineColumns(commandsLines, " "));
|
|
455
|
+
usageLines.push("", `Use ${cyan(`${commandName} <command> --help`)} for more information about a command.`);
|
|
456
|
+
}
|
|
457
|
+
return usageLines.filter((l) => typeof l === "string").join(`
|
|
458
|
+
`);
|
|
459
|
+
}
|
|
460
|
+
function renderValueHint(arg) {
|
|
461
|
+
const valueHint = arg.valueHint ? `=<${arg.valueHint}>` : "";
|
|
462
|
+
const fallbackValueHint = valueHint || `=<${snakeCase(arg.name)}>`;
|
|
463
|
+
if (!arg.type || arg.type === "positional" || arg.type === "boolean")
|
|
464
|
+
return valueHint;
|
|
465
|
+
if (arg.type === "enum" && arg.options?.length)
|
|
466
|
+
return `=<${arg.options.join("|")}>`;
|
|
467
|
+
return fallbackValueHint;
|
|
468
|
+
}
|
|
469
|
+
function renderDescription(arg, required) {
|
|
470
|
+
const requiredHint = required ? gray("(Required)") : "";
|
|
471
|
+
const defaultHint = arg.default === undefined ? "" : gray(`(Default: ${arg.default})`);
|
|
472
|
+
return [
|
|
473
|
+
arg.description,
|
|
474
|
+
requiredHint,
|
|
475
|
+
defaultHint
|
|
476
|
+
].filter(Boolean).join(" ");
|
|
477
|
+
}
|
|
478
|
+
// package.json
|
|
479
|
+
var package_default = {
|
|
480
|
+
name: "tmdb-kit",
|
|
481
|
+
type: "module",
|
|
482
|
+
version: "1.2.0",
|
|
483
|
+
description: "A tiny, typed, zero-runtime-dependency TMDB SDK and CLI for Node.js, Bun, Deno, and edge runtimes.",
|
|
484
|
+
author: "isaced",
|
|
485
|
+
license: "MIT",
|
|
486
|
+
homepage: "https://github.com/isaced/tmdb-kit#readme",
|
|
487
|
+
keywords: [
|
|
488
|
+
"tmdb",
|
|
489
|
+
"themoviedb",
|
|
490
|
+
"sdk",
|
|
491
|
+
"typescript",
|
|
492
|
+
"node",
|
|
493
|
+
"bun",
|
|
494
|
+
"cli",
|
|
495
|
+
"deno",
|
|
496
|
+
"edge",
|
|
497
|
+
"esm",
|
|
498
|
+
"cjs"
|
|
499
|
+
],
|
|
500
|
+
repository: {
|
|
501
|
+
type: "git",
|
|
502
|
+
url: "git+https://github.com/isaced/tmdb-kit.git"
|
|
503
|
+
},
|
|
504
|
+
bugs: {
|
|
505
|
+
url: "https://github.com/isaced/tmdb-kit/issues"
|
|
506
|
+
},
|
|
507
|
+
sideEffects: false,
|
|
508
|
+
engines: {
|
|
509
|
+
node: ">=18"
|
|
510
|
+
},
|
|
511
|
+
packageManager: "bun@1.4.4",
|
|
512
|
+
exports: {
|
|
513
|
+
".": {
|
|
514
|
+
import: {
|
|
515
|
+
types: "./dist/index.d.ts",
|
|
516
|
+
default: "./dist/index.js"
|
|
517
|
+
},
|
|
518
|
+
require: {
|
|
519
|
+
types: "./dist/index.d.cts",
|
|
520
|
+
default: "./dist/index.cjs"
|
|
521
|
+
},
|
|
522
|
+
default: "./dist/index.js"
|
|
523
|
+
},
|
|
524
|
+
"./package.json": "./package.json"
|
|
525
|
+
},
|
|
526
|
+
main: "./dist/index.cjs",
|
|
527
|
+
module: "./dist/index.js",
|
|
528
|
+
types: "./dist/index.d.ts",
|
|
529
|
+
bin: {
|
|
530
|
+
tmdb: "./dist/cli.js",
|
|
531
|
+
"tmdb-kit": "./dist/cli.js"
|
|
532
|
+
},
|
|
533
|
+
files: [
|
|
534
|
+
"dist",
|
|
535
|
+
"README.md"
|
|
536
|
+
],
|
|
537
|
+
publishConfig: {
|
|
538
|
+
access: "public",
|
|
539
|
+
provenance: true
|
|
540
|
+
},
|
|
541
|
+
scripts: {
|
|
542
|
+
build: "bun run scripts/build.ts",
|
|
543
|
+
"build:cli": "bun run scripts/build.ts --cli",
|
|
544
|
+
cli: "bun run build:cli && bun ./dist/cli.js",
|
|
545
|
+
dev: "bun build ./src/index.ts --outdir ./dist --entry-naming '[name].js' --format esm --target browser --sourcemap=linked --watch",
|
|
546
|
+
test: "bun test --isolate tests/unit tests/resources tests/cli",
|
|
547
|
+
"test:resources": "bun test --isolate tests/resources",
|
|
548
|
+
"test:unit": "bun test --isolate tests/unit",
|
|
549
|
+
"test:watch": "bun test --isolate --watch tests/unit tests/resources tests/cli",
|
|
550
|
+
"test:coverage": "bun test --isolate --coverage tests/unit tests/resources tests/cli",
|
|
551
|
+
"test:integration": "bun test --isolate tests/integration",
|
|
552
|
+
typecheck: "bun check",
|
|
553
|
+
format: "bun fmt",
|
|
554
|
+
"format:check": "bun fmt --check",
|
|
555
|
+
ci: "bun run format:check && bun run typecheck && bun run test:coverage && bun run build",
|
|
556
|
+
release: "bumpp",
|
|
557
|
+
prepublishOnly: "bun run ci"
|
|
558
|
+
},
|
|
559
|
+
devDependencies: {
|
|
560
|
+
"@types/bun": "^1.4.4",
|
|
561
|
+
"@types/node": "^25.6.2",
|
|
562
|
+
bumpp: "^11.1.0",
|
|
563
|
+
citty: "^0.2.2",
|
|
564
|
+
typescript: "^6.0.3"
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// src/cli/commands/auth.ts
|
|
569
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
570
|
+
|
|
571
|
+
// src/errors.ts
|
|
572
|
+
class TMDBError extends Error {
|
|
573
|
+
constructor(message, options) {
|
|
574
|
+
super(message, options);
|
|
575
|
+
this.name = "TMDBError";
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
class TMDBRequestError extends TMDBError {
|
|
580
|
+
constructor(message, options) {
|
|
581
|
+
super(message, options);
|
|
582
|
+
this.name = "TMDBRequestError";
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
class TMDBTimeoutError extends TMDBError {
|
|
587
|
+
timeoutMs;
|
|
588
|
+
constructor(timeoutMs, message = `TMDB request timed out after ${timeoutMs}ms`) {
|
|
589
|
+
super(message);
|
|
590
|
+
this.name = "TMDBTimeoutError";
|
|
591
|
+
this.timeoutMs = timeoutMs;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
class TMDBResponseError extends TMDBError {
|
|
596
|
+
body;
|
|
597
|
+
headers;
|
|
598
|
+
requestId;
|
|
599
|
+
status;
|
|
600
|
+
statusCode;
|
|
601
|
+
statusMessage;
|
|
602
|
+
statusText;
|
|
603
|
+
url;
|
|
604
|
+
constructor(message, options) {
|
|
605
|
+
super(message);
|
|
606
|
+
this.name = "TMDBResponseError";
|
|
607
|
+
this.body = options.body;
|
|
608
|
+
this.headers = options.headers;
|
|
609
|
+
this.requestId = options.headers.get("x-request-id");
|
|
610
|
+
this.status = options.status;
|
|
611
|
+
this.statusText = options.statusText;
|
|
612
|
+
this.url = options.url;
|
|
613
|
+
const body = isErrorResponseBody(options.body) ? options.body : undefined;
|
|
614
|
+
this.statusCode = body?.status_code;
|
|
615
|
+
this.statusMessage = body?.status_message;
|
|
616
|
+
}
|
|
617
|
+
get isRateLimit() {
|
|
618
|
+
return this.status === 429;
|
|
619
|
+
}
|
|
620
|
+
get isNotFound() {
|
|
621
|
+
return this.status === 404;
|
|
622
|
+
}
|
|
623
|
+
get isUnauthorized() {
|
|
624
|
+
return this.status === 401;
|
|
625
|
+
}
|
|
626
|
+
get isForbidden() {
|
|
627
|
+
return this.status === 403;
|
|
628
|
+
}
|
|
629
|
+
get isServerError() {
|
|
630
|
+
return this.status >= 500 && this.status < 600;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
class TMDBRateLimitError extends TMDBResponseError {
|
|
635
|
+
retryAfter;
|
|
636
|
+
constructor(message, options) {
|
|
637
|
+
super(message, options);
|
|
638
|
+
this.name = "TMDBRateLimitError";
|
|
639
|
+
this.retryAfter = options.retryAfter;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function createResponseError(options) {
|
|
643
|
+
const body = isErrorResponseBody(options.body) ? options.body : undefined;
|
|
644
|
+
const message = body?.status_message ?? `${options.status} ${options.statusText}`.trim();
|
|
645
|
+
if (options.status === 429) {
|
|
646
|
+
return new TMDBRateLimitError(message, {
|
|
647
|
+
...options,
|
|
648
|
+
retryAfter: parseRetryAfter(options.headers.get("retry-after"))
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return new TMDBResponseError(message, options);
|
|
652
|
+
}
|
|
653
|
+
function parseRetryAfter(value) {
|
|
654
|
+
if (value === null || value === undefined) {
|
|
655
|
+
return 1;
|
|
656
|
+
}
|
|
657
|
+
const trimmed = value.trim();
|
|
658
|
+
if (trimmed === "") {
|
|
659
|
+
return 1;
|
|
660
|
+
}
|
|
661
|
+
const seconds = Number(trimmed);
|
|
662
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
663
|
+
return seconds;
|
|
664
|
+
}
|
|
665
|
+
const date = Date.parse(trimmed);
|
|
666
|
+
if (Number.isFinite(date)) {
|
|
667
|
+
const deltaMs = date - Date.now();
|
|
668
|
+
return Math.max(1, Math.ceil(deltaMs / 1000));
|
|
669
|
+
}
|
|
670
|
+
return 1;
|
|
671
|
+
}
|
|
672
|
+
function isErrorResponseBody(value) {
|
|
673
|
+
return typeof value === "object" && value !== null;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/query.ts
|
|
677
|
+
var DEFAULT_API_BASE_URL = "https://api.themoviedb.org/3";
|
|
678
|
+
var DEFAULT_IMAGE_BASE_URL = "https://image.tmdb.org/t/p";
|
|
679
|
+
function appendQuery(url, query) {
|
|
680
|
+
for (const [key, value] of Object.entries(query)) {
|
|
681
|
+
if (value === undefined || value === null) {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (Array.isArray(value)) {
|
|
685
|
+
url.searchParams.set(key, value.map(String).join(","));
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
url.searchParams.set(key, String(value));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function withLanguage(defaults, query) {
|
|
692
|
+
const language = typeof query.language === "string" ? query.language : defaults.language;
|
|
693
|
+
return {
|
|
694
|
+
...query,
|
|
695
|
+
language
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function withListDefaults(defaults, query) {
|
|
699
|
+
const region = typeof query.region === "string" ? query.region : defaults.region;
|
|
700
|
+
return {
|
|
701
|
+
...withLanguage(defaults, query),
|
|
702
|
+
region
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function toCommaList(value) {
|
|
706
|
+
if (value === undefined) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
return typeof value === "string" ? value : value.join(",");
|
|
710
|
+
}
|
|
711
|
+
function toId(value, name) {
|
|
712
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
713
|
+
throw new TMDBRequestError(`${name} must be a positive integer`);
|
|
714
|
+
}
|
|
715
|
+
return String(value);
|
|
716
|
+
}
|
|
717
|
+
function toSeasonNumber(value) {
|
|
718
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
719
|
+
throw new TMDBRequestError("seasonNumber must be a non-negative integer");
|
|
720
|
+
}
|
|
721
|
+
return String(value);
|
|
722
|
+
}
|
|
723
|
+
function toGroupId(value) {
|
|
724
|
+
const groupId = value.trim();
|
|
725
|
+
if (!/^[A-Za-z0-9_-]+$/.test(groupId)) {
|
|
726
|
+
throw new TMDBRequestError("groupId must be a non-empty identifier");
|
|
727
|
+
}
|
|
728
|
+
return groupId;
|
|
729
|
+
}
|
|
730
|
+
function toSearchQuery(value) {
|
|
731
|
+
const query = value.trim();
|
|
732
|
+
if (query.length === 0) {
|
|
733
|
+
throw new TMDBRequestError("search query must not be empty");
|
|
734
|
+
}
|
|
735
|
+
return query;
|
|
736
|
+
}
|
|
737
|
+
function toTimeWindow(value) {
|
|
738
|
+
if (value !== "day" && value !== "week") {
|
|
739
|
+
throw new TMDBRequestError('timeWindow must be either "day" or "week"');
|
|
740
|
+
}
|
|
741
|
+
return value;
|
|
742
|
+
}
|
|
743
|
+
function buildImageUrl(path, size = "original", options = {}) {
|
|
744
|
+
if (path === undefined || path === null || path.trim() === "") {
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_IMAGE_BASE_URL);
|
|
748
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
749
|
+
const url = `${baseUrl}/${size}${normalizedPath}`;
|
|
750
|
+
return options.transform ? options.transform(url) : url;
|
|
751
|
+
}
|
|
752
|
+
function normalizeBaseUrl(baseUrl) {
|
|
753
|
+
return baseUrl.replace(/\/+$/, "");
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/http.ts
|
|
757
|
+
class TMDBHttpClient {
|
|
758
|
+
defaults;
|
|
759
|
+
imageTransform;
|
|
760
|
+
#apiKey;
|
|
761
|
+
#accessToken;
|
|
762
|
+
#baseUrl;
|
|
763
|
+
#fetch;
|
|
764
|
+
#headers;
|
|
765
|
+
constructor(options) {
|
|
766
|
+
const accessToken = normalizeSecret("accessToken" in options ? options.accessToken : undefined);
|
|
767
|
+
const apiKey = normalizeSecret("apiKey" in options ? options.apiKey : undefined);
|
|
768
|
+
if (Boolean(accessToken) === Boolean(apiKey)) {
|
|
769
|
+
throw new TMDBRequestError("Provide exactly one of accessToken or apiKey");
|
|
770
|
+
}
|
|
771
|
+
this.#accessToken = accessToken;
|
|
772
|
+
this.#apiKey = apiKey;
|
|
773
|
+
this.#baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_API_BASE_URL);
|
|
774
|
+
this.#fetch = options.fetch;
|
|
775
|
+
this.#headers = options.headers;
|
|
776
|
+
this.defaults = {
|
|
777
|
+
imageBaseUrl: normalizeBaseUrl(options.imageBaseUrl ?? DEFAULT_IMAGE_BASE_URL),
|
|
778
|
+
language: options.defaultLanguage,
|
|
779
|
+
region: options.defaultRegion
|
|
780
|
+
};
|
|
781
|
+
this.imageTransform = options.images?.transform;
|
|
782
|
+
}
|
|
783
|
+
async get(path, options = {}) {
|
|
784
|
+
const url = this.#createUrl(path, options.query ?? {});
|
|
785
|
+
const headers = this.#createHeaders(options.headers);
|
|
786
|
+
const fetchImpl = this.#resolveFetch();
|
|
787
|
+
let response;
|
|
788
|
+
try {
|
|
789
|
+
response = await fetchImpl(url, {
|
|
790
|
+
headers,
|
|
791
|
+
method: "GET",
|
|
792
|
+
signal: options.signal
|
|
793
|
+
});
|
|
794
|
+
} catch (error) {
|
|
795
|
+
throw new TMDBRequestError(`TMDB request failed: ${String(error)}`, { cause: error });
|
|
796
|
+
}
|
|
797
|
+
const body = await readJson(response);
|
|
798
|
+
if (!response.ok) {
|
|
799
|
+
throw createResponseError({
|
|
800
|
+
body,
|
|
801
|
+
headers: response.headers,
|
|
802
|
+
status: response.status,
|
|
803
|
+
statusText: response.statusText,
|
|
804
|
+
url
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return body;
|
|
808
|
+
}
|
|
809
|
+
#resolveFetch() {
|
|
810
|
+
if (this.#fetch !== undefined) {
|
|
811
|
+
return this.#fetch;
|
|
812
|
+
}
|
|
813
|
+
const globalFetch = globalThis.fetch;
|
|
814
|
+
if (typeof globalFetch !== "function") {
|
|
815
|
+
throw new TMDBRequestError("No fetch implementation found. Pass options.fetch in this runtime.");
|
|
816
|
+
}
|
|
817
|
+
return globalFetch.bind(globalThis);
|
|
818
|
+
}
|
|
819
|
+
#createHeaders(headersInit) {
|
|
820
|
+
const headers = new Headers(this.#headers);
|
|
821
|
+
const requestHeaders = new Headers(headersInit);
|
|
822
|
+
headers.set("Accept", "application/json");
|
|
823
|
+
for (const [key, value] of requestHeaders) {
|
|
824
|
+
headers.set(key, value);
|
|
825
|
+
}
|
|
826
|
+
if (this.#accessToken !== undefined) {
|
|
827
|
+
headers.set("Authorization", `Bearer ${this.#accessToken}`);
|
|
828
|
+
}
|
|
829
|
+
return headers;
|
|
830
|
+
}
|
|
831
|
+
#createUrl(path, query) {
|
|
832
|
+
const relativePath = path.replace(/^\/+/, "");
|
|
833
|
+
const url = new URL(relativePath, `${this.#baseUrl}/`);
|
|
834
|
+
appendQuery(url, {
|
|
835
|
+
...query,
|
|
836
|
+
api_key: this.#apiKey
|
|
837
|
+
});
|
|
838
|
+
return url.toString();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async function readJson(response) {
|
|
842
|
+
const text = await response.text();
|
|
843
|
+
if (text.length === 0) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
return JSON.parse(text);
|
|
848
|
+
} catch {
|
|
849
|
+
return text;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function normalizeSecret(value) {
|
|
853
|
+
const secret = value?.trim();
|
|
854
|
+
return secret === "" ? undefined : secret;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/resources/configuration.ts
|
|
858
|
+
class ConfigurationResource {
|
|
859
|
+
#transport;
|
|
860
|
+
constructor(transport) {
|
|
861
|
+
this.#transport = transport;
|
|
862
|
+
}
|
|
863
|
+
details(options = {}) {
|
|
864
|
+
return this.#transport.get("/configuration", { signal: options.signal });
|
|
865
|
+
}
|
|
866
|
+
countries(options = {}) {
|
|
867
|
+
return this.#transport.get("/configuration/countries", { signal: options.signal });
|
|
868
|
+
}
|
|
869
|
+
jobs(options = {}) {
|
|
870
|
+
return this.#transport.get("/configuration/jobs", { signal: options.signal });
|
|
871
|
+
}
|
|
872
|
+
languages(options = {}) {
|
|
873
|
+
return this.#transport.get("/configuration/languages", { signal: options.signal });
|
|
874
|
+
}
|
|
875
|
+
primaryTranslations(options = {}) {
|
|
876
|
+
return this.#transport.get("/configuration/primary_translations", {
|
|
877
|
+
signal: options.signal
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
timezones(options = {}) {
|
|
881
|
+
return this.#transport.get("/configuration/timezones", { signal: options.signal });
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/resources/genres.ts
|
|
886
|
+
class GenresResource {
|
|
887
|
+
#transport;
|
|
888
|
+
constructor(transport) {
|
|
889
|
+
this.#transport = transport;
|
|
890
|
+
}
|
|
891
|
+
movies(options = {}) {
|
|
892
|
+
return this.#transport.get("/genre/movie/list", {
|
|
893
|
+
query: withLanguage(this.#transport.defaults, {
|
|
894
|
+
language: options.language
|
|
895
|
+
}),
|
|
896
|
+
signal: options.signal
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
tv(options = {}) {
|
|
900
|
+
return this.#transport.get("/genre/tv/list", {
|
|
901
|
+
query: withLanguage(this.#transport.defaults, {
|
|
902
|
+
language: options.language
|
|
903
|
+
}),
|
|
904
|
+
signal: options.signal
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/resources/images.ts
|
|
910
|
+
class ImagesHelper {
|
|
911
|
+
#transport;
|
|
912
|
+
#transform;
|
|
913
|
+
constructor(transport, transform) {
|
|
914
|
+
this.#transport = transport;
|
|
915
|
+
this.#transform = transform;
|
|
916
|
+
}
|
|
917
|
+
url(path, size = "original", options = {}) {
|
|
918
|
+
return buildImageUrl(path, size, {
|
|
919
|
+
baseUrl: this.#transport.defaults.imageBaseUrl,
|
|
920
|
+
transform: options.transform ?? this.#transform
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/resources/movies.ts
|
|
926
|
+
class MoviesResource {
|
|
927
|
+
#transport;
|
|
928
|
+
constructor(transport) {
|
|
929
|
+
this.#transport = transport;
|
|
930
|
+
}
|
|
931
|
+
popular(options = {}) {
|
|
932
|
+
return this.#transport.get("/movie/popular", {
|
|
933
|
+
query: movieListQuery(this.#transport, options),
|
|
934
|
+
signal: options.signal
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
topRated(options = {}) {
|
|
938
|
+
return this.#transport.get("/movie/top_rated", {
|
|
939
|
+
query: movieListQuery(this.#transport, options),
|
|
940
|
+
signal: options.signal
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
nowPlaying(options = {}) {
|
|
944
|
+
return this.#transport.get("/movie/now_playing", {
|
|
945
|
+
query: movieListQuery(this.#transport, options),
|
|
946
|
+
signal: options.signal
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
upcoming(options = {}) {
|
|
950
|
+
return this.#transport.get("/movie/upcoming", {
|
|
951
|
+
query: movieListQuery(this.#transport, options),
|
|
952
|
+
signal: options.signal
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
details(movieId, options = {}) {
|
|
956
|
+
return this.#transport.get(`/movie/${toId(movieId, "movieId")}`, {
|
|
957
|
+
query: withLanguage(this.#transport.defaults, {
|
|
958
|
+
append_to_response: toCommaList(options.appendToResponse),
|
|
959
|
+
language: options.language
|
|
960
|
+
}),
|
|
961
|
+
signal: options.signal
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
credits(movieId, options = {}) {
|
|
965
|
+
return this.#transport.get(`/movie/${toId(movieId, "movieId")}/credits`, {
|
|
966
|
+
query: withLanguage(this.#transport.defaults, {
|
|
967
|
+
language: options.language
|
|
968
|
+
}),
|
|
969
|
+
signal: options.signal
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
images(movieId, options = {}) {
|
|
973
|
+
return this.#transport.get(`/movie/${toId(movieId, "movieId")}/images`, {
|
|
974
|
+
query: withLanguage(this.#transport.defaults, {
|
|
975
|
+
include_image_language: toCommaList(options.includeImageLanguage),
|
|
976
|
+
language: options.language
|
|
977
|
+
}),
|
|
978
|
+
signal: options.signal
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
recommendations(movieId, options = {}) {
|
|
982
|
+
return this.#transport.get(`/movie/${toId(movieId, "movieId")}/recommendations`, {
|
|
983
|
+
query: movieListQuery(this.#transport, options),
|
|
984
|
+
signal: options.signal
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
similar(movieId, options = {}) {
|
|
988
|
+
return this.#transport.get(`/movie/${toId(movieId, "movieId")}/similar`, {
|
|
989
|
+
query: movieListQuery(this.#transport, options),
|
|
990
|
+
signal: options.signal
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
videos(movieId, options = {}) {
|
|
994
|
+
return this.#transport.get(`/movie/${toId(movieId, "movieId")}/videos`, {
|
|
995
|
+
query: withLanguage(this.#transport.defaults, {
|
|
996
|
+
language: options.language
|
|
997
|
+
}),
|
|
998
|
+
signal: options.signal
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
function movieListQuery(transport, options) {
|
|
1003
|
+
return withListDefaults(transport.defaults, {
|
|
1004
|
+
language: options.language,
|
|
1005
|
+
page: options.page,
|
|
1006
|
+
region: options.region
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// src/resources/people.ts
|
|
1011
|
+
class PeopleResource {
|
|
1012
|
+
#transport;
|
|
1013
|
+
constructor(transport) {
|
|
1014
|
+
this.#transport = transport;
|
|
1015
|
+
}
|
|
1016
|
+
popular(options = {}) {
|
|
1017
|
+
return this.#transport.get("/person/popular", {
|
|
1018
|
+
query: peopleListQuery(this.#transport, options),
|
|
1019
|
+
signal: options.signal
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
details(personId, options = {}) {
|
|
1023
|
+
return this.#transport.get(`/person/${toId(personId, "personId")}`, {
|
|
1024
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1025
|
+
language: options.language
|
|
1026
|
+
}),
|
|
1027
|
+
signal: options.signal
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
combinedCredits(personId, options = {}) {
|
|
1031
|
+
return this.#transport.get(`/person/${toId(personId, "personId")}/combined_credits`, {
|
|
1032
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1033
|
+
language: options.language
|
|
1034
|
+
}),
|
|
1035
|
+
signal: options.signal
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function peopleListQuery(transport, options) {
|
|
1040
|
+
return withLanguage(transport.defaults, {
|
|
1041
|
+
language: options.language,
|
|
1042
|
+
page: options.page
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/resources/search.ts
|
|
1047
|
+
class SearchResource {
|
|
1048
|
+
#transport;
|
|
1049
|
+
constructor(transport) {
|
|
1050
|
+
this.#transport = transport;
|
|
1051
|
+
}
|
|
1052
|
+
movies(query, options = {}) {
|
|
1053
|
+
return this.#transport.get("/search/movie", {
|
|
1054
|
+
query: movieSearchQuery(this.#transport, query, options),
|
|
1055
|
+
signal: options.signal
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
tv(query, options = {}) {
|
|
1059
|
+
return this.#transport.get("/search/tv", {
|
|
1060
|
+
query: tvSearchQuery(this.#transport, query, options),
|
|
1061
|
+
signal: options.signal
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
people(query, options = {}) {
|
|
1065
|
+
return this.#transport.get("/search/person", {
|
|
1066
|
+
query: commonSearchQuery(this.#transport, query, options),
|
|
1067
|
+
signal: options.signal
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
async multi(query, options = {}) {
|
|
1071
|
+
const response = await this.#transport.get("/search/multi", {
|
|
1072
|
+
query: commonSearchQuery(this.#transport, query, options),
|
|
1073
|
+
signal: options.signal
|
|
1074
|
+
});
|
|
1075
|
+
const excluded = options.exclude;
|
|
1076
|
+
if (excluded === undefined || excluded.length === 0) {
|
|
1077
|
+
return response;
|
|
1078
|
+
}
|
|
1079
|
+
const excludedSet = new Set(excluded);
|
|
1080
|
+
return {
|
|
1081
|
+
...response,
|
|
1082
|
+
results: response.results.filter((result) => !excludedSet.has(result.media_type))
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
function movieSearchQuery(transport, query, options) {
|
|
1087
|
+
return withListDefaults(transport.defaults, {
|
|
1088
|
+
include_adult: options.includeAdult,
|
|
1089
|
+
language: options.language,
|
|
1090
|
+
page: options.page,
|
|
1091
|
+
primary_release_year: options.primaryReleaseYear,
|
|
1092
|
+
query: toSearchQuery(query),
|
|
1093
|
+
region: options.region,
|
|
1094
|
+
year: options.year
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
function tvSearchQuery(transport, query, options) {
|
|
1098
|
+
return withLanguage(transport.defaults, {
|
|
1099
|
+
first_air_date_year: options.firstAirDateYear,
|
|
1100
|
+
include_adult: options.includeAdult,
|
|
1101
|
+
language: options.language,
|
|
1102
|
+
page: options.page,
|
|
1103
|
+
query: toSearchQuery(query),
|
|
1104
|
+
year: options.year
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
function commonSearchQuery(transport, query, options) {
|
|
1108
|
+
return withLanguage(transport.defaults, {
|
|
1109
|
+
include_adult: options.includeAdult,
|
|
1110
|
+
language: options.language,
|
|
1111
|
+
page: options.page,
|
|
1112
|
+
query: toSearchQuery(query)
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/resources/trending.ts
|
|
1117
|
+
class TrendingResource {
|
|
1118
|
+
#transport;
|
|
1119
|
+
constructor(transport) {
|
|
1120
|
+
this.#transport = transport;
|
|
1121
|
+
}
|
|
1122
|
+
all(timeWindow = "day", options = {}) {
|
|
1123
|
+
return this.#transport.get(`/trending/all/${toTimeWindow(timeWindow)}`, {
|
|
1124
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1125
|
+
language: options.language,
|
|
1126
|
+
page: options.page
|
|
1127
|
+
}),
|
|
1128
|
+
signal: options.signal
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
movies(timeWindow = "day", options = {}) {
|
|
1132
|
+
return this.#transport.get(`/trending/movie/${toTimeWindow(timeWindow)}`, {
|
|
1133
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1134
|
+
language: options.language,
|
|
1135
|
+
page: options.page
|
|
1136
|
+
}),
|
|
1137
|
+
signal: options.signal
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
tv(timeWindow = "day", options = {}) {
|
|
1141
|
+
return this.#transport.get(`/trending/tv/${toTimeWindow(timeWindow)}`, {
|
|
1142
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1143
|
+
language: options.language,
|
|
1144
|
+
page: options.page
|
|
1145
|
+
}),
|
|
1146
|
+
signal: options.signal
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
people(timeWindow = "day", options = {}) {
|
|
1150
|
+
return this.#transport.get(`/trending/person/${toTimeWindow(timeWindow)}`, {
|
|
1151
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1152
|
+
language: options.language,
|
|
1153
|
+
page: options.page
|
|
1154
|
+
}),
|
|
1155
|
+
signal: options.signal
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// src/resources/tv.ts
|
|
1161
|
+
class TVResource {
|
|
1162
|
+
#transport;
|
|
1163
|
+
constructor(transport) {
|
|
1164
|
+
this.#transport = transport;
|
|
1165
|
+
}
|
|
1166
|
+
popular(options = {}) {
|
|
1167
|
+
return this.#transport.get("/tv/popular", {
|
|
1168
|
+
query: tvListQuery(this.#transport, options),
|
|
1169
|
+
signal: options.signal
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
topRated(options = {}) {
|
|
1173
|
+
return this.#transport.get("/tv/top_rated", {
|
|
1174
|
+
query: tvListQuery(this.#transport, options),
|
|
1175
|
+
signal: options.signal
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
airingToday(options = {}) {
|
|
1179
|
+
return this.#transport.get("/tv/airing_today", {
|
|
1180
|
+
query: tvListQuery(this.#transport, options),
|
|
1181
|
+
signal: options.signal
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
onTheAir(options = {}) {
|
|
1185
|
+
return this.#transport.get("/tv/on_the_air", {
|
|
1186
|
+
query: tvListQuery(this.#transport, options),
|
|
1187
|
+
signal: options.signal
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
details(seriesId, options = {}) {
|
|
1191
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}`, {
|
|
1192
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1193
|
+
append_to_response: toCommaList(options.appendToResponse),
|
|
1194
|
+
language: options.language
|
|
1195
|
+
}),
|
|
1196
|
+
signal: options.signal
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
credits(seriesId, options = {}) {
|
|
1200
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/credits`, {
|
|
1201
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1202
|
+
language: options.language
|
|
1203
|
+
}),
|
|
1204
|
+
signal: options.signal
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
images(seriesId, options = {}) {
|
|
1208
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/images`, {
|
|
1209
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1210
|
+
include_image_language: toCommaList(options.includeImageLanguage),
|
|
1211
|
+
language: options.language
|
|
1212
|
+
}),
|
|
1213
|
+
signal: options.signal
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
recommendations(seriesId, options = {}) {
|
|
1217
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/recommendations`, {
|
|
1218
|
+
query: tvListQuery(this.#transport, options),
|
|
1219
|
+
signal: options.signal
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
similar(seriesId, options = {}) {
|
|
1223
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/similar`, {
|
|
1224
|
+
query: tvListQuery(this.#transport, options),
|
|
1225
|
+
signal: options.signal
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
videos(seriesId, options = {}) {
|
|
1229
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/videos`, {
|
|
1230
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1231
|
+
language: options.language
|
|
1232
|
+
}),
|
|
1233
|
+
signal: options.signal
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
seasonDetails(seriesId, seasonNumber, options = {}) {
|
|
1237
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}`, {
|
|
1238
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1239
|
+
append_to_response: toCommaList(options.appendToResponse),
|
|
1240
|
+
language: options.language
|
|
1241
|
+
}),
|
|
1242
|
+
signal: options.signal
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
seasonCredits(seriesId, seasonNumber, options = {}) {
|
|
1246
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/credits`, {
|
|
1247
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1248
|
+
language: options.language
|
|
1249
|
+
}),
|
|
1250
|
+
signal: options.signal
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
seasonAggregateCredits(seriesId, seasonNumber, options = {}) {
|
|
1254
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/aggregate_credits`, {
|
|
1255
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1256
|
+
language: options.language
|
|
1257
|
+
}),
|
|
1258
|
+
signal: options.signal
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
seasonImages(seriesId, seasonNumber, options = {}) {
|
|
1262
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/images`, {
|
|
1263
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1264
|
+
include_image_language: toCommaList(options.includeImageLanguage),
|
|
1265
|
+
language: options.language
|
|
1266
|
+
}),
|
|
1267
|
+
signal: options.signal
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
seasonVideos(seriesId, seasonNumber, options = {}) {
|
|
1271
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/videos`, {
|
|
1272
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1273
|
+
language: options.language
|
|
1274
|
+
}),
|
|
1275
|
+
signal: options.signal
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
episodeDetails(seriesId, seasonNumber, episodeNumber, options = {}) {
|
|
1279
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/episode/${toId(episodeNumber, "episodeNumber")}`, {
|
|
1280
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1281
|
+
append_to_response: toCommaList(options.appendToResponse),
|
|
1282
|
+
language: options.language
|
|
1283
|
+
}),
|
|
1284
|
+
signal: options.signal
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
aggregateCredits(seriesId, options = {}) {
|
|
1288
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/aggregate_credits`, {
|
|
1289
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1290
|
+
language: options.language
|
|
1291
|
+
}),
|
|
1292
|
+
signal: options.signal
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
episodeGroups(seriesId, options = {}) {
|
|
1296
|
+
return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/episode_groups`, {
|
|
1297
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1298
|
+
language: options.language
|
|
1299
|
+
}),
|
|
1300
|
+
signal: options.signal
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
episodeGroup(groupId, options = {}) {
|
|
1304
|
+
return this.#transport.get(`/tv/episode_group/${toGroupId(groupId)}`, {
|
|
1305
|
+
query: withLanguage(this.#transport.defaults, {
|
|
1306
|
+
language: options.language
|
|
1307
|
+
}),
|
|
1308
|
+
signal: options.signal
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
function tvListQuery(transport, options) {
|
|
1313
|
+
return withLanguage(transport.defaults, {
|
|
1314
|
+
language: options.language,
|
|
1315
|
+
page: options.page
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/client.ts
|
|
1320
|
+
class TMDBClient {
|
|
1321
|
+
configuration;
|
|
1322
|
+
genres;
|
|
1323
|
+
images;
|
|
1324
|
+
movies;
|
|
1325
|
+
people;
|
|
1326
|
+
search;
|
|
1327
|
+
trending;
|
|
1328
|
+
tv;
|
|
1329
|
+
#transport;
|
|
1330
|
+
constructor(options) {
|
|
1331
|
+
let imageTransform = options.images?.transform;
|
|
1332
|
+
if (options.transport === undefined) {
|
|
1333
|
+
const builtInClient = new TMDBHttpClient(options);
|
|
1334
|
+
this.#transport = builtInClient;
|
|
1335
|
+
imageTransform = builtInClient.imageTransform;
|
|
1336
|
+
} else {
|
|
1337
|
+
this.#transport = options.transport;
|
|
1338
|
+
}
|
|
1339
|
+
this.configuration = new ConfigurationResource(this.#transport);
|
|
1340
|
+
this.genres = new GenresResource(this.#transport);
|
|
1341
|
+
this.images = new ImagesHelper(this.#transport, imageTransform);
|
|
1342
|
+
this.movies = new MoviesResource(this.#transport);
|
|
1343
|
+
this.people = new PeopleResource(this.#transport);
|
|
1344
|
+
this.search = new SearchResource(this.#transport);
|
|
1345
|
+
this.trending = new TrendingResource(this.#transport);
|
|
1346
|
+
this.tv = new TVResource(this.#transport);
|
|
1347
|
+
}
|
|
1348
|
+
request(path, options) {
|
|
1349
|
+
return this.#transport.get(path, options);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
function createTMDB(options) {
|
|
1353
|
+
return new TMDBClient(options);
|
|
1354
|
+
}
|
|
1355
|
+
// src/cli/config.ts
|
|
1356
|
+
import {
|
|
1357
|
+
chmodSync,
|
|
1358
|
+
existsSync,
|
|
1359
|
+
mkdirSync,
|
|
1360
|
+
readFileSync,
|
|
1361
|
+
renameSync,
|
|
1362
|
+
unlinkSync,
|
|
1363
|
+
writeFileSync
|
|
1364
|
+
} from "node:fs";
|
|
1365
|
+
import { homedir } from "node:os";
|
|
1366
|
+
import { dirname, join } from "node:path";
|
|
1367
|
+
|
|
1368
|
+
// src/cli/errors.ts
|
|
1369
|
+
class CliError extends Error {
|
|
1370
|
+
exitCode;
|
|
1371
|
+
constructor(message, exitCode = 1) {
|
|
1372
|
+
super(message);
|
|
1373
|
+
this.name = "CliError";
|
|
1374
|
+
this.exitCode = exitCode;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
function exitCodeFor(error) {
|
|
1378
|
+
if (error instanceof CliError) {
|
|
1379
|
+
return error.exitCode;
|
|
1380
|
+
}
|
|
1381
|
+
if (error instanceof TMDBRateLimitError) {
|
|
1382
|
+
return 5;
|
|
1383
|
+
}
|
|
1384
|
+
if (error instanceof TMDBResponseError) {
|
|
1385
|
+
if (error.status === 401 || error.status === 403)
|
|
1386
|
+
return 3;
|
|
1387
|
+
if (error.status === 404)
|
|
1388
|
+
return 4;
|
|
1389
|
+
if (error.status === 429)
|
|
1390
|
+
return 5;
|
|
1391
|
+
}
|
|
1392
|
+
return 1;
|
|
1393
|
+
}
|
|
1394
|
+
function errorMessage(error) {
|
|
1395
|
+
if (error instanceof CliError) {
|
|
1396
|
+
return error.message;
|
|
1397
|
+
}
|
|
1398
|
+
if (error instanceof TMDBRateLimitError) {
|
|
1399
|
+
return `TMDB rate limit exceeded. Retry after ${error.retryAfter} seconds.`;
|
|
1400
|
+
}
|
|
1401
|
+
if (error instanceof TMDBResponseError) {
|
|
1402
|
+
if (error.status === 401 || error.status === 403) {
|
|
1403
|
+
return [
|
|
1404
|
+
`TMDB rejected the credentials (HTTP ${error.status}): ${error.message}`,
|
|
1405
|
+
"Run `tmdb auth login` to replace the token, or check `tmdb config show`."
|
|
1406
|
+
].join(`
|
|
1407
|
+
`);
|
|
1408
|
+
}
|
|
1409
|
+
return `TMDB request failed (${error.status}): ${error.message}`;
|
|
1410
|
+
}
|
|
1411
|
+
if (error instanceof TMDBRequestError) {
|
|
1412
|
+
return error.message;
|
|
1413
|
+
}
|
|
1414
|
+
if (error instanceof Error) {
|
|
1415
|
+
return error.message;
|
|
1416
|
+
}
|
|
1417
|
+
return String(error);
|
|
1418
|
+
}
|
|
1419
|
+
var EXIT_CODE_KINDS = {
|
|
1420
|
+
2: "usage",
|
|
1421
|
+
3: "auth",
|
|
1422
|
+
4: "not_found",
|
|
1423
|
+
5: "rate_limit"
|
|
1424
|
+
};
|
|
1425
|
+
function errorKind(error) {
|
|
1426
|
+
if (error instanceof CliError) {
|
|
1427
|
+
return EXIT_CODE_KINDS[error.exitCode] ?? "internal";
|
|
1428
|
+
}
|
|
1429
|
+
if (error instanceof TMDBRateLimitError) {
|
|
1430
|
+
return "rate_limit";
|
|
1431
|
+
}
|
|
1432
|
+
if (error instanceof TMDBResponseError) {
|
|
1433
|
+
if (error.status === 401 || error.status === 403)
|
|
1434
|
+
return "auth";
|
|
1435
|
+
if (error.status === 404)
|
|
1436
|
+
return "not_found";
|
|
1437
|
+
if (error.status === 429)
|
|
1438
|
+
return "rate_limit";
|
|
1439
|
+
return "api";
|
|
1440
|
+
}
|
|
1441
|
+
if (error instanceof TMDBRequestError || error instanceof TMDBTimeoutError) {
|
|
1442
|
+
return "network";
|
|
1443
|
+
}
|
|
1444
|
+
return "internal";
|
|
1445
|
+
}
|
|
1446
|
+
function toErrorRecord(error) {
|
|
1447
|
+
const record = {
|
|
1448
|
+
type: "error",
|
|
1449
|
+
kind: errorKind(error),
|
|
1450
|
+
message: errorMessage(error),
|
|
1451
|
+
exitCode: exitCodeFor(error)
|
|
1452
|
+
};
|
|
1453
|
+
if (error instanceof TMDBResponseError) {
|
|
1454
|
+
record.status = error.status;
|
|
1455
|
+
if (error.requestId !== null) {
|
|
1456
|
+
record.requestId = error.requestId;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (error instanceof TMDBRateLimitError) {
|
|
1460
|
+
record.retryAfter = error.retryAfter;
|
|
1461
|
+
}
|
|
1462
|
+
return record;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/cli/config.ts
|
|
1466
|
+
var CONFIG_DIR = "tmdb-kit";
|
|
1467
|
+
function getConfigPath() {
|
|
1468
|
+
if (process.platform === "win32") {
|
|
1469
|
+
return join(process.env.APPDATA ?? homedir(), CONFIG_DIR, "config.json");
|
|
1470
|
+
}
|
|
1471
|
+
const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
1472
|
+
return join(configHome, CONFIG_DIR, "config.json");
|
|
1473
|
+
}
|
|
1474
|
+
function readConfig() {
|
|
1475
|
+
const path = getConfigPath();
|
|
1476
|
+
let raw;
|
|
1477
|
+
try {
|
|
1478
|
+
raw = readFileSync(path, "utf8");
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
if (isNotFound(error)) {
|
|
1481
|
+
return {};
|
|
1482
|
+
}
|
|
1483
|
+
throw new CliError(`Failed to read config file at ${path}: ${messageOf(error)}`, 3);
|
|
1484
|
+
}
|
|
1485
|
+
try {
|
|
1486
|
+
const parsed = JSON.parse(raw);
|
|
1487
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1488
|
+
throw new Error("expected a JSON object");
|
|
1489
|
+
}
|
|
1490
|
+
const record = parsed;
|
|
1491
|
+
return {
|
|
1492
|
+
accessToken: readStringField(record, "accessToken"),
|
|
1493
|
+
apiKey: readStringField(record, "apiKey"),
|
|
1494
|
+
language: readStringField(record, "language"),
|
|
1495
|
+
region: readStringField(record, "region")
|
|
1496
|
+
};
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
throw new CliError(`Invalid config file at ${path}: ${messageOf(error)}`, 3);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function setConfigValue(field, value) {
|
|
1502
|
+
writeConfig({ ...readConfig(), [field]: value });
|
|
1503
|
+
}
|
|
1504
|
+
function unsetConfigValue(field) {
|
|
1505
|
+
const config = readConfig();
|
|
1506
|
+
delete config[field];
|
|
1507
|
+
writeConfig(config);
|
|
1508
|
+
}
|
|
1509
|
+
function writeConfig(config) {
|
|
1510
|
+
const path = getConfigPath();
|
|
1511
|
+
const directory = dirname(path);
|
|
1512
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1513
|
+
try {
|
|
1514
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
1515
|
+
if (process.platform !== "win32") {
|
|
1516
|
+
chmodSync(directory, 448);
|
|
1517
|
+
}
|
|
1518
|
+
writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}
|
|
1519
|
+
`, {
|
|
1520
|
+
mode: 384
|
|
1521
|
+
});
|
|
1522
|
+
if (process.platform !== "win32") {
|
|
1523
|
+
chmodSync(tempPath, 384);
|
|
1524
|
+
}
|
|
1525
|
+
renameSync(tempPath, path);
|
|
1526
|
+
if (process.platform !== "win32") {
|
|
1527
|
+
chmodSync(path, 384);
|
|
1528
|
+
}
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
if (existsSync(tempPath)) {
|
|
1531
|
+
unlinkSync(tempPath);
|
|
1532
|
+
}
|
|
1533
|
+
throw new CliError(`Failed to write config file at ${path}: ${messageOf(error)}`, 3);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
function readStringField(record, key) {
|
|
1537
|
+
const value = record[key];
|
|
1538
|
+
if (value === undefined) {
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
if (typeof value !== "string") {
|
|
1542
|
+
throw new Error(`${key} must be a string`);
|
|
1543
|
+
}
|
|
1544
|
+
return value;
|
|
1545
|
+
}
|
|
1546
|
+
function isNotFound(error) {
|
|
1547
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1548
|
+
}
|
|
1549
|
+
function messageOf(error) {
|
|
1550
|
+
return error instanceof Error ? error.message : String(error);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/cli/credential-format.ts
|
|
1554
|
+
var ACCESS_TOKEN_PATTERN = /^eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
1555
|
+
var API_KEY_PATTERN = /^[a-fA-F0-9]{32}$/;
|
|
1556
|
+
function detectCredential(input) {
|
|
1557
|
+
const value = input.trim().replace(/^Bearer\s+/i, "");
|
|
1558
|
+
if (API_KEY_PATTERN.test(value)) {
|
|
1559
|
+
return { kind: "apiKey", value };
|
|
1560
|
+
}
|
|
1561
|
+
if (ACCESS_TOKEN_PATTERN.test(value)) {
|
|
1562
|
+
return { kind: "accessToken", value };
|
|
1563
|
+
}
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
function unrecognizedCredentialMessage() {
|
|
1567
|
+
return [
|
|
1568
|
+
"Unrecognized TMDB credential format.",
|
|
1569
|
+
"",
|
|
1570
|
+
"Expected one of:",
|
|
1571
|
+
" - API Read Access Token (JWT, three dot-separated segments)",
|
|
1572
|
+
" - API Key (32 hexadecimal characters)",
|
|
1573
|
+
"",
|
|
1574
|
+
"Create credentials:",
|
|
1575
|
+
" https://www.themoviedb.org/settings/api"
|
|
1576
|
+
].join(`
|
|
1577
|
+
`);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// src/cli/redact.ts
|
|
1581
|
+
var PLACEHOLDER_CREDENTIALS = new Set(["YOUR_TMDB_ACCESS_TOKEN", "YOUR_TMDB_API_KEY"]);
|
|
1582
|
+
function isPlaceholderCredential(value) {
|
|
1583
|
+
return value !== undefined && PLACEHOLDER_CREDENTIALS.has(value.trim());
|
|
1584
|
+
}
|
|
1585
|
+
function maskSecret(value) {
|
|
1586
|
+
if (!value) {
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
return value.length <= 4 ? "••••" : `••••${value.slice(-4)}`;
|
|
1590
|
+
}
|
|
1591
|
+
function describeSecret(value) {
|
|
1592
|
+
if (!value) {
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
return isPlaceholderCredential(value) ? "(placeholder)" : maskSecret(value);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/cli/credentials.ts
|
|
1599
|
+
function resolveCredentials() {
|
|
1600
|
+
const config = readConfig();
|
|
1601
|
+
const envAccessToken = usableCredential(process.env.TMDB_ACCESS_TOKEN);
|
|
1602
|
+
const envApiKey = usableCredential(process.env.TMDB_API_KEY);
|
|
1603
|
+
const configAccessToken = usableCredential(config.accessToken);
|
|
1604
|
+
const configApiKey = usableCredential(config.apiKey);
|
|
1605
|
+
if (envAccessToken) {
|
|
1606
|
+
return {
|
|
1607
|
+
accessToken: envAccessToken,
|
|
1608
|
+
config,
|
|
1609
|
+
configField: null,
|
|
1610
|
+
envVar: "TMDB_ACCESS_TOKEN",
|
|
1611
|
+
source: "environment"
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
if (envApiKey) {
|
|
1615
|
+
return {
|
|
1616
|
+
apiKey: envApiKey,
|
|
1617
|
+
config,
|
|
1618
|
+
configField: null,
|
|
1619
|
+
envVar: "TMDB_API_KEY",
|
|
1620
|
+
source: "environment"
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
if (configAccessToken) {
|
|
1624
|
+
return {
|
|
1625
|
+
accessToken: configAccessToken,
|
|
1626
|
+
config,
|
|
1627
|
+
configField: "accessToken",
|
|
1628
|
+
envVar: null,
|
|
1629
|
+
source: "config"
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
if (configApiKey) {
|
|
1633
|
+
return {
|
|
1634
|
+
apiKey: configApiKey,
|
|
1635
|
+
config,
|
|
1636
|
+
configField: "apiKey",
|
|
1637
|
+
envVar: null,
|
|
1638
|
+
source: "config"
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
return {
|
|
1642
|
+
config,
|
|
1643
|
+
configField: null,
|
|
1644
|
+
envVar: null,
|
|
1645
|
+
source: "none"
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
function usableCredential(value) {
|
|
1649
|
+
const trimmed = value?.trim();
|
|
1650
|
+
if (!trimmed || isPlaceholderCredential(trimmed)) {
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
return trimmed;
|
|
1654
|
+
}
|
|
1655
|
+
function missingCredentialsMessage() {
|
|
1656
|
+
return [
|
|
1657
|
+
"No TMDB credentials found.",
|
|
1658
|
+
"",
|
|
1659
|
+
"Recommended:",
|
|
1660
|
+
" tmdb auth login",
|
|
1661
|
+
"",
|
|
1662
|
+
"Or use an environment variable:",
|
|
1663
|
+
" export TMDB_ACCESS_TOKEN=...",
|
|
1664
|
+
" export TMDB_API_KEY=... (legacy v3 API key)",
|
|
1665
|
+
"",
|
|
1666
|
+
"Or edit the config file:",
|
|
1667
|
+
` ${getConfigPath()}`,
|
|
1668
|
+
"",
|
|
1669
|
+
" {",
|
|
1670
|
+
' "accessToken": "YOUR_TMDB_ACCESS_TOKEN",',
|
|
1671
|
+
' "language": "en-US",',
|
|
1672
|
+
' "region": "US"',
|
|
1673
|
+
" }",
|
|
1674
|
+
"",
|
|
1675
|
+
"Create a token:",
|
|
1676
|
+
" https://www.themoviedb.org/settings/api",
|
|
1677
|
+
"",
|
|
1678
|
+
"Use the API Read Access Token, not the legacy API Key."
|
|
1679
|
+
].join(`
|
|
1680
|
+
`);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// src/cli/options.ts
|
|
1684
|
+
var jsonArg = {
|
|
1685
|
+
json: {
|
|
1686
|
+
type: "boolean",
|
|
1687
|
+
description: "Alias for --format json"
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
var formatArg = {
|
|
1691
|
+
format: {
|
|
1692
|
+
type: "string",
|
|
1693
|
+
description: "Output format: text, json, or ndjson (default: text)"
|
|
1694
|
+
},
|
|
1695
|
+
compact: {
|
|
1696
|
+
type: "boolean",
|
|
1697
|
+
description: "Print JSON on a single line"
|
|
1698
|
+
},
|
|
1699
|
+
fields: {
|
|
1700
|
+
type: "string",
|
|
1701
|
+
description: "Comma-separated top-level fields to keep in JSON output"
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
var outputArgs = {
|
|
1705
|
+
...jsonArg,
|
|
1706
|
+
...formatArg
|
|
1707
|
+
};
|
|
1708
|
+
var paginationArgs = {
|
|
1709
|
+
all: {
|
|
1710
|
+
type: "boolean",
|
|
1711
|
+
description: "Fetch every page instead of one (requires --format ndjson)"
|
|
1712
|
+
},
|
|
1713
|
+
"max-items": {
|
|
1714
|
+
type: "string",
|
|
1715
|
+
description: "Stop after this many items (requires --format ndjson)"
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
var commonArgs = {
|
|
1719
|
+
...outputArgs,
|
|
1720
|
+
lang: {
|
|
1721
|
+
type: "string",
|
|
1722
|
+
description: "TMDB language code (default: en-US)"
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
var SEARCH_TYPES = ["all", "movie", "tv", "person"];
|
|
1726
|
+
function parseSearchType(value) {
|
|
1727
|
+
if (!SEARCH_TYPES.includes(value)) {
|
|
1728
|
+
throw new CliError(`Invalid --type "${value}". Expected one of: ${SEARCH_TYPES.join(", ")}.`, 2);
|
|
1729
|
+
}
|
|
1730
|
+
return value;
|
|
1731
|
+
}
|
|
1732
|
+
function parsePage(value) {
|
|
1733
|
+
if (value === undefined || value === "") {
|
|
1734
|
+
return 1;
|
|
1735
|
+
}
|
|
1736
|
+
const page = Number(value);
|
|
1737
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
1738
|
+
throw new CliError("Invalid --page value: expected a positive integer.", 2);
|
|
1739
|
+
}
|
|
1740
|
+
return page;
|
|
1741
|
+
}
|
|
1742
|
+
function parseId(value, label) {
|
|
1743
|
+
const id = Number(value);
|
|
1744
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
1745
|
+
throw new CliError(`Invalid ${label}: expected a positive integer.`, 2);
|
|
1746
|
+
}
|
|
1747
|
+
return id;
|
|
1748
|
+
}
|
|
1749
|
+
function parseSeasonNumber(value) {
|
|
1750
|
+
const season = Number(value);
|
|
1751
|
+
if (!Number.isInteger(season) || season < 0) {
|
|
1752
|
+
throw new CliError("Invalid season number: expected a non-negative integer.", 2);
|
|
1753
|
+
}
|
|
1754
|
+
return season;
|
|
1755
|
+
}
|
|
1756
|
+
function parseGroupId(value) {
|
|
1757
|
+
const groupId = value.trim();
|
|
1758
|
+
if (!/^[A-Za-z0-9_-]+$/.test(groupId)) {
|
|
1759
|
+
throw new CliError("Invalid group ID: expected a non-empty identifier.", 2);
|
|
1760
|
+
}
|
|
1761
|
+
return groupId;
|
|
1762
|
+
}
|
|
1763
|
+
function parseLimit(value, fallback = 10) {
|
|
1764
|
+
if (value === undefined || value === "") {
|
|
1765
|
+
return fallback;
|
|
1766
|
+
}
|
|
1767
|
+
const limit = Number(value);
|
|
1768
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
1769
|
+
throw new CliError("Invalid --limit value: expected a positive integer.", 2);
|
|
1770
|
+
}
|
|
1771
|
+
return limit;
|
|
1772
|
+
}
|
|
1773
|
+
function parseCommaList(value) {
|
|
1774
|
+
if (value === undefined) {
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
const values = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1778
|
+
return values.length > 0 ? values : undefined;
|
|
1779
|
+
}
|
|
1780
|
+
var TRENDING_WINDOWS = ["day", "week"];
|
|
1781
|
+
function parseWindow(value) {
|
|
1782
|
+
if (!TRENDING_WINDOWS.includes(value)) {
|
|
1783
|
+
throw new CliError(`Invalid --window "${value}". Expected one of: ${TRENDING_WINDOWS.join(", ")}.`, 2);
|
|
1784
|
+
}
|
|
1785
|
+
return value;
|
|
1786
|
+
}
|
|
1787
|
+
var GENRE_TYPES = ["movie", "tv"];
|
|
1788
|
+
function parseGenreType(value) {
|
|
1789
|
+
if (!GENRE_TYPES.includes(value)) {
|
|
1790
|
+
throw new CliError(`Invalid --type "${value}". Expected one of: ${GENRE_TYPES.join(", ")}.`, 2);
|
|
1791
|
+
}
|
|
1792
|
+
return value;
|
|
1793
|
+
}
|
|
1794
|
+
var MOVIE_LIST_CATEGORIES = ["popular", "top-rated", "now-playing", "upcoming"];
|
|
1795
|
+
function parseMovieListCategory(value) {
|
|
1796
|
+
if (!MOVIE_LIST_CATEGORIES.includes(value)) {
|
|
1797
|
+
throw new CliError(`Invalid movie list category "${value}". Expected one of: ${MOVIE_LIST_CATEGORIES.join(", ")}.`, 2);
|
|
1798
|
+
}
|
|
1799
|
+
return value;
|
|
1800
|
+
}
|
|
1801
|
+
var TV_LIST_CATEGORIES = ["popular", "top-rated", "airing-today", "on-the-air"];
|
|
1802
|
+
function parseTVListCategory(value) {
|
|
1803
|
+
if (!TV_LIST_CATEGORIES.includes(value)) {
|
|
1804
|
+
throw new CliError(`Invalid TV list category "${value}". Expected one of: ${TV_LIST_CATEGORIES.join(", ")}.`, 2);
|
|
1805
|
+
}
|
|
1806
|
+
return value;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/cli/format.ts
|
|
1810
|
+
function printJson(value, compact = false) {
|
|
1811
|
+
process.stdout.write(`${compact ? JSON.stringify(value) : JSON.stringify(value, null, 2)}
|
|
1812
|
+
`);
|
|
1813
|
+
}
|
|
1814
|
+
function printSearchResults(response, type) {
|
|
1815
|
+
const lines = [
|
|
1816
|
+
`Page ${response.page}/${response.total_pages} · ${response.total_results} results`
|
|
1817
|
+
];
|
|
1818
|
+
if (response.results.length === 0) {
|
|
1819
|
+
lines.push("No results.");
|
|
1820
|
+
} else {
|
|
1821
|
+
response.results.forEach((result, index) => {
|
|
1822
|
+
lines.push(formatSearchResult(result, index, type));
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
writeLines(lines);
|
|
1826
|
+
}
|
|
1827
|
+
function printMovie(movie, tmdb) {
|
|
1828
|
+
const year = movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : "";
|
|
1829
|
+
const lines = [`${movie.title}${year}`];
|
|
1830
|
+
if (movie.original_title !== movie.title) {
|
|
1831
|
+
lines.push(`Original title: ${movie.original_title}`);
|
|
1832
|
+
}
|
|
1833
|
+
if (movie.tagline) {
|
|
1834
|
+
lines.push(movie.tagline);
|
|
1835
|
+
}
|
|
1836
|
+
const facts = [
|
|
1837
|
+
movie.status,
|
|
1838
|
+
movie.runtime ? `${movie.runtime} min` : undefined,
|
|
1839
|
+
`★ ${movie.vote_average.toFixed(1)}/10 (${movie.vote_count} votes)`
|
|
1840
|
+
].filter(Boolean);
|
|
1841
|
+
lines.push(facts.join(" · "));
|
|
1842
|
+
if (movie.genres.length > 0) {
|
|
1843
|
+
lines.push(`Genres: ${movie.genres.map((genre) => genre.name).join(", ")}`);
|
|
1844
|
+
}
|
|
1845
|
+
lines.push(`TMDB: https://www.themoviedb.org/movie/${movie.id}`);
|
|
1846
|
+
const poster = tmdb.images.url(movie.poster_path, "w500");
|
|
1847
|
+
if (poster) {
|
|
1848
|
+
lines.push(`Poster: ${poster}`);
|
|
1849
|
+
}
|
|
1850
|
+
const backdrop = tmdb.images.url(movie.backdrop_path, "w780");
|
|
1851
|
+
if (backdrop) {
|
|
1852
|
+
lines.push(`Backdrop: ${backdrop}`);
|
|
1853
|
+
}
|
|
1854
|
+
if (movie.homepage) {
|
|
1855
|
+
lines.push(`Homepage: ${movie.homepage}`);
|
|
1856
|
+
}
|
|
1857
|
+
if (movie.overview) {
|
|
1858
|
+
lines.push("", movie.overview);
|
|
1859
|
+
}
|
|
1860
|
+
writeLines(lines);
|
|
1861
|
+
}
|
|
1862
|
+
function printTV(tv, tmdb) {
|
|
1863
|
+
const year = tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : "";
|
|
1864
|
+
const lines = [`${tv.name}${year}`];
|
|
1865
|
+
if (tv.original_name !== tv.name) {
|
|
1866
|
+
lines.push(`Original name: ${tv.original_name}`);
|
|
1867
|
+
}
|
|
1868
|
+
if (tv.tagline) {
|
|
1869
|
+
lines.push(tv.tagline);
|
|
1870
|
+
}
|
|
1871
|
+
const facts = [
|
|
1872
|
+
tv.status,
|
|
1873
|
+
`${tv.number_of_seasons} seasons`,
|
|
1874
|
+
`${tv.number_of_episodes} episodes`,
|
|
1875
|
+
`★ ${tv.vote_average.toFixed(1)}/10 (${tv.vote_count} votes)`
|
|
1876
|
+
].filter(Boolean);
|
|
1877
|
+
lines.push(facts.join(" · "));
|
|
1878
|
+
if (tv.genres.length > 0) {
|
|
1879
|
+
lines.push(`Genres: ${tv.genres.map((genre) => genre.name).join(", ")}`);
|
|
1880
|
+
}
|
|
1881
|
+
if (tv.networks.length > 0) {
|
|
1882
|
+
lines.push(`Networks: ${tv.networks.map((network) => network.name).join(", ")}`);
|
|
1883
|
+
}
|
|
1884
|
+
if (tv.created_by.length > 0) {
|
|
1885
|
+
lines.push(`Created by: ${tv.created_by.map((person) => person.name).join(", ")}`);
|
|
1886
|
+
}
|
|
1887
|
+
lines.push(`TMDB: https://www.themoviedb.org/tv/${tv.id}`);
|
|
1888
|
+
const poster = tmdb.images.url(tv.poster_path, "w500");
|
|
1889
|
+
if (poster) {
|
|
1890
|
+
lines.push(`Poster: ${poster}`);
|
|
1891
|
+
}
|
|
1892
|
+
const backdrop = tmdb.images.url(tv.backdrop_path, "w780");
|
|
1893
|
+
if (backdrop) {
|
|
1894
|
+
lines.push(`Backdrop: ${backdrop}`);
|
|
1895
|
+
}
|
|
1896
|
+
if (tv.homepage) {
|
|
1897
|
+
lines.push(`Homepage: ${tv.homepage}`);
|
|
1898
|
+
}
|
|
1899
|
+
if (tv.overview) {
|
|
1900
|
+
lines.push("", tv.overview);
|
|
1901
|
+
}
|
|
1902
|
+
if (tv.seasons.length > 0) {
|
|
1903
|
+
lines.push("", "Seasons:");
|
|
1904
|
+
for (const season of tv.seasons) {
|
|
1905
|
+
const airDate = season.air_date ? ` (${season.air_date.slice(0, 4)})` : "";
|
|
1906
|
+
lines.push(` ${season.season_number}. ${season.name}${airDate} · ${season.episode_count} episodes`);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
writeLines(lines);
|
|
1910
|
+
}
|
|
1911
|
+
function printCredits(credits, limit = 10) {
|
|
1912
|
+
const lines = [];
|
|
1913
|
+
if (credits.cast.length > 0) {
|
|
1914
|
+
lines.push("Cast:");
|
|
1915
|
+
for (const member of credits.cast.slice(0, limit)) {
|
|
1916
|
+
const character = member.character ? ` as ${member.character}` : "";
|
|
1917
|
+
lines.push(` ${member.name}${character} #${member.id}`);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
if (credits.crew.length > 0) {
|
|
1921
|
+
if (lines.length > 0) {
|
|
1922
|
+
lines.push("");
|
|
1923
|
+
}
|
|
1924
|
+
lines.push("Crew:");
|
|
1925
|
+
for (const member of credits.crew.slice(0, limit)) {
|
|
1926
|
+
lines.push(` ${member.name} — ${member.job} (${member.department}) #${member.id}`);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
if (lines.length === 0) {
|
|
1930
|
+
lines.push("No credits.");
|
|
1931
|
+
}
|
|
1932
|
+
writeLines(lines);
|
|
1933
|
+
}
|
|
1934
|
+
function printAggregateCredits(credits, limit = 10) {
|
|
1935
|
+
const lines = [];
|
|
1936
|
+
if (credits.cast.length > 0) {
|
|
1937
|
+
lines.push("Cast:");
|
|
1938
|
+
for (const member of credits.cast.slice(0, limit)) {
|
|
1939
|
+
const roles = member.roles.map((role) => `${role.character} (${role.episode_count} eps)`).join(", ");
|
|
1940
|
+
lines.push(` ${member.name} as ${roles} #${member.id}`);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
if (credits.crew.length > 0) {
|
|
1944
|
+
if (lines.length > 0) {
|
|
1945
|
+
lines.push("");
|
|
1946
|
+
}
|
|
1947
|
+
lines.push("Crew:");
|
|
1948
|
+
for (const member of credits.crew.slice(0, limit)) {
|
|
1949
|
+
const jobs = member.jobs.map((job) => `${job.job} (${job.episode_count} eps)`).join(", ");
|
|
1950
|
+
lines.push(` ${member.name} — ${jobs} (${member.department}) #${member.id}`);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
if (lines.length === 0) {
|
|
1954
|
+
lines.push("No credits.");
|
|
1955
|
+
}
|
|
1956
|
+
writeLines(lines);
|
|
1957
|
+
}
|
|
1958
|
+
function printImages(images, tmdb) {
|
|
1959
|
+
const lines = [];
|
|
1960
|
+
appendImageSection(lines, "Posters", images.posters, "w500", tmdb);
|
|
1961
|
+
appendImageSection(lines, "Backdrops", images.backdrops, "w780", tmdb);
|
|
1962
|
+
appendImageSection(lines, "Logos", images.logos ?? [], "w300", tmdb);
|
|
1963
|
+
if (lines.length === 0) {
|
|
1964
|
+
lines.push("No images.");
|
|
1965
|
+
}
|
|
1966
|
+
writeLines(lines);
|
|
1967
|
+
}
|
|
1968
|
+
function printSeasonImages(images, tmdb) {
|
|
1969
|
+
const lines = [];
|
|
1970
|
+
appendImageSection(lines, "Posters", images.posters, "w500", tmdb);
|
|
1971
|
+
if (lines.length === 0) {
|
|
1972
|
+
lines.push("No images.");
|
|
1973
|
+
}
|
|
1974
|
+
writeLines(lines);
|
|
1975
|
+
}
|
|
1976
|
+
function printVideos(videos) {
|
|
1977
|
+
if (videos.results.length === 0) {
|
|
1978
|
+
writeLines(["No videos."]);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const lines = videos.results.map((video) => {
|
|
1982
|
+
const url = video.site.toLowerCase() === "youtube" ? `https://www.youtube.com/watch?v=${video.key}` : video.key;
|
|
1983
|
+
return `${video.type}: ${video.name} ${url}`;
|
|
1984
|
+
});
|
|
1985
|
+
writeLines(lines);
|
|
1986
|
+
}
|
|
1987
|
+
function printSeason(season, tmdb) {
|
|
1988
|
+
const year = season.air_date ? ` (${season.air_date.slice(0, 4)})` : "";
|
|
1989
|
+
const lines = [
|
|
1990
|
+
`${season.name}${year}`,
|
|
1991
|
+
`Episodes: ${season.episodes.length} · ★ ${season.vote_average.toFixed(1)}/10`
|
|
1992
|
+
];
|
|
1993
|
+
if (season.overview) {
|
|
1994
|
+
lines.push("", season.overview);
|
|
1995
|
+
}
|
|
1996
|
+
const poster = tmdb.images.url(season.poster_path, "w500");
|
|
1997
|
+
if (poster) {
|
|
1998
|
+
lines.push(`Poster: ${poster}`);
|
|
1999
|
+
}
|
|
2000
|
+
if (season.episodes.length > 0) {
|
|
2001
|
+
lines.push("", "Episodes:");
|
|
2002
|
+
for (const episode of season.episodes) {
|
|
2003
|
+
const airDate = episode.air_date ? ` ${episode.air_date}` : "";
|
|
2004
|
+
const runtime = episode.runtime ? ` · ${episode.runtime} min` : "";
|
|
2005
|
+
lines.push(` E${String(episode.episode_number).padStart(2, "0")} ${episode.name}${airDate}${runtime} ★ ${episode.vote_average.toFixed(1)} #${episode.id}`);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
writeLines(lines);
|
|
2009
|
+
}
|
|
2010
|
+
function printEpisode(episode, tmdb, seriesId) {
|
|
2011
|
+
const lines = [`${episodeCode(episode)} · ${episode.name}`];
|
|
2012
|
+
const facts = [
|
|
2013
|
+
episode.air_date,
|
|
2014
|
+
episode.runtime ? `${episode.runtime} min` : undefined,
|
|
2015
|
+
`★ ${episode.vote_average.toFixed(1)}/10 (${episode.vote_count} votes)`
|
|
2016
|
+
].filter(Boolean);
|
|
2017
|
+
lines.push(facts.join(" · "));
|
|
2018
|
+
lines.push(`TMDB: https://www.themoviedb.org/tv/${seriesId}/season/${episode.season_number}/episode/${episode.episode_number}`);
|
|
2019
|
+
const still = tmdb.images.url(episode.still_path, "w300");
|
|
2020
|
+
if (still) {
|
|
2021
|
+
lines.push(`Still: ${still}`);
|
|
2022
|
+
}
|
|
2023
|
+
if (episode.overview) {
|
|
2024
|
+
lines.push("", episode.overview);
|
|
2025
|
+
}
|
|
2026
|
+
writeLines(lines);
|
|
2027
|
+
}
|
|
2028
|
+
function printEpisodeGroups(response) {
|
|
2029
|
+
if (response.results.length === 0) {
|
|
2030
|
+
writeLines(["No episode groups."]);
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
writeLines(response.results.map((group) => `${group.id} ${group.name} (${group.group_count} groups, ${group.episode_count} episodes)`));
|
|
2034
|
+
}
|
|
2035
|
+
function printEpisodeGroup(group) {
|
|
2036
|
+
const lines = [group.name];
|
|
2037
|
+
if (group.description) {
|
|
2038
|
+
lines.push(group.description);
|
|
2039
|
+
}
|
|
2040
|
+
if (group.groups.length === 0) {
|
|
2041
|
+
lines.push("", "No episodes.");
|
|
2042
|
+
writeLines(lines);
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
for (const section of group.groups) {
|
|
2046
|
+
lines.push("", section.name);
|
|
2047
|
+
for (const episode of section.episodes) {
|
|
2048
|
+
lines.push(` ${episode.order}. ${episodeCode(episode)} · ${episode.name} #${episode.id}`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
writeLines(lines);
|
|
2052
|
+
}
|
|
2053
|
+
function printPerson(person, tmdb) {
|
|
2054
|
+
const lines = [person.name];
|
|
2055
|
+
const facts = [
|
|
2056
|
+
person.known_for_department,
|
|
2057
|
+
person.birthday ? `Born: ${person.birthday}` : undefined,
|
|
2058
|
+
person.deathday ? `Died: ${person.deathday}` : undefined
|
|
2059
|
+
].filter(Boolean);
|
|
2060
|
+
if (facts.length > 0) {
|
|
2061
|
+
lines.push(facts.join(" · "));
|
|
2062
|
+
}
|
|
2063
|
+
if (person.place_of_birth) {
|
|
2064
|
+
lines.push(`From: ${person.place_of_birth}`);
|
|
2065
|
+
}
|
|
2066
|
+
lines.push(`TMDB: https://www.themoviedb.org/person/${person.id}`);
|
|
2067
|
+
const profile = tmdb.images.url(person.profile_path, "h632");
|
|
2068
|
+
if (profile) {
|
|
2069
|
+
lines.push(`Profile: ${profile}`);
|
|
2070
|
+
}
|
|
2071
|
+
if (person.homepage) {
|
|
2072
|
+
lines.push(`Homepage: ${person.homepage}`);
|
|
2073
|
+
}
|
|
2074
|
+
if (person.imdb_id) {
|
|
2075
|
+
lines.push(`IMDb: https://www.imdb.com/name/${person.imdb_id}`);
|
|
2076
|
+
}
|
|
2077
|
+
if (person.biography) {
|
|
2078
|
+
lines.push("", person.biography);
|
|
2079
|
+
}
|
|
2080
|
+
writeLines(lines);
|
|
2081
|
+
}
|
|
2082
|
+
function printPersonCredits(credits, limit = 10) {
|
|
2083
|
+
const lines = [];
|
|
2084
|
+
const cast = [...credits.cast].sort(byCreditDate);
|
|
2085
|
+
const crew = [...credits.crew].sort(byCreditDate);
|
|
2086
|
+
if (cast.length > 0) {
|
|
2087
|
+
lines.push("Cast:");
|
|
2088
|
+
for (const item of cast.slice(0, limit)) {
|
|
2089
|
+
lines.push(formatCombinedCast(item));
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
if (crew.length > 0) {
|
|
2093
|
+
if (lines.length > 0) {
|
|
2094
|
+
lines.push("");
|
|
2095
|
+
}
|
|
2096
|
+
lines.push("Crew:");
|
|
2097
|
+
for (const item of crew.slice(0, limit)) {
|
|
2098
|
+
lines.push(formatCombinedCrew(item));
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
if (lines.length === 0) {
|
|
2102
|
+
lines.push("No credits.");
|
|
2103
|
+
}
|
|
2104
|
+
writeLines(lines);
|
|
2105
|
+
}
|
|
2106
|
+
function printGenres(response) {
|
|
2107
|
+
if (response.genres.length === 0) {
|
|
2108
|
+
writeLines(["No genres."]);
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
writeLines(response.genres.map((genre) => `${genre.id} ${genre.name}`));
|
|
2112
|
+
}
|
|
2113
|
+
function appendImageSection(lines, title, images, size, tmdb) {
|
|
2114
|
+
if (images.length === 0) {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
if (lines.length > 0) {
|
|
2118
|
+
lines.push("");
|
|
2119
|
+
}
|
|
2120
|
+
lines.push(`${title}:`);
|
|
2121
|
+
for (const image of images) {
|
|
2122
|
+
const url = tmdb.images.url(image.file_path, size);
|
|
2123
|
+
if (url) {
|
|
2124
|
+
lines.push(` ${url} (${image.width}x${image.height})`);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
function formatCombinedCast(item) {
|
|
2129
|
+
const character = item.character ? ` as ${item.character}` : "";
|
|
2130
|
+
const episodes = item.media_type === "tv" && item.episode_count ? ` · ${item.episode_count} eps` : "";
|
|
2131
|
+
return ` ${creditYear(item)} ${creditTitle(item)}${character}${episodes} (${item.media_type}, ${item.id})`;
|
|
2132
|
+
}
|
|
2133
|
+
function formatCombinedCrew(item) {
|
|
2134
|
+
return ` ${creditYear(item)} ${creditTitle(item)} — ${item.job} (${item.department}) (${item.media_type}, ${item.id})`;
|
|
2135
|
+
}
|
|
2136
|
+
function byCreditDate(a, b) {
|
|
2137
|
+
return creditDate(b).localeCompare(creditDate(a));
|
|
2138
|
+
}
|
|
2139
|
+
function creditDate(item) {
|
|
2140
|
+
return "release_date" in item ? item.release_date : item.first_air_date;
|
|
2141
|
+
}
|
|
2142
|
+
function creditTitle(item) {
|
|
2143
|
+
return "title" in item ? item.title : item.name;
|
|
2144
|
+
}
|
|
2145
|
+
function creditYear(item) {
|
|
2146
|
+
const date = creditDate(item);
|
|
2147
|
+
return date ? date.slice(0, 4) : "----";
|
|
2148
|
+
}
|
|
2149
|
+
function writeLines(lines) {
|
|
2150
|
+
process.stdout.write(`${lines.join(`
|
|
2151
|
+
`)}
|
|
2152
|
+
`);
|
|
2153
|
+
}
|
|
2154
|
+
function episodeCode(episode) {
|
|
2155
|
+
return `S${String(episode.season_number).padStart(2, "0")}E${String(episode.episode_number).padStart(2, "0")}`;
|
|
2156
|
+
}
|
|
2157
|
+
function formatSearchResult(result, index, fallbackType) {
|
|
2158
|
+
const title = "title" in result ? result.title : result.name;
|
|
2159
|
+
const date = "release_date" in result ? result.release_date : ("first_air_date" in result) ? result.first_air_date : "";
|
|
2160
|
+
const year = date ? ` (${date.slice(0, 4)})` : "";
|
|
2161
|
+
const mediaType = "media_type" in result && result.media_type ? result.media_type : fallbackType;
|
|
2162
|
+
const score = "vote_average" in result && result.vote_average > 0 ? ` ★ ${result.vote_average.toFixed(1)}` : "";
|
|
2163
|
+
return `${String(index + 1).padStart(2)}. ${title}${year}${score} ${mediaType} ${result.id}`;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// src/cli/output.ts
|
|
2167
|
+
var OUTPUT_FORMATS = ["text", "json", "ndjson"];
|
|
2168
|
+
var MAX_PAGE = 500;
|
|
2169
|
+
var FORMAT_ALIASES = { jsonl: "ndjson" };
|
|
2170
|
+
function resolveOutput(args, options = {}) {
|
|
2171
|
+
const explicitFormat = args.format !== undefined && args.format.trim() !== "";
|
|
2172
|
+
const format = parseFormat(args.format);
|
|
2173
|
+
const fields = parseFields(args.fields);
|
|
2174
|
+
const all = args.all === true;
|
|
2175
|
+
const maxItems = parseMaxItems(args["max-items"]);
|
|
2176
|
+
if (args.json && explicitFormat && format !== "json") {
|
|
2177
|
+
throw new CliError(`--json cannot be combined with --format ${format}.`, 2);
|
|
2178
|
+
}
|
|
2179
|
+
const resolvedFormat = args.json ? "json" : format;
|
|
2180
|
+
if (fields && resolvedFormat === "text") {
|
|
2181
|
+
throw new CliError("--fields requires --format json or ndjson.", 2);
|
|
2182
|
+
}
|
|
2183
|
+
if (all || maxItems !== undefined) {
|
|
2184
|
+
if (resolvedFormat !== "ndjson") {
|
|
2185
|
+
throw new CliError(`${all ? "--all" : "--max-items"} requires --format ndjson.`, 2);
|
|
2186
|
+
}
|
|
2187
|
+
if (options.paginated !== true) {
|
|
2188
|
+
throw new CliError(`${all ? "--all" : "--max-items"} is only supported by list commands.`, 2);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return {
|
|
2192
|
+
...all ? { all: true } : {},
|
|
2193
|
+
compact: args.compact === true,
|
|
2194
|
+
...fields ? { fields } : {},
|
|
2195
|
+
format: resolvedFormat,
|
|
2196
|
+
...maxItems !== undefined ? { maxItems } : {}
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
function printResult(value, output, renderText) {
|
|
2200
|
+
if (output.format === "text") {
|
|
2201
|
+
renderText();
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
if (output.format === "json") {
|
|
2205
|
+
printJson(output.fields ? projectPayload(value, output.fields) : value, output.compact);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
printNdjson(value, output);
|
|
2209
|
+
}
|
|
2210
|
+
async function printPaged({
|
|
2211
|
+
fetchPage,
|
|
2212
|
+
output,
|
|
2213
|
+
renderText,
|
|
2214
|
+
startPage
|
|
2215
|
+
}) {
|
|
2216
|
+
let response = await fetchPage(startPage);
|
|
2217
|
+
if (output.format === "text") {
|
|
2218
|
+
renderText(response);
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
if (output.format === "json") {
|
|
2222
|
+
printJson(output.fields ? projectPayload(response, output.fields) : response, output.compact);
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
const maxItems = output.maxItems ?? Number.POSITIVE_INFINITY;
|
|
2226
|
+
let page = startPage;
|
|
2227
|
+
let pages = 0;
|
|
2228
|
+
let count = 0;
|
|
2229
|
+
writeNdjson({ ...envelopeOf(response), type: "meta" });
|
|
2230
|
+
while (true) {
|
|
2231
|
+
pages += 1;
|
|
2232
|
+
for (const item of response.results) {
|
|
2233
|
+
if (count >= maxItems) {
|
|
2234
|
+
break;
|
|
2235
|
+
}
|
|
2236
|
+
writeNdjson({
|
|
2237
|
+
type: "item",
|
|
2238
|
+
index: count,
|
|
2239
|
+
value: output.fields ? projectRecord(item, output.fields, count === 0) : item
|
|
2240
|
+
});
|
|
2241
|
+
count += 1;
|
|
2242
|
+
}
|
|
2243
|
+
if (!output.all || count >= maxItems || response.results.length === 0) {
|
|
2244
|
+
break;
|
|
2245
|
+
}
|
|
2246
|
+
if (page >= Math.min(response.total_pages, MAX_PAGE)) {
|
|
2247
|
+
break;
|
|
2248
|
+
}
|
|
2249
|
+
page += 1;
|
|
2250
|
+
response = await fetchPage(page);
|
|
2251
|
+
}
|
|
2252
|
+
writeNdjson({ type: "summary", count, pages });
|
|
2253
|
+
}
|
|
2254
|
+
async function runSafely(args, task, options = {}) {
|
|
2255
|
+
let output;
|
|
2256
|
+
try {
|
|
2257
|
+
output = resolveOutput(args, options);
|
|
2258
|
+
await task(output);
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
writeError(error, output ?? fallbackOutput(args));
|
|
2261
|
+
process.exitCode = exitCodeFor(error);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
function writeError(error, output) {
|
|
2265
|
+
if (output.format === "text") {
|
|
2266
|
+
process.stderr.write(`${errorMessage(error)}
|
|
2267
|
+
`);
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
process.stderr.write(`${JSON.stringify(toErrorRecord(error))}
|
|
2271
|
+
`);
|
|
2272
|
+
}
|
|
2273
|
+
function fallbackOutput(args) {
|
|
2274
|
+
const format = args.format?.trim().toLowerCase();
|
|
2275
|
+
const wantsJson = args.json === true || format === "json" || format === "jsonl" || format === "ndjson";
|
|
2276
|
+
return {
|
|
2277
|
+
compact: args.compact === true,
|
|
2278
|
+
format: wantsJson ? "json" : "text"
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
function parseFormat(value) {
|
|
2282
|
+
if (value === undefined || value === "") {
|
|
2283
|
+
return "text";
|
|
2284
|
+
}
|
|
2285
|
+
const requested = value.trim().toLowerCase();
|
|
2286
|
+
const format = FORMAT_ALIASES[requested] ?? requested;
|
|
2287
|
+
if (!OUTPUT_FORMATS.includes(format)) {
|
|
2288
|
+
throw new CliError(`Invalid --format "${value}". Expected one of: ${OUTPUT_FORMATS.join(", ")}.`, 2);
|
|
2289
|
+
}
|
|
2290
|
+
return format;
|
|
2291
|
+
}
|
|
2292
|
+
function parseFields(value) {
|
|
2293
|
+
if (value === undefined) {
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
const fields = [
|
|
2297
|
+
...new Set(value.split(",").map((field) => field.trim()).filter(Boolean))
|
|
2298
|
+
];
|
|
2299
|
+
if (fields.length === 0) {
|
|
2300
|
+
throw new CliError("--fields requires at least one field name.", 2);
|
|
2301
|
+
}
|
|
2302
|
+
return fields;
|
|
2303
|
+
}
|
|
2304
|
+
function parseMaxItems(value) {
|
|
2305
|
+
if (value === undefined) {
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const maxItems = Number(value);
|
|
2309
|
+
if (!Number.isInteger(maxItems) || maxItems < 1) {
|
|
2310
|
+
throw new CliError("Invalid --max-items value: expected a positive integer.", 2);
|
|
2311
|
+
}
|
|
2312
|
+
return maxItems;
|
|
2313
|
+
}
|
|
2314
|
+
function printNdjson(value, output) {
|
|
2315
|
+
if (isRecord(value) && Array.isArray(value.results)) {
|
|
2316
|
+
const { results, ...envelope } = value;
|
|
2317
|
+
writeNdjson({ ...envelope, type: "meta" });
|
|
2318
|
+
results.forEach((item, index) => {
|
|
2319
|
+
writeNdjson({
|
|
2320
|
+
type: "item",
|
|
2321
|
+
index,
|
|
2322
|
+
value: output.fields ? projectRecord(item, output.fields, index === 0) : item
|
|
2323
|
+
});
|
|
2324
|
+
});
|
|
2325
|
+
writeNdjson({ type: "summary", count: results.length, pages: 1 });
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
writeNdjson({
|
|
2329
|
+
type: "result",
|
|
2330
|
+
value: output.fields ? projectRecord(value, output.fields, true) : value
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
function writeNdjson(record) {
|
|
2334
|
+
process.stdout.write(`${JSON.stringify(record)}
|
|
2335
|
+
`);
|
|
2336
|
+
}
|
|
2337
|
+
function envelopeOf(response) {
|
|
2338
|
+
const envelope = { ...response };
|
|
2339
|
+
delete envelope.results;
|
|
2340
|
+
return envelope;
|
|
2341
|
+
}
|
|
2342
|
+
function projectPayload(value, fields) {
|
|
2343
|
+
if (!isRecord(value)) {
|
|
2344
|
+
throw new CliError("--fields requires an object response.", 2);
|
|
2345
|
+
}
|
|
2346
|
+
if (Array.isArray(value.results)) {
|
|
2347
|
+
return {
|
|
2348
|
+
...value,
|
|
2349
|
+
results: value.results.map((item, index) => projectRecord(item, fields, index === 0))
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
return projectRecord(value, fields, true);
|
|
2353
|
+
}
|
|
2354
|
+
function projectRecord(record, fields, validate) {
|
|
2355
|
+
if (!isRecord(record)) {
|
|
2356
|
+
throw new CliError("--fields requires object records.", 2);
|
|
2357
|
+
}
|
|
2358
|
+
const projected = {};
|
|
2359
|
+
for (const field of fields) {
|
|
2360
|
+
if (Object.hasOwn(record, field)) {
|
|
2361
|
+
projected[field] = record[field];
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
if (validate && Object.keys(projected).length === 0) {
|
|
2365
|
+
throw new CliError(`Unknown --fields value(s): ${fields.join(", ")}. Available: ${Object.keys(record).sort().join(", ")}.`, 2);
|
|
2366
|
+
}
|
|
2367
|
+
return projected;
|
|
2368
|
+
}
|
|
2369
|
+
function isRecord(value) {
|
|
2370
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// src/cli/prompt.ts
|
|
2374
|
+
import { createInterface } from "node:readline/promises";
|
|
2375
|
+
async function readSecret(prompt) {
|
|
2376
|
+
if (!process.stdin.isTTY) {
|
|
2377
|
+
return readStdinSecret();
|
|
2378
|
+
}
|
|
2379
|
+
const rl = createInterface({
|
|
2380
|
+
input: process.stdin,
|
|
2381
|
+
output: process.stdout,
|
|
2382
|
+
terminal: true
|
|
2383
|
+
});
|
|
2384
|
+
const target = rl;
|
|
2385
|
+
const writeToOutput = target._writeToOutput;
|
|
2386
|
+
target._writeToOutput = () => {};
|
|
2387
|
+
try {
|
|
2388
|
+
const answer = await rl.question(prompt);
|
|
2389
|
+
process.stdout.write(`
|
|
2390
|
+
`);
|
|
2391
|
+
return answer.trim();
|
|
2392
|
+
} finally {
|
|
2393
|
+
target._writeToOutput = writeToOutput;
|
|
2394
|
+
rl.close();
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
async function confirm(question) {
|
|
2398
|
+
const rl = createInterface({
|
|
2399
|
+
input: process.stdin,
|
|
2400
|
+
output: process.stdout
|
|
2401
|
+
});
|
|
2402
|
+
try {
|
|
2403
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
2404
|
+
return answer === "y" || answer === "yes";
|
|
2405
|
+
} finally {
|
|
2406
|
+
rl.close();
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
async function readStdinSecret() {
|
|
2410
|
+
const chunks = [];
|
|
2411
|
+
for await (const chunk of process.stdin) {
|
|
2412
|
+
chunks.push(Buffer.from(chunk));
|
|
2413
|
+
}
|
|
2414
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
// src/cli/commands/auth.ts
|
|
2418
|
+
var authCommand = defineCommand({
|
|
2419
|
+
meta: {
|
|
2420
|
+
name: "auth",
|
|
2421
|
+
description: "Manage TMDB API credentials"
|
|
2422
|
+
},
|
|
2423
|
+
subCommands: {
|
|
2424
|
+
login: defineCommand({
|
|
2425
|
+
meta: {
|
|
2426
|
+
name: "login",
|
|
2427
|
+
description: "Validate and save TMDB API credentials"
|
|
2428
|
+
},
|
|
2429
|
+
args: {
|
|
2430
|
+
"token-stdin": {
|
|
2431
|
+
type: "boolean",
|
|
2432
|
+
description: "Read the credential from stdin instead of prompting"
|
|
2433
|
+
},
|
|
2434
|
+
force: {
|
|
2435
|
+
type: "boolean",
|
|
2436
|
+
description: "Replace existing credentials without confirmation"
|
|
2437
|
+
}
|
|
2438
|
+
},
|
|
2439
|
+
async run({ args }) {
|
|
2440
|
+
await runSafely({}, async () => {
|
|
2441
|
+
const existing = resolveCredentials();
|
|
2442
|
+
if (!args.force && existing.source !== "none") {
|
|
2443
|
+
if (!process.stdin.isTTY) {
|
|
2444
|
+
throw new CliError(existingCredentialsMessage(existing), 2);
|
|
2445
|
+
}
|
|
2446
|
+
process.stdout.write(`${describeExistingCredentials(existing)}
|
|
2447
|
+
`);
|
|
2448
|
+
const question = existing.source === "environment" ? "Save a config token anyway? [y/N] " : "Replace existing credentials? [y/N] ";
|
|
2449
|
+
if (!await confirm(question)) {
|
|
2450
|
+
process.stdout.write(`Kept existing credentials.
|
|
2451
|
+
`);
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
const input = args["token-stdin"] ? await readStdinSecret() : await promptForCredential();
|
|
2456
|
+
if (!input) {
|
|
2457
|
+
throw new CliError("Credential must not be empty.", 2);
|
|
2458
|
+
}
|
|
2459
|
+
const credential = detectCredential(input);
|
|
2460
|
+
if (!credential) {
|
|
2461
|
+
throw new CliError(unrecognizedCredentialMessage(), 2);
|
|
2462
|
+
}
|
|
2463
|
+
const config = readConfig();
|
|
2464
|
+
const common = {
|
|
2465
|
+
baseUrl: process.env.TMDB_BASE_URL,
|
|
2466
|
+
defaultLanguage: config.language ?? "en-US"
|
|
2467
|
+
};
|
|
2468
|
+
const client = credential.kind === "accessToken" ? createTMDB({ accessToken: credential.value, ...common }) : createTMDB({ apiKey: credential.value, ...common });
|
|
2469
|
+
try {
|
|
2470
|
+
await client.movies.details(550);
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
if (error instanceof TMDBResponseError && (error.status === 401 || error.status === 403)) {
|
|
2473
|
+
throw new CliError([
|
|
2474
|
+
"Credential validation failed: TMDB rejected the credential.",
|
|
2475
|
+
"Make sure you copied the API Read Access Token or API Key correctly.",
|
|
2476
|
+
"Existing config was not changed."
|
|
2477
|
+
].join(`
|
|
2478
|
+
`), 3);
|
|
2479
|
+
}
|
|
2480
|
+
throw new CliError(`Credential validation failed; existing config was not changed.
|
|
2481
|
+
${errorMessage(error)}`, exitCodeFor(error));
|
|
2482
|
+
}
|
|
2483
|
+
delete config.accessToken;
|
|
2484
|
+
delete config.apiKey;
|
|
2485
|
+
config[credential.kind] = credential.value;
|
|
2486
|
+
writeConfig(config);
|
|
2487
|
+
process.stdout.write(`Credential validated.
|
|
2488
|
+
Saved ${credential.kind} to ${getConfigPath()}
|
|
2489
|
+
`);
|
|
2490
|
+
if (credential.kind === "apiKey") {
|
|
2491
|
+
process.stdout.write(`Note: API Key is the legacy v3 method. The API Read Access Token is recommended.
|
|
2492
|
+
`);
|
|
2493
|
+
}
|
|
2494
|
+
const effective = resolveCredentials();
|
|
2495
|
+
if (effective.source === "environment") {
|
|
2496
|
+
process.stdout.write(`Note: ${effective.envVar} is set and takes precedence over the config file.
|
|
2497
|
+
`);
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
}),
|
|
2502
|
+
status: defineCommand({
|
|
2503
|
+
meta: {
|
|
2504
|
+
name: "status",
|
|
2505
|
+
description: "Show credential source, token mask, and config path"
|
|
2506
|
+
},
|
|
2507
|
+
args: { ...outputArgs },
|
|
2508
|
+
async run({ args }) {
|
|
2509
|
+
await runSafely(args, async (output) => {
|
|
2510
|
+
const credentials = resolveCredentials();
|
|
2511
|
+
const path = getConfigPath();
|
|
2512
|
+
const configExists = existsSync2(path);
|
|
2513
|
+
const view = {
|
|
2514
|
+
source: credentials.source,
|
|
2515
|
+
environmentVariable: credentials.envVar,
|
|
2516
|
+
configField: credentials.configField,
|
|
2517
|
+
token: maskSecret(credentials.accessToken ?? credentials.apiKey),
|
|
2518
|
+
configPath: path,
|
|
2519
|
+
configExists,
|
|
2520
|
+
language: credentials.config.language ?? null,
|
|
2521
|
+
region: credentials.config.region ?? null
|
|
2522
|
+
};
|
|
2523
|
+
const lines = [
|
|
2524
|
+
`Credential source: ${describeSource(credentials)}`,
|
|
2525
|
+
`Token: ${view.token ?? "(not set)"}`,
|
|
2526
|
+
`Config: ${path}${configExists ? "" : " (missing)"}`,
|
|
2527
|
+
`Language: ${view.language ?? "(not set)"}`,
|
|
2528
|
+
`Region: ${view.region ?? "(not set)"}`
|
|
2529
|
+
];
|
|
2530
|
+
const hasConfigCredentials = Boolean(credentials.config.accessToken && !isPlaceholderCredential(credentials.config.accessToken) || credentials.config.apiKey && !isPlaceholderCredential(credentials.config.apiKey));
|
|
2531
|
+
if (credentials.source === "environment" && hasConfigCredentials) {
|
|
2532
|
+
lines.push("Note: config credentials are shadowed by environment variables.");
|
|
2533
|
+
}
|
|
2534
|
+
if (credentials.source === "none") {
|
|
2535
|
+
lines.push("", "Run `tmdb auth login` to set up credentials.");
|
|
2536
|
+
}
|
|
2537
|
+
printResult(view, output, () => process.stdout.write(`${lines.join(`
|
|
2538
|
+
`)}
|
|
2539
|
+
`));
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
}),
|
|
2543
|
+
logout: defineCommand({
|
|
2544
|
+
meta: {
|
|
2545
|
+
name: "logout",
|
|
2546
|
+
description: "Remove saved API credentials from the config file"
|
|
2547
|
+
},
|
|
2548
|
+
async run() {
|
|
2549
|
+
await runSafely({}, async () => {
|
|
2550
|
+
const config = readConfig();
|
|
2551
|
+
const hadCredentials = Boolean(config.accessToken || config.apiKey);
|
|
2552
|
+
if (hadCredentials) {
|
|
2553
|
+
delete config.accessToken;
|
|
2554
|
+
delete config.apiKey;
|
|
2555
|
+
writeConfig(config);
|
|
2556
|
+
process.stdout.write(`Removed API credentials from ${getConfigPath()}
|
|
2557
|
+
`);
|
|
2558
|
+
} else {
|
|
2559
|
+
process.stdout.write(`No saved API credentials found.
|
|
2560
|
+
`);
|
|
2561
|
+
}
|
|
2562
|
+
if (resolveCredentials().source === "environment") {
|
|
2563
|
+
process.stdout.write(`Environment credentials are still set and will continue to be used.
|
|
2564
|
+
`);
|
|
2565
|
+
}
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
})
|
|
2569
|
+
}
|
|
2570
|
+
});
|
|
2571
|
+
async function promptForCredential() {
|
|
2572
|
+
if (!process.stdin.isTTY) {
|
|
2573
|
+
throw new CliError([
|
|
2574
|
+
"No interactive terminal available.",
|
|
2575
|
+
"Pipe the credential with --token-stdin:",
|
|
2576
|
+
` printf '%s' "$TMDB_ACCESS_TOKEN" | tmdb auth login --token-stdin`,
|
|
2577
|
+
"Or set TMDB_ACCESS_TOKEN / TMDB_API_KEY directly."
|
|
2578
|
+
].join(`
|
|
2579
|
+
`), 2);
|
|
2580
|
+
}
|
|
2581
|
+
process.stdout.write([
|
|
2582
|
+
"Create credentials at https://www.themoviedb.org/settings/api",
|
|
2583
|
+
"API Read Access Token (recommended) or API Key are accepted.",
|
|
2584
|
+
""
|
|
2585
|
+
].join(`
|
|
2586
|
+
`));
|
|
2587
|
+
return readSecret("TMDB credential: ");
|
|
2588
|
+
}
|
|
2589
|
+
function describeSource(credentials) {
|
|
2590
|
+
if (credentials.source === "environment") {
|
|
2591
|
+
return `environment (${credentials.envVar})`;
|
|
2592
|
+
}
|
|
2593
|
+
if (credentials.source === "config") {
|
|
2594
|
+
return `config (${credentials.configField})`;
|
|
2595
|
+
}
|
|
2596
|
+
return "none";
|
|
2597
|
+
}
|
|
2598
|
+
function describeExistingCredentials(credentials) {
|
|
2599
|
+
const secret = maskSecret(credentials.accessToken ?? credentials.apiKey) ?? "(unknown)";
|
|
2600
|
+
if (credentials.source === "environment") {
|
|
2601
|
+
return [
|
|
2602
|
+
`Credentials are currently provided by ${credentials.envVar} (${secret}).`,
|
|
2603
|
+
"A config token will not take effect while that environment variable is set."
|
|
2604
|
+
].join(`
|
|
2605
|
+
`);
|
|
2606
|
+
}
|
|
2607
|
+
return `Existing credentials: config (${credentials.configField}, ${secret}).`;
|
|
2608
|
+
}
|
|
2609
|
+
function existingCredentialsMessage(credentials) {
|
|
2610
|
+
const secret = maskSecret(credentials.accessToken ?? credentials.apiKey) ?? "(unknown)";
|
|
2611
|
+
const label = credentials.source === "environment" ? `environment (${credentials.envVar})` : `config (${credentials.configField})`;
|
|
2612
|
+
return [
|
|
2613
|
+
`Existing credentials found: ${label} (${secret}).`,
|
|
2614
|
+
credentials.source === "environment" ? "Re-run with --force to save a config token anyway." : "Re-run with --force to replace them."
|
|
2615
|
+
].join(`
|
|
2616
|
+
`);
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
// src/cli/client.ts
|
|
2620
|
+
function createClient(options = {}) {
|
|
2621
|
+
const credentials = resolveCredentials();
|
|
2622
|
+
const common = {
|
|
2623
|
+
baseUrl: process.env.TMDB_BASE_URL,
|
|
2624
|
+
defaultLanguage: options.language ?? credentials.config.language ?? "en-US",
|
|
2625
|
+
defaultRegion: credentials.config.region
|
|
2626
|
+
};
|
|
2627
|
+
if (credentials.accessToken) {
|
|
2628
|
+
return createTMDB({ accessToken: credentials.accessToken, ...common });
|
|
2629
|
+
}
|
|
2630
|
+
if (credentials.apiKey) {
|
|
2631
|
+
return createTMDB({ apiKey: credentials.apiKey, ...common });
|
|
2632
|
+
}
|
|
2633
|
+
throw new CliError(missingCredentialsMessage(), 3);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
// src/cli/commands/config.ts
|
|
2637
|
+
var CONFIG_KEY_ALIASES = {
|
|
2638
|
+
token: "accessToken",
|
|
2639
|
+
accessToken: "accessToken",
|
|
2640
|
+
"api-key": "apiKey",
|
|
2641
|
+
apiKey: "apiKey",
|
|
2642
|
+
language: "language",
|
|
2643
|
+
region: "region"
|
|
2644
|
+
};
|
|
2645
|
+
var configCommand = defineCommand({
|
|
2646
|
+
meta: {
|
|
2647
|
+
name: "config",
|
|
2648
|
+
description: "Manage TMDB CLI configuration"
|
|
2649
|
+
},
|
|
2650
|
+
subCommands: {
|
|
2651
|
+
show: defineCommand({
|
|
2652
|
+
meta: {
|
|
2653
|
+
name: "show",
|
|
2654
|
+
description: "Show the resolved config with secrets masked"
|
|
2655
|
+
},
|
|
2656
|
+
args: { ...outputArgs },
|
|
2657
|
+
async run({ args }) {
|
|
2658
|
+
await runSafely(args, async (output) => {
|
|
2659
|
+
const config = readConfig();
|
|
2660
|
+
const path = getConfigPath();
|
|
2661
|
+
const view = {
|
|
2662
|
+
path,
|
|
2663
|
+
accessToken: describeSecret(config.accessToken),
|
|
2664
|
+
apiKey: describeSecret(config.apiKey),
|
|
2665
|
+
language: config.language ?? null,
|
|
2666
|
+
region: config.region ?? null
|
|
2667
|
+
};
|
|
2668
|
+
printResult(view, output, () => {
|
|
2669
|
+
process.stdout.write([
|
|
2670
|
+
`Path: ${path}`,
|
|
2671
|
+
`accessToken: ${view.accessToken ?? "(not set)"}`,
|
|
2672
|
+
`apiKey: ${view.apiKey ?? "(not set)"}`,
|
|
2673
|
+
`language: ${view.language ?? "(not set)"}`,
|
|
2674
|
+
`region: ${view.region ?? "(not set)"}`
|
|
2675
|
+
].join(`
|
|
2676
|
+
`) + `
|
|
2677
|
+
`);
|
|
2678
|
+
});
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
}),
|
|
2682
|
+
path: defineCommand({
|
|
2683
|
+
meta: {
|
|
2684
|
+
name: "path",
|
|
2685
|
+
description: "Print the config file path"
|
|
2686
|
+
},
|
|
2687
|
+
run() {
|
|
2688
|
+
process.stdout.write(`${getConfigPath()}
|
|
2689
|
+
`);
|
|
2690
|
+
}
|
|
2691
|
+
}),
|
|
2692
|
+
test: defineCommand({
|
|
2693
|
+
meta: {
|
|
2694
|
+
name: "test",
|
|
2695
|
+
description: "Verify credentials with an authenticated TMDB request"
|
|
2696
|
+
},
|
|
2697
|
+
args: { ...outputArgs },
|
|
2698
|
+
async run({ args }) {
|
|
2699
|
+
await runSafely(args, async (output) => {
|
|
2700
|
+
const client = createClient();
|
|
2701
|
+
await client.movies.details(550);
|
|
2702
|
+
printResult({ ok: true }, output, () => {
|
|
2703
|
+
process.stdout.write(`TMDB connection OK
|
|
2704
|
+
Credentials are valid.
|
|
2705
|
+
`);
|
|
2706
|
+
});
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
}),
|
|
2710
|
+
set: defineCommand({
|
|
2711
|
+
meta: {
|
|
2712
|
+
name: "set",
|
|
2713
|
+
description: "Set a config value (token and api-key are read securely)"
|
|
2714
|
+
},
|
|
2715
|
+
args: {
|
|
2716
|
+
key: {
|
|
2717
|
+
type: "positional",
|
|
2718
|
+
description: "token, api-key, language, or region",
|
|
2719
|
+
required: true
|
|
2720
|
+
},
|
|
2721
|
+
value: {
|
|
2722
|
+
type: "positional",
|
|
2723
|
+
description: "Value for language or region",
|
|
2724
|
+
required: false
|
|
2725
|
+
}
|
|
2726
|
+
},
|
|
2727
|
+
async run({ args }) {
|
|
2728
|
+
await runSafely({}, async () => {
|
|
2729
|
+
const field = parseConfigKey(args.key);
|
|
2730
|
+
const path = getConfigPath();
|
|
2731
|
+
if (field === "accessToken" || field === "apiKey") {
|
|
2732
|
+
if (args.value !== undefined) {
|
|
2733
|
+
throw new CliError("Do not pass secrets as command arguments. Run the command without a value to enter it securely.", 2);
|
|
2734
|
+
}
|
|
2735
|
+
const label = field === "accessToken" ? "TMDB access token" : "TMDB API key";
|
|
2736
|
+
const secret = await readSecret(`${label}: `);
|
|
2737
|
+
if (!secret) {
|
|
2738
|
+
throw new CliError("Secret must not be empty.", 2);
|
|
2739
|
+
}
|
|
2740
|
+
setConfigValue(field, secret);
|
|
2741
|
+
process.stdout.write(`Saved ${field} to ${path}
|
|
2742
|
+
`);
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
const value = args.value?.trim();
|
|
2746
|
+
if (!value) {
|
|
2747
|
+
throw new CliError(`A value is required for ${args.key}.`, 2);
|
|
2748
|
+
}
|
|
2749
|
+
setConfigValue(field, field === "region" ? value.toUpperCase() : value);
|
|
2750
|
+
process.stdout.write(`Saved ${field} to ${path}
|
|
2751
|
+
`);
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
}),
|
|
2755
|
+
unset: defineCommand({
|
|
2756
|
+
meta: {
|
|
2757
|
+
name: "unset",
|
|
2758
|
+
description: "Remove a config value"
|
|
2759
|
+
},
|
|
2760
|
+
args: {
|
|
2761
|
+
key: {
|
|
2762
|
+
type: "positional",
|
|
2763
|
+
description: "token, api-key, language, or region",
|
|
2764
|
+
required: true
|
|
2765
|
+
}
|
|
2766
|
+
},
|
|
2767
|
+
async run({ args }) {
|
|
2768
|
+
await runSafely({}, async () => {
|
|
2769
|
+
const field = parseConfigKey(args.key);
|
|
2770
|
+
unsetConfigValue(field);
|
|
2771
|
+
process.stdout.write(`Removed ${field} from ${getConfigPath()}
|
|
2772
|
+
`);
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
})
|
|
2776
|
+
}
|
|
2777
|
+
});
|
|
2778
|
+
function parseConfigKey(value) {
|
|
2779
|
+
if (!(value in CONFIG_KEY_ALIASES)) {
|
|
2780
|
+
throw new CliError(`Unknown config key "${value}". Expected one of: ${Object.keys(CONFIG_KEY_ALIASES).join(", ")}.`, 2);
|
|
2781
|
+
}
|
|
2782
|
+
return CONFIG_KEY_ALIASES[value];
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
// src/cli/commands/genres.ts
|
|
2786
|
+
var genresCommand = defineCommand({
|
|
2787
|
+
meta: {
|
|
2788
|
+
name: "genres",
|
|
2789
|
+
description: "List movie or TV genres"
|
|
2790
|
+
},
|
|
2791
|
+
args: {
|
|
2792
|
+
...commonArgs,
|
|
2793
|
+
type: {
|
|
2794
|
+
type: "positional",
|
|
2795
|
+
description: "Genre list type: movie or tv",
|
|
2796
|
+
default: "movie"
|
|
2797
|
+
}
|
|
2798
|
+
},
|
|
2799
|
+
async run({ args }) {
|
|
2800
|
+
await runSafely(args, async (output) => {
|
|
2801
|
+
const type = parseGenreType(args.type);
|
|
2802
|
+
const client = createClient({ language: args.lang });
|
|
2803
|
+
const response = type === "tv" ? await client.genres.tv() : await client.genres.movies();
|
|
2804
|
+
printResult(response, output, () => printGenres(response));
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
|
|
2809
|
+
// src/cli/commands/movie.ts
|
|
2810
|
+
var movieCommand = defineCommand({
|
|
2811
|
+
meta: {
|
|
2812
|
+
name: "movie",
|
|
2813
|
+
description: "Movie commands"
|
|
2814
|
+
},
|
|
2815
|
+
subCommands: {
|
|
2816
|
+
list: defineCommand({
|
|
2817
|
+
meta: {
|
|
2818
|
+
name: "list",
|
|
2819
|
+
description: "List popular, top-rated, now-playing, or upcoming movies"
|
|
2820
|
+
},
|
|
2821
|
+
args: {
|
|
2822
|
+
...commonArgs,
|
|
2823
|
+
...paginationArgs,
|
|
2824
|
+
category: {
|
|
2825
|
+
type: "positional",
|
|
2826
|
+
description: "popular, top-rated, now-playing, or upcoming",
|
|
2827
|
+
default: "popular"
|
|
2828
|
+
},
|
|
2829
|
+
page: {
|
|
2830
|
+
type: "string",
|
|
2831
|
+
description: "Page number"
|
|
2832
|
+
},
|
|
2833
|
+
region: {
|
|
2834
|
+
type: "string",
|
|
2835
|
+
description: "ISO 3166-1 region code"
|
|
2836
|
+
}
|
|
2837
|
+
},
|
|
2838
|
+
async run({ args }) {
|
|
2839
|
+
await runSafely(args, async (output) => {
|
|
2840
|
+
const category = parseMovieListCategory(args.category);
|
|
2841
|
+
const params = { page: parsePage(args.page), region: args.region };
|
|
2842
|
+
const client = createClient({ language: args.lang });
|
|
2843
|
+
await printPaged({
|
|
2844
|
+
fetchPage: (page) => {
|
|
2845
|
+
const options = { ...params, page };
|
|
2846
|
+
return category === "top-rated" ? client.movies.topRated(options) : category === "now-playing" ? client.movies.nowPlaying(options) : category === "upcoming" ? client.movies.upcoming(options) : client.movies.popular(options);
|
|
2847
|
+
},
|
|
2848
|
+
output,
|
|
2849
|
+
renderText: (response) => printSearchResults(response, "movie"),
|
|
2850
|
+
startPage: params.page
|
|
2851
|
+
});
|
|
2852
|
+
}, { paginated: true });
|
|
2853
|
+
}
|
|
2854
|
+
}),
|
|
2855
|
+
get: defineCommand({
|
|
2856
|
+
meta: {
|
|
2857
|
+
name: "get",
|
|
2858
|
+
description: "Get movie details by TMDB ID"
|
|
2859
|
+
},
|
|
2860
|
+
args: {
|
|
2861
|
+
...commonArgs,
|
|
2862
|
+
id: {
|
|
2863
|
+
type: "positional",
|
|
2864
|
+
description: "TMDB movie ID",
|
|
2865
|
+
required: true
|
|
2866
|
+
},
|
|
2867
|
+
append: {
|
|
2868
|
+
type: "string",
|
|
2869
|
+
description: "Comma-separated append_to_response values"
|
|
2870
|
+
}
|
|
2871
|
+
},
|
|
2872
|
+
async run({ args }) {
|
|
2873
|
+
await runSafely(args, async (output) => {
|
|
2874
|
+
const id = parseId(args.id, "movie ID");
|
|
2875
|
+
const client = createClient({ language: args.lang });
|
|
2876
|
+
const appendToResponse = parseCommaList(args.append);
|
|
2877
|
+
const movie = await client.movies.details(id, { appendToResponse });
|
|
2878
|
+
printResult(movie, output, () => printMovie(movie, client));
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
}),
|
|
2882
|
+
credits: defineCommand({
|
|
2883
|
+
meta: {
|
|
2884
|
+
name: "credits",
|
|
2885
|
+
description: "Show movie cast and crew"
|
|
2886
|
+
},
|
|
2887
|
+
args: {
|
|
2888
|
+
...commonArgs,
|
|
2889
|
+
id: {
|
|
2890
|
+
type: "positional",
|
|
2891
|
+
description: "TMDB movie ID",
|
|
2892
|
+
required: true
|
|
2893
|
+
},
|
|
2894
|
+
limit: {
|
|
2895
|
+
type: "string",
|
|
2896
|
+
description: "Maximum cast/crew rows (default: 10)"
|
|
2897
|
+
}
|
|
2898
|
+
},
|
|
2899
|
+
async run({ args }) {
|
|
2900
|
+
await runSafely(args, async (output) => {
|
|
2901
|
+
const id = parseId(args.id, "movie ID");
|
|
2902
|
+
const limit = parseLimit(args.limit);
|
|
2903
|
+
const client = createClient({ language: args.lang });
|
|
2904
|
+
const credits = await client.movies.credits(id);
|
|
2905
|
+
printResult(credits, output, () => printCredits(credits, limit));
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
}),
|
|
2909
|
+
images: defineCommand({
|
|
2910
|
+
meta: {
|
|
2911
|
+
name: "images",
|
|
2912
|
+
description: "Show movie image URLs"
|
|
2913
|
+
},
|
|
2914
|
+
args: {
|
|
2915
|
+
...commonArgs,
|
|
2916
|
+
id: {
|
|
2917
|
+
type: "positional",
|
|
2918
|
+
description: "TMDB movie ID",
|
|
2919
|
+
required: true
|
|
2920
|
+
},
|
|
2921
|
+
"image-language": {
|
|
2922
|
+
type: "string",
|
|
2923
|
+
description: "Comma-separated image language codes"
|
|
2924
|
+
}
|
|
2925
|
+
},
|
|
2926
|
+
async run({ args }) {
|
|
2927
|
+
await runSafely(args, async (output) => {
|
|
2928
|
+
const id = parseId(args.id, "movie ID");
|
|
2929
|
+
const includeImageLanguage = parseCommaList(args["image-language"]);
|
|
2930
|
+
const client = createClient({ language: args.lang });
|
|
2931
|
+
const images = await client.movies.images(id, { includeImageLanguage });
|
|
2932
|
+
printResult(images, output, () => printImages(images, client));
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
}),
|
|
2936
|
+
videos: defineCommand({
|
|
2937
|
+
meta: {
|
|
2938
|
+
name: "videos",
|
|
2939
|
+
description: "Show movie videos and trailers"
|
|
2940
|
+
},
|
|
2941
|
+
args: {
|
|
2942
|
+
...commonArgs,
|
|
2943
|
+
id: {
|
|
2944
|
+
type: "positional",
|
|
2945
|
+
description: "TMDB movie ID",
|
|
2946
|
+
required: true
|
|
2947
|
+
}
|
|
2948
|
+
},
|
|
2949
|
+
async run({ args }) {
|
|
2950
|
+
await runSafely(args, async (output) => {
|
|
2951
|
+
const id = parseId(args.id, "movie ID");
|
|
2952
|
+
const client = createClient({ language: args.lang });
|
|
2953
|
+
const videos = await client.movies.videos(id);
|
|
2954
|
+
printResult(videos, output, () => printVideos(videos));
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
}),
|
|
2958
|
+
similar: defineCommand({
|
|
2959
|
+
meta: {
|
|
2960
|
+
name: "similar",
|
|
2961
|
+
description: "List movies similar to a movie"
|
|
2962
|
+
},
|
|
2963
|
+
args: {
|
|
2964
|
+
...commonArgs,
|
|
2965
|
+
id: {
|
|
2966
|
+
type: "positional",
|
|
2967
|
+
description: "TMDB movie ID",
|
|
2968
|
+
required: true
|
|
2969
|
+
},
|
|
2970
|
+
page: {
|
|
2971
|
+
type: "string",
|
|
2972
|
+
description: "Page number"
|
|
2973
|
+
},
|
|
2974
|
+
region: {
|
|
2975
|
+
type: "string",
|
|
2976
|
+
description: "ISO 3166-1 region code"
|
|
2977
|
+
},
|
|
2978
|
+
...paginationArgs
|
|
2979
|
+
},
|
|
2980
|
+
async run({ args }) {
|
|
2981
|
+
await runSafely(args, async (output) => {
|
|
2982
|
+
const id = parseId(args.id, "movie ID");
|
|
2983
|
+
const params = { page: parsePage(args.page), region: args.region };
|
|
2984
|
+
const client = createClient({ language: args.lang });
|
|
2985
|
+
await printPaged({
|
|
2986
|
+
fetchPage: (page) => client.movies.similar(id, { ...params, page }),
|
|
2987
|
+
output,
|
|
2988
|
+
renderText: (response) => printSearchResults(response, "movie"),
|
|
2989
|
+
startPage: params.page
|
|
2990
|
+
});
|
|
2991
|
+
}, { paginated: true });
|
|
2992
|
+
}
|
|
2993
|
+
}),
|
|
2994
|
+
recommendations: defineCommand({
|
|
2995
|
+
meta: {
|
|
2996
|
+
name: "recommendations",
|
|
2997
|
+
description: "List recommended movies for a movie"
|
|
2998
|
+
},
|
|
2999
|
+
args: {
|
|
3000
|
+
...commonArgs,
|
|
3001
|
+
id: {
|
|
3002
|
+
type: "positional",
|
|
3003
|
+
description: "TMDB movie ID",
|
|
3004
|
+
required: true
|
|
3005
|
+
},
|
|
3006
|
+
page: {
|
|
3007
|
+
type: "string",
|
|
3008
|
+
description: "Page number"
|
|
3009
|
+
},
|
|
3010
|
+
region: {
|
|
3011
|
+
type: "string",
|
|
3012
|
+
description: "ISO 3166-1 region code"
|
|
3013
|
+
},
|
|
3014
|
+
...paginationArgs
|
|
3015
|
+
},
|
|
3016
|
+
async run({ args }) {
|
|
3017
|
+
await runSafely(args, async (output) => {
|
|
3018
|
+
const id = parseId(args.id, "movie ID");
|
|
3019
|
+
const params = { page: parsePage(args.page), region: args.region };
|
|
3020
|
+
const client = createClient({ language: args.lang });
|
|
3021
|
+
await printPaged({
|
|
3022
|
+
fetchPage: (page) => client.movies.recommendations(id, { ...params, page }),
|
|
3023
|
+
output,
|
|
3024
|
+
renderText: (response) => printSearchResults(response, "movie"),
|
|
3025
|
+
startPage: params.page
|
|
3026
|
+
});
|
|
3027
|
+
}, { paginated: true });
|
|
3028
|
+
}
|
|
3029
|
+
})
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
|
|
3033
|
+
// src/cli/commands/person.ts
|
|
3034
|
+
var personCommand = defineCommand({
|
|
3035
|
+
meta: {
|
|
3036
|
+
name: "person",
|
|
3037
|
+
description: "Person commands"
|
|
3038
|
+
},
|
|
3039
|
+
subCommands: {
|
|
3040
|
+
get: defineCommand({
|
|
3041
|
+
meta: {
|
|
3042
|
+
name: "get",
|
|
3043
|
+
description: "Get person details by TMDB ID"
|
|
3044
|
+
},
|
|
3045
|
+
args: {
|
|
3046
|
+
...commonArgs,
|
|
3047
|
+
id: {
|
|
3048
|
+
type: "positional",
|
|
3049
|
+
description: "TMDB person ID",
|
|
3050
|
+
required: true
|
|
3051
|
+
}
|
|
3052
|
+
},
|
|
3053
|
+
async run({ args }) {
|
|
3054
|
+
await runSafely(args, async (output) => {
|
|
3055
|
+
const id = parseId(args.id, "person ID");
|
|
3056
|
+
const client = createClient({ language: args.lang });
|
|
3057
|
+
const person = await client.people.details(id);
|
|
3058
|
+
printResult(person, output, () => printPerson(person, client));
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
}),
|
|
3062
|
+
credits: defineCommand({
|
|
3063
|
+
meta: {
|
|
3064
|
+
name: "credits",
|
|
3065
|
+
description: "Show a person combined cast and crew credits"
|
|
3066
|
+
},
|
|
3067
|
+
args: {
|
|
3068
|
+
...commonArgs,
|
|
3069
|
+
id: {
|
|
3070
|
+
type: "positional",
|
|
3071
|
+
description: "TMDB person ID",
|
|
3072
|
+
required: true
|
|
3073
|
+
},
|
|
3074
|
+
limit: {
|
|
3075
|
+
type: "string",
|
|
3076
|
+
description: "Maximum rows per section (default: 10)"
|
|
3077
|
+
}
|
|
3078
|
+
},
|
|
3079
|
+
async run({ args }) {
|
|
3080
|
+
await runSafely(args, async (output) => {
|
|
3081
|
+
const id = parseId(args.id, "person ID");
|
|
3082
|
+
const limit = parseLimit(args.limit);
|
|
3083
|
+
const client = createClient({ language: args.lang });
|
|
3084
|
+
const credits = await client.people.combinedCredits(id);
|
|
3085
|
+
printResult(credits, output, () => printPersonCredits(credits, limit));
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
})
|
|
3089
|
+
}
|
|
3090
|
+
});
|
|
3091
|
+
|
|
3092
|
+
// src/cli/commands/search.ts
|
|
3093
|
+
var searchCommand = defineCommand({
|
|
3094
|
+
meta: {
|
|
3095
|
+
name: "search",
|
|
3096
|
+
description: "Search movies, TV shows, and people"
|
|
3097
|
+
},
|
|
3098
|
+
args: {
|
|
3099
|
+
...commonArgs,
|
|
3100
|
+
...paginationArgs,
|
|
3101
|
+
query: {
|
|
3102
|
+
type: "positional",
|
|
3103
|
+
description: "Search query",
|
|
3104
|
+
required: true
|
|
3105
|
+
},
|
|
3106
|
+
type: {
|
|
3107
|
+
type: "enum",
|
|
3108
|
+
description: "Media type to search",
|
|
3109
|
+
options: [...SEARCH_TYPES],
|
|
3110
|
+
default: "all"
|
|
3111
|
+
},
|
|
3112
|
+
page: {
|
|
3113
|
+
type: "string",
|
|
3114
|
+
description: "Page number"
|
|
3115
|
+
},
|
|
3116
|
+
year: {
|
|
3117
|
+
type: "string",
|
|
3118
|
+
description: "Release year for movies, first-air year for TV"
|
|
3119
|
+
}
|
|
3120
|
+
},
|
|
3121
|
+
async run({ args }) {
|
|
3122
|
+
await runSafely(args, async (output) => {
|
|
3123
|
+
const query = args.query.trim();
|
|
3124
|
+
if (!query) {
|
|
3125
|
+
throw new CliError("Search query must not be empty.", 2);
|
|
3126
|
+
}
|
|
3127
|
+
const type = parseSearchType(args.type);
|
|
3128
|
+
const page = parsePage(args.page);
|
|
3129
|
+
const year = args.year?.trim() || undefined;
|
|
3130
|
+
if (year && type !== "movie" && type !== "tv") {
|
|
3131
|
+
throw new CliError("--year is only supported with --type movie or tv.", 2);
|
|
3132
|
+
}
|
|
3133
|
+
const client = createClient({ language: args.lang });
|
|
3134
|
+
await printPaged({
|
|
3135
|
+
fetchPage: (nextPage) => {
|
|
3136
|
+
const options = { page: nextPage };
|
|
3137
|
+
return type === "movie" ? client.search.movies(query, { ...options, year }) : type === "tv" ? client.search.tv(query, { ...options, firstAirDateYear: year }) : type === "person" ? client.search.people(query, options) : client.search.multi(query, options);
|
|
3138
|
+
},
|
|
3139
|
+
output,
|
|
3140
|
+
renderText: (response) => printSearchResults(response, type),
|
|
3141
|
+
startPage: page
|
|
3142
|
+
});
|
|
3143
|
+
}, { paginated: true });
|
|
3144
|
+
}
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
// src/cli/commands/trending.ts
|
|
3148
|
+
var trendingCommand = defineCommand({
|
|
3149
|
+
meta: {
|
|
3150
|
+
name: "trending",
|
|
3151
|
+
description: "Show trending movies, TV shows, people, or all media"
|
|
3152
|
+
},
|
|
3153
|
+
args: {
|
|
3154
|
+
...commonArgs,
|
|
3155
|
+
...paginationArgs,
|
|
3156
|
+
type: {
|
|
3157
|
+
type: "positional",
|
|
3158
|
+
description: "Media type: all, movie, tv, or person",
|
|
3159
|
+
default: "all"
|
|
3160
|
+
},
|
|
3161
|
+
window: {
|
|
3162
|
+
type: "enum",
|
|
3163
|
+
description: "Trending window",
|
|
3164
|
+
options: [...TRENDING_WINDOWS],
|
|
3165
|
+
default: "week"
|
|
3166
|
+
},
|
|
3167
|
+
page: {
|
|
3168
|
+
type: "string",
|
|
3169
|
+
description: "Page number"
|
|
3170
|
+
}
|
|
3171
|
+
},
|
|
3172
|
+
async run({ args }) {
|
|
3173
|
+
await runSafely(args, async (output) => {
|
|
3174
|
+
const type = parseSearchType(args.type);
|
|
3175
|
+
const window = parseWindow(args.window);
|
|
3176
|
+
const page = parsePage(args.page);
|
|
3177
|
+
const client = createClient({ language: args.lang });
|
|
3178
|
+
await printPaged({
|
|
3179
|
+
fetchPage: (nextPage) => {
|
|
3180
|
+
const options = { page: nextPage };
|
|
3181
|
+
return type === "movie" ? client.trending.movies(window, options) : type === "tv" ? client.trending.tv(window, options) : type === "person" ? client.trending.people(window, options) : client.trending.all(window, options);
|
|
3182
|
+
},
|
|
3183
|
+
output,
|
|
3184
|
+
renderText: (response) => printSearchResults(response, type),
|
|
3185
|
+
startPage: page
|
|
3186
|
+
});
|
|
3187
|
+
}, { paginated: true });
|
|
3188
|
+
}
|
|
3189
|
+
});
|
|
3190
|
+
|
|
3191
|
+
// src/cli/commands/tv.ts
|
|
3192
|
+
var tvCommand = defineCommand({
|
|
3193
|
+
meta: {
|
|
3194
|
+
name: "tv",
|
|
3195
|
+
description: "TV show commands"
|
|
3196
|
+
},
|
|
3197
|
+
subCommands: {
|
|
3198
|
+
list: defineCommand({
|
|
3199
|
+
meta: {
|
|
3200
|
+
name: "list",
|
|
3201
|
+
description: "List popular, top-rated, airing-today, or on-the-air shows"
|
|
3202
|
+
},
|
|
3203
|
+
args: {
|
|
3204
|
+
...commonArgs,
|
|
3205
|
+
...paginationArgs,
|
|
3206
|
+
category: {
|
|
3207
|
+
type: "positional",
|
|
3208
|
+
description: "popular, top-rated, airing-today, or on-the-air",
|
|
3209
|
+
default: "popular"
|
|
3210
|
+
},
|
|
3211
|
+
page: {
|
|
3212
|
+
type: "string",
|
|
3213
|
+
description: "Page number"
|
|
3214
|
+
}
|
|
3215
|
+
},
|
|
3216
|
+
async run({ args }) {
|
|
3217
|
+
await runSafely(args, async (output) => {
|
|
3218
|
+
const category = parseTVListCategory(args.category);
|
|
3219
|
+
const params = { page: parsePage(args.page) };
|
|
3220
|
+
const client = createClient({ language: args.lang });
|
|
3221
|
+
await printPaged({
|
|
3222
|
+
fetchPage: (page) => {
|
|
3223
|
+
const options = { ...params, page };
|
|
3224
|
+
return category === "top-rated" ? client.tv.topRated(options) : category === "airing-today" ? client.tv.airingToday(options) : category === "on-the-air" ? client.tv.onTheAir(options) : client.tv.popular(options);
|
|
3225
|
+
},
|
|
3226
|
+
output,
|
|
3227
|
+
renderText: (response) => printSearchResults(response, "tv"),
|
|
3228
|
+
startPage: params.page
|
|
3229
|
+
});
|
|
3230
|
+
}, { paginated: true });
|
|
3231
|
+
}
|
|
3232
|
+
}),
|
|
3233
|
+
get: defineCommand({
|
|
3234
|
+
meta: {
|
|
3235
|
+
name: "get",
|
|
3236
|
+
description: "Get TV show details by TMDB ID"
|
|
3237
|
+
},
|
|
3238
|
+
args: {
|
|
3239
|
+
...commonArgs,
|
|
3240
|
+
id: {
|
|
3241
|
+
type: "positional",
|
|
3242
|
+
description: "TMDB TV show ID",
|
|
3243
|
+
required: true
|
|
3244
|
+
},
|
|
3245
|
+
append: {
|
|
3246
|
+
type: "string",
|
|
3247
|
+
description: "Comma-separated append_to_response values"
|
|
3248
|
+
}
|
|
3249
|
+
},
|
|
3250
|
+
async run({ args }) {
|
|
3251
|
+
await runSafely(args, async (output) => {
|
|
3252
|
+
const id = parseId(args.id, "TV show ID");
|
|
3253
|
+
const client = createClient({ language: args.lang });
|
|
3254
|
+
const appendToResponse = parseCommaList(args.append);
|
|
3255
|
+
const tv = await client.tv.details(id, { appendToResponse });
|
|
3256
|
+
printResult(tv, output, () => printTV(tv, client));
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
}),
|
|
3260
|
+
credits: defineCommand({
|
|
3261
|
+
meta: {
|
|
3262
|
+
name: "credits",
|
|
3263
|
+
description: "Show TV show cast and crew"
|
|
3264
|
+
},
|
|
3265
|
+
args: {
|
|
3266
|
+
...commonArgs,
|
|
3267
|
+
id: {
|
|
3268
|
+
type: "positional",
|
|
3269
|
+
description: "TMDB TV show ID",
|
|
3270
|
+
required: true
|
|
3271
|
+
},
|
|
3272
|
+
limit: {
|
|
3273
|
+
type: "string",
|
|
3274
|
+
description: "Maximum cast/crew rows (default: 10)"
|
|
3275
|
+
}
|
|
3276
|
+
},
|
|
3277
|
+
async run({ args }) {
|
|
3278
|
+
await runSafely(args, async (output) => {
|
|
3279
|
+
const id = parseId(args.id, "TV show ID");
|
|
3280
|
+
const limit = parseLimit(args.limit);
|
|
3281
|
+
const client = createClient({ language: args.lang });
|
|
3282
|
+
const credits = await client.tv.credits(id);
|
|
3283
|
+
printResult(credits, output, () => printCredits(credits, limit));
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
}),
|
|
3287
|
+
images: defineCommand({
|
|
3288
|
+
meta: {
|
|
3289
|
+
name: "images",
|
|
3290
|
+
description: "Show TV show image URLs"
|
|
3291
|
+
},
|
|
3292
|
+
args: {
|
|
3293
|
+
...commonArgs,
|
|
3294
|
+
id: {
|
|
3295
|
+
type: "positional",
|
|
3296
|
+
description: "TMDB TV show ID",
|
|
3297
|
+
required: true
|
|
3298
|
+
},
|
|
3299
|
+
"image-language": {
|
|
3300
|
+
type: "string",
|
|
3301
|
+
description: "Comma-separated image language codes"
|
|
3302
|
+
}
|
|
3303
|
+
},
|
|
3304
|
+
async run({ args }) {
|
|
3305
|
+
await runSafely(args, async (output) => {
|
|
3306
|
+
const id = parseId(args.id, "TV show ID");
|
|
3307
|
+
const includeImageLanguage = parseCommaList(args["image-language"]);
|
|
3308
|
+
const client = createClient({ language: args.lang });
|
|
3309
|
+
const images = await client.tv.images(id, { includeImageLanguage });
|
|
3310
|
+
printResult(images, output, () => printImages(images, client));
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
}),
|
|
3314
|
+
videos: defineCommand({
|
|
3315
|
+
meta: {
|
|
3316
|
+
name: "videos",
|
|
3317
|
+
description: "Show TV show videos and trailers"
|
|
3318
|
+
},
|
|
3319
|
+
args: {
|
|
3320
|
+
...commonArgs,
|
|
3321
|
+
id: {
|
|
3322
|
+
type: "positional",
|
|
3323
|
+
description: "TMDB TV show ID",
|
|
3324
|
+
required: true
|
|
3325
|
+
}
|
|
3326
|
+
},
|
|
3327
|
+
async run({ args }) {
|
|
3328
|
+
await runSafely(args, async (output) => {
|
|
3329
|
+
const id = parseId(args.id, "TV show ID");
|
|
3330
|
+
const client = createClient({ language: args.lang });
|
|
3331
|
+
const videos = await client.tv.videos(id);
|
|
3332
|
+
printResult(videos, output, () => printVideos(videos));
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
}),
|
|
3336
|
+
similar: defineCommand({
|
|
3337
|
+
meta: {
|
|
3338
|
+
name: "similar",
|
|
3339
|
+
description: "List TV shows similar to a show"
|
|
3340
|
+
},
|
|
3341
|
+
args: {
|
|
3342
|
+
...commonArgs,
|
|
3343
|
+
...paginationArgs,
|
|
3344
|
+
id: {
|
|
3345
|
+
type: "positional",
|
|
3346
|
+
description: "TMDB TV show ID",
|
|
3347
|
+
required: true
|
|
3348
|
+
},
|
|
3349
|
+
page: {
|
|
3350
|
+
type: "string",
|
|
3351
|
+
description: "Page number"
|
|
3352
|
+
}
|
|
3353
|
+
},
|
|
3354
|
+
async run({ args }) {
|
|
3355
|
+
await runSafely(args, async (output) => {
|
|
3356
|
+
const id = parseId(args.id, "TV show ID");
|
|
3357
|
+
const params = { page: parsePage(args.page) };
|
|
3358
|
+
const client = createClient({ language: args.lang });
|
|
3359
|
+
await printPaged({
|
|
3360
|
+
fetchPage: (page) => client.tv.similar(id, { ...params, page }),
|
|
3361
|
+
output,
|
|
3362
|
+
renderText: (response) => printSearchResults(response, "tv"),
|
|
3363
|
+
startPage: params.page
|
|
3364
|
+
});
|
|
3365
|
+
}, { paginated: true });
|
|
3366
|
+
}
|
|
3367
|
+
}),
|
|
3368
|
+
recommendations: defineCommand({
|
|
3369
|
+
meta: {
|
|
3370
|
+
name: "recommendations",
|
|
3371
|
+
description: "List recommended TV shows for a show"
|
|
3372
|
+
},
|
|
3373
|
+
args: {
|
|
3374
|
+
...commonArgs,
|
|
3375
|
+
...paginationArgs,
|
|
3376
|
+
id: {
|
|
3377
|
+
type: "positional",
|
|
3378
|
+
description: "TMDB TV show ID",
|
|
3379
|
+
required: true
|
|
3380
|
+
},
|
|
3381
|
+
page: {
|
|
3382
|
+
type: "string",
|
|
3383
|
+
description: "Page number"
|
|
3384
|
+
}
|
|
3385
|
+
},
|
|
3386
|
+
async run({ args }) {
|
|
3387
|
+
await runSafely(args, async (output) => {
|
|
3388
|
+
const id = parseId(args.id, "TV show ID");
|
|
3389
|
+
const params = { page: parsePage(args.page) };
|
|
3390
|
+
const client = createClient({ language: args.lang });
|
|
3391
|
+
await printPaged({
|
|
3392
|
+
fetchPage: (page) => client.tv.recommendations(id, { ...params, page }),
|
|
3393
|
+
output,
|
|
3394
|
+
renderText: (response) => printSearchResults(response, "tv"),
|
|
3395
|
+
startPage: params.page
|
|
3396
|
+
});
|
|
3397
|
+
}, { paginated: true });
|
|
3398
|
+
}
|
|
3399
|
+
}),
|
|
3400
|
+
season: defineCommand({
|
|
3401
|
+
meta: {
|
|
3402
|
+
name: "season",
|
|
3403
|
+
description: "Show a TV season and its episodes"
|
|
3404
|
+
},
|
|
3405
|
+
args: {
|
|
3406
|
+
...commonArgs,
|
|
3407
|
+
id: {
|
|
3408
|
+
type: "positional",
|
|
3409
|
+
description: "TMDB TV show ID",
|
|
3410
|
+
required: true
|
|
3411
|
+
},
|
|
3412
|
+
season: {
|
|
3413
|
+
type: "positional",
|
|
3414
|
+
description: "Season number (0 for specials)",
|
|
3415
|
+
required: true
|
|
3416
|
+
}
|
|
3417
|
+
},
|
|
3418
|
+
async run({ args }) {
|
|
3419
|
+
await runSafely(args, async (output) => {
|
|
3420
|
+
const id = parseId(args.id, "TV show ID");
|
|
3421
|
+
const seasonNumber = parseSeasonNumber(args.season);
|
|
3422
|
+
const client = createClient({ language: args.lang });
|
|
3423
|
+
const season = await client.tv.seasonDetails(id, seasonNumber);
|
|
3424
|
+
printResult(season, output, () => printSeason(season, client));
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
}),
|
|
3428
|
+
episode: defineCommand({
|
|
3429
|
+
meta: {
|
|
3430
|
+
name: "episode",
|
|
3431
|
+
description: "Show a TV episode"
|
|
3432
|
+
},
|
|
3433
|
+
args: {
|
|
3434
|
+
...commonArgs,
|
|
3435
|
+
id: {
|
|
3436
|
+
type: "positional",
|
|
3437
|
+
description: "TMDB TV show ID",
|
|
3438
|
+
required: true
|
|
3439
|
+
},
|
|
3440
|
+
season: {
|
|
3441
|
+
type: "positional",
|
|
3442
|
+
description: "Season number (0 for specials)",
|
|
3443
|
+
required: true
|
|
3444
|
+
},
|
|
3445
|
+
episode: {
|
|
3446
|
+
type: "positional",
|
|
3447
|
+
description: "Episode number",
|
|
3448
|
+
required: true
|
|
3449
|
+
},
|
|
3450
|
+
append: {
|
|
3451
|
+
type: "string",
|
|
3452
|
+
description: "Comma-separated append_to_response values"
|
|
3453
|
+
}
|
|
3454
|
+
},
|
|
3455
|
+
async run({ args }) {
|
|
3456
|
+
await runSafely(args, async (output) => {
|
|
3457
|
+
const id = parseId(args.id, "TV show ID");
|
|
3458
|
+
const seasonNumber = parseSeasonNumber(args.season);
|
|
3459
|
+
const episodeNumber = parseId(args.episode, "episode number");
|
|
3460
|
+
const appendToResponse = parseCommaList(args.append);
|
|
3461
|
+
const client = createClient({ language: args.lang });
|
|
3462
|
+
const episode = await client.tv.episodeDetails(id, seasonNumber, episodeNumber, {
|
|
3463
|
+
appendToResponse
|
|
3464
|
+
});
|
|
3465
|
+
printResult(episode, output, () => printEpisode(episode, client, id));
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
}),
|
|
3469
|
+
"aggregate-credits": defineCommand({
|
|
3470
|
+
meta: {
|
|
3471
|
+
name: "aggregate-credits",
|
|
3472
|
+
description: "Show aggregate cast and crew credits for a TV show"
|
|
3473
|
+
},
|
|
3474
|
+
args: {
|
|
3475
|
+
...commonArgs,
|
|
3476
|
+
id: {
|
|
3477
|
+
type: "positional",
|
|
3478
|
+
description: "TMDB TV show ID",
|
|
3479
|
+
required: true
|
|
3480
|
+
},
|
|
3481
|
+
limit: {
|
|
3482
|
+
type: "string",
|
|
3483
|
+
description: "Maximum cast/crew rows (default: 10)"
|
|
3484
|
+
}
|
|
3485
|
+
},
|
|
3486
|
+
async run({ args }) {
|
|
3487
|
+
await runSafely(args, async (output) => {
|
|
3488
|
+
const id = parseId(args.id, "TV show ID");
|
|
3489
|
+
const limit = parseLimit(args.limit);
|
|
3490
|
+
const client = createClient({ language: args.lang });
|
|
3491
|
+
const credits = await client.tv.aggregateCredits(id);
|
|
3492
|
+
printResult(credits, output, () => printAggregateCredits(credits, limit));
|
|
3493
|
+
});
|
|
3494
|
+
}
|
|
3495
|
+
}),
|
|
3496
|
+
"season-credits": defineCommand({
|
|
3497
|
+
meta: {
|
|
3498
|
+
name: "season-credits",
|
|
3499
|
+
description: "Show cast and crew credits for a TV season"
|
|
3500
|
+
},
|
|
3501
|
+
args: {
|
|
3502
|
+
...commonArgs,
|
|
3503
|
+
id: {
|
|
3504
|
+
type: "positional",
|
|
3505
|
+
description: "TMDB TV show ID",
|
|
3506
|
+
required: true
|
|
3507
|
+
},
|
|
3508
|
+
season: {
|
|
3509
|
+
type: "positional",
|
|
3510
|
+
description: "Season number (0 for specials)",
|
|
3511
|
+
required: true
|
|
3512
|
+
},
|
|
3513
|
+
limit: {
|
|
3514
|
+
type: "string",
|
|
3515
|
+
description: "Maximum cast/crew rows (default: 10)"
|
|
3516
|
+
}
|
|
3517
|
+
},
|
|
3518
|
+
async run({ args }) {
|
|
3519
|
+
await runSafely(args, async (output) => {
|
|
3520
|
+
const id = parseId(args.id, "TV show ID");
|
|
3521
|
+
const seasonNumber = parseSeasonNumber(args.season);
|
|
3522
|
+
const limit = parseLimit(args.limit);
|
|
3523
|
+
const client = createClient({ language: args.lang });
|
|
3524
|
+
const credits = await client.tv.seasonCredits(id, seasonNumber);
|
|
3525
|
+
printResult(credits, output, () => printCredits(credits, limit));
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
}),
|
|
3529
|
+
"season-aggregate-credits": defineCommand({
|
|
3530
|
+
meta: {
|
|
3531
|
+
name: "season-aggregate-credits",
|
|
3532
|
+
description: "Show aggregate cast and crew credits for a TV season"
|
|
3533
|
+
},
|
|
3534
|
+
args: {
|
|
3535
|
+
...commonArgs,
|
|
3536
|
+
id: {
|
|
3537
|
+
type: "positional",
|
|
3538
|
+
description: "TMDB TV show ID",
|
|
3539
|
+
required: true
|
|
3540
|
+
},
|
|
3541
|
+
season: {
|
|
3542
|
+
type: "positional",
|
|
3543
|
+
description: "Season number (0 for specials)",
|
|
3544
|
+
required: true
|
|
3545
|
+
},
|
|
3546
|
+
limit: {
|
|
3547
|
+
type: "string",
|
|
3548
|
+
description: "Maximum cast/crew rows (default: 10)"
|
|
3549
|
+
}
|
|
3550
|
+
},
|
|
3551
|
+
async run({ args }) {
|
|
3552
|
+
await runSafely(args, async (output) => {
|
|
3553
|
+
const id = parseId(args.id, "TV show ID");
|
|
3554
|
+
const seasonNumber = parseSeasonNumber(args.season);
|
|
3555
|
+
const limit = parseLimit(args.limit);
|
|
3556
|
+
const client = createClient({ language: args.lang });
|
|
3557
|
+
const credits = await client.tv.seasonAggregateCredits(id, seasonNumber);
|
|
3558
|
+
printResult(credits, output, () => printAggregateCredits(credits, limit));
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
}),
|
|
3562
|
+
"season-images": defineCommand({
|
|
3563
|
+
meta: {
|
|
3564
|
+
name: "season-images",
|
|
3565
|
+
description: "Show TV season image URLs"
|
|
3566
|
+
},
|
|
3567
|
+
args: {
|
|
3568
|
+
...commonArgs,
|
|
3569
|
+
id: {
|
|
3570
|
+
type: "positional",
|
|
3571
|
+
description: "TMDB TV show ID",
|
|
3572
|
+
required: true
|
|
3573
|
+
},
|
|
3574
|
+
season: {
|
|
3575
|
+
type: "positional",
|
|
3576
|
+
description: "Season number (0 for specials)",
|
|
3577
|
+
required: true
|
|
3578
|
+
},
|
|
3579
|
+
"image-language": {
|
|
3580
|
+
type: "string",
|
|
3581
|
+
description: "Comma-separated image language codes"
|
|
3582
|
+
}
|
|
3583
|
+
},
|
|
3584
|
+
async run({ args }) {
|
|
3585
|
+
await runSafely(args, async (output) => {
|
|
3586
|
+
const id = parseId(args.id, "TV show ID");
|
|
3587
|
+
const seasonNumber = parseSeasonNumber(args.season);
|
|
3588
|
+
const includeImageLanguage = parseCommaList(args["image-language"]);
|
|
3589
|
+
const client = createClient({ language: args.lang });
|
|
3590
|
+
const images = await client.tv.seasonImages(id, seasonNumber, { includeImageLanguage });
|
|
3591
|
+
printResult(images, output, () => printSeasonImages(images, client));
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
}),
|
|
3595
|
+
"season-videos": defineCommand({
|
|
3596
|
+
meta: {
|
|
3597
|
+
name: "season-videos",
|
|
3598
|
+
description: "Show TV season videos and trailers"
|
|
3599
|
+
},
|
|
3600
|
+
args: {
|
|
3601
|
+
...commonArgs,
|
|
3602
|
+
id: {
|
|
3603
|
+
type: "positional",
|
|
3604
|
+
description: "TMDB TV show ID",
|
|
3605
|
+
required: true
|
|
3606
|
+
},
|
|
3607
|
+
season: {
|
|
3608
|
+
type: "positional",
|
|
3609
|
+
description: "Season number (0 for specials)",
|
|
3610
|
+
required: true
|
|
3611
|
+
}
|
|
3612
|
+
},
|
|
3613
|
+
async run({ args }) {
|
|
3614
|
+
await runSafely(args, async (output) => {
|
|
3615
|
+
const id = parseId(args.id, "TV show ID");
|
|
3616
|
+
const seasonNumber = parseSeasonNumber(args.season);
|
|
3617
|
+
const client = createClient({ language: args.lang });
|
|
3618
|
+
const videos = await client.tv.seasonVideos(id, seasonNumber);
|
|
3619
|
+
printResult(videos, output, () => printVideos(videos));
|
|
3620
|
+
});
|
|
3621
|
+
}
|
|
3622
|
+
}),
|
|
3623
|
+
"episode-groups": defineCommand({
|
|
3624
|
+
meta: {
|
|
3625
|
+
name: "episode-groups",
|
|
3626
|
+
description: "List alternate episode groupings for a TV show"
|
|
3627
|
+
},
|
|
3628
|
+
args: {
|
|
3629
|
+
...commonArgs,
|
|
3630
|
+
id: {
|
|
3631
|
+
type: "positional",
|
|
3632
|
+
description: "TMDB TV show ID",
|
|
3633
|
+
required: true
|
|
3634
|
+
}
|
|
3635
|
+
},
|
|
3636
|
+
async run({ args }) {
|
|
3637
|
+
await runSafely(args, async (output) => {
|
|
3638
|
+
const id = parseId(args.id, "TV show ID");
|
|
3639
|
+
const client = createClient({ language: args.lang });
|
|
3640
|
+
const groups = await client.tv.episodeGroups(id);
|
|
3641
|
+
printResult(groups, output, () => printEpisodeGroups(groups));
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
}),
|
|
3645
|
+
"episode-group": defineCommand({
|
|
3646
|
+
meta: {
|
|
3647
|
+
name: "episode-group",
|
|
3648
|
+
description: "Show an alternate episode group"
|
|
3649
|
+
},
|
|
3650
|
+
args: {
|
|
3651
|
+
...commonArgs,
|
|
3652
|
+
"group-id": {
|
|
3653
|
+
type: "positional",
|
|
3654
|
+
description: "TMDB episode group ID",
|
|
3655
|
+
required: true
|
|
3656
|
+
}
|
|
3657
|
+
},
|
|
3658
|
+
async run({ args }) {
|
|
3659
|
+
await runSafely(args, async (output) => {
|
|
3660
|
+
const groupId = parseGroupId(args["group-id"]);
|
|
3661
|
+
const client = createClient({ language: args.lang });
|
|
3662
|
+
const group = await client.tv.episodeGroup(groupId);
|
|
3663
|
+
printResult(group, output, () => printEpisodeGroup(group));
|
|
3664
|
+
});
|
|
3665
|
+
}
|
|
3666
|
+
})
|
|
3667
|
+
}
|
|
3668
|
+
});
|
|
3669
|
+
|
|
3670
|
+
// src/cli/command.ts
|
|
3671
|
+
var CLI_VERSION = package_default.version;
|
|
3672
|
+
var mainCommand = defineCommand({
|
|
3673
|
+
meta: {
|
|
3674
|
+
name: "tmdb",
|
|
3675
|
+
version: CLI_VERSION,
|
|
3676
|
+
description: "A read-only TMDB command-line client"
|
|
3677
|
+
},
|
|
3678
|
+
subCommands: {
|
|
3679
|
+
search: searchCommand,
|
|
3680
|
+
movie: movieCommand,
|
|
3681
|
+
tv: tvCommand,
|
|
3682
|
+
person: personCommand,
|
|
3683
|
+
trending: trendingCommand,
|
|
3684
|
+
genres: genresCommand,
|
|
3685
|
+
auth: authCommand,
|
|
3686
|
+
config: configCommand
|
|
3687
|
+
}
|
|
3688
|
+
});
|
|
3689
|
+
|
|
3690
|
+
// src/cli/help.ts
|
|
3691
|
+
var AUTHENTICATION_HELP = [
|
|
3692
|
+
"AUTHENTICATION",
|
|
3693
|
+
"",
|
|
3694
|
+
" tmdb auth login Set up a TMDB API Read Access Token",
|
|
3695
|
+
" tmdb auth status Show credential source and config path",
|
|
3696
|
+
" tmdb auth logout Remove saved API credentials",
|
|
3697
|
+
" tmdb config show Show config path and masked values",
|
|
3698
|
+
" tmdb config path Print the config file path",
|
|
3699
|
+
"",
|
|
3700
|
+
" TMDB_ACCESS_TOKEN Environment variable (takes precedence)",
|
|
3701
|
+
" TMDB_API_KEY Legacy v3 API key (takes precedence)"
|
|
3702
|
+
].join(`
|
|
3703
|
+
`);
|
|
3704
|
+
|
|
3705
|
+
// src/cli/main.ts
|
|
3706
|
+
var rawArgs = process.argv.slice(2);
|
|
3707
|
+
if (rawArgs.length === 0) {
|
|
3708
|
+
await showFullUsage(mainCommand);
|
|
3709
|
+
process.exitCode = 1;
|
|
3710
|
+
} else if (rawArgs.some((arg) => arg === "--help" || arg === "-h")) {
|
|
3711
|
+
await showFullUsage(await resolveCommand(rawArgs));
|
|
3712
|
+
} else if (isVersionRequest(rawArgs)) {
|
|
3713
|
+
process.stdout.write(`${CLI_VERSION}
|
|
3714
|
+
`);
|
|
3715
|
+
} else {
|
|
3716
|
+
try {
|
|
3717
|
+
await runCommand(mainCommand, { rawArgs });
|
|
3718
|
+
} catch (error) {
|
|
3719
|
+
reportUsageError(error, rawArgs);
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
function reportUsageError(error, args) {
|
|
3723
|
+
const output = outputFromArgs(args);
|
|
3724
|
+
const message = stripAnsi(error instanceof Error ? error.message : String(error));
|
|
3725
|
+
const usageError = new CliError(message, 2);
|
|
3726
|
+
if (output.format === "json") {
|
|
3727
|
+
process.stderr.write(`${JSON.stringify(toErrorRecord(usageError))}
|
|
3728
|
+
`);
|
|
3729
|
+
} else {
|
|
3730
|
+
process.stderr.write(`${message}
|
|
3731
|
+
`);
|
|
3732
|
+
}
|
|
3733
|
+
process.exitCode = 2;
|
|
3734
|
+
}
|
|
3735
|
+
function outputFromArgs(args) {
|
|
3736
|
+
const outputArgs = {};
|
|
3737
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
3738
|
+
const arg = args[index];
|
|
3739
|
+
if (arg === "--json") {
|
|
3740
|
+
outputArgs.json = true;
|
|
3741
|
+
} else if (arg === "--compact") {
|
|
3742
|
+
outputArgs.compact = true;
|
|
3743
|
+
} else if (arg === "--format") {
|
|
3744
|
+
outputArgs.format = args[index + 1];
|
|
3745
|
+
index += 1;
|
|
3746
|
+
} else if (arg.startsWith("--format=")) {
|
|
3747
|
+
outputArgs.format = arg.slice("--format=".length);
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
try {
|
|
3751
|
+
return resolveOutput(outputArgs);
|
|
3752
|
+
} catch {
|
|
3753
|
+
return { compact: false, format: "text" };
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
function isVersionRequest(args) {
|
|
3757
|
+
return args.length === 1 && (args[0] === "--version" || args[0] === "-v");
|
|
3758
|
+
}
|
|
3759
|
+
function stripAnsi(value) {
|
|
3760
|
+
return value.replace(/\u001B\[[0-9;]*m/g, "");
|
|
3761
|
+
}
|
|
3762
|
+
async function resolveCommand(args) {
|
|
3763
|
+
let command = mainCommand;
|
|
3764
|
+
for (const arg of args) {
|
|
3765
|
+
if (arg.startsWith("-")) {
|
|
3766
|
+
continue;
|
|
3767
|
+
}
|
|
3768
|
+
const subCommands = await resolveValue2(command.subCommands);
|
|
3769
|
+
const next = subCommands?.[arg];
|
|
3770
|
+
if (next === undefined) {
|
|
3771
|
+
break;
|
|
3772
|
+
}
|
|
3773
|
+
command = await resolveValue2(next);
|
|
3774
|
+
}
|
|
3775
|
+
return command;
|
|
3776
|
+
}
|
|
3777
|
+
async function showFullUsage(command) {
|
|
3778
|
+
const path = await findPath(mainCommand, command);
|
|
3779
|
+
if (!path || path.length === 0) {
|
|
3780
|
+
const usage = await renderUsage(command);
|
|
3781
|
+
const authentication = command === mainCommand ? `
|
|
3782
|
+
|
|
3783
|
+
${AUTHENTICATION_HELP}` : "";
|
|
3784
|
+
process.stdout.write(`${usage}${authentication}
|
|
3785
|
+
`);
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
const parentName = ["tmdb", ...path.slice(0, -1)].join(" ");
|
|
3789
|
+
const parent = defineCommand({
|
|
3790
|
+
meta: { name: parentName, version: CLI_VERSION }
|
|
3791
|
+
});
|
|
3792
|
+
process.stdout.write(`${await renderUsage(command, parent)}
|
|
3793
|
+
`);
|
|
3794
|
+
}
|
|
3795
|
+
async function findPath(root, target, path = []) {
|
|
3796
|
+
if (root === target) {
|
|
3797
|
+
return path;
|
|
3798
|
+
}
|
|
3799
|
+
const subCommands = await resolveValue2(root.subCommands);
|
|
3800
|
+
if (!subCommands) {
|
|
3801
|
+
return;
|
|
3802
|
+
}
|
|
3803
|
+
for (const [name, subCommand] of Object.entries(subCommands)) {
|
|
3804
|
+
const resolved = await resolveValue2(subCommand);
|
|
3805
|
+
if (!resolved) {
|
|
3806
|
+
continue;
|
|
3807
|
+
}
|
|
3808
|
+
const found = await findPath(resolved, target, [...path, name]);
|
|
3809
|
+
if (found) {
|
|
3810
|
+
return found;
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
async function resolveValue2(value) {
|
|
3816
|
+
if (value === undefined) {
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
if (typeof value === "function") {
|
|
3820
|
+
return await value();
|
|
3821
|
+
}
|
|
3822
|
+
return await value;
|
|
3823
|
+
}
|