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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watchwhere",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "region-aware streaming availability CLI — find which of your subs has a given title",
5
5
  "type": "module",
6
6
  "bin": {
@@ -77,6 +77,7 @@ export async function runInit(): Promise<void> {
77
77
  choices,
78
78
  pageSize: 15,
79
79
  loop: false,
80
+ theme: { keybindings: ["vim"] },
80
81
  });
81
82
 
82
83
  const saved = await saveConfig({
@@ -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
- // pipe mode: no prompts
225
- if (!process.stdin.isTTY) {
226
- process.stdout.write(c.dim(` ${m.searching(currentQuery)}`));
227
- const results = await searchAll(currentQuery, cfg.tmdbToken, {
228
- region: cfg.region,
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
- process.stdout.write(c.dim(` ${m.searching(currentQuery)}`));
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) {
@@ -35,6 +35,7 @@ export async function runSubs(): Promise<void> {
35
35
  })),
36
36
  pageSize: 15,
37
37
  loop: false,
38
+ theme: { keybindings: ["vim"] },
38
39
  });
39
40
 
40
41
  const added = subscriptions.filter((id) => !current.has(id)).length;
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
- if (key.name === "escape") {
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
- if (isUpKey(key, [])) {
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
- if (isDownKey(key, [])) {
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