youtube-knowledge-mcp 1.0.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/LICENSE +21 -0
- package/README.md +225 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/download-video.d.ts +38 -0
- package/dist/tools/download-video.d.ts.map +1 -0
- package/dist/tools/download-video.js +107 -0
- package/dist/tools/download-video.js.map +1 -0
- package/dist/tools/fetch-videos.d.ts +15 -0
- package/dist/tools/fetch-videos.d.ts.map +1 -0
- package/dist/tools/fetch-videos.js +25 -0
- package/dist/tools/fetch-videos.js.map +1 -0
- package/dist/tools/get-transcript.d.ts +15 -0
- package/dist/tools/get-transcript.d.ts.map +1 -0
- package/dist/tools/get-transcript.js +25 -0
- package/dist/tools/get-transcript.js.map +1 -0
- package/dist/tools/get-video-info.d.ts +13 -0
- package/dist/tools/get-video-info.d.ts.map +1 -0
- package/dist/tools/get-video-info.js +32 -0
- package/dist/tools/get-video-info.js.map +1 -0
- package/dist/tools/list-library.d.ts +13 -0
- package/dist/tools/list-library.d.ts.map +1 -0
- package/dist/tools/list-library.js +44 -0
- package/dist/tools/list-library.js.map +1 -0
- package/dist/tools/save-to-library.d.ts +23 -0
- package/dist/tools/save-to-library.d.ts.map +1 -0
- package/dist/tools/save-to-library.js +41 -0
- package/dist/tools/save-to-library.js.map +1 -0
- package/dist/utils/storage.d.ts +38 -0
- package/dist/utils/storage.d.ts.map +1 -0
- package/dist/utils/storage.js +128 -0
- package/dist/utils/storage.js.map +1 -0
- package/dist/utils/youtube.d.ts +48 -0
- package/dist/utils/youtube.d.ts.map +1 -0
- package/dist/utils/youtube.js +288 -0
- package/dist/utils/youtube.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { listLibrary } from '../utils/storage.js';
|
|
3
|
+
export const listLibrarySchema = {
|
|
4
|
+
tag: z.string().optional().describe('Filter by tag (partial match)'),
|
|
5
|
+
};
|
|
6
|
+
export async function listLibraryHandler({ tag }) {
|
|
7
|
+
const items = await listLibrary(tag ? { tag } : undefined);
|
|
8
|
+
const lines = [];
|
|
9
|
+
if (items.length === 0) {
|
|
10
|
+
lines.push('Your library is empty.');
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
const header = tag
|
|
14
|
+
? `${items.length} item${items.length !== 1 ? 's' : ''} matching "${tag}"`
|
|
15
|
+
: `${items.length} item${items.length !== 1 ? 's' : ''} in library`;
|
|
16
|
+
lines.push(header);
|
|
17
|
+
lines.push('');
|
|
18
|
+
items.forEach((item, i) => {
|
|
19
|
+
const types = [];
|
|
20
|
+
if (item.hasSummary)
|
|
21
|
+
types.push('summary');
|
|
22
|
+
if (item.hasSkill)
|
|
23
|
+
types.push('skill');
|
|
24
|
+
lines.push(`${i + 1}. ${item.title}`);
|
|
25
|
+
if (item.channel) {
|
|
26
|
+
lines.push(` by ${item.channel}`);
|
|
27
|
+
}
|
|
28
|
+
lines.push(` ${types.join(', ')} · saved ${item.dateSaved}`);
|
|
29
|
+
if (item.tags.length > 0) {
|
|
30
|
+
lines.push(` tags: ${item.tags.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
lines.push('');
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: lines.join('\n'),
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=list-library.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-library.js","sourceRoot":"","sources":["../../src/tools/list-library.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;CACrE,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,EAAE,GAAG,EAAoB;IAChE,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAE3D,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;IACvC,CAAC;SAAM,CAAC;QACN,MAAM,MAAM,GAAG,GAAG;YAChB,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,QAAQ,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,cAAc,GAAG,GAAG;YAC1E,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,QAAQ,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC;QACtE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;YACxB,MAAM,KAAK,GAAa,EAAE,CAAC;YAC3B,IAAI,IAAI,CAAC,UAAU;gBAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC3C,IAAI,IAAI,CAAC,QAAQ;gBAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAEvC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;YACtC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBACjB,KAAK,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;YACtC,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;YAC/D,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzB,KAAK,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACjD,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;aACvB;SACF;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const saveToLibrarySchema: {
|
|
3
|
+
video_id: z.ZodString;
|
|
4
|
+
title: z.ZodString;
|
|
5
|
+
content: z.ZodString;
|
|
6
|
+
content_type: z.ZodDefault<z.ZodEnum<["summary", "skill"]>>;
|
|
7
|
+
channel: z.ZodOptional<z.ZodString>;
|
|
8
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
9
|
+
};
|
|
10
|
+
export declare function saveToLibraryHandler({ video_id, title, content, content_type, channel, tags, }: {
|
|
11
|
+
video_id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
content: string;
|
|
14
|
+
content_type: 'summary' | 'skill';
|
|
15
|
+
channel?: string;
|
|
16
|
+
tags?: string[];
|
|
17
|
+
}): Promise<{
|
|
18
|
+
content: {
|
|
19
|
+
type: "text";
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
}>;
|
|
23
|
+
//# sourceMappingURL=save-to-library.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save-to-library.d.ts","sourceRoot":"","sources":["../../src/tools/save-to-library.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,mBAAmB;;;;;;;CAU/B,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,EACzC,QAAQ,EACR,KAAK,EACL,OAAO,EACP,YAAY,EACZ,OAAO,EACP,IAAI,GACL,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,SAAS,GAAG,OAAO,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;;;;;GA+BA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { saveToLibrary } from '../utils/storage.js';
|
|
3
|
+
export const saveToLibrarySchema = {
|
|
4
|
+
video_id: z.string().describe('YouTube video ID'),
|
|
5
|
+
title: z.string().describe('Video title'),
|
|
6
|
+
content: z.string().describe('Content to save (summary, notes, or skill)'),
|
|
7
|
+
content_type: z
|
|
8
|
+
.enum(['summary', 'skill'])
|
|
9
|
+
.default('summary')
|
|
10
|
+
.describe('Type of content being saved'),
|
|
11
|
+
channel: z.string().optional().describe('Channel name'),
|
|
12
|
+
tags: z.array(z.string()).optional().describe('Tags for categorization'),
|
|
13
|
+
};
|
|
14
|
+
export async function saveToLibraryHandler({ video_id, title, content, content_type, channel, tags, }) {
|
|
15
|
+
const result = await saveToLibrary({
|
|
16
|
+
videoId: video_id,
|
|
17
|
+
title,
|
|
18
|
+
content,
|
|
19
|
+
contentType: content_type,
|
|
20
|
+
channel,
|
|
21
|
+
tags,
|
|
22
|
+
});
|
|
23
|
+
const lines = [`✓ Saved ${content_type} to library`, '', title];
|
|
24
|
+
if (channel) {
|
|
25
|
+
lines.push(`by ${channel}`);
|
|
26
|
+
}
|
|
27
|
+
if (tags && tags.length > 0) {
|
|
28
|
+
lines.push(`tags: ${tags.join(', ')}`);
|
|
29
|
+
}
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push(result.path);
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: 'text',
|
|
36
|
+
text: lines.join('\n'),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=save-to-library.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save-to-library.js","sourceRoot":"","sources":["../../src/tools/save-to-library.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IACjD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,aAAa,CAAC;IACzC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,4CAA4C,CAAC;IAC1E,YAAY,EAAE,CAAC;SACZ,IAAI,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;SAC1B,OAAO,CAAC,SAAS,CAAC;SAClB,QAAQ,CAAC,6BAA6B,CAAC;IAC1C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;IACvD,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;CACzE,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,EACzC,QAAQ,EACR,KAAK,EACL,OAAO,EACP,YAAY,EACZ,OAAO,EACP,IAAI,GAQL;IACC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC;QACjC,OAAO,EAAE,QAAQ;QACjB,KAAK;QACL,OAAO;QACP,WAAW,EAAE,YAAY;QACzB,OAAO;QACP,IAAI;KACL,CAAC,CAAC;IAEH,MAAM,KAAK,GAAa,CAAC,WAAW,YAAY,aAAa,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;IAE1E,IAAI,OAAO,EAAE,CAAC;QACZ,KAAK,CAAC,IAAI,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED,IAAI,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAExB,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;aACvB;SACF;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface LibraryMetadata {
|
|
2
|
+
videoId: string;
|
|
3
|
+
title: string;
|
|
4
|
+
channel: string;
|
|
5
|
+
url: string;
|
|
6
|
+
tags: string[];
|
|
7
|
+
dateSaved: string;
|
|
8
|
+
hasTranscript: boolean;
|
|
9
|
+
hasSummary: boolean;
|
|
10
|
+
hasSkill: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface LibraryIndex {
|
|
13
|
+
version: number;
|
|
14
|
+
items: Record<string, LibraryMetadata>;
|
|
15
|
+
}
|
|
16
|
+
export interface LibraryItem extends LibraryMetadata {
|
|
17
|
+
summary?: string;
|
|
18
|
+
skill?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SaveOptions {
|
|
21
|
+
videoId: string;
|
|
22
|
+
title: string;
|
|
23
|
+
channel?: string;
|
|
24
|
+
url?: string;
|
|
25
|
+
content: string;
|
|
26
|
+
contentType: 'summary' | 'skill';
|
|
27
|
+
tags?: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare function saveToLibrary(options: SaveOptions): Promise<{
|
|
30
|
+
path: string;
|
|
31
|
+
saved: boolean;
|
|
32
|
+
}>;
|
|
33
|
+
export declare function listLibrary(filter?: {
|
|
34
|
+
tag?: string;
|
|
35
|
+
}): Promise<LibraryMetadata[]>;
|
|
36
|
+
export declare function getFromLibrary(videoId: string): Promise<LibraryItem | null>;
|
|
37
|
+
export declare function deleteFromLibrary(videoId: string): Promise<boolean>;
|
|
38
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/utils/storage.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,WAAY,SAAQ,eAAe;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA6CD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,SAAS,GAAG,OAAO,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CAgD3C;AAED,wBAAsB,WAAW,CAAC,MAAM,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAavF;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAwBjF;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBzE"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
const BASE_DIR = join(homedir(), '.youtube-knowledge');
|
|
6
|
+
const LIBRARY_DIR = join(BASE_DIR, 'library');
|
|
7
|
+
const INDEX_FILE = join(BASE_DIR, 'index.json');
|
|
8
|
+
async function ensureDirectories() {
|
|
9
|
+
await mkdir(BASE_DIR, { recursive: true });
|
|
10
|
+
await mkdir(LIBRARY_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
async function loadIndex() {
|
|
13
|
+
await ensureDirectories();
|
|
14
|
+
if (!existsSync(INDEX_FILE)) {
|
|
15
|
+
return { version: 1, items: {} };
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const content = await readFile(INDEX_FILE, 'utf-8');
|
|
19
|
+
const parsed = JSON.parse(content);
|
|
20
|
+
if (isLibraryIndex(parsed)) {
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
return { version: 1, items: {} };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { version: 1, items: {} };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function isLibraryIndex(value) {
|
|
30
|
+
return (typeof value === 'object' &&
|
|
31
|
+
value !== null &&
|
|
32
|
+
'version' in value &&
|
|
33
|
+
'items' in value &&
|
|
34
|
+
typeof value.version === 'number');
|
|
35
|
+
}
|
|
36
|
+
function isPartialMetadata(value) {
|
|
37
|
+
return typeof value === 'object' && value !== null;
|
|
38
|
+
}
|
|
39
|
+
async function saveIndex(index) {
|
|
40
|
+
await ensureDirectories();
|
|
41
|
+
await writeFile(INDEX_FILE, JSON.stringify(index, null, 2), 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
export async function saveToLibrary(options) {
|
|
44
|
+
const { videoId, title, channel, url, content, contentType, tags = [] } = options;
|
|
45
|
+
await ensureDirectories();
|
|
46
|
+
const itemDir = join(LIBRARY_DIR, videoId);
|
|
47
|
+
await mkdir(itemDir, { recursive: true });
|
|
48
|
+
// Save content
|
|
49
|
+
const filename = contentType === 'summary' ? 'summary.md' : 'skill.md';
|
|
50
|
+
const filePath = join(itemDir, filename);
|
|
51
|
+
await writeFile(filePath, content, 'utf-8');
|
|
52
|
+
// Update metadata
|
|
53
|
+
const metadataPath = join(itemDir, 'metadata.json');
|
|
54
|
+
let metadata = {};
|
|
55
|
+
if (existsSync(metadataPath)) {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(await readFile(metadataPath, 'utf-8'));
|
|
58
|
+
if (isPartialMetadata(parsed)) {
|
|
59
|
+
metadata = parsed;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Ignore parse errors
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const updatedMetadata = {
|
|
67
|
+
videoId,
|
|
68
|
+
title,
|
|
69
|
+
channel: channel ?? metadata.channel ?? '',
|
|
70
|
+
url: url ?? metadata.url ?? `https://www.youtube.com/watch?v=${videoId}`,
|
|
71
|
+
tags: [...new Set([...(metadata.tags ?? []), ...tags])],
|
|
72
|
+
dateSaved: new Date().toISOString(),
|
|
73
|
+
hasTranscript: metadata.hasTranscript ?? false,
|
|
74
|
+
hasSummary: contentType === 'summary' ? true : (metadata.hasSummary ?? false),
|
|
75
|
+
hasSkill: contentType === 'skill' ? true : (metadata.hasSkill ?? false),
|
|
76
|
+
};
|
|
77
|
+
await writeFile(metadataPath, JSON.stringify(updatedMetadata, null, 2), 'utf-8');
|
|
78
|
+
// Update index
|
|
79
|
+
const index = await loadIndex();
|
|
80
|
+
index.items[videoId] = updatedMetadata;
|
|
81
|
+
await saveIndex(index);
|
|
82
|
+
return { path: filePath, saved: true };
|
|
83
|
+
}
|
|
84
|
+
export async function listLibrary(filter) {
|
|
85
|
+
const index = await loadIndex();
|
|
86
|
+
let items = Object.values(index.items);
|
|
87
|
+
if (filter?.tag) {
|
|
88
|
+
const tagLower = filter.tag.toLowerCase();
|
|
89
|
+
items = items.filter((item) => item.tags.some((t) => t.toLowerCase().includes(tagLower)));
|
|
90
|
+
}
|
|
91
|
+
// Sort by date saved (most recent first)
|
|
92
|
+
items.sort((a, b) => new Date(b.dateSaved).getTime() - new Date(a.dateSaved).getTime());
|
|
93
|
+
return items;
|
|
94
|
+
}
|
|
95
|
+
export async function getFromLibrary(videoId) {
|
|
96
|
+
const index = await loadIndex();
|
|
97
|
+
if (!Object.hasOwn(index.items, videoId)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const metadata = index.items[videoId];
|
|
101
|
+
const itemDir = join(LIBRARY_DIR, videoId);
|
|
102
|
+
const item = { ...metadata };
|
|
103
|
+
// Load summary if exists
|
|
104
|
+
const summaryPath = join(itemDir, 'summary.md');
|
|
105
|
+
if (existsSync(summaryPath)) {
|
|
106
|
+
item.summary = await readFile(summaryPath, 'utf-8');
|
|
107
|
+
}
|
|
108
|
+
// Load skill if exists
|
|
109
|
+
const skillPath = join(itemDir, 'skill.md');
|
|
110
|
+
if (existsSync(skillPath)) {
|
|
111
|
+
item.skill = await readFile(skillPath, 'utf-8');
|
|
112
|
+
}
|
|
113
|
+
return item;
|
|
114
|
+
}
|
|
115
|
+
export async function deleteFromLibrary(videoId) {
|
|
116
|
+
const index = await loadIndex();
|
|
117
|
+
if (!Object.hasOwn(index.items, videoId)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
// Create new items object without the deleted videoId
|
|
121
|
+
const { [videoId]: _removed, ...remainingItems } = index.items;
|
|
122
|
+
index.items = remainingItems;
|
|
123
|
+
await saveIndex(index);
|
|
124
|
+
// Note: We don't delete the files, just remove from index
|
|
125
|
+
// This allows recovery if needed
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/utils/storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface VideoInfo {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
channel: string;
|
|
5
|
+
duration: number;
|
|
6
|
+
durationFormatted: string;
|
|
7
|
+
uploadDate: string;
|
|
8
|
+
description: string;
|
|
9
|
+
tags: string[];
|
|
10
|
+
url: string;
|
|
11
|
+
thumbnailUrl: string;
|
|
12
|
+
}
|
|
13
|
+
export interface VideoListItem {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
duration: number;
|
|
17
|
+
durationFormatted: string;
|
|
18
|
+
uploadDate: string;
|
|
19
|
+
url: string;
|
|
20
|
+
}
|
|
21
|
+
export interface TranscriptResult {
|
|
22
|
+
transcript: string;
|
|
23
|
+
language: string;
|
|
24
|
+
videoId: string;
|
|
25
|
+
}
|
|
26
|
+
export interface VideoFormat {
|
|
27
|
+
formatId: string;
|
|
28
|
+
ext: string;
|
|
29
|
+
resolution: string;
|
|
30
|
+
fps?: number;
|
|
31
|
+
vcodec: string;
|
|
32
|
+
acodec: string;
|
|
33
|
+
filesize?: number;
|
|
34
|
+
note: string;
|
|
35
|
+
}
|
|
36
|
+
export interface DownloadResult {
|
|
37
|
+
videoId: string;
|
|
38
|
+
title: string;
|
|
39
|
+
filePath: string;
|
|
40
|
+
format: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function getVideoInfo(urlOrId: string): Promise<VideoInfo>;
|
|
43
|
+
export declare function listVideos(urlOrChannel: string, limit?: number): Promise<VideoListItem[]>;
|
|
44
|
+
export declare function getTranscript(urlOrId: string, preferredLang?: string): Promise<TranscriptResult>;
|
|
45
|
+
export declare function listFormats(urlOrId: string): Promise<VideoFormat[]>;
|
|
46
|
+
export type VideoQuality = 'best' | '2160p' | '1440p' | '1080p' | '720p' | '480p' | '360p' | 'audio';
|
|
47
|
+
export declare function downloadVideo(urlOrId: string, formatId: string, outputDir?: string, quality?: VideoQuality): Promise<DownloadResult>;
|
|
48
|
+
//# sourceMappingURL=youtube.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"youtube.d.ts","sourceRoot":"","sources":["../../src/utils/youtube.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAmCD,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAuCtE;AAED,wBAAsB,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA4B3F;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,aAAa,SAAO,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAoF3B;AAkBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CA0BzE;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAcrG,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,cAAc,CAAC,CAmEzB"}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { mkdir, readFile, writeFile, unlink } from 'fs/promises';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
const CACHE_DIR = join(homedir(), '.youtube-knowledge', 'transcripts');
|
|
7
|
+
function extractVideoId(urlOrId) {
|
|
8
|
+
// If it's already an ID (11 characters, no special chars except - and _)
|
|
9
|
+
if (/^[a-zA-Z0-9_-]{11}$/.test(urlOrId)) {
|
|
10
|
+
return urlOrId;
|
|
11
|
+
}
|
|
12
|
+
// Try to extract from URL
|
|
13
|
+
const patterns = [
|
|
14
|
+
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/v\/)([a-zA-Z0-9_-]{11})/,
|
|
15
|
+
/(?:youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/,
|
|
16
|
+
];
|
|
17
|
+
for (const pattern of patterns) {
|
|
18
|
+
const match = urlOrId.match(pattern);
|
|
19
|
+
if (match) {
|
|
20
|
+
return match[1];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`Could not extract video ID from: ${urlOrId}`);
|
|
24
|
+
}
|
|
25
|
+
function formatDuration(seconds) {
|
|
26
|
+
const hours = Math.floor(seconds / 3600);
|
|
27
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
28
|
+
const secs = seconds % 60;
|
|
29
|
+
if (hours > 0) {
|
|
30
|
+
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
31
|
+
}
|
|
32
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
33
|
+
}
|
|
34
|
+
export async function getVideoInfo(urlOrId) {
|
|
35
|
+
const videoId = extractVideoId(urlOrId);
|
|
36
|
+
const url = `https://www.youtube.com/watch?v=${videoId}`;
|
|
37
|
+
const { stdout } = await execa('yt-dlp', [
|
|
38
|
+
'--skip-download',
|
|
39
|
+
'--print',
|
|
40
|
+
'%(id)s|||%(title)s|||%(channel)s|||%(duration)s|||%(upload_date)s|||%(description)s|||%(tags)j|||%(thumbnail)s',
|
|
41
|
+
url,
|
|
42
|
+
]);
|
|
43
|
+
const [id, title, channel, durationStr, uploadDate, description, tagsJson, thumbnailUrl] = stdout.split('|||');
|
|
44
|
+
const duration = parseInt(durationStr, 10) || 0;
|
|
45
|
+
let tags = [];
|
|
46
|
+
try {
|
|
47
|
+
const parsedTags = JSON.parse(tagsJson || '[]');
|
|
48
|
+
if (Array.isArray(parsedTags)) {
|
|
49
|
+
tags = parsedTags.filter((t) => typeof t === 'string');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
tags = [];
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
id,
|
|
57
|
+
title,
|
|
58
|
+
channel,
|
|
59
|
+
duration,
|
|
60
|
+
durationFormatted: formatDuration(duration),
|
|
61
|
+
uploadDate: uploadDate
|
|
62
|
+
? `${uploadDate.slice(0, 4)}-${uploadDate.slice(4, 6)}-${uploadDate.slice(6, 8)}`
|
|
63
|
+
: '',
|
|
64
|
+
description: description || '',
|
|
65
|
+
tags,
|
|
66
|
+
url,
|
|
67
|
+
thumbnailUrl: thumbnailUrl || '',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function listVideos(urlOrChannel, limit = 20) {
|
|
71
|
+
const { stdout } = await execa('yt-dlp', [
|
|
72
|
+
'--skip-download',
|
|
73
|
+
'--flat-playlist',
|
|
74
|
+
'--print',
|
|
75
|
+
'%(id)s|||%(title)s|||%(duration)s|||%(upload_date)s',
|
|
76
|
+
'--playlist-end',
|
|
77
|
+
limit.toString(),
|
|
78
|
+
urlOrChannel,
|
|
79
|
+
]);
|
|
80
|
+
const lines = stdout.trim().split('\n').filter(Boolean);
|
|
81
|
+
return lines.map((line) => {
|
|
82
|
+
const [id, title, durationStr, uploadDate] = line.split('|||');
|
|
83
|
+
const duration = parseInt(durationStr, 10) || 0;
|
|
84
|
+
return {
|
|
85
|
+
id,
|
|
86
|
+
title: title || 'Unknown title',
|
|
87
|
+
duration,
|
|
88
|
+
durationFormatted: formatDuration(duration),
|
|
89
|
+
uploadDate: uploadDate
|
|
90
|
+
? `${uploadDate.slice(0, 4)}-${uploadDate.slice(4, 6)}-${uploadDate.slice(6, 8)}`
|
|
91
|
+
: '',
|
|
92
|
+
url: `https://www.youtube.com/watch?v=${id}`,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export async function getTranscript(urlOrId, preferredLang = 'en') {
|
|
97
|
+
const videoId = extractVideoId(urlOrId);
|
|
98
|
+
const url = `https://www.youtube.com/watch?v=${videoId}`;
|
|
99
|
+
// Ensure cache directory exists
|
|
100
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
101
|
+
// Check cache first
|
|
102
|
+
const cachedPath = join(CACHE_DIR, `${videoId}.txt`);
|
|
103
|
+
if (existsSync(cachedPath)) {
|
|
104
|
+
const transcript = await readFile(cachedPath, 'utf-8');
|
|
105
|
+
return {
|
|
106
|
+
transcript,
|
|
107
|
+
language: preferredLang,
|
|
108
|
+
videoId,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Use a temp directory for yt-dlp subtitle output
|
|
112
|
+
const tempDir = join(CACHE_DIR, 'temp');
|
|
113
|
+
await mkdir(tempDir, { recursive: true });
|
|
114
|
+
const outputTemplate = join(tempDir, videoId);
|
|
115
|
+
try {
|
|
116
|
+
// Try to get subtitles (auto-generated or manual)
|
|
117
|
+
await execa('yt-dlp', [
|
|
118
|
+
'--skip-download',
|
|
119
|
+
'--write-auto-sub',
|
|
120
|
+
'--write-sub',
|
|
121
|
+
'--sub-lang',
|
|
122
|
+
`${preferredLang},${preferredLang}-orig`,
|
|
123
|
+
'--sub-format',
|
|
124
|
+
'vtt',
|
|
125
|
+
'--convert-subs',
|
|
126
|
+
'vtt',
|
|
127
|
+
'-o',
|
|
128
|
+
outputTemplate,
|
|
129
|
+
url,
|
|
130
|
+
]);
|
|
131
|
+
// Find the generated subtitle file
|
|
132
|
+
const possibleFiles = [
|
|
133
|
+
`${outputTemplate}.${preferredLang}.vtt`,
|
|
134
|
+
`${outputTemplate}.${preferredLang}-orig.vtt`,
|
|
135
|
+
`${outputTemplate}.en.vtt`,
|
|
136
|
+
];
|
|
137
|
+
let subtitleFile = null;
|
|
138
|
+
let detectedLang = preferredLang;
|
|
139
|
+
for (const file of possibleFiles) {
|
|
140
|
+
if (existsSync(file)) {
|
|
141
|
+
subtitleFile = file;
|
|
142
|
+
if (file.includes('-orig')) {
|
|
143
|
+
detectedLang = `${preferredLang} (auto-generated)`;
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!subtitleFile) {
|
|
149
|
+
throw new Error(`No subtitles found for video ${videoId}`);
|
|
150
|
+
}
|
|
151
|
+
// Read and parse VTT file
|
|
152
|
+
const vttContent = await readFile(subtitleFile, 'utf-8');
|
|
153
|
+
const transcript = parseVtt(vttContent);
|
|
154
|
+
// Cache the transcript
|
|
155
|
+
await writeFile(cachedPath, transcript, 'utf-8');
|
|
156
|
+
// Clean up temp file (ignore errors)
|
|
157
|
+
await unlink(subtitleFile).catch(() => undefined);
|
|
158
|
+
return {
|
|
159
|
+
transcript,
|
|
160
|
+
language: detectedLang,
|
|
161
|
+
videoId,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
throw new Error(`Failed to get transcript for ${videoId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const DOWNLOADS_DIR = join(homedir(), '.youtube-knowledge', 'downloads');
|
|
169
|
+
export async function listFormats(urlOrId) {
|
|
170
|
+
const videoId = extractVideoId(urlOrId);
|
|
171
|
+
const url = `https://www.youtube.com/watch?v=${videoId}`;
|
|
172
|
+
const { stdout } = await execa('yt-dlp', ['-j', '--skip-download', url]);
|
|
173
|
+
const data = JSON.parse(stdout);
|
|
174
|
+
const formats = data.formats ?? [];
|
|
175
|
+
return formats
|
|
176
|
+
.filter((f) => !f.format_id.startsWith('sb')) // skip storyboards
|
|
177
|
+
.map((f) => {
|
|
178
|
+
const resolution = f.resolution ?? (f.width && f.height ? `${f.width}x${f.height}` : 'audio only');
|
|
179
|
+
return {
|
|
180
|
+
formatId: f.format_id,
|
|
181
|
+
ext: f.ext,
|
|
182
|
+
resolution,
|
|
183
|
+
fps: f.fps,
|
|
184
|
+
vcodec: f.vcodec ?? 'none',
|
|
185
|
+
acodec: f.acodec ?? 'none',
|
|
186
|
+
filesize: f.filesize ?? f.filesize_approx,
|
|
187
|
+
note: f.format_note ?? '',
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// Smart format selectors that use yt-dlp's fallback syntax
|
|
192
|
+
const QUALITY_FORMAT_SELECTORS = {
|
|
193
|
+
best: 'bestvideo*+bestaudio/best',
|
|
194
|
+
'2160p': 'bestvideo[height<=2160]+bestaudio/bestvideo*[height<=2160]+bestaudio/best[height<=2160]/bestvideo+bestaudio/best',
|
|
195
|
+
'1440p': 'bestvideo[height<=1440]+bestaudio/bestvideo*[height<=1440]+bestaudio/best[height<=1440]/bestvideo+bestaudio/best',
|
|
196
|
+
'1080p': 'bestvideo[height<=1080]+bestaudio/bestvideo*[height<=1080]+bestaudio/best[height<=1080]/bestvideo+bestaudio/best',
|
|
197
|
+
'720p': 'bestvideo[height<=720]+bestaudio/bestvideo*[height<=720]+bestaudio/best[height<=720]/bestvideo+bestaudio/best',
|
|
198
|
+
'480p': 'bestvideo[height<=480]+bestaudio/bestvideo*[height<=480]+bestaudio/best[height<=480]/bestvideo+bestaudio/best',
|
|
199
|
+
'360p': 'bestvideo[height<=360]+bestaudio/bestvideo*[height<=360]+bestaudio/best[height<=360]/bestvideo+bestaudio/best',
|
|
200
|
+
audio: 'bestaudio/best',
|
|
201
|
+
};
|
|
202
|
+
export async function downloadVideo(urlOrId, formatId, outputDir, quality) {
|
|
203
|
+
const videoId = extractVideoId(urlOrId);
|
|
204
|
+
const url = `https://www.youtube.com/watch?v=${videoId}`;
|
|
205
|
+
const targetDir = outputDir ?? DOWNLOADS_DIR;
|
|
206
|
+
// Ensure download directory exists
|
|
207
|
+
await mkdir(targetDir, { recursive: true });
|
|
208
|
+
// Get video title first for the result
|
|
209
|
+
const { stdout: titleOutput } = await execa('yt-dlp', [
|
|
210
|
+
'--skip-download',
|
|
211
|
+
'--print',
|
|
212
|
+
'%(title)s',
|
|
213
|
+
url,
|
|
214
|
+
]);
|
|
215
|
+
const title = titleOutput.trim();
|
|
216
|
+
// Download with specified format
|
|
217
|
+
const outputTemplate = join(targetDir, '%(title)s.%(ext)s');
|
|
218
|
+
// Determine format selector - use quality preset or explicit formatId
|
|
219
|
+
const formatSelector = quality ? QUALITY_FORMAT_SELECTORS[quality] : formatId;
|
|
220
|
+
// Build yt-dlp arguments with merge format for combining video+audio
|
|
221
|
+
const ytdlpArgs = [
|
|
222
|
+
'-f', formatSelector,
|
|
223
|
+
'-o', outputTemplate,
|
|
224
|
+
'--no-playlist',
|
|
225
|
+
'--merge-output-format', 'mp4', // Ensure merged output is mp4
|
|
226
|
+
];
|
|
227
|
+
// Try download, with fallback to best available if format fails
|
|
228
|
+
try {
|
|
229
|
+
await execa('yt-dlp', [...ytdlpArgs, url]);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
// If specific format failed, try with best available as fallback
|
|
233
|
+
if (!quality && formatId !== 'best') {
|
|
234
|
+
console.error(`Format ${formatId} failed, trying best available...`);
|
|
235
|
+
await execa('yt-dlp', [
|
|
236
|
+
'-f', QUALITY_FORMAT_SELECTORS.best,
|
|
237
|
+
'-o', outputTemplate,
|
|
238
|
+
'--no-playlist',
|
|
239
|
+
'--merge-output-format', 'mp4',
|
|
240
|
+
url,
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Get the actual filename that was created
|
|
248
|
+
const { stdout: filenameOutput } = await execa('yt-dlp', [
|
|
249
|
+
'-f', formatSelector,
|
|
250
|
+
'--print', 'filename',
|
|
251
|
+
'-o', outputTemplate,
|
|
252
|
+
'--no-playlist',
|
|
253
|
+
'--merge-output-format', 'mp4',
|
|
254
|
+
url,
|
|
255
|
+
]);
|
|
256
|
+
const filePath = filenameOutput.trim();
|
|
257
|
+
return {
|
|
258
|
+
videoId,
|
|
259
|
+
title,
|
|
260
|
+
filePath,
|
|
261
|
+
format: quality ?? formatId,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function parseVtt(vttContent) {
|
|
265
|
+
const lines = vttContent.split('\n');
|
|
266
|
+
const textLines = [];
|
|
267
|
+
let lastText = '';
|
|
268
|
+
for (const line of lines) {
|
|
269
|
+
// Skip VTT headers and timestamps
|
|
270
|
+
if (line.startsWith('WEBVTT') ||
|
|
271
|
+
line.startsWith('Kind:') ||
|
|
272
|
+
line.startsWith('Language:') ||
|
|
273
|
+
line.includes('-->') ||
|
|
274
|
+
/^\d{2}:\d{2}/.exec(line) ||
|
|
275
|
+
line.trim() === '') {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
// Remove VTT tags like <c> </c>
|
|
279
|
+
const cleanLine = line.replace(/<[^>]+>/g, '').trim();
|
|
280
|
+
// Skip duplicate lines (common in auto-generated subs)
|
|
281
|
+
if (cleanLine && cleanLine !== lastText) {
|
|
282
|
+
textLines.push(cleanLine);
|
|
283
|
+
lastText = cleanLine;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return textLines.join(' ').replace(/\s+/g, ' ').trim();
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=youtube.js.map
|