@tacone/prosey 0.9.1 → 0.10.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/bin/prosey CHANGED
@@ -101628,6 +101628,7 @@ function mergeOverDefaults(user, defaults) {
101628
101628
  pager: user.pager ?? defaults.pager,
101629
101629
  hints: user.hints ?? defaults.hints,
101630
101630
  format: user.format ?? defaults.format,
101631
+ dataDir: user.dataDir ?? defaults.dataDir,
101631
101632
  ai: user.ai?.command !== undefined ? user.ai : defaults.ai,
101632
101633
  summarize: {
101633
101634
  command: user.summarize?.command ?? defaults.summarize?.command,
@@ -101639,9 +101640,17 @@ function mergeOverDefaults(user, defaults) {
101639
101640
  }
101640
101641
  };
101641
101642
  }
101643
+ function camelizeKeys(obj) {
101644
+ const out = {};
101645
+ for (const [key, value] of Object.entries(obj)) {
101646
+ const camel = key.replace(/_([a-z])/g, (_2, c2) => c2.toUpperCase());
101647
+ out[camel] = value !== null && typeof value === "object" && !Array.isArray(value) ? camelizeKeys(value) : value;
101648
+ }
101649
+ return out;
101650
+ }
101642
101651
  async function parseOrEmpty(text) {
101643
101652
  try {
101644
- return lt(text);
101653
+ return camelizeKeys(lt(text));
101645
101654
  } catch {
101646
101655
  return {};
101647
101656
  }
@@ -101734,6 +101743,7 @@ import { createHash } from "node:crypto";
101734
101743
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
101735
101744
  import { existsSync as existsSync2 } from "node:fs";
101736
101745
  import { join as join2 } from "node:path";
101746
+ import { homedir as homedir2 } from "node:os";
101737
101747
  var RE_YOUTUBE2 = /(?:v=|\/|v\/|embed\/|watch\?.*v=|youtu\.be\/|\/v\/|e\/|watch\?.*vi?=|\/embed\/|\/v\/|vi?\/|watch\?.*vi?=|youtu\.be\/|\/vi?\/|\/e\/)([a-zA-Z0-9_-]{11})/i;
101738
101748
  var RE_BARE_ID = /^[a-zA-Z0-9_-]{11}$/;
101739
101749
  function extractVideoId(input) {
@@ -101750,8 +101760,19 @@ function hashOptions(opts) {
101750
101760
  function cacheKey(videoId, opts) {
101751
101761
  return `${videoId}_${hashOptions(opts)}`;
101752
101762
  }
101753
- function cacheDir(videoId, opts) {
101754
- return join2("/tmp", "prosey", cacheKey(videoId, opts));
101763
+ function dataDir(configDataDir) {
101764
+ const env = process.env.PROSEY_DATA_PATH;
101765
+ if (env)
101766
+ return env;
101767
+ if (configDataDir)
101768
+ return configDataDir;
101769
+ const xdg = process.env.XDG_DATA_HOME;
101770
+ if (xdg)
101771
+ return join2(xdg, "prosey");
101772
+ return join2(homedir2(), ".local", "share", "prosey");
101773
+ }
101774
+ function cacheDir(videoId, opts, baseDir) {
101775
+ return join2(baseDir ?? dataDir(), cacheKey(videoId, opts));
101755
101776
  }
101756
101777
  async function readCache(dir, filename) {
101757
101778
  try {
@@ -103340,7 +103361,7 @@ async function fetchChannelDescription(channelId) {
103340
103361
  // package.json
103341
103362
  var package_default = {
103342
103363
  name: "@tacone/prosey",
103343
- version: "0.9.1",
103364
+ version: "0.10.0",
103344
103365
  description: "Download YouTube video transcripts from the CLI",
103345
103366
  module: "src/index.ts",
103346
103367
  type: "module",
@@ -122323,6 +122344,7 @@ if (extractTimestamps) {
122323
122344
  exitProcess(0);
122324
122345
  }
122325
122346
  try {
122347
+ const activeDataDir = dataDir(config.dataDir);
122326
122348
  if (mode === "info") {
122327
122349
  const result = await fetchTranscript(videoId, { videoDetails: true, lang });
122328
122350
  if (outputJson) {
@@ -122339,7 +122361,7 @@ try {
122339
122361
  exitProcess(1);
122340
122362
  }
122341
122363
  const cacheOpts2 = { lang, mode: "summarize", noDecode };
122342
- const dir2 = cacheDir(videoId, cacheOpts2);
122364
+ const dir2 = cacheDir(videoId, cacheOpts2, activeDataDir);
122343
122365
  let segments2 = null;
122344
122366
  let summary = null;
122345
122367
  let cachedInfo = null;
@@ -122514,7 +122536,7 @@ ${structuredContent}
122514
122536
  exitProcess(1);
122515
122537
  }
122516
122538
  const cacheOpts2 = { lang, mode: "transcribe", noDecode };
122517
- const dir2 = cacheDir(videoId, cacheOpts2);
122539
+ const dir2 = cacheDir(videoId, cacheOpts2, activeDataDir);
122518
122540
  let segments2 = null;
122519
122541
  let md = null;
122520
122542
  let videoTitle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tacone/prosey",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Download YouTube video transcripts from the CLI",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/src/cache.test.ts CHANGED
@@ -2,12 +2,18 @@ import { describe, expect, test, afterEach } from "bun:test";
2
2
  import { rm, readFile } from "node:fs/promises";
3
3
  import { existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { cacheKey, cacheDir, readCache, writeCache, extractVideoId } from "./cache";
5
+ import { cacheKey, cacheDir, readCache, writeCache, extractVideoId, dataDir } from "./cache";
6
6
 
7
- const testDir = "/tmp/prosey/test-cache-spec";
7
+ const testDir = "/tmp/prosey-test/test-cache-spec";
8
+ const ORIGINAL_DATA_PATH = process.env.PROSEY_DATA_PATH;
9
+ const ORIGINAL_XDG_DATA = process.env.XDG_DATA_HOME;
8
10
 
9
11
  afterEach(async () => {
10
12
  await rm(testDir, { recursive: true, force: true });
13
+ if (ORIGINAL_DATA_PATH) process.env.PROSEY_DATA_PATH = ORIGINAL_DATA_PATH;
14
+ else delete process.env.PROSEY_DATA_PATH;
15
+ if (ORIGINAL_XDG_DATA) process.env.XDG_DATA_HOME = ORIGINAL_XDG_DATA;
16
+ else delete process.env.XDG_DATA_HOME;
11
17
  });
12
18
 
13
19
  describe("cacheKey", () => {
@@ -36,11 +42,32 @@ describe("cacheKey", () => {
36
42
  });
37
43
  });
38
44
 
45
+ describe("dataDir", () => {
46
+ test("PROSEY_DATA_PATH takes precedence", () => {
47
+ process.env.PROSEY_DATA_PATH = "/custom/data";
48
+ delete process.env.XDG_DATA_HOME;
49
+ expect(dataDir()).toBe("/custom/data");
50
+ });
51
+
52
+ test("config dataDir argument used when set", () => {
53
+ delete process.env.PROSEY_DATA_PATH;
54
+ delete process.env.XDG_DATA_HOME;
55
+ expect(dataDir("/from/config")).toBe("/from/config");
56
+ });
57
+
58
+ test("falls back to XDG_DATA_HOME", () => {
59
+ delete process.env.PROSEY_DATA_PATH;
60
+ process.env.XDG_DATA_HOME = "/custom/xdg-data";
61
+ expect(dataDir()).toBe("/custom/xdg-data/prosey");
62
+ });
63
+ });
64
+
39
65
  describe("cacheDir", () => {
40
- test("returns path under /tmp/prosey with cache key", () => {
66
+ test("returns path under the data dir with cache key", () => {
67
+ process.env.PROSEY_DATA_PATH = "/custom/data";
41
68
  const key = cacheKey("abc123def45", {});
42
69
  const dir = cacheDir("abc123def45", {});
43
- expect(dir).toBe(`/tmp/prosey/${key}`);
70
+ expect(dir).toBe(`/custom/data/${key}`);
44
71
  });
45
72
  });
46
73
 
package/src/cache.ts CHANGED
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { readFile, writeFile, mkdir } from "node:fs/promises";
3
3
  import { existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
+ import { homedir } from "node:os";
5
6
 
6
7
  const RE_YOUTUBE =
7
8
  /(?:v=|\/|v\/|embed\/|watch\?.*v=|youtu\.be\/|\/v\/|e\/|watch\?.*vi?=|\/embed\/|\/v\/|vi?\/|watch\?.*vi?=|youtu\.be\/|\/vi?\/|\/e\/)([a-zA-Z0-9_-]{11})/i;
@@ -30,8 +31,17 @@ export function cacheKey(videoId: string, opts: CacheOptions): string {
30
31
  return `${videoId}_${hashOptions(opts)}`;
31
32
  }
32
33
 
33
- export function cacheDir(videoId: string, opts: CacheOptions): string {
34
- return join("/tmp", "prosey", cacheKey(videoId, opts));
34
+ export function dataDir(configDataDir?: string): string {
35
+ const env = process.env.PROSEY_DATA_PATH;
36
+ if (env) return env;
37
+ if (configDataDir) return configDataDir;
38
+ const xdg = process.env.XDG_DATA_HOME;
39
+ if (xdg) return join(xdg, "prosey");
40
+ return join(homedir(), ".local", "share", "prosey");
41
+ }
42
+
43
+ export function cacheDir(videoId: string, opts: CacheOptions, baseDir?: string): string {
44
+ return join(baseDir ?? dataDir(), cacheKey(videoId, opts));
35
45
  }
36
46
 
37
47
  export async function readCache(dir: string, filename: string): Promise<string | null> {
@@ -53,6 +53,7 @@ describe("loadConfig", () => {
53
53
  expect(config.pager).toBe("auto");
54
54
  expect(config.hints).toBe(true);
55
55
  expect(config.format).toBe("html");
56
+ expect(config.dataDir).toBe("");
56
57
  expect(config.ai?.command).toBeString();
57
58
  expect(config.summarize?.prompt).toBeString();
58
59
  expect(existsSync(tmpConfig)).toBe(true);
@@ -125,6 +126,7 @@ describe("mergeOverDefaults", () => {
125
126
  pager: "auto",
126
127
  hints: true,
127
128
  format: "html",
129
+ dataDir: "",
128
130
  ai: { command: "opencode run" },
129
131
  summarize: { prompt: "default summary", command: "opencode run" },
130
132
  transcribe: { prompt: "default transcribe" },
@@ -135,6 +137,7 @@ describe("mergeOverDefaults", () => {
135
137
  expect(result.pager).toBe("auto");
136
138
  expect(result.hints).toBe(true);
137
139
  expect(result.format).toBe("html");
140
+ expect(result.dataDir).toBe("");
138
141
  expect(result.ai?.command).toBe("opencode run");
139
142
  expect(result.summarize?.prompt).toBe("default summary");
140
143
  expect(result.transcribe?.prompt).toBe("default transcribe");
@@ -158,6 +161,12 @@ describe("mergeOverDefaults", () => {
158
161
  expect(result.hints).toBe(true);
159
162
  });
160
163
 
164
+ test("user dataDir overrides default", () => {
165
+ const result = mergeOverDefaults({ dataDir: "/custom/data" }, defaults);
166
+ expect(result.dataDir).toBe("/custom/data");
167
+ expect(result.pager).toBe("auto");
168
+ });
169
+
161
170
  test("user ai.command overrides default", () => {
162
171
  const result = mergeOverDefaults({ ai: { command: "my-cmd" } }, defaults);
163
172
  expect(result.ai?.command).toBe("my-cmd");
package/src/config.ts CHANGED
@@ -9,6 +9,7 @@ export interface ProseyConfig {
9
9
  pager?: string;
10
10
  hints?: boolean;
11
11
  format?: string;
12
+ dataDir?: string;
12
13
  ai?: {
13
14
  command?: string;
14
15
  };
@@ -52,6 +53,7 @@ export function mergeOverDefaults(user: ProseyConfig, defaults: ProseyConfig): P
52
53
  pager: user.pager ?? defaults.pager,
53
54
  hints: user.hints ?? defaults.hints,
54
55
  format: user.format ?? defaults.format,
56
+ dataDir: user.dataDir ?? defaults.dataDir,
55
57
  ai: user.ai?.command !== undefined ? user.ai : defaults.ai,
56
58
  summarize: {
57
59
  command: user.summarize?.command ?? defaults.summarize?.command,
@@ -64,9 +66,21 @@ export function mergeOverDefaults(user: ProseyConfig, defaults: ProseyConfig): P
64
66
  };
65
67
  }
66
68
 
69
+ function camelizeKeys(obj: Record<string, unknown>): Record<string, unknown> {
70
+ const out: Record<string, unknown> = {};
71
+ for (const [key, value] of Object.entries(obj)) {
72
+ const camel = key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
73
+ out[camel] =
74
+ value !== null && typeof value === "object" && !Array.isArray(value)
75
+ ? camelizeKeys(value as Record<string, unknown>)
76
+ : value;
77
+ }
78
+ return out;
79
+ }
80
+
67
81
  async function parseOrEmpty(text: string): Promise<ProseyConfig> {
68
82
  try {
69
- return load(text) as ProseyConfig;
83
+ return camelizeKeys(load(text) as Record<string, unknown>) as ProseyConfig;
70
84
  } catch {
71
85
  return {};
72
86
  }
@@ -15,6 +15,11 @@ pager = "auto"
15
15
  # Can also be set via PROSEY_HINTS env var (yes, no, 1, 0, true, false).
16
16
  hints = true
17
17
 
18
+ # Data directory for cached transcripts, summaries, and generated HTML.
19
+ # Empty (default): $XDG_DATA_HOME/prosey, or ~/.local/share/prosey.
20
+ # Can also be set via the PROSEY_DATA_PATH env var (takes precedence).
21
+ data_dir = ""
22
+
18
23
  [ai]
19
24
 
20
25
  # Default command for AI operations (summarize, transcribe).
package/src/index.ts CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  resolveTranscribeCmd,
19
19
  resolveTranscribePrompt,
20
20
  } from "./config-resolve";
21
- import { cacheDir, readCache, writeCache, extractVideoId } from "./cache";
21
+ import { cacheDir, readCache, writeCache, extractVideoId, dataDir } from "./cache";
22
22
  import { extractChapters, formatChaptersAsText, formatChaptersAsJson } from "./extract-chapters";
23
23
  import { generateHtml, openInBrowser } from "./html";
24
24
  import { fetchChannelDescription } from "./channel-description";
@@ -431,6 +431,8 @@ if (extractTimestamps) {
431
431
  }
432
432
 
433
433
  try {
434
+ const activeDataDir = dataDir(config.dataDir);
435
+
434
436
  if (mode === "info") {
435
437
  const result = await fetchTranscript(videoId, { videoDetails: true, lang } as any);
436
438
  if (outputJson) {
@@ -451,7 +453,7 @@ try {
451
453
  }
452
454
 
453
455
  const cacheOpts = { lang, mode: "summarize", noDecode };
454
- const dir = cacheDir(videoId, cacheOpts);
456
+ const dir = cacheDir(videoId, cacheOpts, activeDataDir);
455
457
  let segments: TranscriptSegment[] | null = null;
456
458
  let summary: string | null = null;
457
459
  let cachedInfo: string | null = null;
@@ -637,7 +639,7 @@ try {
637
639
  }
638
640
 
639
641
  const cacheOpts = { lang, mode: "transcribe", noDecode };
640
- const dir = cacheDir(videoId, cacheOpts);
642
+ const dir = cacheDir(videoId, cacheOpts, activeDataDir);
641
643
  let segments: TranscriptSegment[] | null = null;
642
644
  let md: string | null = null;
643
645
  let videoTitle: string | undefined;