@tacone/prosey 0.9.0 → 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 +48 -25
- package/package.json +1 -1
- package/src/cache.test.ts +31 -4
- package/src/cache.ts +12 -2
- package/src/channel-description.test.ts +50 -0
- package/src/channel-description.ts +22 -0
- package/src/config.test.ts +9 -0
- package/src/config.ts +15 -1
- package/src/default-config.toml +5 -0
- package/src/index.ts +6 -27
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 {
|
|
@@ -103317,10 +103338,30 @@ function openInBrowser(htmlPath) {
|
|
|
103317
103338
|
setTimeout(() => resolve(), 5000);
|
|
103318
103339
|
});
|
|
103319
103340
|
}
|
|
103341
|
+
|
|
103342
|
+
// src/channel-description.ts
|
|
103343
|
+
async function fetchChannelDescription(channelId) {
|
|
103344
|
+
try {
|
|
103345
|
+
const resp = await fetch(`https://www.youtube.com/youtubei/v1/browse`, {
|
|
103346
|
+
method: "POST",
|
|
103347
|
+
headers: { "Content-Type": "application/json" },
|
|
103348
|
+
body: JSON.stringify({
|
|
103349
|
+
context: {
|
|
103350
|
+
client: { clientName: "WEB", clientVersion: "2.20240101.00.00" }
|
|
103351
|
+
},
|
|
103352
|
+
browseId: channelId
|
|
103353
|
+
})
|
|
103354
|
+
});
|
|
103355
|
+
const data = await resp.json();
|
|
103356
|
+
return data?.metadata?.channelMetadataRenderer?.description;
|
|
103357
|
+
} catch {
|
|
103358
|
+
return;
|
|
103359
|
+
}
|
|
103360
|
+
}
|
|
103320
103361
|
// package.json
|
|
103321
103362
|
var package_default = {
|
|
103322
103363
|
name: "@tacone/prosey",
|
|
103323
|
-
version: "0.
|
|
103364
|
+
version: "0.10.0",
|
|
103324
103365
|
description: "Download YouTube video transcripts from the CLI",
|
|
103325
103366
|
module: "src/index.ts",
|
|
103326
103367
|
type: "module",
|
|
@@ -121947,25 +121988,6 @@ var debugApis = {
|
|
|
121947
121988
|
};
|
|
121948
121989
|
|
|
121949
121990
|
// src/index.ts
|
|
121950
|
-
var YT_INNERTUBE_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8";
|
|
121951
|
-
async function fetchChannelDescription(channelId) {
|
|
121952
|
-
try {
|
|
121953
|
-
const resp = await fetch(`https://www.youtube.com/youtubei/v1/browse?key=${YT_INNERTUBE_API_KEY}`, {
|
|
121954
|
-
method: "POST",
|
|
121955
|
-
headers: { "Content-Type": "application/json" },
|
|
121956
|
-
body: JSON.stringify({
|
|
121957
|
-
context: {
|
|
121958
|
-
client: { clientName: "WEB", clientVersion: "2.20240101.00.00" }
|
|
121959
|
-
},
|
|
121960
|
-
browseId: channelId
|
|
121961
|
-
})
|
|
121962
|
-
});
|
|
121963
|
-
const data = await resp.json();
|
|
121964
|
-
return data?.metadata?.channelMetadataRenderer?.description;
|
|
121965
|
-
} catch {
|
|
121966
|
-
return;
|
|
121967
|
-
}
|
|
121968
|
-
}
|
|
121969
121991
|
process.stdout.on("error", (err) => {
|
|
121970
121992
|
if (err.code === "EPIPE")
|
|
121971
121993
|
process.exit(0);
|
|
@@ -122322,6 +122344,7 @@ if (extractTimestamps) {
|
|
|
122322
122344
|
exitProcess(0);
|
|
122323
122345
|
}
|
|
122324
122346
|
try {
|
|
122347
|
+
const activeDataDir = dataDir(config.dataDir);
|
|
122325
122348
|
if (mode === "info") {
|
|
122326
122349
|
const result = await fetchTranscript(videoId, { videoDetails: true, lang });
|
|
122327
122350
|
if (outputJson) {
|
|
@@ -122338,7 +122361,7 @@ try {
|
|
|
122338
122361
|
exitProcess(1);
|
|
122339
122362
|
}
|
|
122340
122363
|
const cacheOpts2 = { lang, mode: "summarize", noDecode };
|
|
122341
|
-
const dir2 = cacheDir(videoId, cacheOpts2);
|
|
122364
|
+
const dir2 = cacheDir(videoId, cacheOpts2, activeDataDir);
|
|
122342
122365
|
let segments2 = null;
|
|
122343
122366
|
let summary = null;
|
|
122344
122367
|
let cachedInfo = null;
|
|
@@ -122513,7 +122536,7 @@ ${structuredContent}
|
|
|
122513
122536
|
exitProcess(1);
|
|
122514
122537
|
}
|
|
122515
122538
|
const cacheOpts2 = { lang, mode: "transcribe", noDecode };
|
|
122516
|
-
const dir2 = cacheDir(videoId, cacheOpts2);
|
|
122539
|
+
const dir2 = cacheDir(videoId, cacheOpts2, activeDataDir);
|
|
122517
122540
|
let segments2 = null;
|
|
122518
122541
|
let md = null;
|
|
122519
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> {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, test, mock } from "bun:test";
|
|
2
|
+
import { fetchChannelDescription } from "./channel-description";
|
|
3
|
+
|
|
4
|
+
function mockFetchResponse(data: unknown): void {
|
|
5
|
+
globalThis.fetch = mock(() =>
|
|
6
|
+
Promise.resolve({
|
|
7
|
+
ok: true,
|
|
8
|
+
json: () => Promise.resolve(data),
|
|
9
|
+
}),
|
|
10
|
+
) as unknown as typeof fetch;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("fetchChannelDescription", () => {
|
|
14
|
+
test("calls the browse endpoint without an API key", async () => {
|
|
15
|
+
let capturedUrl = "";
|
|
16
|
+
let capturedInit: RequestInit | undefined;
|
|
17
|
+
globalThis.fetch = ((url: any, init: any) => {
|
|
18
|
+
capturedUrl = String(url);
|
|
19
|
+
capturedInit = init;
|
|
20
|
+
return Promise.resolve({
|
|
21
|
+
ok: true,
|
|
22
|
+
json: () =>
|
|
23
|
+
Promise.resolve({
|
|
24
|
+
metadata: {
|
|
25
|
+
channelMetadataRenderer: { description: "Channel bio" },
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
}) as unknown as typeof fetch;
|
|
30
|
+
|
|
31
|
+
const desc = await fetchChannelDescription("UC123");
|
|
32
|
+
expect(desc).toBe("Channel bio");
|
|
33
|
+
|
|
34
|
+
expect(capturedUrl).toBe("https://www.youtube.com/youtubei/v1/browse");
|
|
35
|
+
expect(capturedUrl).not.toContain("key=");
|
|
36
|
+
expect(capturedInit!.method).toBe("POST");
|
|
37
|
+
const body = JSON.parse(String(capturedInit!.body));
|
|
38
|
+
expect(body.browseId).toBe("UC123");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns undefined when response has no description", async () => {
|
|
42
|
+
mockFetchResponse({ metadata: {} });
|
|
43
|
+
expect(await fetchChannelDescription("UC123")).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("returns undefined on network error", async () => {
|
|
47
|
+
globalThis.fetch = mock(() => Promise.reject(new Error("boom"))) as unknown as typeof fetch;
|
|
48
|
+
expect(await fetchChannelDescription("UC123")).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ChannelDescriptionResult {
|
|
2
|
+
description?: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export async function fetchChannelDescription(channelId: string): Promise<string | undefined> {
|
|
6
|
+
try {
|
|
7
|
+
const resp = await fetch(`https://www.youtube.com/youtubei/v1/browse`, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: { "Content-Type": "application/json" },
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
context: {
|
|
12
|
+
client: { clientName: "WEB", clientVersion: "2.20240101.00.00" },
|
|
13
|
+
},
|
|
14
|
+
browseId: channelId,
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
const data: ChannelDescriptionResult = (await resp.json()) as ChannelDescriptionResult;
|
|
18
|
+
return (data as any)?.metadata?.channelMetadataRenderer?.description;
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
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,37 +18,14 @@ 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
|
+
import { fetchChannelDescription } from "./channel-description";
|
|
24
25
|
import { checkVersion } from "./version-check";
|
|
25
26
|
import pkg from "../package.json";
|
|
26
27
|
import prettier from "prettier";
|
|
27
28
|
|
|
28
|
-
const YT_INNERTUBE_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8";
|
|
29
|
-
|
|
30
|
-
async function fetchChannelDescription(channelId: string): Promise<string | undefined> {
|
|
31
|
-
try {
|
|
32
|
-
const resp = await fetch(
|
|
33
|
-
`https://www.youtube.com/youtubei/v1/browse?key=${YT_INNERTUBE_API_KEY}`,
|
|
34
|
-
{
|
|
35
|
-
method: "POST",
|
|
36
|
-
headers: { "Content-Type": "application/json" },
|
|
37
|
-
body: JSON.stringify({
|
|
38
|
-
context: {
|
|
39
|
-
client: { clientName: "WEB", clientVersion: "2.20240101.00.00" },
|
|
40
|
-
},
|
|
41
|
-
browseId: channelId,
|
|
42
|
-
}),
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
const data: any = await resp.json();
|
|
46
|
-
return data?.metadata?.channelMetadataRenderer?.description;
|
|
47
|
-
} catch {
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
29
|
process.stdout.on("error", (err: NodeJS.ErrnoException) => {
|
|
53
30
|
if (err.code === "EPIPE") process.exit(0);
|
|
54
31
|
});
|
|
@@ -454,6 +431,8 @@ if (extractTimestamps) {
|
|
|
454
431
|
}
|
|
455
432
|
|
|
456
433
|
try {
|
|
434
|
+
const activeDataDir = dataDir(config.dataDir);
|
|
435
|
+
|
|
457
436
|
if (mode === "info") {
|
|
458
437
|
const result = await fetchTranscript(videoId, { videoDetails: true, lang } as any);
|
|
459
438
|
if (outputJson) {
|
|
@@ -474,7 +453,7 @@ try {
|
|
|
474
453
|
}
|
|
475
454
|
|
|
476
455
|
const cacheOpts = { lang, mode: "summarize", noDecode };
|
|
477
|
-
const dir = cacheDir(videoId, cacheOpts);
|
|
456
|
+
const dir = cacheDir(videoId, cacheOpts, activeDataDir);
|
|
478
457
|
let segments: TranscriptSegment[] | null = null;
|
|
479
458
|
let summary: string | null = null;
|
|
480
459
|
let cachedInfo: string | null = null;
|
|
@@ -660,7 +639,7 @@ try {
|
|
|
660
639
|
}
|
|
661
640
|
|
|
662
641
|
const cacheOpts = { lang, mode: "transcribe", noDecode };
|
|
663
|
-
const dir = cacheDir(videoId, cacheOpts);
|
|
642
|
+
const dir = cacheDir(videoId, cacheOpts, activeDataDir);
|
|
664
643
|
let segments: TranscriptSegment[] | null = null;
|
|
665
644
|
let md: string | null = null;
|
|
666
645
|
let videoTitle: string | undefined;
|