@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 +28 -6
- package/package.json +1 -1
- package/src/cache.test.ts +31 -4
- package/src/cache.ts +12 -2
- package/src/config.test.ts +9 -0
- package/src/config.ts +15 -1
- package/src/default-config.toml +5 -0
- package/src/index.ts +5 -3
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
|
|
101754
|
-
|
|
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.
|
|
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
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
|
|
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(`/
|
|
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
|
|
34
|
-
|
|
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> {
|
package/src/config.test.ts
CHANGED
|
@@ -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
|
}
|
package/src/default-config.toml
CHANGED
|
@@ -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;
|