watchwhere 0.3.0 → 0.3.1
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/package.json +1 -1
- package/src/commands/init.ts +1 -0
- package/src/commands/search.ts +34 -11
- package/src/commands/subs.ts +1 -0
- package/src/i18n.ts +3 -0
- package/src/index.ts +1 -0
- package/src/picker.ts +32 -5
package/package.json
CHANGED
package/src/commands/init.ts
CHANGED
package/src/commands/search.ts
CHANGED
|
@@ -15,6 +15,19 @@ import {
|
|
|
15
15
|
const PAGE_SIZE = 10;
|
|
16
16
|
const SOFT_LIMIT = 25;
|
|
17
17
|
const OVERVIEW_MAX = 140;
|
|
18
|
+
const OLDEST_FILM_YEAR = 1888;
|
|
19
|
+
|
|
20
|
+
function parseQuery(input: string): { query: string; year?: number } {
|
|
21
|
+
const match = input.match(/^(.+?)\s+(\d{4})$/);
|
|
22
|
+
if (match && match[1] && match[2]) {
|
|
23
|
+
const year = Number.parseInt(match[2], 10);
|
|
24
|
+
const maxYear = new Date().getFullYear() + 2;
|
|
25
|
+
if (year >= OLDEST_FILM_YEAR && year <= maxYear) {
|
|
26
|
+
return { query: match[1].trim(), year };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { query: input };
|
|
30
|
+
}
|
|
18
31
|
|
|
19
32
|
function year(date: string | null): string {
|
|
20
33
|
return date && date.length >= 4 ? date.slice(0, 4) : "????";
|
|
@@ -221,14 +234,29 @@ export async function runSearch(query: string, cfg: Config): Promise<void> {
|
|
|
221
234
|
let currentQuery = query.trim();
|
|
222
235
|
if (!currentQuery) throw new Error(m.searchEmpty);
|
|
223
236
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
237
|
+
const runSearchOnce = async (
|
|
238
|
+
raw: string,
|
|
239
|
+
): Promise<ReadonlyArray<MediaItem>> => {
|
|
240
|
+
const parsed = parseQuery(raw);
|
|
241
|
+
const displayQ = parsed.year
|
|
242
|
+
? `${parsed.query} (${parsed.year})`
|
|
243
|
+
: parsed.query;
|
|
244
|
+
process.stdout.write(c.dim(` ${m.searching(displayQ)}`));
|
|
245
|
+
// no region: TMDB returns regional release dates with region param,
|
|
246
|
+
// but year filter expects original release year
|
|
247
|
+
const all = await searchAll(parsed.query, cfg.tmdbToken, {
|
|
229
248
|
language: cfg.language,
|
|
230
249
|
});
|
|
250
|
+
const results = parsed.year
|
|
251
|
+
? all.filter((r) => r.date?.slice(0, 4) === String(parsed.year))
|
|
252
|
+
: all;
|
|
231
253
|
console.log(c.dim(m.resultsCount(results.length)));
|
|
254
|
+
return results;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// pipe mode: no prompts
|
|
258
|
+
if (!process.stdin.isTTY) {
|
|
259
|
+
const results = await runSearchOnce(currentQuery);
|
|
232
260
|
if (results.length === 0) throw new Error(m.noMatch);
|
|
233
261
|
if (results.length > 1) throw new Error(m.ambiguousQuery(results.length));
|
|
234
262
|
await displayItem(results[0]!, cfg, m);
|
|
@@ -236,12 +264,7 @@ export async function runSearch(query: string, cfg: Config): Promise<void> {
|
|
|
236
264
|
}
|
|
237
265
|
|
|
238
266
|
while (true) {
|
|
239
|
-
|
|
240
|
-
const results = await searchAll(currentQuery, cfg.tmdbToken, {
|
|
241
|
-
region: cfg.region,
|
|
242
|
-
language: cfg.language,
|
|
243
|
-
});
|
|
244
|
-
console.log(c.dim(m.resultsCount(results.length)));
|
|
267
|
+
const results = await runSearchOnce(currentQuery);
|
|
245
268
|
|
|
246
269
|
let picked: MediaItem | null = null;
|
|
247
270
|
if (results.length > 0) {
|
package/src/commands/subs.ts
CHANGED
package/src/i18n.ts
CHANGED
|
@@ -118,6 +118,7 @@ interface Catalog {
|
|
|
118
118
|
tokenExpired: string;
|
|
119
119
|
networkOffline: string;
|
|
120
120
|
networkTimeout: string;
|
|
121
|
+
rateLimited: string;
|
|
121
122
|
relativeNow: string;
|
|
122
123
|
relativeMinutes: (n: number) => string;
|
|
123
124
|
relativeHours: (n: number) => string;
|
|
@@ -228,6 +229,7 @@ const en: Catalog = {
|
|
|
228
229
|
tokenExpired: "TMDB token rejected (401). run `ww init` to re-enter it.",
|
|
229
230
|
networkOffline: "couldn't reach TMDB. check your internet connection.",
|
|
230
231
|
networkTimeout: "TMDB request timed out. try again, or check your connection.",
|
|
232
|
+
rateLimited: "rate limit hit. try again in an hour, or set WATCHWHERE_PROXY=off to use your own TMDB token.",
|
|
231
233
|
relativeNow: "just now",
|
|
232
234
|
relativeMinutes: (n) => `${n} min ago`,
|
|
233
235
|
relativeHours: (n) => `${n} hr ago`,
|
|
@@ -338,6 +340,7 @@ const tr: Catalog = {
|
|
|
338
340
|
tokenExpired: "TMDB token reddedildi (401). yenilemek için `ww init` çalıştır.",
|
|
339
341
|
networkOffline: "TMDB'ye ulaşılamadı. internet bağlantını kontrol et.",
|
|
340
342
|
networkTimeout: "TMDB isteği zaman aşımına uğradı. tekrar dene veya bağlantını kontrol et.",
|
|
343
|
+
rateLimited: "rate limit doldu. bir saat sonra dene veya WATCHWHERE_PROXY=off ile kendi TMDB token'ını kullan.",
|
|
341
344
|
relativeNow: "şimdi",
|
|
342
345
|
relativeMinutes: (n) => `${n} dk önce`,
|
|
343
346
|
relativeHours: (n) => `${n} saat önce`,
|
package/src/index.ts
CHANGED
|
@@ -162,6 +162,7 @@ main().then(maybeNotifyUpdate).catch(async (err: unknown) => {
|
|
|
162
162
|
let displayMsg = err instanceof Error ? err.message : String(err);
|
|
163
163
|
if (err instanceof TmdbError) {
|
|
164
164
|
if (err.status === 401) displayMsg = m.tokenExpired;
|
|
165
|
+
else if (err.status === 429) displayMsg = m.rateLimited;
|
|
165
166
|
else if (err.status === 0 && err.message.includes("timed out")) {
|
|
166
167
|
displayMsg = m.networkTimeout;
|
|
167
168
|
} else if (err.status === 0) {
|
package/src/picker.ts
CHANGED
|
@@ -7,10 +7,14 @@ import {
|
|
|
7
7
|
useKeypress,
|
|
8
8
|
usePagination,
|
|
9
9
|
usePrefix,
|
|
10
|
+
useRef,
|
|
10
11
|
useState,
|
|
11
12
|
} from "@inquirer/core";
|
|
13
|
+
import { cursorHide } from "@inquirer/ansi";
|
|
12
14
|
import { c } from "./colors.ts";
|
|
13
15
|
|
|
16
|
+
const GG_TIMEOUT_MS = 500;
|
|
17
|
+
|
|
14
18
|
export interface PickerChoice<Value> {
|
|
15
19
|
readonly name: string;
|
|
16
20
|
readonly value: Value;
|
|
@@ -50,9 +54,14 @@ const promptImpl = createPrompt<unknown | null, PickerConfig<unknown>>(
|
|
|
50
54
|
const prefix = usePrefix({ status, theme });
|
|
51
55
|
const items = config.choices;
|
|
52
56
|
const [active, setActive] = useState(0);
|
|
57
|
+
const lastGAt = useRef(0);
|
|
53
58
|
|
|
54
59
|
useKeypress((key) => {
|
|
55
|
-
|
|
60
|
+
// sequence is present at runtime but not in the type; cast to read it
|
|
61
|
+
const seq = (key as unknown as { sequence?: string }).sequence;
|
|
62
|
+
|
|
63
|
+
// esc or q: cancel
|
|
64
|
+
if (key.name === "escape" || seq === "q") {
|
|
56
65
|
setCancelled(true);
|
|
57
66
|
setStatus("done");
|
|
58
67
|
done(null);
|
|
@@ -64,11 +73,29 @@ const promptImpl = createPrompt<unknown | null, PickerConfig<unknown>>(
|
|
|
64
73
|
if (chosen) done(chosen.value);
|
|
65
74
|
return;
|
|
66
75
|
}
|
|
67
|
-
|
|
76
|
+
// G (shift+g): bottom
|
|
77
|
+
if (seq === "G") {
|
|
78
|
+
setActive(items.length - 1);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// gg: top (two g within timeout)
|
|
82
|
+
if (seq === "g") {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (now - lastGAt.current < GG_TIMEOUT_MS) {
|
|
85
|
+
setActive(0);
|
|
86
|
+
lastGAt.current = 0;
|
|
87
|
+
} else {
|
|
88
|
+
lastGAt.current = now;
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// up or k
|
|
93
|
+
if (isUpKey(key, []) || seq === "k") {
|
|
68
94
|
if (active > 0) setActive(active - 1);
|
|
69
95
|
return;
|
|
70
96
|
}
|
|
71
|
-
|
|
97
|
+
// down or j
|
|
98
|
+
if (isDownKey(key, []) || seq === "j") {
|
|
72
99
|
if (active < items.length - 1) setActive(active + 1);
|
|
73
100
|
return;
|
|
74
101
|
}
|
|
@@ -98,7 +125,7 @@ const promptImpl = createPrompt<unknown | null, PickerConfig<unknown>>(
|
|
|
98
125
|
|
|
99
126
|
const description = items[active]?.description;
|
|
100
127
|
const helpLine = [
|
|
101
|
-
["↑↓", "navigate"],
|
|
128
|
+
["↑↓ jk", "navigate"],
|
|
102
129
|
["⏎", "select"],
|
|
103
130
|
...(config.extraKeys ?? []),
|
|
104
131
|
]
|
|
@@ -112,7 +139,7 @@ const promptImpl = createPrompt<unknown | null, PickerConfig<unknown>>(
|
|
|
112
139
|
helpLine,
|
|
113
140
|
]
|
|
114
141
|
.filter((line) => line !== "")
|
|
115
|
-
.join("\n");
|
|
142
|
+
.join("\n") + cursorHide;
|
|
116
143
|
},
|
|
117
144
|
);
|
|
118
145
|
|