youtube-knowledge-mcp 2.1.0 → 3.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/README.md +153 -38
- 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/harvest-schemas.d.ts +75 -0
- package/dist/harvest-schemas.d.ts.map +1 -0
- package/dist/harvest-schemas.js +106 -0
- package/dist/harvest-schemas.js.map +1 -0
- 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/harvest-tools.d.ts +10 -0
- package/dist/registry/harvest-tools.d.ts.map +1 -0
- package/dist/registry/harvest-tools.js +109 -0
- package/dist/registry/harvest-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 +39 -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 +29 -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 +250 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +61 -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/ask-brain.js +1 -1
- package/dist/tools/ask-brain.js.map +1 -1
- package/dist/tools/batch.js +1 -1
- package/dist/tools/batch.js.map +1 -1
- package/dist/tools/brain-info.js +1 -1
- package/dist/tools/brain-info.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 +61 -0
- package/dist/tools/check-health.d.ts.map +1 -1
- package/dist/tools/check-health.js +94 -6
- 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 +18 -4
- package/dist/tools/fetch-videos.d.ts.map +1 -1
- package/dist/tools/fetch-videos.js +64 -20
- 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-comments.d.ts +238 -6
- package/dist/tools/get-comments.d.ts.map +1 -1
- package/dist/tools/get-comments.js +85 -21
- package/dist/tools/get-comments.js.map +1 -1
- package/dist/tools/get-coverage.d.ts +122 -0
- package/dist/tools/get-coverage.d.ts.map +1 -0
- package/dist/tools/get-coverage.js +105 -0
- package/dist/tools/get-coverage.js.map +1 -0
- 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/harvest-channel.d.ts +92 -0
- package/dist/tools/harvest-channel.d.ts.map +1 -0
- package/dist/tools/harvest-channel.js +62 -0
- package/dist/tools/harvest-channel.js.map +1 -0
- package/dist/tools/harvest-comments.d.ts +81 -0
- package/dist/tools/harvest-comments.d.ts.map +1 -0
- package/dist/tools/harvest-comments.js +115 -0
- package/dist/tools/harvest-comments.js.map +1 -0
- package/dist/tools/library.d.ts.map +1 -1
- package/dist/tools/library.js +14 -4
- 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/list-library.js +1 -1
- package/dist/tools/list-library.js.map +1 -1
- package/dist/tools/prune-harvest.d.ts +59 -0
- package/dist/tools/prune-harvest.d.ts.map +1 -0
- package/dist/tools/prune-harvest.js +108 -0
- package/dist/tools/prune-harvest.js.map +1 -0
- package/dist/tools/query-comments.d.ts +125 -0
- package/dist/tools/query-comments.d.ts.map +1 -0
- package/dist/tools/query-comments.js +106 -0
- package/dist/tools/query-comments.js.map +1 -0
- package/dist/tools/query-videos.d.ts +118 -0
- package/dist/tools/query-videos.d.ts.map +1 -0
- package/dist/tools/query-videos.js +97 -0
- package/dist/tools/query-videos.js.map +1 -0
- package/dist/tools/repair-store.d.ts +34 -0
- package/dist/tools/repair-store.d.ts.map +1 -0
- package/dist/tools/repair-store.js +58 -0
- package/dist/tools/repair-store.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-channels.js +5 -1
- package/dist/tools/search-channels.js.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/tools/search-videos.js +1 -1
- package/dist/tools/search-videos.js.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/comment-store.d.ts +71 -0
- package/dist/utils/comment-store.d.ts.map +1 -0
- package/dist/utils/comment-store.js +129 -0
- package/dist/utils/comment-store.js.map +1 -0
- package/dist/utils/comment-threads.d.ts +51 -0
- package/dist/utils/comment-threads.d.ts.map +1 -0
- package/dist/utils/comment-threads.js +76 -0
- package/dist/utils/comment-threads.js.map +1 -0
- package/dist/utils/coverage-text.d.ts +3 -0
- package/dist/utils/coverage-text.d.ts.map +1 -0
- package/dist/utils/coverage-text.js +62 -0
- package/dist/utils/coverage-text.js.map +1 -0
- package/dist/utils/coverage.d.ts +57 -0
- package/dist/utils/coverage.d.ts.map +1 -0
- package/dist/utils/coverage.js +142 -0
- package/dist/utils/coverage.js.map +1 -0
- 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 +28 -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 +18 -4
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +2 -6
- 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/harvest-catalog.d.ts +44 -0
- package/dist/utils/harvest-catalog.d.ts.map +1 -0
- package/dist/utils/harvest-catalog.js +164 -0
- package/dist/utils/harvest-catalog.js.map +1 -0
- package/dist/utils/harvest-lock.d.ts +3 -0
- package/dist/utils/harvest-lock.d.ts.map +1 -0
- package/dist/utils/harvest-lock.js +26 -0
- package/dist/utils/harvest-lock.js.map +1 -0
- package/dist/utils/harvest-receipts.d.ts +14 -0
- package/dist/utils/harvest-receipts.d.ts.map +1 -0
- package/dist/utils/harvest-receipts.js +110 -0
- package/dist/utils/harvest-receipts.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/listing-cursor.d.ts +31 -0
- package/dist/utils/listing-cursor.d.ts.map +1 -0
- package/dist/utils/listing-cursor.js +39 -0
- package/dist/utils/listing-cursor.js.map +1 -0
- 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/pot-preflight.d.ts +53 -0
- package/dist/utils/pot-preflight.d.ts.map +1 -0
- package/dist/utils/pot-preflight.js +146 -0
- package/dist/utils/pot-preflight.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/sleep.d.ts +9 -0
- package/dist/utils/sleep.d.ts.map +1 -0
- package/dist/utils/sleep.js +25 -0
- package/dist/utils/sleep.js.map +1 -0
- 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/store-health.d.ts +37 -0
- package/dist/utils/store-health.d.ts.map +1 -0
- package/dist/utils/store-health.js +92 -0
- package/dist/utils/store-health.js.map +1 -0
- package/dist/utils/store-migrations.d.ts +33 -0
- package/dist/utils/store-migrations.d.ts.map +1 -0
- package/dist/utils/store-migrations.js +41 -0
- package/dist/utils/store-migrations.js.map +1 -0
- package/dist/utils/store-paths.d.ts +33 -0
- package/dist/utils/store-paths.d.ts.map +1 -0
- package/dist/utils/store-paths.js +48 -0
- package/dist/utils/store-paths.js.map +1 -0
- package/dist/utils/store-rows.d.ts +22 -0
- package/dist/utils/store-rows.d.ts.map +1 -0
- package/dist/utils/store-rows.js +62 -0
- package/dist/utils/store-rows.js.map +1 -0
- package/dist/utils/store-schema.d.ts +16 -0
- package/dist/utils/store-schema.d.ts.map +1 -0
- package/dist/utils/store-schema.js +168 -0
- package/dist/utils/store-schema.js.map +1 -0
- package/dist/utils/store.d.ts +34 -0
- package/dist/utils/store.d.ts.map +1 -0
- package/dist/utils/store.js +157 -0
- package/dist/utils/store.js.map +1 -0
- 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/video-stats-cache.d.ts +16 -0
- package/dist/utils/video-stats-cache.d.ts.map +1 -0
- package/dist/utils/video-stats-cache.js +81 -0
- package/dist/utils/video-stats-cache.js.map +1 -0
- package/dist/utils/video-stats.d.ts +186 -0
- package/dist/utils/video-stats.d.ts.map +1 -0
- package/dist/utils/video-stats.js +168 -0
- package/dist/utils/video-stats.js.map +1 -0
- package/dist/utils/video-store.d.ts +69 -0
- package/dist/utils/video-store.d.ts.map +1 -0
- package/dist/utils/video-store.js +77 -0
- package/dist/utils/video-store.js.map +1 -0
- package/dist/utils/youtube-channel.d.ts +51 -0
- package/dist/utils/youtube-channel.d.ts.map +1 -0
- package/dist/utils/youtube-channel.js +91 -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 +492 -0
- package/dist/utils/youtube-schemas.d.ts.map +1 -0
- package/dist/utils/youtube-schemas.js +148 -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 +68 -0
- package/dist/utils/youtube-video.d.ts.map +1 -0
- package/dist/utils/youtube-video.js +110 -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 +27 -0
- package/dist/utils/ytdlp-limiter.d.ts.map +1 -0
- package/dist/utils/ytdlp-limiter.js +85 -0
- package/dist/utils/ytdlp-limiter.js.map +1 -0
- package/dist/utils/ytdlp-pacer.d.ts +64 -0
- package/dist/utils/ytdlp-pacer.d.ts.map +1 -0
- package/dist/utils/ytdlp-pacer.js +165 -0
- package/dist/utils/ytdlp-pacer.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-stream.d.ts +32 -0
- package/dist/utils/ytdlp-stream.d.ts.map +1 -0
- package/dist/utils/ytdlp-stream.js +61 -0
- package/dist/utils/ytdlp-stream.js.map +1 -0
- package/dist/utils/ytdlp.d.ts +23 -18
- package/dist/utils/ytdlp.d.ts.map +1 -1
- package/dist/utils/ytdlp.js +56 -134
- package/dist/utils/ytdlp.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of the harvested store, as one reviewable unit.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from the module that opens the database so the schema can be read
|
|
5
|
+
* end to end without the connection handling in the way.
|
|
6
|
+
*
|
|
7
|
+
* `STRICT` on every table: SQLite's default type affinity would silently accept
|
|
8
|
+
* a string where a count belongs, and this store exists to make claims about
|
|
9
|
+
* counts. It has no BOOLEAN, so flags are 0/1 integers decoded explicitly on
|
|
10
|
+
* the way out (see store-rows.ts) rather than cast.
|
|
11
|
+
*/
|
|
12
|
+
/** Bumped only by a migration in store-migrations.ts. Independent of the server's semver. */
|
|
13
|
+
export const STORE_VERSION = 1;
|
|
14
|
+
export const STORE_PRAGMAS = [
|
|
15
|
+
// One writer and many readers across processes, which rollback journalling
|
|
16
|
+
// cannot do. Required for the "kill the server mid-harvest" guarantee.
|
|
17
|
+
'PRAGMA journal_mode = WAL',
|
|
18
|
+
// A crash loses at most the last committed transaction and can never corrupt
|
|
19
|
+
// the file. FULL would fsync per commit for a store whose worst case is
|
|
20
|
+
// re-fetching one video.
|
|
21
|
+
'PRAGMA synchronous = NORMAL',
|
|
22
|
+
'PRAGMA foreign_keys = ON',
|
|
23
|
+
'PRAGMA temp_store = MEMORY',
|
|
24
|
+
];
|
|
25
|
+
export const STORE_SCHEMA = `
|
|
26
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
27
|
+
key TEXT PRIMARY KEY,
|
|
28
|
+
value TEXT NOT NULL
|
|
29
|
+
) STRICT;
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS channel (
|
|
32
|
+
channel_id TEXT PRIMARY KEY,
|
|
33
|
+
handle TEXT NOT NULL DEFAULT '',
|
|
34
|
+
name TEXT NOT NULL DEFAULT '',
|
|
35
|
+
channel_url TEXT NOT NULL DEFAULT '',
|
|
36
|
+
subscriber_count INTEGER,
|
|
37
|
+
description TEXT NOT NULL DEFAULT '',
|
|
38
|
+
avatar_url TEXT,
|
|
39
|
+
banner_url TEXT,
|
|
40
|
+
first_seen_at INTEGER NOT NULL,
|
|
41
|
+
updated_at INTEGER NOT NULL
|
|
42
|
+
) STRICT;
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS video (
|
|
45
|
+
video_id TEXT PRIMARY KEY,
|
|
46
|
+
channel_id TEXT REFERENCES channel(channel_id) ON DELETE CASCADE,
|
|
47
|
+
title TEXT NOT NULL DEFAULT '',
|
|
48
|
+
duration_s INTEGER,
|
|
49
|
+
-- 'YYYY-MM-DD'. NULL, never '', when a flat listing carried none: a date
|
|
50
|
+
-- filter must be able to tell "not known yet" from "known to be empty".
|
|
51
|
+
upload_date TEXT,
|
|
52
|
+
view_count INTEGER,
|
|
53
|
+
like_count INTEGER,
|
|
54
|
+
-- YouTube's own claim, read WITHOUT --write-comments, which overwrites it
|
|
55
|
+
-- with the number extracted. This is the denominator every comment receipt
|
|
56
|
+
-- is measured against, so it may only ever come from a metadata pass.
|
|
57
|
+
comment_count INTEGER,
|
|
58
|
+
live_status TEXT,
|
|
59
|
+
availability TEXT,
|
|
60
|
+
thumbnail_url TEXT,
|
|
61
|
+
tab TEXT,
|
|
62
|
+
also_in_tabs TEXT NOT NULL DEFAULT '[]',
|
|
63
|
+
catalog_rank INTEGER,
|
|
64
|
+
listed_at INTEGER,
|
|
65
|
+
detail_at INTEGER,
|
|
66
|
+
-- Stripped before storage: formats, automatic_captions, subtitles,
|
|
67
|
+
-- thumbnails and comments are 96% of a raw -j payload and are mostly
|
|
68
|
+
-- expiring URLs. See MAX_DETAIL_JSON_BYTES.
|
|
69
|
+
detail_json TEXT,
|
|
70
|
+
heatmap_json TEXT
|
|
71
|
+
) STRICT;
|
|
72
|
+
|
|
73
|
+
CREATE INDEX IF NOT EXISTS video_by_channel ON video(channel_id, catalog_rank);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS video_by_date ON video(channel_id, upload_date DESC);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS comment (
|
|
77
|
+
comment_id TEXT PRIMARY KEY,
|
|
78
|
+
video_id TEXT NOT NULL REFERENCES video(video_id) ON DELETE CASCADE,
|
|
79
|
+
-- NULL for a top-level comment. yt-dlp writes the string 'root'; normalising
|
|
80
|
+
-- it here means a reply count is a COUNT(parent_id) rather than a filter
|
|
81
|
+
-- everyone has to remember.
|
|
82
|
+
parent_id TEXT,
|
|
83
|
+
text TEXT NOT NULL DEFAULT '',
|
|
84
|
+
like_count INTEGER NOT NULL DEFAULT 0,
|
|
85
|
+
author TEXT NOT NULL DEFAULT '',
|
|
86
|
+
author_id TEXT,
|
|
87
|
+
author_url TEXT,
|
|
88
|
+
author_thumbnail TEXT,
|
|
89
|
+
author_is_uploader INTEGER NOT NULL DEFAULT 0,
|
|
90
|
+
author_is_verified INTEGER NOT NULL DEFAULT 0,
|
|
91
|
+
is_pinned INTEGER NOT NULL DEFAULT 0,
|
|
92
|
+
is_favorited INTEGER NOT NULL DEFAULT 0,
|
|
93
|
+
time_text TEXT,
|
|
94
|
+
published_at INTEGER,
|
|
95
|
+
harvested_at INTEGER NOT NULL
|
|
96
|
+
) STRICT;
|
|
97
|
+
|
|
98
|
+
CREATE INDEX IF NOT EXISTS comment_by_video ON comment(video_id, parent_id, like_count DESC);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS comment_by_parent ON comment(parent_id);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS comment_by_author ON comment(author_id);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS comment_by_time ON comment(video_id, published_at);
|
|
102
|
+
|
|
103
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS comment_fts USING fts5(
|
|
104
|
+
text,
|
|
105
|
+
content = 'comment',
|
|
106
|
+
content_rowid = 'rowid',
|
|
107
|
+
tokenize = "unicode61 remove_diacritics 2"
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
-- Triggers rather than a periodic 'rebuild'. Measured: triggers cost 6.2x on
|
|
111
|
+
-- bulk insert (2.3s vs 366ms per 200k rows) while 'rebuild' is O(whole table)
|
|
112
|
+
-- and would take seconds per checkpoint at millions of rows. Against a network
|
|
113
|
+
-- that delivers ~17 comments/second, the trigger cost is noise, and there is no
|
|
114
|
+
-- "did we remember to rebuild" failure mode.
|
|
115
|
+
CREATE TRIGGER IF NOT EXISTS comment_ai AFTER INSERT ON comment BEGIN
|
|
116
|
+
INSERT INTO comment_fts(rowid, text) VALUES (new.rowid, new.text);
|
|
117
|
+
END;
|
|
118
|
+
CREATE TRIGGER IF NOT EXISTS comment_ad AFTER DELETE ON comment BEGIN
|
|
119
|
+
INSERT INTO comment_fts(comment_fts, rowid, text) VALUES ('delete', old.rowid, old.text);
|
|
120
|
+
END;
|
|
121
|
+
CREATE TRIGGER IF NOT EXISTS comment_au AFTER UPDATE ON comment BEGIN
|
|
122
|
+
INSERT INTO comment_fts(comment_fts, rowid, text) VALUES ('delete', old.rowid, old.text);
|
|
123
|
+
INSERT INTO comment_fts(rowid, text) VALUES (new.rowid, new.text);
|
|
124
|
+
END;
|
|
125
|
+
|
|
126
|
+
-- What this store can prove about one target. Written in the SAME transaction
|
|
127
|
+
-- as the rows it describes, so there is no interval in which a receipt can
|
|
128
|
+
-- claim something the store does not hold.
|
|
129
|
+
CREATE TABLE IF NOT EXISTS harvest_receipt (
|
|
130
|
+
scope TEXT NOT NULL,
|
|
131
|
+
target_id TEXT NOT NULL,
|
|
132
|
+
state TEXT NOT NULL,
|
|
133
|
+
reason TEXT NOT NULL,
|
|
134
|
+
have INTEGER NOT NULL DEFAULT 0,
|
|
135
|
+
-- What the SOURCE stated. NULL when it stated nothing; never derived from
|
|
136
|
+
-- 'have', which would turn "we stopped" into "there was nothing more".
|
|
137
|
+
expected INTEGER,
|
|
138
|
+
expected_source TEXT,
|
|
139
|
+
source TEXT NOT NULL,
|
|
140
|
+
limit_applied INTEGER,
|
|
141
|
+
sort_applied TEXT,
|
|
142
|
+
ran_to_exhaustion INTEGER,
|
|
143
|
+
resume_token TEXT,
|
|
144
|
+
started_at INTEGER NOT NULL,
|
|
145
|
+
finished_at INTEGER,
|
|
146
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
147
|
+
error_code TEXT,
|
|
148
|
+
note TEXT,
|
|
149
|
+
PRIMARY KEY (scope, target_id)
|
|
150
|
+
) STRICT;
|
|
151
|
+
|
|
152
|
+
CREATE INDEX IF NOT EXISTS receipt_by_state ON harvest_receipt(state, finished_at DESC);
|
|
153
|
+
|
|
154
|
+
-- Append-only audit trail: cheap, and it is what makes a receipt defensible
|
|
155
|
+
-- after the fact rather than merely asserted.
|
|
156
|
+
CREATE TABLE IF NOT EXISTS harvest_event (
|
|
157
|
+
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
158
|
+
scope TEXT NOT NULL,
|
|
159
|
+
target_id TEXT NOT NULL,
|
|
160
|
+
at INTEGER NOT NULL,
|
|
161
|
+
state TEXT NOT NULL,
|
|
162
|
+
delta INTEGER NOT NULL DEFAULT 0,
|
|
163
|
+
note TEXT
|
|
164
|
+
) STRICT;
|
|
165
|
+
|
|
166
|
+
CREATE INDEX IF NOT EXISTS event_by_target ON harvest_event(scope, target_id, at DESC);
|
|
167
|
+
`;
|
|
168
|
+
//# sourceMappingURL=store-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-schema.js","sourceRoot":"","sources":["../../src/utils/store-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,6FAA6F;AAC7F,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC;AAE/B,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,2EAA2E;IAC3E,uEAAuE;IACvE,2BAA2B;IAC3B,6EAA6E;IAC7E,wEAAwE;IACxE,yBAAyB;IACzB,6BAA6B;IAC7B,0BAA0B;IAC1B,4BAA4B;CACpB,CAAC;AAEX,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8I3B,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
/**
|
|
3
|
+
* The one connection to the harvested store.
|
|
4
|
+
*
|
|
5
|
+
* `node:sqlite` is a runtime builtin, so this adds nothing to package.json —
|
|
6
|
+
* the same category as `node:fs`. It is reached through a dynamic import
|
|
7
|
+
* behind an async accessor so that a Node without it still loads every module
|
|
8
|
+
* and still serves the tools that do not touch the store; only the harvest
|
|
9
|
+
* tools fail, with a sentence naming the version to install.
|
|
10
|
+
*
|
|
11
|
+
* The reason for a database rather than more JSON files is transactional
|
|
12
|
+
* receipts. `brain-build.ts` carries a long comment explaining that its
|
|
13
|
+
* manifest and its chunks are two documents, only one can be written first,
|
|
14
|
+
* and a crash in between strands a brain. Here the rows and the receipt that
|
|
15
|
+
* describes them commit together, so that failure is not representable.
|
|
16
|
+
*/
|
|
17
|
+
/** The exact Node where `node:sqlite` is unflagged and `StatementSync.iterate()` exists. */
|
|
18
|
+
export declare const MIN_NODE_VERSION = "22.13.0";
|
|
19
|
+
export declare function readUserVersion(database: DatabaseSync): number;
|
|
20
|
+
export declare function writeUserVersion(database: DatabaseSync, version: number): void;
|
|
21
|
+
/** The process-wide store connection, opened on first use. */
|
|
22
|
+
export declare function getStore(): Promise<DatabaseSync>;
|
|
23
|
+
export declare function closeStore(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Moves a damaged database aside and starts a fresh one.
|
|
26
|
+
*
|
|
27
|
+
* Renames rather than deletes. Unlike `search-index.json`, which is derivable
|
|
28
|
+
* and is correctly rebuilt from disk, a harvest costs hours of network that no
|
|
29
|
+
* local cache can replay — so nothing here removes one without being asked.
|
|
30
|
+
*/
|
|
31
|
+
export declare function quarantineStore(now?: number): Promise<string>;
|
|
32
|
+
/** Runs `work` in a transaction, rolling back if it throws. */
|
|
33
|
+
export declare function inTransaction<T>(database: DatabaseSync, work: () => T): T;
|
|
34
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/utils/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAQhD;;;;;;;;;;;;;;GAcG;AAEH,4FAA4F;AAC5F,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAsG1C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAI9D;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAI9E;AAED,8DAA8D;AAC9D,wBAAsB,QAAQ,IAAI,OAAO,CAAC,YAAY,CAAC,CAGtD;AAED,wBAAgB,UAAU,IAAI,IAAI,CAGjC;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,GAAG,SAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAKvE;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAUzE"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { chmod, rename } from 'node:fs/promises';
|
|
2
|
+
import { YouTubeError } from './errors.js';
|
|
3
|
+
import { ensurePrivateDir } from './paths.js';
|
|
4
|
+
import { corruptStorePath, storeDatabasePath, storeDir } from './store-paths.js';
|
|
5
|
+
import { STORE_PRAGMAS, STORE_SCHEMA, STORE_VERSION } from './store-schema.js';
|
|
6
|
+
import { runStoreMigrations } from './store-migrations.js';
|
|
7
|
+
/**
|
|
8
|
+
* The one connection to the harvested store.
|
|
9
|
+
*
|
|
10
|
+
* `node:sqlite` is a runtime builtin, so this adds nothing to package.json —
|
|
11
|
+
* the same category as `node:fs`. It is reached through a dynamic import
|
|
12
|
+
* behind an async accessor so that a Node without it still loads every module
|
|
13
|
+
* and still serves the tools that do not touch the store; only the harvest
|
|
14
|
+
* tools fail, with a sentence naming the version to install.
|
|
15
|
+
*
|
|
16
|
+
* The reason for a database rather than more JSON files is transactional
|
|
17
|
+
* receipts. `brain-build.ts` carries a long comment explaining that its
|
|
18
|
+
* manifest and its chunks are two documents, only one can be written first,
|
|
19
|
+
* and a crash in between strands a brain. Here the rows and the receipt that
|
|
20
|
+
* describes them commit together, so that failure is not representable.
|
|
21
|
+
*/
|
|
22
|
+
/** The exact Node where `node:sqlite` is unflagged and `StatementSync.iterate()` exists. */
|
|
23
|
+
export const MIN_NODE_VERSION = '22.13.0';
|
|
24
|
+
const BUSY_TIMEOUT_MS = 5_000;
|
|
25
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
26
|
+
let db;
|
|
27
|
+
let warningFilterInstalled = false;
|
|
28
|
+
/**
|
|
29
|
+
* Silence only Node's own "SQLite is experimental" notice.
|
|
30
|
+
*
|
|
31
|
+
* It goes to stderr, so it cannot corrupt the stdio protocol, but it is noise
|
|
32
|
+
* in every client's log. Matched on both the name and the message so that an
|
|
33
|
+
* unrelated ExperimentalWarning still reaches whoever was listening for it.
|
|
34
|
+
*
|
|
35
|
+
* Only listeners registered before the first store open are wrapped — which is
|
|
36
|
+
* the one that matters, Node's own default printer. A listener added later
|
|
37
|
+
* sees every warning, including this one, and that is the right trade: this
|
|
38
|
+
* exists to quieten a default, not to hide a warning from someone who went
|
|
39
|
+
* looking for it.
|
|
40
|
+
*/
|
|
41
|
+
function installWarningFilter() {
|
|
42
|
+
if (warningFilterInstalled)
|
|
43
|
+
return;
|
|
44
|
+
warningFilterInstalled = true;
|
|
45
|
+
const existing = process.listeners('warning');
|
|
46
|
+
for (const listener of existing)
|
|
47
|
+
process.removeListener('warning', listener);
|
|
48
|
+
process.on('warning', (warning) => {
|
|
49
|
+
if (warning.name === 'ExperimentalWarning' && warning.message.includes('SQLite'))
|
|
50
|
+
return;
|
|
51
|
+
for (const listener of existing)
|
|
52
|
+
listener(warning);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function loadSqlite() {
|
|
56
|
+
try {
|
|
57
|
+
installWarningFilter();
|
|
58
|
+
// Annotated rather than inferred: a bare dynamic import widens to `any`,
|
|
59
|
+
// which would let every later misuse of the module through unchecked.
|
|
60
|
+
const sqlite = await import('node:sqlite');
|
|
61
|
+
return sqlite;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new YouTubeError('STORE_UNAVAILABLE', 'This Node build has no node:sqlite, which the harvest tools need.', {
|
|
65
|
+
nextStep: `Install Node ${MIN_NODE_VERSION} or newer (this process is ${process.version}). Every other tool works without it.`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function isCorruption(error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
+
return /malformed|not a database|SQLITE_CORRUPT|SQLITE_NOTADB/i.test(message);
|
|
72
|
+
}
|
|
73
|
+
function assertUsableVersion(found) {
|
|
74
|
+
if (found <= STORE_VERSION)
|
|
75
|
+
return;
|
|
76
|
+
// Operating on a schema written by a newer server is how data gets
|
|
77
|
+
// destroyed. Refusing is the only safe move, and naming both versions is
|
|
78
|
+
// what makes the refusal actionable.
|
|
79
|
+
throw new YouTubeError('STORE_CORRUPT', `This store was written by a newer version of the server (store v${String(found)}; this build understands v${String(STORE_VERSION)}).`, { nextStep: 'Upgrade youtube-knowledge-mcp, or point HOME at a different data directory.' });
|
|
80
|
+
}
|
|
81
|
+
async function openDatabase() {
|
|
82
|
+
const sqlite = await loadSqlite();
|
|
83
|
+
await ensurePrivateDir(storeDir());
|
|
84
|
+
const path = storeDatabasePath();
|
|
85
|
+
let opened;
|
|
86
|
+
try {
|
|
87
|
+
opened = new sqlite.DatabaseSync(path, {
|
|
88
|
+
timeout: BUSY_TIMEOUT_MS,
|
|
89
|
+
enableForeignKeyConstraints: true,
|
|
90
|
+
});
|
|
91
|
+
for (const pragma of STORE_PRAGMAS)
|
|
92
|
+
opened.exec(pragma);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (!isCorruption(error))
|
|
96
|
+
throw error;
|
|
97
|
+
throw new YouTubeError('STORE_CORRUPT', 'The harvested store could not be opened.', {
|
|
98
|
+
nextStep: 'Call repair_store to move the damaged file aside and start a fresh store.',
|
|
99
|
+
cause: error,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const version = readUserVersion(opened);
|
|
103
|
+
assertUsableVersion(version);
|
|
104
|
+
opened.exec(STORE_SCHEMA);
|
|
105
|
+
runStoreMigrations(opened, version);
|
|
106
|
+
// The database holds harvested personal data — commenter names, ids and
|
|
107
|
+
// free text — so it gets the same bits as the transcript cache rather than
|
|
108
|
+
// the umask default.
|
|
109
|
+
await chmod(path, PRIVATE_FILE_MODE).catch(() => undefined);
|
|
110
|
+
return opened;
|
|
111
|
+
}
|
|
112
|
+
export function readUserVersion(database) {
|
|
113
|
+
const row = database.prepare('PRAGMA user_version').get();
|
|
114
|
+
const value = row?.user_version;
|
|
115
|
+
return typeof value === 'number' ? value : 0;
|
|
116
|
+
}
|
|
117
|
+
export function writeUserVersion(database, version) {
|
|
118
|
+
// PRAGMA does not accept a bound parameter, so the value is constrained to an
|
|
119
|
+
// integer here rather than interpolated from anything a caller controls.
|
|
120
|
+
database.exec(`PRAGMA user_version = ${String(Math.trunc(version))}`);
|
|
121
|
+
}
|
|
122
|
+
/** The process-wide store connection, opened on first use. */
|
|
123
|
+
export async function getStore() {
|
|
124
|
+
db ??= await openDatabase();
|
|
125
|
+
return db;
|
|
126
|
+
}
|
|
127
|
+
export function closeStore() {
|
|
128
|
+
db?.close();
|
|
129
|
+
db = undefined;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Moves a damaged database aside and starts a fresh one.
|
|
133
|
+
*
|
|
134
|
+
* Renames rather than deletes. Unlike `search-index.json`, which is derivable
|
|
135
|
+
* and is correctly rebuilt from disk, a harvest costs hours of network that no
|
|
136
|
+
* local cache can replay — so nothing here removes one without being asked.
|
|
137
|
+
*/
|
|
138
|
+
export async function quarantineStore(now = Date.now()) {
|
|
139
|
+
closeStore();
|
|
140
|
+
const target = corruptStorePath(now);
|
|
141
|
+
await rename(storeDatabasePath(), target);
|
|
142
|
+
return target;
|
|
143
|
+
}
|
|
144
|
+
/** Runs `work` in a transaction, rolling back if it throws. */
|
|
145
|
+
export function inTransaction(database, work) {
|
|
146
|
+
database.exec('BEGIN');
|
|
147
|
+
try {
|
|
148
|
+
const result = work();
|
|
149
|
+
database.exec('COMMIT');
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
database.exec('ROLLBACK');
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/utils/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TranscriptSegment } from './transcript.js';
|
|
2
|
+
/** Subtitle export: SRT and WebVTT for editors. */
|
|
3
|
+
/** 3723.5 -> "01:02:03,500" — the SRT wire format. */
|
|
4
|
+
export declare function formatSrtTimestamp(totalSeconds: number): string;
|
|
5
|
+
export declare function toSrt(segments: TranscriptSegment[]): string;
|
|
6
|
+
export declare function toVtt(segments: TranscriptSegment[]): string;
|
|
7
|
+
//# sourceMappingURL=subtitles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subtitles.d.ts","sourceRoot":"","sources":["../../src/utils/subtitles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,mDAAmD;AAEnD,sDAAsD;AACtD,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAS/D;AAED,wBAAgB,KAAK,CAAC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAY3D;AAED,wBAAgB,KAAK,CAAC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAW3D"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Subtitle export: SRT and WebVTT for editors. */
|
|
2
|
+
/** 3723.5 -> "01:02:03,500" — the SRT wire format. */
|
|
3
|
+
export function formatSrtTimestamp(totalSeconds) {
|
|
4
|
+
const clamped = Math.max(0, totalSeconds);
|
|
5
|
+
const hours = Math.floor(clamped / 3600);
|
|
6
|
+
const minutes = Math.floor((clamped % 3600) / 60);
|
|
7
|
+
const seconds = Math.floor(clamped % 60);
|
|
8
|
+
const millis = Math.round((clamped - Math.floor(clamped)) * 1000);
|
|
9
|
+
const pad = (value, width = 2) => value.toString().padStart(width, '0');
|
|
10
|
+
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)},${pad(millis, 3)}`;
|
|
11
|
+
}
|
|
12
|
+
export function toSrt(segments) {
|
|
13
|
+
return (segments
|
|
14
|
+
.map((segment, index) => [
|
|
15
|
+
index + 1,
|
|
16
|
+
`${formatSrtTimestamp(segment.start)} --> ${formatSrtTimestamp(segment.end)}`,
|
|
17
|
+
segment.text,
|
|
18
|
+
].join('\n'))
|
|
19
|
+
.join('\n\n') + '\n');
|
|
20
|
+
}
|
|
21
|
+
export function toVtt(segments) {
|
|
22
|
+
const body = segments
|
|
23
|
+
.map((segment) => [
|
|
24
|
+
`${formatSrtTimestamp(segment.start).replace(',', '.')} --> ${formatSrtTimestamp(segment.end).replace(',', '.')}`,
|
|
25
|
+
segment.text,
|
|
26
|
+
].join('\n'))
|
|
27
|
+
.join('\n\n');
|
|
28
|
+
return `WEBVTT\n\n${body}\n`;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=subtitles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subtitles.js","sourceRoot":"","sources":["../../src/utils/subtitles.ts"],"names":[],"mappings":"AAEA,mDAAmD;AAEnD,sDAAsD;AACtD,MAAM,UAAU,kBAAkB,CAAC,YAAoB;IACrD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAElE,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,KAAK,GAAG,CAAC,EAAU,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACxF,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,QAA6B;IACjD,OAAO,CACL,QAAQ;SACL,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CACtB;QACE,KAAK,GAAG,CAAC;QACT,GAAG,kBAAkB,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;QAC7E,OAAO,CAAC,IAAI;KACb,CAAC,IAAI,CAAC,IAAI,CAAC,CACb;SACA,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CACvB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,QAA6B;IACjD,MAAM,IAAI,GAAG,QAAQ;SAClB,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CACf;QACE,GAAG,kBAAkB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE;QACjH,OAAO,CAAC,IAAI;KACb,CAAC,IAAI,CAAC,IAAI,CAAC,CACb;SACA,IAAI,CAAC,MAAM,CAAC,CAAC;IAEhB,OAAO,aAAa,IAAI,IAAI,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ChannelImage, type ThumbnailEntry, type ThumbnailManifest, type ThumbnailQuality, type ThumbnailTab } from '../thumbnail-schemas.js';
|
|
2
|
+
import type { VideoListItem } from './youtube.js';
|
|
3
|
+
/** One thumbnail at a time: what is recorded about it, and how it is fetched and saved. */
|
|
4
|
+
export interface TabListing {
|
|
5
|
+
tab: ThumbnailTab;
|
|
6
|
+
videos: VideoListItem[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Per-video entries, reconciled against the files actually on disk.
|
|
10
|
+
*
|
|
11
|
+
* A record that says `saved` whose file is missing or the wrong size goes back
|
|
12
|
+
* to `pending`; a video in today's listing that has no record gets one; and a
|
|
13
|
+
* record for a video not listed today is kept, since its file is still a
|
|
14
|
+
* thumbnail and the tab may simply not have been requested this time.
|
|
15
|
+
*/
|
|
16
|
+
export declare function reconciled(channelId: string, existing: ThumbnailManifest | undefined, listings: TabListing[]): Promise<Map<string, ThumbnailEntry>>;
|
|
17
|
+
export declare function fetchOne(channelId: string, entry: ThumbnailEntry, quality: ThumbnailQuality): Promise<ThumbnailEntry>;
|
|
18
|
+
export declare function channelImage(channelId: string, kind: 'avatar' | 'banner', url: string | undefined, recorded: ChannelImage | undefined): Promise<ChannelImage | undefined>;
|
|
19
|
+
//# sourceMappingURL=thumbnail-entry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnail-entry.d.ts","sourceRoot":"","sources":["../../src/utils/thumbnail-entry.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,yBAAyB,CAAC;AASjC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,2FAA2F;AAE3F,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,YAAY,CAAC;IAClB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAID;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,QAAQ,EAAE,UAAU,EAAE,GACrB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAetC;AA4BD,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,cAAc,CAAC,CAwBzB;AAED,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,QAAQ,EAAE,YAAY,GAAG,SAAS,GACjC,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAkBnC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises';
|
|
2
|
+
import { imageFormatSchema, } from '../thumbnail-schemas.js';
|
|
3
|
+
import { throwIfAborted } from './context.js';
|
|
4
|
+
import { asYouTubeError } from './errors.js';
|
|
5
|
+
import { largestThumbnail } from './flat-listing.js';
|
|
6
|
+
import { MAX_IMAGE_BYTES } from './image-fetch.js';
|
|
7
|
+
import { writeFileAtomic } from './json-file.js';
|
|
8
|
+
import { climb, videoRungs } from './thumbnail-ladder.js';
|
|
9
|
+
import { channelImagePath, relativeFile, videoThumbnailPath } from './thumbnail-paths.js';
|
|
10
|
+
import { channelImageFile, entryPath, isIntact } from './thumbnail-store.js';
|
|
11
|
+
const ALL_FORMATS = imageFormatSchema.options;
|
|
12
|
+
/**
|
|
13
|
+
* Per-video entries, reconciled against the files actually on disk.
|
|
14
|
+
*
|
|
15
|
+
* A record that says `saved` whose file is missing or the wrong size goes back
|
|
16
|
+
* to `pending`; a video in today's listing that has no record gets one; and a
|
|
17
|
+
* record for a video not listed today is kept, since its file is still a
|
|
18
|
+
* thumbnail and the tab may simply not have been requested this time.
|
|
19
|
+
*/
|
|
20
|
+
export async function reconciled(channelId, existing, listings) {
|
|
21
|
+
const entries = new Map();
|
|
22
|
+
for (const [videoId, recorded] of Object.entries(existing?.videos ?? {})) {
|
|
23
|
+
const intact = await isIntact(entryPath(channelId, recorded), recorded.bytes);
|
|
24
|
+
entries.set(videoId, intact ? recorded : { ...recorded, state: 'pending' });
|
|
25
|
+
}
|
|
26
|
+
for (const { tab, videos } of listings) {
|
|
27
|
+
for (const video of videos) {
|
|
28
|
+
if (!entries.has(video.id))
|
|
29
|
+
entries.set(video.id, pendingEntry(tab, video));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return entries;
|
|
33
|
+
}
|
|
34
|
+
function pendingEntry(tab, video) {
|
|
35
|
+
const listed = largestThumbnail(video.thumbnails ?? []);
|
|
36
|
+
const portrait = listed?.width !== null &&
|
|
37
|
+
listed?.width !== undefined &&
|
|
38
|
+
listed.height !== null &&
|
|
39
|
+
listed.height !== undefined &&
|
|
40
|
+
listed.height > listed.width;
|
|
41
|
+
return {
|
|
42
|
+
videoId: video.id,
|
|
43
|
+
title: video.title,
|
|
44
|
+
url: video.url,
|
|
45
|
+
tab,
|
|
46
|
+
isShort: tab === 'shorts' || portrait,
|
|
47
|
+
durationSeconds: video.duration,
|
|
48
|
+
...(video.liveStatus === undefined ? {} : { liveStatus: video.liveStatus }),
|
|
49
|
+
...(listed === undefined ? {} : { listedUrl: listed.url }),
|
|
50
|
+
...(listed?.width === null || listed?.width === undefined ? {} : { listedWidth: listed.width }),
|
|
51
|
+
...(listed?.height === null || listed?.height === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { listedHeight: listed.height }),
|
|
54
|
+
state: 'pending',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function fetchOne(channelId, entry, quality) {
|
|
58
|
+
const listed = entry.listedUrl === undefined
|
|
59
|
+
? undefined
|
|
60
|
+
: {
|
|
61
|
+
url: entry.listedUrl,
|
|
62
|
+
...(entry.listedWidth === undefined ? {} : { width: entry.listedWidth }),
|
|
63
|
+
...(entry.listedHeight === undefined ? {} : { height: entry.listedHeight }),
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
const climbed = await climb(videoRungs(entry.videoId, listed, entry.isShort, quality), MAX_IMAGE_BYTES);
|
|
67
|
+
const path = videoThumbnailPath(channelId, entry.tab, entry.videoId, climbed.probe.format);
|
|
68
|
+
await replaceFile(path, climbed, (format) => videoThumbnailPath(channelId, entry.tab, entry.videoId, format));
|
|
69
|
+
return { ...withoutSaved(entry), state: 'saved', ...saved(channelId, path, climbed) };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throwIfAborted();
|
|
73
|
+
return { ...withoutSaved(entry), state: 'failed', error: asYouTubeError(error).code };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function channelImage(channelId, kind, url, recorded) {
|
|
77
|
+
if (url === undefined)
|
|
78
|
+
return recorded;
|
|
79
|
+
if (recorded !== undefined &&
|
|
80
|
+
(await isIntact(channelImageFile(channelId, recorded), recorded.bytes))) {
|
|
81
|
+
return recorded;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const climbed = await climb([{ variant: 'uncropped', url }], MAX_IMAGE_BYTES);
|
|
85
|
+
const path = channelImagePath(channelId, kind, climbed.probe.format);
|
|
86
|
+
await replaceFile(path, climbed, (format) => channelImagePath(channelId, kind, format));
|
|
87
|
+
return { kind, state: 'saved', ...saved(channelId, path, climbed) };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throwIfAborted();
|
|
91
|
+
return { kind, state: 'failed', error: asYouTubeError(error).code };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Write the image, and remove an earlier file of the same name in another format. */
|
|
95
|
+
async function replaceFile(path, climbed, pathFor) {
|
|
96
|
+
await writeFileAtomic(path, climbed.bytes);
|
|
97
|
+
for (const format of ALL_FORMATS) {
|
|
98
|
+
if (format !== climbed.probe.format)
|
|
99
|
+
await rm(pathFor(format), { force: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function saved(channelId, path, climbed) {
|
|
103
|
+
return {
|
|
104
|
+
file: relativeFile(channelId, path),
|
|
105
|
+
sourceUrl: climbed.rung.url,
|
|
106
|
+
variant: climbed.rung.variant,
|
|
107
|
+
width: climbed.probe.width,
|
|
108
|
+
height: climbed.probe.height,
|
|
109
|
+
bytes: climbed.bytes.byteLength,
|
|
110
|
+
format: climbed.probe.format,
|
|
111
|
+
fetchedAt: new Date().toISOString(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The entry with every field of a previous save removed, so a failure cannot keep stale sizes. */
|
|
115
|
+
function withoutSaved(entry) {
|
|
116
|
+
const { file: _file, sourceUrl: _sourceUrl, variant: _variant, width: _width, height: _height, bytes: _bytes, format: _format, fetchedAt: _fetchedAt, error: _error, ...rest } = entry;
|
|
117
|
+
return rest;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=thumbnail-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnail-entry.js","sourceRoot":"","sources":["../../src/utils/thumbnail-entry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ThumbnailManifest, ThumbnailQuality, ThumbnailTab } from '../thumbnail-schemas.js';
|
|
2
|
+
import { type TabListing } from './thumbnail-entry.js';
|
|
3
|
+
export type { TabListing } from './thumbnail-entry.js';
|
|
4
|
+
import type { ChannelInfo } from './youtube.js';
|
|
5
|
+
/**
|
|
6
|
+
* Fetching a channel's thumbnails, resumably.
|
|
7
|
+
*
|
|
8
|
+
* Shaped like a brain build, and for the same reason: several hundred images
|
|
9
|
+
* is several hundred requests, so the job gets interrupted — cancelled by the
|
|
10
|
+
* client, throttled by YouTube, or killed with the editor. Per-image state
|
|
11
|
+
* rather than one flag, checkpoints rather than one write at the end, a
|
|
12
|
+
* failed image costing only itself, and a second call that continues.
|
|
13
|
+
*/
|
|
14
|
+
export declare const CHECKPOINT_EVERY_ITEMS = 20;
|
|
15
|
+
export declare const RATE_LIMIT_TOLERANCE = 3;
|
|
16
|
+
export interface FetchThumbnailsOptions {
|
|
17
|
+
channel: ChannelInfo;
|
|
18
|
+
listings: TabListing[];
|
|
19
|
+
existing: ThumbnailManifest | undefined;
|
|
20
|
+
quality: ThumbnailQuality;
|
|
21
|
+
}
|
|
22
|
+
export interface FetchThumbnailsResult {
|
|
23
|
+
manifest: ThumbnailManifest;
|
|
24
|
+
considered: number;
|
|
25
|
+
fetched: number;
|
|
26
|
+
skipped: number;
|
|
27
|
+
failed: number;
|
|
28
|
+
failures: {
|
|
29
|
+
videoId: string;
|
|
30
|
+
tab: ThumbnailTab;
|
|
31
|
+
error: string;
|
|
32
|
+
}[];
|
|
33
|
+
stoppedEarly: boolean;
|
|
34
|
+
stopReason?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function fetchChannelThumbnails(options: FetchThumbnailsOptions): Promise<FetchThumbnailsResult>;
|
|
37
|
+
//# sourceMappingURL=thumbnail-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnail-fetch.d.ts","sourceRoot":"","sources":["../../src/utils/thumbnail-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAGjG,OAAO,EAAsC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAE3F,YAAY,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAOvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;;;GAQG;AAEH,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,QAAQ,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACxC,OAAO,EAAE,gBAAgB,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,YAAY,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAClE,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CAiFhC"}
|