youtube-knowledge-mcp 2.1.0 → 2.2.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/README.md +105 -34
- package/dist/brain-schemas.d.ts +10 -10
- package/dist/brain-schemas.d.ts.map +1 -1
- package/dist/brain-schemas.js +0 -4
- package/dist/brain-schemas.js.map +1 -1
- package/dist/http/app.d.ts +16 -0
- package/dist/http/app.d.ts.map +1 -0
- package/dist/http/app.js +176 -0
- package/dist/http/app.js.map +1 -0
- package/dist/http/auth.d.ts +38 -0
- package/dist/http/auth.d.ts.map +1 -0
- package/dist/http/auth.js +79 -0
- package/dist/http/auth.js.map +1 -0
- package/dist/http/config.d.ts +41 -0
- package/dist/http/config.d.ts.map +1 -0
- package/dist/http/config.js +65 -0
- package/dist/http/config.js.map +1 -0
- package/dist/http/rate-limiter.d.ts +24 -0
- package/dist/http/rate-limiter.d.ts.map +1 -0
- package/dist/http/rate-limiter.js +55 -0
- package/dist/http/rate-limiter.js.map +1 -0
- package/dist/http/sessions.d.ts +34 -0
- package/dist/http/sessions.d.ts.map +1 -0
- package/dist/http/sessions.js +62 -0
- package/dist/http/sessions.js.map +1 -0
- package/dist/http.d.ts +6 -48
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +35 -252
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -517
- package/dist/index.js.map +1 -1
- package/dist/prompts-brain.d.ts +4 -0
- package/dist/prompts-brain.d.ts.map +1 -0
- package/dist/prompts-brain.js +48 -0
- package/dist/prompts-brain.js.map +1 -0
- package/dist/prompts-research.d.ts +4 -0
- package/dist/prompts-research.d.ts.map +1 -0
- package/dist/prompts-research.js +74 -0
- package/dist/prompts-research.js.map +1 -0
- package/dist/prompts-shared.d.ts +13 -0
- package/dist/prompts-shared.d.ts.map +1 -0
- package/dist/prompts-shared.js +7 -0
- package/dist/prompts-shared.js.map +1 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +5 -100
- package/dist/prompts.js.map +1 -1
- package/dist/registry/brain-tools.d.ts +4 -0
- package/dist/registry/brain-tools.d.ts.map +1 -0
- package/dist/registry/brain-tools.js +104 -0
- package/dist/registry/brain-tools.js.map +1 -0
- package/dist/registry/channel-tools.d.ts +4 -0
- package/dist/registry/channel-tools.d.ts.map +1 -0
- package/dist/registry/channel-tools.js +53 -0
- package/dist/registry/channel-tools.js.map +1 -0
- package/dist/registry/health-tools.d.ts +4 -0
- package/dist/registry/health-tools.d.ts.map +1 -0
- package/dist/registry/health-tools.js +21 -0
- package/dist/registry/health-tools.js.map +1 -0
- package/dist/registry/index.d.ts +8 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +27 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/library-tools.d.ts +4 -0
- package/dist/registry/library-tools.d.ts.map +1 -0
- package/dist/registry/library-tools.js +116 -0
- package/dist/registry/library-tools.js.map +1 -0
- package/dist/registry/media-tools.d.ts +4 -0
- package/dist/registry/media-tools.d.ts.map +1 -0
- package/dist/registry/media-tools.js +100 -0
- package/dist/registry/media-tools.js.map +1 -0
- package/dist/registry/thumbnail-tools.d.ts +7 -0
- package/dist/registry/thumbnail-tools.d.ts.map +1 -0
- package/dist/registry/thumbnail-tools.js +72 -0
- package/dist/registry/thumbnail-tools.js.map +1 -0
- package/dist/registry/transcript-tools.d.ts +4 -0
- package/dist/registry/transcript-tools.d.ts.map +1 -0
- package/dist/registry/transcript-tools.js +68 -0
- package/dist/registry/transcript-tools.js.map +1 -0
- package/dist/registry/types.d.ts +41 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +17 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/registry/video-tools.d.ts +4 -0
- package/dist/registry/video-tools.d.ts.map +1 -0
- package/dist/registry/video-tools.js +101 -0
- package/dist/registry/video-tools.js.map +1 -0
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +13 -10
- package/dist/resources.js.map +1 -1
- package/dist/schemas.d.ts +11 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +5 -0
- package/dist/schemas.js.map +1 -1
- package/dist/thumbnail-resource.d.ts +8 -0
- package/dist/thumbnail-resource.d.ts.map +1 -0
- package/dist/thumbnail-resource.js +38 -0
- package/dist/thumbnail-resource.js.map +1 -0
- package/dist/thumbnail-schemas.d.ts +512 -0
- package/dist/thumbnail-schemas.d.ts.map +1 -0
- package/dist/thumbnail-schemas.js +95 -0
- package/dist/thumbnail-schemas.js.map +1 -0
- package/dist/tools/batch.js +1 -1
- package/dist/tools/batch.js.map +1 -1
- package/dist/tools/build-brain.js +1 -1
- package/dist/tools/build-brain.js.map +1 -1
- package/dist/tools/check-health.d.ts +2 -0
- package/dist/tools/check-health.d.ts.map +1 -1
- package/dist/tools/check-health.js +9 -0
- package/dist/tools/check-health.js.map +1 -1
- package/dist/tools/clips.d.ts +9 -41
- package/dist/tools/clips.d.ts.map +1 -1
- package/dist/tools/clips.js +52 -128
- package/dist/tools/clips.js.map +1 -1
- package/dist/tools/fetch-channel-thumbnails.d.ts +179 -0
- package/dist/tools/fetch-channel-thumbnails.d.ts.map +1 -0
- package/dist/tools/fetch-channel-thumbnails.js +123 -0
- package/dist/tools/fetch-channel-thumbnails.js.map +1 -0
- package/dist/tools/fetch-videos.d.ts +3 -0
- package/dist/tools/fetch-videos.d.ts.map +1 -1
- package/dist/tools/fetch-videos.js +3 -0
- package/dist/tools/fetch-videos.js.map +1 -1
- package/dist/tools/frames-subtitles.d.ts +44 -0
- package/dist/tools/frames-subtitles.d.ts.map +1 -0
- package/dist/tools/frames-subtitles.js +104 -0
- package/dist/tools/frames-subtitles.js.map +1 -0
- package/dist/tools/get-channel-info.d.ts +2 -0
- package/dist/tools/get-channel-info.d.ts.map +1 -1
- package/dist/tools/get-channel-info.js +8 -1
- package/dist/tools/get-channel-info.js.map +1 -1
- package/dist/tools/get-playlist-info.d.ts.map +1 -1
- package/dist/tools/get-playlist-info.js +1 -0
- package/dist/tools/get-playlist-info.js.map +1 -1
- package/dist/tools/get-thumbnail.d.ts +38 -0
- package/dist/tools/get-thumbnail.d.ts.map +1 -0
- package/dist/tools/get-thumbnail.js +152 -0
- package/dist/tools/get-thumbnail.js.map +1 -0
- package/dist/tools/get-transcript.d.ts.map +1 -1
- package/dist/tools/get-transcript.js +2 -14
- package/dist/tools/get-transcript.js.map +1 -1
- package/dist/tools/library.d.ts.map +1 -1
- package/dist/tools/library.js +13 -3
- package/dist/tools/library.js.map +1 -1
- package/dist/tools/list-channel-thumbnails.d.ts +235 -0
- package/dist/tools/list-channel-thumbnails.d.ts.map +1 -0
- package/dist/tools/list-channel-thumbnails.js +79 -0
- package/dist/tools/list-channel-thumbnails.js.map +1 -0
- package/dist/tools/save-to-library.d.ts.map +1 -1
- package/dist/tools/save-to-library.js +5 -1
- package/dist/tools/save-to-library.js.map +1 -1
- package/dist/tools/search-channels.d.ts +6 -0
- package/dist/tools/search-channels.d.ts.map +1 -1
- package/dist/tools/search-transcript.d.ts.map +1 -1
- package/dist/tools/search-transcript.js +6 -13
- package/dist/tools/search-transcript.js.map +1 -1
- package/dist/tools/search-videos.d.ts +3 -0
- package/dist/tools/search-videos.d.ts.map +1 -1
- package/dist/utils/batches.d.ts +15 -0
- package/dist/utils/batches.d.ts.map +1 -0
- package/dist/utils/batches.js +31 -0
- package/dist/utils/batches.js.map +1 -0
- package/dist/utils/brain-build.d.ts.map +1 -1
- package/dist/utils/brain-build.js +1 -12
- package/dist/utils/brain-build.js.map +1 -1
- package/dist/utils/brain-index.d.ts.map +1 -1
- package/dist/utils/brain-index.js +3 -2
- package/dist/utils/brain-index.js.map +1 -1
- package/dist/utils/brain-lock.d.ts +2 -18
- package/dist/utils/brain-lock.d.ts.map +1 -1
- package/dist/utils/brain-lock.js +4 -80
- package/dist/utils/brain-lock.js.map +1 -1
- package/dist/utils/brain-lookup.d.ts +8 -0
- package/dist/utils/brain-lookup.d.ts.map +1 -1
- package/dist/utils/brain-lookup.js +5 -31
- package/dist/utils/brain-lookup.js.map +1 -1
- package/dist/utils/brain-storage.d.ts.map +1 -1
- package/dist/utils/brain-storage.js +5 -5
- package/dist/utils/brain-storage.js.map +1 -1
- package/dist/utils/caption-probe.d.ts +7 -0
- package/dist/utils/caption-probe.d.ts.map +1 -0
- package/dist/utils/caption-probe.js +36 -0
- package/dist/utils/caption-probe.js.map +1 -0
- package/dist/utils/channel-images.d.ts +16 -0
- package/dist/utils/channel-images.d.ts.map +1 -0
- package/dist/utils/channel-images.js +35 -0
- package/dist/utils/channel-images.js.map +1 -0
- package/dist/utils/channel-lookup.d.ts +5 -0
- package/dist/utils/channel-lookup.d.ts.map +1 -0
- package/dist/utils/channel-lookup.js +37 -0
- package/dist/utils/channel-lookup.js.map +1 -0
- package/dist/utils/chapters.d.ts +11 -0
- package/dist/utils/chapters.d.ts.map +1 -0
- package/dist/utils/chapters.js +24 -0
- package/dist/utils/chapters.js.map +1 -0
- package/dist/utils/clips.d.ts.map +1 -1
- package/dist/utils/clips.js +18 -26
- package/dist/utils/clips.js.map +1 -1
- package/dist/utils/env.d.ts +26 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +72 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +20 -2
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/file-lock.d.ts +34 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +91 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/flat-listing.d.ts +89 -0
- package/dist/utils/flat-listing.d.ts.map +1 -0
- package/dist/utils/flat-listing.js +68 -0
- package/dist/utils/flat-listing.js.map +1 -0
- package/dist/utils/format.d.ts +1 -3
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +1 -5
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/guard.d.ts +21 -0
- package/dist/utils/guard.d.ts.map +1 -0
- package/dist/utils/guard.js +105 -0
- package/dist/utils/guard.js.map +1 -0
- package/dist/utils/image-dimensions.d.ts +19 -0
- package/dist/utils/image-dimensions.d.ts.map +1 -0
- package/dist/utils/image-dimensions.js +93 -0
- package/dist/utils/image-dimensions.js.map +1 -0
- package/dist/utils/image-fetch.d.ts +31 -0
- package/dist/utils/image-fetch.d.ts.map +1 -0
- package/dist/utils/image-fetch.js +141 -0
- package/dist/utils/image-fetch.js.map +1 -0
- package/dist/utils/json-file.d.ts +14 -5
- package/dist/utils/json-file.d.ts.map +1 -1
- package/dist/utils/json-file.js +24 -9
- package/dist/utils/json-file.js.map +1 -1
- package/dist/utils/paths.d.ts +10 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -0
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/pattern.d.ts +30 -0
- package/dist/utils/pattern.d.ts.map +1 -0
- package/dist/utils/pattern.js +130 -0
- package/dist/utils/pattern.js.map +1 -0
- package/dist/utils/search-index.d.ts.map +1 -1
- package/dist/utils/search-index.js +9 -4
- package/dist/utils/search-index.js.map +1 -1
- package/dist/utils/storage.d.ts.map +1 -1
- package/dist/utils/storage.js +14 -8
- package/dist/utils/storage.js.map +1 -1
- package/dist/utils/subtitles.d.ts +7 -0
- package/dist/utils/subtitles.d.ts.map +1 -0
- package/dist/utils/subtitles.js +30 -0
- package/dist/utils/subtitles.js.map +1 -0
- package/dist/utils/thumbnail-entry.d.ts +19 -0
- package/dist/utils/thumbnail-entry.d.ts.map +1 -0
- package/dist/utils/thumbnail-entry.js +119 -0
- package/dist/utils/thumbnail-entry.js.map +1 -0
- package/dist/utils/thumbnail-fetch.d.ts +37 -0
- package/dist/utils/thumbnail-fetch.d.ts.map +1 -0
- package/dist/utils/thumbnail-fetch.js +89 -0
- package/dist/utils/thumbnail-fetch.js.map +1 -0
- package/dist/utils/thumbnail-ladder.d.ts +44 -0
- package/dist/utils/thumbnail-ladder.d.ts.map +1 -0
- package/dist/utils/thumbnail-ladder.js +68 -0
- package/dist/utils/thumbnail-ladder.js.map +1 -0
- package/dist/utils/thumbnail-listing.d.ts +16 -0
- package/dist/utils/thumbnail-listing.d.ts.map +1 -0
- package/dist/utils/thumbnail-listing.js +26 -0
- package/dist/utils/thumbnail-listing.js.map +1 -0
- package/dist/utils/thumbnail-paths.d.ts +20 -0
- package/dist/utils/thumbnail-paths.d.ts.map +1 -0
- package/dist/utils/thumbnail-paths.js +35 -0
- package/dist/utils/thumbnail-paths.js.map +1 -0
- package/dist/utils/thumbnail-store.d.ts +19 -0
- package/dist/utils/thumbnail-store.d.ts.map +1 -0
- package/dist/utils/thumbnail-store.js +125 -0
- package/dist/utils/thumbnail-store.js.map +1 -0
- package/dist/utils/transcript-cache.d.ts +20 -0
- package/dist/utils/transcript-cache.d.ts.map +1 -0
- package/dist/utils/transcript-cache.js +127 -0
- package/dist/utils/transcript-cache.js.map +1 -0
- package/dist/utils/transcript-search.d.ts +20 -0
- package/dist/utils/transcript-search.d.ts.map +1 -0
- package/dist/utils/transcript-search.js +35 -0
- package/dist/utils/transcript-search.js.map +1 -0
- package/dist/utils/transcript.d.ts +10 -31
- package/dist/utils/transcript.d.ts.map +1 -1
- package/dist/utils/transcript.js +2 -64
- package/dist/utils/transcript.js.map +1 -1
- package/dist/utils/validate-youtube.d.ts +35 -0
- package/dist/utils/validate-youtube.d.ts.map +1 -0
- package/dist/utils/validate-youtube.js +87 -0
- package/dist/utils/validate-youtube.js.map +1 -0
- package/dist/utils/validate.d.ts +11 -4
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +43 -36
- package/dist/utils/validate.js.map +1 -1
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +19 -0
- package/dist/utils/version.js.map +1 -0
- package/dist/utils/youtube-channel.d.ts +44 -0
- package/dist/utils/youtube-channel.d.ts.map +1 -0
- package/dist/utils/youtube-channel.js +61 -0
- package/dist/utils/youtube-channel.js.map +1 -0
- package/dist/utils/youtube-download.d.ts +27 -0
- package/dist/utils/youtube-download.d.ts.map +1 -0
- package/dist/utils/youtube-download.js +118 -0
- package/dist/utils/youtube-download.js.map +1 -0
- package/dist/utils/youtube-schemas.d.ts +394 -0
- package/dist/utils/youtube-schemas.d.ts.map +1 -0
- package/dist/utils/youtube-schemas.js +118 -0
- package/dist/utils/youtube-schemas.js.map +1 -0
- package/dist/utils/youtube-search.d.ts +14 -0
- package/dist/utils/youtube-search.d.ts.map +1 -0
- package/dist/utils/youtube-search.js +39 -0
- package/dist/utils/youtube-search.js.map +1 -0
- package/dist/utils/youtube-url.d.ts +25 -0
- package/dist/utils/youtube-url.d.ts.map +1 -0
- package/dist/utils/youtube-url.js +75 -0
- package/dist/utils/youtube-url.js.map +1 -0
- package/dist/utils/youtube-video.d.ts +47 -0
- package/dist/utils/youtube-video.d.ts.map +1 -0
- package/dist/utils/youtube-video.js +98 -0
- package/dist/utils/youtube-video.js.map +1 -0
- package/dist/utils/youtube.d.ts +10 -122
- package/dist/utils/youtube.d.ts.map +1 -1
- package/dist/utils/youtube.js +11 -486
- package/dist/utils/youtube.js.map +1 -1
- package/dist/utils/ytdlp-env.d.ts +33 -0
- package/dist/utils/ytdlp-env.d.ts.map +1 -0
- package/dist/utils/ytdlp-env.js +121 -0
- package/dist/utils/ytdlp-env.js.map +1 -0
- package/dist/utils/ytdlp-limiter.d.ts +24 -0
- package/dist/utils/ytdlp-limiter.d.ts.map +1 -0
- package/dist/utils/ytdlp-limiter.js +69 -0
- package/dist/utils/ytdlp-limiter.js.map +1 -0
- package/dist/utils/ytdlp-parse.d.ts +13 -0
- package/dist/utils/ytdlp-parse.d.ts.map +1 -0
- package/dist/utils/ytdlp-parse.js +48 -0
- package/dist/utils/ytdlp-parse.js.map +1 -0
- package/dist/utils/ytdlp.d.ts +13 -18
- package/dist/utils/ytdlp.d.ts.map +1 -1
- package/dist/utils/ytdlp.js +21 -110
- package/dist/utils/ytdlp.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ImageFormat } from './image-dimensions.js';
|
|
2
|
+
import type { ThumbnailTab } from '../thumbnail-schemas.js';
|
|
3
|
+
/**
|
|
4
|
+
* Every file a channel's thumbnails are made of.
|
|
5
|
+
*
|
|
6
|
+
* A sibling of the brains rather than inside them: deleting a brain removes
|
|
7
|
+
* its whole directory, and a channel's images should outlive its corpus. Keyed
|
|
8
|
+
* on the channel id, validated once here; a filename is the video id, also
|
|
9
|
+
* validated here; and the tab and format are closed enums. Nothing a caller or
|
|
10
|
+
* a hand-edited manifest supplies reaches a path unchecked.
|
|
11
|
+
*/
|
|
12
|
+
export declare function thumbnailsDir(): string;
|
|
13
|
+
export declare function thumbnailDir(channelId: string): string;
|
|
14
|
+
export declare function thumbnailManifestPath(channelId: string): string;
|
|
15
|
+
export declare function thumbnailLockPath(channelId: string): string;
|
|
16
|
+
export declare function channelImagePath(channelId: string, kind: 'avatar' | 'banner', format: ImageFormat): string;
|
|
17
|
+
export declare function videoThumbnailPath(channelId: string, tab: ThumbnailTab, videoId: string, format: ImageFormat): string;
|
|
18
|
+
/** The informational `file` field: where a file sits inside the channel directory. */
|
|
19
|
+
export declare function relativeFile(channelId: string, absolute: string): string;
|
|
20
|
+
//# sourceMappingURL=thumbnail-paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnail-paths.d.ts","sourceRoot":"","sources":["../../src/utils/thumbnail-paths.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;;;;GAQG;AAEH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,MAAM,EAAE,WAAW,GAClB,MAAM,CAER;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,WAAW,GAClB,MAAM,CAER;AAED,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { join, relative } from 'node:path';
|
|
2
|
+
import { dataDir } from './paths.js';
|
|
3
|
+
import { assertChannelId, assertVideoId } from './validate.js';
|
|
4
|
+
/**
|
|
5
|
+
* Every file a channel's thumbnails are made of.
|
|
6
|
+
*
|
|
7
|
+
* A sibling of the brains rather than inside them: deleting a brain removes
|
|
8
|
+
* its whole directory, and a channel's images should outlive its corpus. Keyed
|
|
9
|
+
* on the channel id, validated once here; a filename is the video id, also
|
|
10
|
+
* validated here; and the tab and format are closed enums. Nothing a caller or
|
|
11
|
+
* a hand-edited manifest supplies reaches a path unchecked.
|
|
12
|
+
*/
|
|
13
|
+
export function thumbnailsDir() {
|
|
14
|
+
return dataDir('thumbnails');
|
|
15
|
+
}
|
|
16
|
+
export function thumbnailDir(channelId) {
|
|
17
|
+
return join(thumbnailsDir(), assertChannelId(channelId));
|
|
18
|
+
}
|
|
19
|
+
export function thumbnailManifestPath(channelId) {
|
|
20
|
+
return join(thumbnailDir(channelId), 'manifest.json');
|
|
21
|
+
}
|
|
22
|
+
export function thumbnailLockPath(channelId) {
|
|
23
|
+
return join(thumbnailDir(channelId), 'fetch.lock');
|
|
24
|
+
}
|
|
25
|
+
export function channelImagePath(channelId, kind, format) {
|
|
26
|
+
return join(thumbnailDir(channelId), 'channel', `${kind}.${format}`);
|
|
27
|
+
}
|
|
28
|
+
export function videoThumbnailPath(channelId, tab, videoId, format) {
|
|
29
|
+
return join(thumbnailDir(channelId), tab, `${assertVideoId(videoId)}.${format}`);
|
|
30
|
+
}
|
|
31
|
+
/** The informational `file` field: where a file sits inside the channel directory. */
|
|
32
|
+
export function relativeFile(channelId, absolute) {
|
|
33
|
+
return relative(thumbnailDir(channelId), absolute);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=thumbnail-paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnail-paths.js","sourceRoot":"","sources":["../../src/utils/thumbnail-paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG/D;;;;;;;;GAQG;AAEH,MAAM,UAAU,aAAa;IAC3B,OAAO,OAAO,CAAC,YAAY,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,SAAiB;IAC5C,OAAO,IAAI,CAAC,aAAa,EAAE,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,SAAiB;IACrD,OAAO,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,eAAe,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,OAAO,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,YAAY,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,SAAiB,EACjB,IAAyB,EACzB,MAAmB;IAEnB,OAAO,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,IAAI,MAAM,EAAE,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,SAAiB,EACjB,GAAiB,EACjB,OAAe,EACf,MAAmB;IAEnB,OAAO,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC;AACnF,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,YAAY,CAAC,SAAiB,EAAE,QAAgB;IAC9D,OAAO,QAAQ,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ChannelImage, type ThumbnailEntry, type ThumbnailManifest, type ThumbnailStats, type ThumbnailTab } from '../thumbnail-schemas.js';
|
|
2
|
+
/** The manifest of a channel's thumbnails: reading, writing, finding and forgetting it. */
|
|
3
|
+
export declare const THUMBNAIL_MANIFEST_VERSION = 1;
|
|
4
|
+
export declare function ensureThumbnailDirs(channelId: string, tabs: ThumbnailTab[]): Promise<string>;
|
|
5
|
+
export declare function readThumbnailManifest(channelId: string): Promise<ThumbnailManifest | undefined>;
|
|
6
|
+
export declare function requireThumbnailManifest(channelId: string): Promise<ThumbnailManifest>;
|
|
7
|
+
export declare function writeThumbnailManifest(manifest: ThumbnailManifest): Promise<void>;
|
|
8
|
+
/** Every thumbnail set on disk, newest first. Unreadable directories are skipped. */
|
|
9
|
+
export declare function listThumbnailManifests(): Promise<ThumbnailManifest[]>;
|
|
10
|
+
/** The thumbnail set for a channel named any way the caller likes, from disk alone. */
|
|
11
|
+
export declare function resolveThumbnails(channel: string): Promise<ThumbnailManifest>;
|
|
12
|
+
export declare function deleteThumbnails(channelId: string): Promise<boolean>;
|
|
13
|
+
/** Where a saved entry's file is, recomputed from validated parts; never read from the manifest. */
|
|
14
|
+
export declare function entryPath(channelId: string, entry: ThumbnailEntry): string | undefined;
|
|
15
|
+
export declare function channelImageFile(channelId: string, image: ChannelImage): string | undefined;
|
|
16
|
+
/** True when the file a saved record describes is on disk at the recorded size. */
|
|
17
|
+
export declare function isIntact(path: string | undefined, bytes: number | undefined): Promise<boolean>;
|
|
18
|
+
export declare function computeThumbnailStats(entries: ThumbnailEntry[]): ThumbnailStats;
|
|
19
|
+
//# sourceMappingURL=thumbnail-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnail-store.d.ts","sourceRoot":"","sources":["../../src/utils/thumbnail-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,YAAY,EAClB,MAAM,yBAAyB,CAAC;AAajC,2FAA2F;AAE3F,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,YAAY,EAAE,GACnB,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAExC;AAED,wBAAsB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAY5F;AAED,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAGvF;AAED,qFAAqF;AACrF,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAa3E;AAED,uFAAuF;AACvF,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAmBnF;AAED,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAK1E;AAED,oGAAoG;AACpG,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAGtF;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CAG3F;AAED,mFAAmF;AACnF,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,cAAc,CA2B/E"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { thumbnailManifestSchema, } from '../thumbnail-schemas.js';
|
|
5
|
+
import { findByChannel } from './channel-lookup.js';
|
|
6
|
+
import { YouTubeError } from './errors.js';
|
|
7
|
+
import { readJsonFile, writeJsonAtomic } from './json-file.js';
|
|
8
|
+
import { ensurePrivateDir } from './paths.js';
|
|
9
|
+
import { channelImagePath, thumbnailDir, thumbnailManifestPath, thumbnailsDir, videoThumbnailPath, } from './thumbnail-paths.js';
|
|
10
|
+
/** The manifest of a channel's thumbnails: reading, writing, finding and forgetting it. */
|
|
11
|
+
export const THUMBNAIL_MANIFEST_VERSION = 1;
|
|
12
|
+
export async function ensureThumbnailDirs(channelId, tabs) {
|
|
13
|
+
const directory = await ensurePrivateDir(thumbnailDir(channelId));
|
|
14
|
+
await ensurePrivateDir(join(directory, 'channel'));
|
|
15
|
+
for (const tab of tabs)
|
|
16
|
+
await ensurePrivateDir(join(directory, tab));
|
|
17
|
+
return directory;
|
|
18
|
+
}
|
|
19
|
+
export async function readThumbnailManifest(channelId) {
|
|
20
|
+
return readJsonFile(thumbnailManifestPath(channelId), thumbnailManifestSchema);
|
|
21
|
+
}
|
|
22
|
+
export async function requireThumbnailManifest(channelId) {
|
|
23
|
+
const manifest = await readThumbnailManifest(channelId);
|
|
24
|
+
if (manifest === undefined) {
|
|
25
|
+
throw new YouTubeError('NOT_FOUND', `No thumbnails have been fetched for channel ${channelId}.`, {
|
|
26
|
+
nextStep: 'Call fetch_channel_thumbnails with the channel URL or handle.',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return manifest;
|
|
30
|
+
}
|
|
31
|
+
export async function writeThumbnailManifest(manifest) {
|
|
32
|
+
await ensurePrivateDir(thumbnailDir(manifest.channel.channelId));
|
|
33
|
+
await writeJsonAtomic(thumbnailManifestPath(manifest.channel.channelId), manifest);
|
|
34
|
+
}
|
|
35
|
+
/** Every thumbnail set on disk, newest first. Unreadable directories are skipped. */
|
|
36
|
+
export async function listThumbnailManifests() {
|
|
37
|
+
if (!existsSync(thumbnailsDir()))
|
|
38
|
+
return [];
|
|
39
|
+
const manifests = [];
|
|
40
|
+
for (const entry of await readdir(thumbnailsDir(), { withFileTypes: true })) {
|
|
41
|
+
if (!entry.isDirectory())
|
|
42
|
+
continue;
|
|
43
|
+
const manifest = await readJsonFile(join(thumbnailsDir(), entry.name, 'manifest.json'), thumbnailManifestSchema);
|
|
44
|
+
if (manifest !== undefined)
|
|
45
|
+
manifests.push(manifest);
|
|
46
|
+
}
|
|
47
|
+
return manifests.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
48
|
+
}
|
|
49
|
+
/** The thumbnail set for a channel named any way the caller likes, from disk alone. */
|
|
50
|
+
export async function resolveThumbnails(channel) {
|
|
51
|
+
const manifests = await listThumbnailManifests();
|
|
52
|
+
const [first, second] = findByChannel(manifests, channel);
|
|
53
|
+
if (first === undefined) {
|
|
54
|
+
const fetched = manifests.map((m) => m.channel.handle || m.channel.name);
|
|
55
|
+
throw new YouTubeError('NOT_FOUND', `No thumbnails have been fetched for "${channel}".`, {
|
|
56
|
+
nextStep: fetched.length === 0
|
|
57
|
+
? 'Call fetch_channel_thumbnails with the channel URL or handle.'
|
|
58
|
+
: `Call fetch_channel_thumbnails to fetch them. Channels with thumbnails: ${fetched.join(', ')}.`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (second !== undefined) {
|
|
62
|
+
throw new YouTubeError('INVALID_INPUT', `"${channel}" matches more than one channel.`, {
|
|
63
|
+
nextStep: `Use the channel id instead: ${[first, second].map((m) => m.channel.channelId).join(', ')}.`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return first;
|
|
67
|
+
}
|
|
68
|
+
export async function deleteThumbnails(channelId) {
|
|
69
|
+
const directory = thumbnailDir(channelId);
|
|
70
|
+
const existed = existsSync(directory);
|
|
71
|
+
await rm(directory, { recursive: true, force: true });
|
|
72
|
+
return existed;
|
|
73
|
+
}
|
|
74
|
+
/** Where a saved entry's file is, recomputed from validated parts; never read from the manifest. */
|
|
75
|
+
export function entryPath(channelId, entry) {
|
|
76
|
+
if (entry.state !== 'saved' || entry.format === undefined)
|
|
77
|
+
return undefined;
|
|
78
|
+
return videoThumbnailPath(channelId, entry.tab, entry.videoId, entry.format);
|
|
79
|
+
}
|
|
80
|
+
export function channelImageFile(channelId, image) {
|
|
81
|
+
if (image.state !== 'saved' || image.format === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
return channelImagePath(channelId, image.kind, image.format);
|
|
84
|
+
}
|
|
85
|
+
/** True when the file a saved record describes is on disk at the recorded size. */
|
|
86
|
+
export async function isIntact(path, bytes) {
|
|
87
|
+
if (path === undefined)
|
|
88
|
+
return false;
|
|
89
|
+
try {
|
|
90
|
+
return (await stat(path)).size === bytes;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function computeThumbnailStats(entries) {
|
|
97
|
+
const tabs = new Map();
|
|
98
|
+
let saved = 0;
|
|
99
|
+
let failed = 0;
|
|
100
|
+
let pending = 0;
|
|
101
|
+
let totalBytes = 0;
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const tab = tabs.get(entry.tab) ?? { videoCount: 0, savedCount: 0 };
|
|
104
|
+
tab.videoCount++;
|
|
105
|
+
if (entry.state === 'saved') {
|
|
106
|
+
saved++;
|
|
107
|
+
tab.savedCount++;
|
|
108
|
+
totalBytes += entry.bytes ?? 0;
|
|
109
|
+
}
|
|
110
|
+
else if (entry.state === 'failed')
|
|
111
|
+
failed++;
|
|
112
|
+
else
|
|
113
|
+
pending++;
|
|
114
|
+
tabs.set(entry.tab, tab);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
videoCount: entries.length,
|
|
118
|
+
savedCount: saved,
|
|
119
|
+
failedCount: failed,
|
|
120
|
+
pendingCount: pending,
|
|
121
|
+
totalBytes,
|
|
122
|
+
tabs: [...tabs.entries()].map(([tab, counts]) => ({ tab, ...counts })),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=thumbnail-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnail-store.js","sourceRoot":"","sources":["../../src/utils/thumbnail-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type TranscriptSegment } from './transcript.js';
|
|
2
|
+
export interface TranscriptResult {
|
|
3
|
+
/** The whole transcript as plain text, unchanged from previous releases. */
|
|
4
|
+
transcript: string;
|
|
5
|
+
/** The same content with cue timings preserved. */
|
|
6
|
+
segments: TranscriptSegment[];
|
|
7
|
+
language: string;
|
|
8
|
+
videoId: string;
|
|
9
|
+
/** True when served from the local cache rather than refetched. */
|
|
10
|
+
cached: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface GetTranscriptOptions {
|
|
13
|
+
language?: string;
|
|
14
|
+
/** Ignore any cached copy and refetch from YouTube. */
|
|
15
|
+
refresh?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** True when a usable transcript is already on disk, in any language. */
|
|
18
|
+
export declare function hasCachedTranscript(videoId: string): boolean;
|
|
19
|
+
export declare function getTranscript(urlOrId: string, options?: GetTranscriptOptions): Promise<TranscriptResult>;
|
|
20
|
+
//# sourceMappingURL=transcript-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript-cache.d.ts","sourceRoot":"","sources":["../../src/utils/transcript-cache.ts"],"names":[],"mappings":"AAUA,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAC;AAOzB,MAAM,WAAW,gBAAgB;IAC/B,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAqCD,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,gBAAgB,CAAC,CAqF3B"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { readFile, unlink } from 'fs/promises';
|
|
3
|
+
import { existsSync, readdirSync } from 'fs';
|
|
4
|
+
import { YouTubeError } from './errors.js';
|
|
5
|
+
import { TIMEOUTS, runYtDlp } from './ytdlp.js';
|
|
6
|
+
import { noCaptionsError } from './caption-probe.js';
|
|
7
|
+
import { assertLanguageTag } from './validate.js';
|
|
8
|
+
import { envInt } from './env.js';
|
|
9
|
+
import { dataDir, ensurePrivateDir } from './paths.js';
|
|
10
|
+
import { writeJsonAtomic } from './json-file.js';
|
|
11
|
+
import { TRANSCRIPT_CACHE_VERSION, cachedTranscriptSchema, parseVtt, segmentsToText, } from './transcript.js';
|
|
12
|
+
import { extractVideoId, watchUrl } from './youtube-url.js';
|
|
13
|
+
/** Fetching a transcript from YouTube and keeping it on disk. */
|
|
14
|
+
const CACHE_DIR = dataDir('transcripts');
|
|
15
|
+
/** Captions rarely change, but a cache with no expiry is a cache that goes wrong. */
|
|
16
|
+
const TRANSCRIPT_TTL_MS = envInt('YOUTUBE_MCP_TRANSCRIPT_TTL_MS', 30 * 86_400_000, { min: 0 });
|
|
17
|
+
function cachePath(videoId, language) {
|
|
18
|
+
return join(CACHE_DIR, `${videoId}.${language}.json`);
|
|
19
|
+
}
|
|
20
|
+
async function readCachedTranscript(videoId, language) {
|
|
21
|
+
const path = cachePath(videoId, language);
|
|
22
|
+
if (!existsSync(path))
|
|
23
|
+
return undefined;
|
|
24
|
+
try {
|
|
25
|
+
const parsed = cachedTranscriptSchema.safeParse(JSON.parse(await readFile(path, 'utf-8')));
|
|
26
|
+
// Anything unreadable — an older layout, a truncated write, a hand-edit —
|
|
27
|
+
// is treated as absent and refetched, rather than half-trusted.
|
|
28
|
+
if (!parsed.success)
|
|
29
|
+
return undefined;
|
|
30
|
+
const cached = parsed.data;
|
|
31
|
+
// A cache written by an older version holds untimed text; regenerate it
|
|
32
|
+
// rather than reading it back as if it had timings.
|
|
33
|
+
if (cached.version !== TRANSCRIPT_CACHE_VERSION)
|
|
34
|
+
return undefined;
|
|
35
|
+
const fetchedAt = Date.parse(cached.fetchedAt);
|
|
36
|
+
if (Number.isFinite(fetchedAt) && Date.now() - fetchedAt > TRANSCRIPT_TTL_MS)
|
|
37
|
+
return undefined;
|
|
38
|
+
return cached;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** True when a usable transcript is already on disk, in any language. */
|
|
45
|
+
export function hasCachedTranscript(videoId) {
|
|
46
|
+
if (!existsSync(CACHE_DIR))
|
|
47
|
+
return false;
|
|
48
|
+
return readdirSync(CACHE_DIR).some((name) => name.startsWith(`${videoId}.`) && name.endsWith('.json'));
|
|
49
|
+
}
|
|
50
|
+
export async function getTranscript(urlOrId, options = {}) {
|
|
51
|
+
const preferredLang = options.language ?? 'en';
|
|
52
|
+
const videoId = extractVideoId(urlOrId);
|
|
53
|
+
assertLanguageTag(preferredLang);
|
|
54
|
+
const url = watchUrl(videoId);
|
|
55
|
+
await ensurePrivateDir(CACHE_DIR);
|
|
56
|
+
if (!options.refresh) {
|
|
57
|
+
const cached = await readCachedTranscript(videoId, preferredLang);
|
|
58
|
+
if (cached) {
|
|
59
|
+
return {
|
|
60
|
+
transcript: segmentsToText(cached.segments),
|
|
61
|
+
segments: cached.segments,
|
|
62
|
+
language: cached.language,
|
|
63
|
+
videoId,
|
|
64
|
+
cached: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const tempDir = join(CACHE_DIR, 'temp');
|
|
69
|
+
await ensurePrivateDir(tempDir);
|
|
70
|
+
const outputTemplate = join(tempDir, videoId);
|
|
71
|
+
try {
|
|
72
|
+
await runYtDlp([
|
|
73
|
+
'--skip-download',
|
|
74
|
+
'--write-auto-sub',
|
|
75
|
+
'--write-sub',
|
|
76
|
+
'--sub-lang',
|
|
77
|
+
`${preferredLang},${preferredLang}-orig`,
|
|
78
|
+
'--sub-format',
|
|
79
|
+
'vtt',
|
|
80
|
+
'--convert-subs',
|
|
81
|
+
'vtt',
|
|
82
|
+
'-o',
|
|
83
|
+
outputTemplate,
|
|
84
|
+
], { label: 'get_transcript', timeoutMs: TIMEOUTS.transcript, target: url });
|
|
85
|
+
const candidates = [
|
|
86
|
+
{ file: `${outputTemplate}.${preferredLang}.vtt`, language: preferredLang },
|
|
87
|
+
{
|
|
88
|
+
file: `${outputTemplate}.${preferredLang}-orig.vtt`,
|
|
89
|
+
language: `${preferredLang} (auto-generated)`,
|
|
90
|
+
},
|
|
91
|
+
{ file: `${outputTemplate}.en.vtt`, language: 'en' },
|
|
92
|
+
];
|
|
93
|
+
const found = candidates.find((candidate) => existsSync(candidate.file));
|
|
94
|
+
if (!found)
|
|
95
|
+
throw await noCaptionsError(url, preferredLang);
|
|
96
|
+
const segments = parseVtt(await readFile(found.file, 'utf-8'));
|
|
97
|
+
if (segments.length === 0)
|
|
98
|
+
throw await noCaptionsError(url, preferredLang);
|
|
99
|
+
const entry = {
|
|
100
|
+
version: TRANSCRIPT_CACHE_VERSION,
|
|
101
|
+
videoId,
|
|
102
|
+
language: found.language,
|
|
103
|
+
fetchedAt: new Date().toISOString(),
|
|
104
|
+
segments,
|
|
105
|
+
};
|
|
106
|
+
await writeJsonAtomic(cachePath(videoId, preferredLang), entry, { pretty: false });
|
|
107
|
+
await unlink(found.file).catch(() => undefined);
|
|
108
|
+
return {
|
|
109
|
+
transcript: segmentsToText(segments),
|
|
110
|
+
segments,
|
|
111
|
+
language: found.language,
|
|
112
|
+
videoId,
|
|
113
|
+
cached: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// Already typed and actionable — pass it through rather than flattening it
|
|
118
|
+
// into a generic string, which is what the old wrapper did to every failure.
|
|
119
|
+
if (error instanceof YouTubeError)
|
|
120
|
+
throw error;
|
|
121
|
+
throw new YouTubeError('YTDLP_FAILED', `Could not read the transcript for ${videoId}.`, {
|
|
122
|
+
nextStep: 'Verify the video exists and has captions, or call check_health.',
|
|
123
|
+
cause: error,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=transcript-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript-cache.js","sourceRoot":"","sources":["../../src/utils/transcript-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TranscriptSegment } from './transcript.js';
|
|
2
|
+
/** Finding a phrase in a transcript. */
|
|
3
|
+
export interface TranscriptMatch {
|
|
4
|
+
segment: TranscriptSegment;
|
|
5
|
+
/** The matched text, for highlighting. */
|
|
6
|
+
match: string;
|
|
7
|
+
url: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Find a compiled pattern in a transcript.
|
|
11
|
+
*
|
|
12
|
+
* Matching runs over the joined text of a small window of segments, not each
|
|
13
|
+
* segment alone, so a phrase split across a caption boundary is still found —
|
|
14
|
+
* which is most phrases, since caption breaks fall mid-sentence.
|
|
15
|
+
*
|
|
16
|
+
* The pattern arrives compiled (see `compilePattern`), so it was validated
|
|
17
|
+
* exactly once, with the flags in force here.
|
|
18
|
+
*/
|
|
19
|
+
export declare function searchSegments(segments: TranscriptSegment[], pattern: RegExp, limit?: number): TranscriptMatch[];
|
|
20
|
+
//# sourceMappingURL=transcript-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript-search.d.ts","sourceRoot":"","sources":["../../src/utils/transcript-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,wCAAwC;AAExC,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,OAAO,EAAE,MAAM,EACf,KAAK,SAAK,GACT,eAAe,EAAE,CA0BnB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find a compiled pattern in a transcript.
|
|
3
|
+
*
|
|
4
|
+
* Matching runs over the joined text of a small window of segments, not each
|
|
5
|
+
* segment alone, so a phrase split across a caption boundary is still found —
|
|
6
|
+
* which is most phrases, since caption breaks fall mid-sentence.
|
|
7
|
+
*
|
|
8
|
+
* The pattern arrives compiled (see `compilePattern`), so it was validated
|
|
9
|
+
* exactly once, with the flags in force here.
|
|
10
|
+
*/
|
|
11
|
+
export function searchSegments(segments, pattern, limit = 20) {
|
|
12
|
+
const matches = [];
|
|
13
|
+
for (let index = 0; index < segments.length && matches.length < limit; index++) {
|
|
14
|
+
const current = segments[index];
|
|
15
|
+
if (!current)
|
|
16
|
+
continue;
|
|
17
|
+
// Two segments of lookahead covers phrases spanning a caption break.
|
|
18
|
+
const haystack = segments
|
|
19
|
+
.slice(index, index + 3)
|
|
20
|
+
.map((segment) => segment.text)
|
|
21
|
+
.join(' ');
|
|
22
|
+
pattern.lastIndex = 0;
|
|
23
|
+
const found = pattern.exec(haystack);
|
|
24
|
+
if (!found)
|
|
25
|
+
continue;
|
|
26
|
+
// The lookahead window means a hit can start in a *later* segment. Only
|
|
27
|
+
// accept it here if it begins inside this one; otherwise the same hit is
|
|
28
|
+
// reported once per preceding segment, and attributed to the wrong time.
|
|
29
|
+
if (found.index > current.text.length)
|
|
30
|
+
continue;
|
|
31
|
+
matches.push({ segment: current, match: found[0], url: '' });
|
|
32
|
+
}
|
|
33
|
+
return matches;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=transcript-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript-search.js","sourceRoot":"","sources":["../../src/utils/transcript-search.ts"],"names":[],"mappings":"AAWA;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC5B,QAA6B,EAC7B,OAAe,EACf,KAAK,GAAG,EAAE;IAEV,MAAM,OAAO,GAAsB,EAAE,CAAC;IAEtC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,GAAG,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;QAC/E,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,CAAC,OAAO;YAAE,SAAS;QAEvB,qEAAqE;QACrE,MAAM,QAAQ,GAAG,QAAQ;aACtB,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC;aACvB,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;aAC9B,IAAI,CAAC,GAAG,CAAC,CAAC;QAEb,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC;QACtB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK;YAAE,SAAS;QAErB,wEAAwE;QACxE,yEAAyE;QACzE,yEAAyE;QACzE,IAAI,KAAK,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM;YAAE,SAAS;QAEhD,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;IAC/D,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -15,15 +15,14 @@ export declare const transcriptSegmentSchema: z.ZodObject<{
|
|
|
15
15
|
text: z.ZodString;
|
|
16
16
|
}, "strip", z.ZodTypeAny, {
|
|
17
17
|
text: string;
|
|
18
|
-
start: number;
|
|
19
18
|
end: number;
|
|
19
|
+
start: number;
|
|
20
20
|
}, {
|
|
21
21
|
text: string;
|
|
22
|
-
start: number;
|
|
23
22
|
end: number;
|
|
23
|
+
start: number;
|
|
24
24
|
}>;
|
|
25
25
|
export type TranscriptSegment = z.infer<typeof transcriptSegmentSchema>;
|
|
26
|
-
export type TranscriptFormat = 'text' | 'timestamped' | 'segments';
|
|
27
26
|
/** Bumped whenever the cache representation changes, so old files are refetched. */
|
|
28
27
|
export declare const TRANSCRIPT_CACHE_VERSION = 2;
|
|
29
28
|
/**
|
|
@@ -47,39 +46,37 @@ export declare const cachedTranscriptSchema: z.ZodObject<{
|
|
|
47
46
|
text: z.ZodString;
|
|
48
47
|
}, "strip", z.ZodTypeAny, {
|
|
49
48
|
text: string;
|
|
50
|
-
start: number;
|
|
51
49
|
end: number;
|
|
50
|
+
start: number;
|
|
52
51
|
}, {
|
|
53
52
|
text: string;
|
|
54
|
-
start: number;
|
|
55
53
|
end: number;
|
|
54
|
+
start: number;
|
|
56
55
|
}>, "many">;
|
|
57
56
|
}, "strip", z.ZodTypeAny, {
|
|
58
57
|
videoId: string;
|
|
59
58
|
version: number;
|
|
60
59
|
language: string;
|
|
60
|
+
fetchedAt: string;
|
|
61
61
|
segments: {
|
|
62
62
|
text: string;
|
|
63
|
-
start: number;
|
|
64
63
|
end: number;
|
|
64
|
+
start: number;
|
|
65
65
|
}[];
|
|
66
|
-
fetchedAt: string;
|
|
67
66
|
}, {
|
|
68
67
|
videoId: string;
|
|
69
68
|
version: number;
|
|
70
69
|
language: string;
|
|
70
|
+
fetchedAt: string;
|
|
71
71
|
segments: {
|
|
72
72
|
text: string;
|
|
73
|
-
start: number;
|
|
74
73
|
end: number;
|
|
74
|
+
start: number;
|
|
75
75
|
}[];
|
|
76
|
-
fetchedAt: string;
|
|
77
76
|
}>;
|
|
78
77
|
export type CachedTranscript = z.infer<typeof cachedTranscriptSchema>;
|
|
79
78
|
/** 3723.5 -> "1:02:03", 62 -> "1:02" */
|
|
80
79
|
export declare function formatTimestamp(totalSeconds: number): string;
|
|
81
|
-
/** 3723.5 -> "01:02:03,500" — the SRT wire format. */
|
|
82
|
-
export declare function formatSrtTimestamp(totalSeconds: number): string;
|
|
83
80
|
/**
|
|
84
81
|
* Parse WebVTT into segments.
|
|
85
82
|
*
|
|
@@ -105,24 +102,6 @@ export declare function sliceSegments(segments: TranscriptSegment[], range: {
|
|
|
105
102
|
startTime?: number;
|
|
106
103
|
endTime?: number;
|
|
107
104
|
}): TranscriptSegment[];
|
|
108
|
-
export interface TranscriptMatch {
|
|
109
|
-
segment: TranscriptSegment;
|
|
110
|
-
/** The matched text, for highlighting. */
|
|
111
|
-
match: string;
|
|
112
|
-
url: string;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Find a phrase or pattern in a transcript.
|
|
116
|
-
*
|
|
117
|
-
* Matching runs over the joined text of a small window of segments, not each
|
|
118
|
-
* segment alone, so a phrase split across a caption boundary is still found —
|
|
119
|
-
* which is most phrases, since caption breaks fall mid-sentence.
|
|
120
|
-
*/
|
|
121
|
-
export declare function searchSegments(segments: TranscriptSegment[], query: string, options?: {
|
|
122
|
-
regex?: boolean;
|
|
123
|
-
caseSensitive?: boolean;
|
|
124
|
-
limit?: number;
|
|
125
|
-
}): TranscriptMatch[];
|
|
126
105
|
export interface TruncationResult {
|
|
127
106
|
text: string;
|
|
128
107
|
truncated: boolean;
|
|
@@ -138,6 +117,6 @@ export interface TruncationResult {
|
|
|
138
117
|
* continue from.
|
|
139
118
|
*/
|
|
140
119
|
export declare function windowText(text: string, offset?: number, maxChars?: number): TruncationResult;
|
|
141
|
-
export
|
|
142
|
-
export
|
|
120
|
+
export * from './subtitles.js';
|
|
121
|
+
export * from './transcript-search.js';
|
|
143
122
|
//# sourceMappingURL=transcript.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../src/utils/transcript.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;GAQG;AAEH,eAAO,MAAM,uBAAuB;IAClC,2CAA2C;;;;;;;;;;;;EAI3C,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,
|
|
1
|
+
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../src/utils/transcript.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;GAQG;AAEH,eAAO,MAAM,uBAAuB;IAClC,2CAA2C;;;;;;;;;;;;EAI3C,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB;;;;;;QApBjC,2CAA2C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0B3C,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,wCAAwC;AACxC,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAS5D;AA0BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB,EAAE,CA4BhE;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAMpE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAI3E;AAED,iDAAiD;AACjD,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,KAAK,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C,iBAAiB,EAAE,CAOrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAcxF;AAED,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC"}
|
package/dist/utils/transcript.js
CHANGED
|
@@ -44,16 +44,6 @@ export function formatTimestamp(totalSeconds) {
|
|
|
44
44
|
return `${hours}:${minutes.toString().padStart(2, '0')}:${paddedSeconds}`;
|
|
45
45
|
return `${minutes}:${paddedSeconds}`;
|
|
46
46
|
}
|
|
47
|
-
/** 3723.5 -> "01:02:03,500" — the SRT wire format. */
|
|
48
|
-
export function formatSrtTimestamp(totalSeconds) {
|
|
49
|
-
const clamped = Math.max(0, totalSeconds);
|
|
50
|
-
const hours = Math.floor(clamped / 3600);
|
|
51
|
-
const minutes = Math.floor((clamped % 3600) / 60);
|
|
52
|
-
const seconds = Math.floor(clamped % 60);
|
|
53
|
-
const millis = Math.round((clamped - Math.floor(clamped)) * 1000);
|
|
54
|
-
const pad = (value, width = 2) => value.toString().padStart(width, '0');
|
|
55
|
-
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)},${pad(millis, 3)}`;
|
|
56
|
-
}
|
|
57
47
|
/**
|
|
58
48
|
* WebVTT cue text is a small markup language: inline timestamps and <c> spans
|
|
59
49
|
* for karaoke-style highlighting. The parser hands back the spec's parsed tree,
|
|
@@ -143,42 +133,6 @@ export function sliceSegments(segments, range) {
|
|
|
143
133
|
// relevant to the range the caller asked about.
|
|
144
134
|
return segments.filter((segment) => segment.end > from && segment.start < to);
|
|
145
135
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Find a phrase or pattern in a transcript.
|
|
148
|
-
*
|
|
149
|
-
* Matching runs over the joined text of a small window of segments, not each
|
|
150
|
-
* segment alone, so a phrase split across a caption boundary is still found —
|
|
151
|
-
* which is most phrases, since caption breaks fall mid-sentence.
|
|
152
|
-
*/
|
|
153
|
-
export function searchSegments(segments, query, options = {}) {
|
|
154
|
-
const { regex = false, caseSensitive = false, limit = 20 } = options;
|
|
155
|
-
const flags = caseSensitive ? 'g' : 'gi';
|
|
156
|
-
const pattern = regex
|
|
157
|
-
? new RegExp(query, flags)
|
|
158
|
-
: new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
|
|
159
|
-
const matches = [];
|
|
160
|
-
for (let index = 0; index < segments.length && matches.length < limit; index++) {
|
|
161
|
-
const current = segments[index];
|
|
162
|
-
if (!current)
|
|
163
|
-
continue;
|
|
164
|
-
// Two segments of lookahead covers phrases spanning a caption break.
|
|
165
|
-
const haystack = segments
|
|
166
|
-
.slice(index, index + 3)
|
|
167
|
-
.map((segment) => segment.text)
|
|
168
|
-
.join(' ');
|
|
169
|
-
pattern.lastIndex = 0;
|
|
170
|
-
const found = pattern.exec(haystack);
|
|
171
|
-
if (!found)
|
|
172
|
-
continue;
|
|
173
|
-
// The lookahead window means a hit can start in a *later* segment. Only
|
|
174
|
-
// accept it here if it begins inside this one; otherwise the same hit is
|
|
175
|
-
// reported once per preceding segment, and attributed to the wrong time.
|
|
176
|
-
if (found.index > current.text.length)
|
|
177
|
-
continue;
|
|
178
|
-
matches.push({ segment: current, match: found[0], url: '' });
|
|
179
|
-
}
|
|
180
|
-
return matches;
|
|
181
|
-
}
|
|
182
136
|
/**
|
|
183
137
|
* Window a long string.
|
|
184
138
|
*
|
|
@@ -198,22 +152,6 @@ export function windowText(text, offset = 0, maxChars) {
|
|
|
198
152
|
const end = softEnd > from ? softEnd : hardEnd;
|
|
199
153
|
return { text: text.slice(from, end), truncated: true, nextOffset: end, totalChars };
|
|
200
154
|
}
|
|
201
|
-
export
|
|
202
|
-
|
|
203
|
-
.map((segment, index) => [
|
|
204
|
-
index + 1,
|
|
205
|
-
`${formatSrtTimestamp(segment.start)} --> ${formatSrtTimestamp(segment.end)}`,
|
|
206
|
-
segment.text,
|
|
207
|
-
].join('\n'))
|
|
208
|
-
.join('\n\n') + '\n');
|
|
209
|
-
}
|
|
210
|
-
export function toVtt(segments) {
|
|
211
|
-
const body = segments
|
|
212
|
-
.map((segment) => [
|
|
213
|
-
`${formatSrtTimestamp(segment.start).replace(',', '.')} --> ${formatSrtTimestamp(segment.end).replace(',', '.')}`,
|
|
214
|
-
segment.text,
|
|
215
|
-
].join('\n'))
|
|
216
|
-
.join('\n\n');
|
|
217
|
-
return `WEBVTT\n\n${body}\n`;
|
|
218
|
-
}
|
|
155
|
+
export * from './subtitles.js';
|
|
156
|
+
export * from './transcript-search.js';
|
|
219
157
|
//# sourceMappingURL=transcript.js.map
|