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,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The human- and model-readable form of a receipt.
|
|
3
|
+
*
|
|
4
|
+
* `structuredContent` carries the machine-readable truth, but a model reads
|
|
5
|
+
* the text block, and that is where an overclaim actually happens. So every
|
|
6
|
+
* extraction tool prepends this, from one function, rather than each writing
|
|
7
|
+
* its own wording and drifting.
|
|
8
|
+
*
|
|
9
|
+
* The incomplete form says three things on purpose: the numbers, why it
|
|
10
|
+
* stopped, and an explicit instruction not to describe it as everything. The
|
|
11
|
+
* last one reads as belt-and-braces next to the `complete: false` field, and
|
|
12
|
+
* it is: the field is for programs, the sentence is for the reader.
|
|
13
|
+
*/
|
|
14
|
+
const UNITS = {
|
|
15
|
+
'channel-catalog': 'videos',
|
|
16
|
+
'video-details': 'videos',
|
|
17
|
+
'video-comments': 'comments',
|
|
18
|
+
};
|
|
19
|
+
const WHY = {
|
|
20
|
+
COMPLETE: 'complete',
|
|
21
|
+
CAP_REACHED: 'stopped by the requested cap',
|
|
22
|
+
BUDGET_SPENT: 'stopped when the time budget ran out',
|
|
23
|
+
RATE_LIMITED: 'stopped because YouTube throttled this client',
|
|
24
|
+
CANCELLED: 'cancelled by the client',
|
|
25
|
+
TIMEOUT: 'timed out',
|
|
26
|
+
SOURCE_REFUSED: 'YouTube refused this content to the current session',
|
|
27
|
+
COMMENTS_DISABLED: 'comments are turned off for this video',
|
|
28
|
+
SOURCE_SILENT: 'YouTube states no total, so exhaustion cannot be proven',
|
|
29
|
+
PARTIAL_ERROR: 'some items failed',
|
|
30
|
+
NOT_ATTEMPTED: 'not attempted yet',
|
|
31
|
+
};
|
|
32
|
+
function count(value) {
|
|
33
|
+
return value.toLocaleString('en-US');
|
|
34
|
+
}
|
|
35
|
+
function ratio(coverage) {
|
|
36
|
+
if (coverage.expected === undefined || coverage.expected === 0)
|
|
37
|
+
return '';
|
|
38
|
+
const percent = (coverage.have / coverage.expected) * 100;
|
|
39
|
+
return ` (${percent < 0.1 && percent > 0 ? '<0.1' : percent.toFixed(1)}%)`;
|
|
40
|
+
}
|
|
41
|
+
export function renderCoverage(coverage) {
|
|
42
|
+
const unit = UNITS[coverage.scope];
|
|
43
|
+
const of = coverage.expected === undefined
|
|
44
|
+
? `${count(coverage.have)} ${unit}`
|
|
45
|
+
: `${count(coverage.have)} of ${coverage.expectedSource === 'youtube:comment_count' ? 'about ' : ''}${count(coverage.expected)} ${unit}${ratio(coverage)}`;
|
|
46
|
+
const lines = [`Coverage: ${of} for ${coverage.targetId}.`];
|
|
47
|
+
if (coverage.complete) {
|
|
48
|
+
lines.push(`COMPLETE — ${WHY[coverage.reason]}, harvested ${coverage.harvestedAt}.`);
|
|
49
|
+
if (coverage.staleAfter !== undefined) {
|
|
50
|
+
lines.push(`Re-check after ${coverage.staleAfter} (this can change upstream).`);
|
|
51
|
+
}
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
const bias = coverage.sortApplied === 'top' && coverage.limitApplied !== undefined
|
|
55
|
+
? ', sorted by "top" (a biased prefix, not a random sample)'
|
|
56
|
+
: '';
|
|
57
|
+
lines.push(`INCOMPLETE — ${WHY[coverage.reason]}${bias}.`, `Do not describe this as the full ${unit === 'comments' ? 'comment history' : 'video list'}.`);
|
|
58
|
+
if (coverage.note !== undefined)
|
|
59
|
+
lines.push(coverage.note);
|
|
60
|
+
return lines.join('\n');
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=coverage-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage-text.js","sourceRoot":"","sources":["../../src/utils/coverage-text.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Coverage, type CompletenessReason, type ExpectedSource, type HarvestScope } from '../harvest-schemas.js';
|
|
2
|
+
/**
|
|
3
|
+
* The one constructor for a completeness receipt.
|
|
4
|
+
*
|
|
5
|
+
* A wrong `complete: true` is the worst thing this server can emit: it is the
|
|
6
|
+
* difference between a consumer saying "here is a sample of the comments" and
|
|
7
|
+
* "here is the video's full comment history". So `complete` is not a field a
|
|
8
|
+
* caller sets — it is derived here, and the derivation is guarded by
|
|
9
|
+
* invariants that are asserted in code rather than merely documented.
|
|
10
|
+
*
|
|
11
|
+
* The rule that does the most work: `expected` is what the SOURCE said, and is
|
|
12
|
+
* never derived from `have`. Copying the number we collected into the field
|
|
13
|
+
* that means "how many there are" would turn "we stopped" into "there was
|
|
14
|
+
* nothing more", silently, and every check downstream would agree with it.
|
|
15
|
+
*/
|
|
16
|
+
/** A complete receipt is still only as fresh as its harvest; comments accrue. */
|
|
17
|
+
export declare const RECEIPT_STALE_MS: number;
|
|
18
|
+
/** Beyond this, receipts are summarised rather than listed in a tool result. */
|
|
19
|
+
export declare const COVERAGE_IN_RESULT_LIMIT = 100;
|
|
20
|
+
export interface ExpectedTotal {
|
|
21
|
+
value: number;
|
|
22
|
+
source: ExpectedSource;
|
|
23
|
+
}
|
|
24
|
+
export interface CoverageInput {
|
|
25
|
+
scope: HarvestScope;
|
|
26
|
+
targetId: string;
|
|
27
|
+
have: number;
|
|
28
|
+
source: string;
|
|
29
|
+
/**
|
|
30
|
+
* Only ever an explicit pair or nothing. There is deliberately no code path
|
|
31
|
+
* that accepts a bare number, because the number would have to come from
|
|
32
|
+
* somewhere and `have` is the nearest thing to hand.
|
|
33
|
+
*/
|
|
34
|
+
expected?: ExpectedTotal;
|
|
35
|
+
ranToExhaustion?: boolean;
|
|
36
|
+
reason?: CompletenessReason;
|
|
37
|
+
limitApplied?: number;
|
|
38
|
+
sortApplied?: 'top' | 'new';
|
|
39
|
+
resumeToken?: string;
|
|
40
|
+
harvestedAt?: Date;
|
|
41
|
+
note?: string;
|
|
42
|
+
}
|
|
43
|
+
/** Thrown for a receipt that contradicts itself. Not a YouTubeError: no caller can fix it. */
|
|
44
|
+
export declare class CoverageInvariantError extends Error {
|
|
45
|
+
constructor(message: string);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Checks a receipt against every invariant.
|
|
49
|
+
*
|
|
50
|
+
* Called inside `coverageOf` and again by handlers before a result goes out,
|
|
51
|
+
* because the cost of the second call is nothing and the cost of a wrong
|
|
52
|
+
* `complete: true` reaching a consumer is the whole point of this module.
|
|
53
|
+
*/
|
|
54
|
+
export declare function assertCoverageConsistent(coverage: Coverage): void;
|
|
55
|
+
/** Builds a validated receipt. The only way to make one. */
|
|
56
|
+
export declare function coverageOf(input: CoverageInput): Coverage;
|
|
57
|
+
//# sourceMappingURL=coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../src/utils/coverage.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;;;;GAaG;AAEH,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,QAAiB,CAAC;AAE/C,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,8FAA8F;AAC9F,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAwCD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CA0CjE;AAED,4DAA4D;AAC5D,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,CA8BzD"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { coverageSchema, } from '../harvest-schemas.js';
|
|
2
|
+
/**
|
|
3
|
+
* The one constructor for a completeness receipt.
|
|
4
|
+
*
|
|
5
|
+
* A wrong `complete: true` is the worst thing this server can emit: it is the
|
|
6
|
+
* difference between a consumer saying "here is a sample of the comments" and
|
|
7
|
+
* "here is the video's full comment history". So `complete` is not a field a
|
|
8
|
+
* caller sets — it is derived here, and the derivation is guarded by
|
|
9
|
+
* invariants that are asserted in code rather than merely documented.
|
|
10
|
+
*
|
|
11
|
+
* The rule that does the most work: `expected` is what the SOURCE said, and is
|
|
12
|
+
* never derived from `have`. Copying the number we collected into the field
|
|
13
|
+
* that means "how many there are" would turn "we stopped" into "there was
|
|
14
|
+
* nothing more", silently, and every check downstream would agree with it.
|
|
15
|
+
*/
|
|
16
|
+
/** A complete receipt is still only as fresh as its harvest; comments accrue. */
|
|
17
|
+
export const RECEIPT_STALE_MS = 7 * 86_400_000;
|
|
18
|
+
/** Beyond this, receipts are summarised rather than listed in a tool result. */
|
|
19
|
+
export const COVERAGE_IN_RESULT_LIMIT = 100;
|
|
20
|
+
/** Thrown for a receipt that contradicts itself. Not a YouTubeError: no caller can fix it. */
|
|
21
|
+
export class CoverageInvariantError extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(`Coverage invariant violated: ${message}`);
|
|
24
|
+
this.name = 'CoverageInvariantError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function capWasBinding(input) {
|
|
28
|
+
return input.limitApplied !== undefined && input.have >= input.limitApplied;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether this server can prove it holds everything in scope.
|
|
32
|
+
*
|
|
33
|
+
* Two independent ways to prove it, and a cap overrides both:
|
|
34
|
+
*
|
|
35
|
+
* - the source stated a total and we hold at least that many, or
|
|
36
|
+
* - the source's own iteration ran out (`source-exhausted`).
|
|
37
|
+
*
|
|
38
|
+
* Exhaustion alone is not proof. Measured against yt-dlp 2026.08.19: a run
|
|
39
|
+
* capped at 40 parent comments returned a non-null `comment_count` of 58,
|
|
40
|
+
* because `itertools.islice` ends iteration at the cap exactly as a natural
|
|
41
|
+
* end would. So a binding cap disqualifies completeness even when the source
|
|
42
|
+
* says it finished.
|
|
43
|
+
*/
|
|
44
|
+
function isProvablyComplete(input) {
|
|
45
|
+
if (capWasBinding(input))
|
|
46
|
+
return false;
|
|
47
|
+
if (input.expected !== undefined) {
|
|
48
|
+
if (input.expected.source === 'source-exhausted')
|
|
49
|
+
return input.ranToExhaustion !== false;
|
|
50
|
+
return input.have >= input.expected.value;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
function derivedReason(input, complete) {
|
|
55
|
+
if (complete)
|
|
56
|
+
return 'COMPLETE';
|
|
57
|
+
if (input.reason !== undefined && input.reason !== 'COMPLETE')
|
|
58
|
+
return input.reason;
|
|
59
|
+
if (capWasBinding(input))
|
|
60
|
+
return 'CAP_REACHED';
|
|
61
|
+
// Nothing said there was more, and nothing proved there was not. That is not
|
|
62
|
+
// completeness; it is the absence of evidence, and it has its own name.
|
|
63
|
+
return 'SOURCE_SILENT';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Checks a receipt against every invariant.
|
|
67
|
+
*
|
|
68
|
+
* Called inside `coverageOf` and again by handlers before a result goes out,
|
|
69
|
+
* because the cost of the second call is nothing and the cost of a wrong
|
|
70
|
+
* `complete: true` reaching a consumer is the whole point of this module.
|
|
71
|
+
*/
|
|
72
|
+
export function assertCoverageConsistent(coverage) {
|
|
73
|
+
const fail = (message) => {
|
|
74
|
+
throw new CoverageInvariantError(message);
|
|
75
|
+
};
|
|
76
|
+
if (coverage.complete && coverage.reason !== 'COMPLETE') {
|
|
77
|
+
fail(`complete is true but reason is ${coverage.reason}`);
|
|
78
|
+
}
|
|
79
|
+
if (!coverage.complete && coverage.reason === 'COMPLETE') {
|
|
80
|
+
fail('reason is COMPLETE but complete is false');
|
|
81
|
+
}
|
|
82
|
+
if ((coverage.expected === undefined) !== (coverage.expectedSource === undefined)) {
|
|
83
|
+
fail('expected and expectedSource must be present or absent together');
|
|
84
|
+
}
|
|
85
|
+
if (coverage.complete) {
|
|
86
|
+
const exhausted = coverage.expectedSource === 'source-exhausted';
|
|
87
|
+
const enough = coverage.expected !== undefined && coverage.have >= coverage.expected;
|
|
88
|
+
if (!exhausted && !enough) {
|
|
89
|
+
fail('complete is true without either a met expected total or an exhausted source');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// There is deliberately no separate "COMPLETE without an expected total"
|
|
93
|
+
// guard. It reads like a necessary invariant and is unreachable: reaching it
|
|
94
|
+
// needs reason COMPLETE, which the checks above already force to pair with
|
|
95
|
+
// complete:true, which in turn already demands either a met total or an
|
|
96
|
+
// exhausted source. An unreachable guard is not defence in depth, it is code
|
|
97
|
+
// no test can justify.
|
|
98
|
+
// One-directional on purpose. "A token exactly when incomplete" is the
|
|
99
|
+
// tempting symmetry, and it is wrong: plenty of incomplete receipts have
|
|
100
|
+
// nothing to resume — SOURCE_REFUSED, COMMENTS_DISABLED, a caller that
|
|
101
|
+
// simply did not supply one. What must never happen is a token on a
|
|
102
|
+
// complete receipt, which would invite a caller to page past the end.
|
|
103
|
+
if (coverage.resumeToken !== undefined && coverage.complete) {
|
|
104
|
+
fail('a complete receipt cannot carry a resumeToken');
|
|
105
|
+
}
|
|
106
|
+
if (coverage.limitApplied !== undefined &&
|
|
107
|
+
coverage.have >= coverage.limitApplied &&
|
|
108
|
+
coverage.reason === 'COMPLETE') {
|
|
109
|
+
fail('a binding cap cannot produce COMPLETE');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Builds a validated receipt. The only way to make one. */
|
|
113
|
+
export function coverageOf(input) {
|
|
114
|
+
const complete = isProvablyComplete(input);
|
|
115
|
+
const reason = derivedReason(input, complete);
|
|
116
|
+
const harvestedAt = input.harvestedAt ?? new Date();
|
|
117
|
+
const coverage = coverageSchema.parse({
|
|
118
|
+
scope: input.scope,
|
|
119
|
+
targetId: input.targetId,
|
|
120
|
+
complete,
|
|
121
|
+
reason,
|
|
122
|
+
have: input.have,
|
|
123
|
+
...(input.expected === undefined
|
|
124
|
+
? {}
|
|
125
|
+
: { expected: input.expected.value, expectedSource: input.expected.source }),
|
|
126
|
+
...(input.ranToExhaustion === undefined ? {} : { ranToExhaustion: input.ranToExhaustion }),
|
|
127
|
+
source: input.source,
|
|
128
|
+
harvestedAt: harvestedAt.toISOString(),
|
|
129
|
+
...(complete
|
|
130
|
+
? { staleAfter: new Date(harvestedAt.getTime() + RECEIPT_STALE_MS).toISOString() }
|
|
131
|
+
: {}),
|
|
132
|
+
...(input.limitApplied === undefined ? {} : { limitApplied: input.limitApplied }),
|
|
133
|
+
...(input.sortApplied === undefined ? {} : { sortApplied: input.sortApplied }),
|
|
134
|
+
// A resume token is meaningless on a complete receipt, and its absence is
|
|
135
|
+
// how a caller knows there is nothing left to ask for.
|
|
136
|
+
...(complete || input.resumeToken === undefined ? {} : { resumeToken: input.resumeToken }),
|
|
137
|
+
...(input.note === undefined ? {} : { note: input.note }),
|
|
138
|
+
});
|
|
139
|
+
assertCoverageConsistent(coverage);
|
|
140
|
+
return coverage;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=coverage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/utils/coverage.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAKf,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;;;;GAaG;AAEH,iFAAiF;AACjF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,UAAU,CAAC;AAE/C,gFAAgF;AAChF,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC;AA2B5C,8FAA8F;AAC9F,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAC/C,YAAY,OAAe;QACzB,KAAK,CAAC,gCAAgC,OAAO,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED,SAAS,aAAa,CAAC,KAAoB;IACzC,OAAO,KAAK,CAAC,YAAY,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,YAAY,CAAC;AAC9E,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,kBAAkB,CAAC,KAAoB;IAC9C,IAAI,aAAa,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAEvC,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,kBAAkB;YAAE,OAAO,KAAK,CAAC,eAAe,KAAK,KAAK,CAAC;QACzF,OAAO,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC;IAC5C,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,aAAa,CAAC,KAAoB,EAAE,QAAiB;IAC5D,IAAI,QAAQ;QAAE,OAAO,UAAU,CAAC;IAChC,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC,MAAM,CAAC;IACnF,IAAI,aAAa,CAAC,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC;IAC/C,6EAA6E;IAC7E,wEAAwE;IACxE,OAAO,eAAe,CAAC;AACzB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CAAC,QAAkB;IACzD,MAAM,IAAI,GAAG,CAAC,OAAe,EAAS,EAAE;QACtC,MAAM,IAAI,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC,CAAC;IAEF,IAAI,QAAQ,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACxD,IAAI,CAAC,kCAAkC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACzD,IAAI,CAAC,0CAA0C,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,QAAQ,KAAK,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,cAAc,KAAK,SAAS,CAAC,EAAE,CAAC;QAClF,IAAI,CAAC,gEAAgE,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACtB,MAAM,SAAS,GAAG,QAAQ,CAAC,cAAc,KAAK,kBAAkB,CAAC;QACjE,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,QAAQ,CAAC;QACrF,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,CAAC;YAC1B,IAAI,CAAC,6EAA6E,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IACD,yEAAyE;IACzE,6EAA6E;IAC7E,2EAA2E;IAC3E,wEAAwE;IACxE,6EAA6E;IAC7E,uBAAuB;IACvB,uEAAuE;IACvE,yEAAyE;IACzE,uEAAuE;IACvE,oEAAoE;IACpE,sEAAsE;IACtE,IAAI,QAAQ,CAAC,WAAW,KAAK,SAAS,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;QAC5D,IAAI,CAAC,+CAA+C,CAAC,CAAC;IACxD,CAAC;IACD,IACE,QAAQ,CAAC,YAAY,KAAK,SAAS;QACnC,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,YAAY;QACtC,QAAQ,CAAC,MAAM,KAAK,UAAU,EAC9B,CAAC;QACD,IAAI,CAAC,uCAAuC,CAAC,CAAC;IAChD,CAAC;AACH,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,UAAU,CAAC,KAAoB;IAC7C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,IAAI,IAAI,IAAI,EAAE,CAAC;IAEpD,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC;QACpC,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,QAAQ;QACR,MAAM;QACN,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,GAAG,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS;YAC9B,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC9E,GAAG,CAAC,KAAK,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,eAAe,EAAE,CAAC;QAC1F,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,WAAW,EAAE,WAAW,CAAC,WAAW,EAAE;QACtC,GAAG,CAAC,QAAQ;YACV,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,gBAAgB,CAAC,CAAC,WAAW,EAAE,EAAE;YAClF,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,KAAK,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC;QACjF,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC;QAC9E,0EAA0E;QAC1E,uDAAuD;QACvD,GAAG,CAAC,QAAQ,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC;QAC1F,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;KAC1D,CAAC,CAAC;IAEH,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IACnC,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading configuration from the environment without trusting it.
|
|
3
|
+
*
|
|
4
|
+
* A bare `Number()` of an environment value turns a typo into `NaN`, and `NaN` compares
|
|
5
|
+
* false with everything: a concurrency limit of `NaN` queued every yt-dlp call
|
|
6
|
+
* forever, and a cache TTL of `NaN` never expired anything. Every numeric or
|
|
7
|
+
* enumerated setting therefore comes through here, where an unusable value
|
|
8
|
+
* falls back to the default and says so once on stderr.
|
|
9
|
+
*/
|
|
10
|
+
export interface IntBounds {
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function envString(name: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
15
|
+
export declare function envInt(name: string, fallback: number, bounds?: IntBounds, env?: NodeJS.ProcessEnv): number;
|
|
16
|
+
export declare function envBool(name: string, fallback: boolean, env?: NodeJS.ProcessEnv): boolean;
|
|
17
|
+
export declare function envList(name: string, env?: NodeJS.ProcessEnv): string[];
|
|
18
|
+
/**
|
|
19
|
+
* One of a closed set of values, or `undefined` when unset.
|
|
20
|
+
*
|
|
21
|
+
* Unlike the helpers above this throws on an unknown value: a browser name or
|
|
22
|
+
* proxy scheme that is misspelt is a configuration mistake the operator wants
|
|
23
|
+
* to hear about at boot, not a setting to quietly ignore.
|
|
24
|
+
*/
|
|
25
|
+
export declare function envEnum<T extends string>(name: string, values: readonly T[], env?: NodeJS.ProcessEnv): T | undefined;
|
|
26
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/utils/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAMD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,GAAG,SAAS,CAGhG;AAED,wBAAgB,MAAM,CACpB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,SAAc,EACtB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAkBR;AAKD,wBAAgB,OAAO,CACrB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,OAAO,EACjB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAOT;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,EAAE,CAKpF;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EACtC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,CAAC,EAAE,EACpB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,CAAC,GAAG,SAAS,CASf"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading configuration from the environment without trusting it.
|
|
3
|
+
*
|
|
4
|
+
* A bare `Number()` of an environment value turns a typo into `NaN`, and `NaN` compares
|
|
5
|
+
* false with everything: a concurrency limit of `NaN` queued every yt-dlp call
|
|
6
|
+
* forever, and a cache TTL of `NaN` never expired anything. Every numeric or
|
|
7
|
+
* enumerated setting therefore comes through here, where an unusable value
|
|
8
|
+
* falls back to the default and says so once on stderr.
|
|
9
|
+
*/
|
|
10
|
+
function warn(name, reason) {
|
|
11
|
+
console.error(`Ignoring ${name}: ${reason}.`);
|
|
12
|
+
}
|
|
13
|
+
export function envString(name, env = process.env) {
|
|
14
|
+
const value = env[name]?.trim();
|
|
15
|
+
return value === undefined || value === '' ? undefined : value;
|
|
16
|
+
}
|
|
17
|
+
export function envInt(name, fallback, bounds = {}, env = process.env) {
|
|
18
|
+
const raw = envString(name, env);
|
|
19
|
+
if (raw === undefined)
|
|
20
|
+
return fallback;
|
|
21
|
+
const parsed = Number(raw);
|
|
22
|
+
if (!Number.isInteger(parsed)) {
|
|
23
|
+
warn(name, `"${raw}" is not a whole number, using ${fallback}`);
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
if (bounds.min !== undefined && parsed < bounds.min) {
|
|
27
|
+
warn(name, `${parsed} is below the minimum of ${bounds.min}, using ${fallback}`);
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
if (bounds.max !== undefined && parsed > bounds.max) {
|
|
31
|
+
warn(name, `${parsed} is above the maximum of ${bounds.max}, using ${fallback}`);
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
const TRUE_WORDS = new Set(['1', 'true', 'yes', 'on']);
|
|
37
|
+
const FALSE_WORDS = new Set(['0', 'false', 'no', 'off']);
|
|
38
|
+
export function envBool(name, fallback, env = process.env) {
|
|
39
|
+
const raw = envString(name, env)?.toLowerCase();
|
|
40
|
+
if (raw === undefined)
|
|
41
|
+
return fallback;
|
|
42
|
+
if (TRUE_WORDS.has(raw))
|
|
43
|
+
return true;
|
|
44
|
+
if (FALSE_WORDS.has(raw))
|
|
45
|
+
return false;
|
|
46
|
+
warn(name, `"${raw}" is not a boolean, using ${String(fallback)}`);
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
export function envList(name, env = process.env) {
|
|
50
|
+
return (env[name] ?? '')
|
|
51
|
+
.split(',')
|
|
52
|
+
.map((value) => value.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One of a closed set of values, or `undefined` when unset.
|
|
57
|
+
*
|
|
58
|
+
* Unlike the helpers above this throws on an unknown value: a browser name or
|
|
59
|
+
* proxy scheme that is misspelt is a configuration mistake the operator wants
|
|
60
|
+
* to hear about at boot, not a setting to quietly ignore.
|
|
61
|
+
*/
|
|
62
|
+
export function envEnum(name, values, env = process.env) {
|
|
63
|
+
const raw = envString(name, env);
|
|
64
|
+
if (raw === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
const match = values.find((value) => value === raw.toLowerCase());
|
|
67
|
+
if (match === undefined) {
|
|
68
|
+
throw new Error(`${name} must be one of ${values.join(', ')}; got "${raw}".`);
|
|
69
|
+
}
|
|
70
|
+
return match;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/utils/env.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
|
7
7
|
* than a wall of yt-dlp stderr it has to guess at — and never a leaked command
|
|
8
8
|
* line or stack trace.
|
|
9
9
|
*/
|
|
10
|
-
export type YouTubeErrorCode = 'PRIVATE' | 'AGE_GATED' | 'MEMBERS_ONLY' | 'PREMIUM_ONLY' | 'LOGIN_REQUIRED' | 'NOT_FOUND' | 'NO_CAPTIONS' | 'LIVE_NOT_ENDED' | 'RATE_LIMITED' | 'TIMEOUT' | 'CANCELLED' | 'YTDLP_MISSING' | 'FFMPEG_MISSING' | 'YTDLP_FAILED' | 'MALFORMED_RESPONSE' | 'INVALID_INPUT';
|
|
10
|
+
export type YouTubeErrorCode = 'PRIVATE' | 'AGE_GATED' | 'MEMBERS_ONLY' | 'PREMIUM_ONLY' | 'LOGIN_REQUIRED' | 'BOT_CHECK' | 'NOT_FOUND' | 'NO_CAPTIONS' | 'LIVE_NOT_ENDED' | 'RATE_LIMITED' | 'TIMEOUT' | 'CANCELLED' | 'YTDLP_MISSING' | 'FFMPEG_MISSING' | 'YTDLP_FAILED' | 'FETCH_FAILED' | 'MALFORMED_RESPONSE' | 'INVALID_INPUT' | 'STORE_UNAVAILABLE' | 'STORE_CORRUPT';
|
|
11
11
|
export interface YouTubeErrorOptions {
|
|
12
12
|
/** What the caller should try next. Shown to the model verbatim. */
|
|
13
13
|
nextStep?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,WAAW,GACX,cAAc,GACd,cAAc,GACd,gBAAgB,GAChB,WAAW,GACX,aAAa,GACb,gBAAgB,GAChB,cAAc,GACd,SAAS,GACT,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,cAAc,GACd,oBAAoB,GACpB,eAAe,CAAC;AAEpB,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;gBAEhB,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB;IAQtF,iFAAiF;IACjF,aAAa,IAAI,MAAM;CAGxB;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,WAAW,GACX,cAAc,GACd,cAAc,GACd,gBAAgB,GAChB,WAAW,GACX,WAAW,GACX,aAAa,GACb,gBAAgB,GAChB,cAAc,GACd,SAAS,GACT,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,cAAc,GACd,cAAc,GACd,oBAAoB,GACpB,eAAe,GACf,mBAAmB,GACnB,eAAe,CAAC;AAEpB,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;gBAEhB,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB;IAQtF,iFAAiF;IACjF,aAAa,IAAI,MAAM;CAGxB;AAwGD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,YAAY,CAkBlF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,YAAY,GAAG,SAAS,CA2C3B;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAQ3D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAM1D"}
|
package/dist/utils/errors.js
CHANGED
|
@@ -14,7 +14,33 @@ export class YouTubeError extends Error {
|
|
|
14
14
|
return this.nextStep ? `${this.message}\n\n${this.nextStep}` : this.message;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
/** The two settings that let yt-dlp read what a signed-out client cannot. */
|
|
18
|
+
const COOKIE_HINT = 'Set YOUTUBE_MCP_COOKIES_FROM_BROWSER (for example chrome) or YOUTUBE_MCP_COOKIES_FILE and restart the server; see the README section on signed-in content.';
|
|
17
19
|
const RULES = [
|
|
20
|
+
{
|
|
21
|
+
// The one YouTube-authored sentence matched here, and the reason is that
|
|
22
|
+
// it was observed rather than remembered: on 2026-09-05 every per-video
|
|
23
|
+
// read from this project's own address printed it, verbatim, while flat
|
|
24
|
+
// channel listings kept working — so it is about the client, not the
|
|
25
|
+
// video, and the remedy is different. Matched on the words before the
|
|
26
|
+
// apostrophe, which YouTube writes as U+2019. Should YouTube reword it,
|
|
27
|
+
// the login hint yt-dlp appends still lands on LOGIN_REQUIRED below, whose
|
|
28
|
+
// next step names the same cookie settings. Listed first because both
|
|
29
|
+
// strings appear in the same output.
|
|
30
|
+
code: 'BOT_CHECK',
|
|
31
|
+
emits: 'Sign in to confirm you',
|
|
32
|
+
source: 'playabilityStatus.reason, re-raised by yt_dlp/extractor/youtube/_video.py',
|
|
33
|
+
message: 'YouTube is asking this client to prove it is not a bot.',
|
|
34
|
+
// Ordered by what was measured on 2026-09-06, not by what sounds most
|
|
35
|
+
// technical. From a datacenter address every account-free lever was
|
|
36
|
+
// installed and tested — a PO token provider (yt-dlp never even reaches
|
|
37
|
+
// the point of requesting one; it fails at the player response first),
|
|
38
|
+
// eight player clients, a 15-minute cooldown, and curl_cffi TLS
|
|
39
|
+
// impersonation — and none of them changed the answer. So the address
|
|
40
|
+
// comes first and the PO token provider comes last: leading with a remedy
|
|
41
|
+
// that was measured not to work sends the reader down a dead end.
|
|
42
|
+
nextStep: `Per-video reads are refused from this address. Datacenter and VPN/exit-node addresses are gated regardless of client settings, so check whether this host egresses through one. Otherwise: ${COOKIE_HINT} On a residential address a missing PO token provider can be the cause instead — check \`check_health\`.`,
|
|
43
|
+
},
|
|
18
44
|
{
|
|
19
45
|
code: 'RATE_LIMITED',
|
|
20
46
|
// exceptions.py formats `HTTP Error {status}: {reason}`; the reason for 429
|
|
@@ -55,7 +81,7 @@ const RULES = [
|
|
|
55
81
|
emits: 'for tips on effectively exporting YouTube cookies',
|
|
56
82
|
source: 'yt_dlp/extractor/youtube/_base.py:664',
|
|
57
83
|
message: 'This video requires a signed-in YouTube account.',
|
|
58
|
-
nextStep:
|
|
84
|
+
nextStep: `It may be private, age-restricted or members-only. ${COOKIE_HINT}`,
|
|
59
85
|
},
|
|
60
86
|
];
|
|
61
87
|
/**
|
|
@@ -108,7 +134,7 @@ export function classifyPlayability(fields) {
|
|
|
108
134
|
});
|
|
109
135
|
case 'needs_auth':
|
|
110
136
|
return new YouTubeError('AGE_GATED', 'This video is age-restricted and requires a signed-in account.', {
|
|
111
|
-
nextStep:
|
|
137
|
+
nextStep: `Age-gated videos cannot be read without authentication. ${COOKIE_HINT}`,
|
|
112
138
|
});
|
|
113
139
|
}
|
|
114
140
|
// 'is_live' is deliberately absent: a stream in progress has real metadata,
|
package/dist/utils/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAwCA,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC5B,IAAI,CAAmB;IACvB,QAAQ,CAAU;IAClB,SAAS,CAAU;IAE5B,YAAY,IAAsB,EAAE,OAAe,EAAE,UAA+B,EAAE;QACpF,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACjC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAC9C,CAAC;IAED,iFAAiF;IACjF,aAAa;QACX,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC;IAC9E,CAAC;CACF;AA8BD,6EAA6E;AAC7E,MAAM,WAAW,GACf,4JAA4J,CAAC;AAE/J,MAAM,KAAK,GAAW;IACpB;QACE,yEAAyE;QACzE,wEAAwE;QACxE,wEAAwE;QACxE,qEAAqE;QACrE,sEAAsE;QACtE,wEAAwE;QACxE,2EAA2E;QAC3E,sEAAsE;QACtE,qCAAqC;QACrC,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,wBAAwB;QAC/B,MAAM,EAAE,2EAA2E;QACnF,OAAO,EAAE,yDAAyD;QAClE,sEAAsE;QACtE,oEAAoE;QACpE,wEAAwE;QACxE,uEAAuE;QACvE,gEAAgE;QAChE,sEAAsE;QACtE,0EAA0E;QAC1E,kEAAkE;QAClE,QAAQ,EAAE,8LAA8L,WAAW,0GAA0G;KAC9T;IACD;QACE,IAAI,EAAE,cAAc;QACpB,4EAA4E;QAC5E,wDAAwD;QACxD,KAAK,EAAE,gBAAgB;QACvB,MAAM,EAAE,oCAAoC;QAC5C,OAAO,EAAE,uCAAuC;QAChD,QAAQ,EAAE,kEAAkE;QAC5E,SAAS,EAAE,IAAI;KAChB;IACD;QACE,IAAI,EAAE,aAAa;QACnB,KAAK,EAAE,oDAAoD;QAC3D,MAAM,EAAE,0BAA0B;QAClC,OAAO,EAAE,qEAAqE;QAC9E,QAAQ,EACN,uFAAuF;KAC1F;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,6BAA6B;QACpC,MAAM,EAAE,0BAA0B;QAClC,OAAO,EAAE,6DAA6D;QACtE,QAAQ,EAAE,gEAAgE;KAC3E;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,8EAA8E;QACrF,MAAM,EAAE,oCAAoC;QAC5C,OAAO,EAAE,6DAA6D;QACtE,QAAQ,EAAE,gEAAgE;KAC3E;IACD;QACE,oEAAoE;QACpE,yEAAyE;QACzE,yEAAyE;QACzE,iEAAiE;QACjE,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,mDAAmD;QAC1D,MAAM,EAAE,uCAAuC;QAC/C,OAAO,EAAE,kDAAkD;QAC3D,QAAQ,EAAE,sDAAsD,WAAW,EAAE;KAC9E;CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAc,EAAE,KAAe;IAClE,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IAEtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YAChD,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE;gBAC/C,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,KAAK;aACN,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,IAAI,YAAY,CAAC,cAAc,EAAE,yCAAyC,EAAE;QACjF,QAAQ,EACN,kLAAkL;QACpL,KAAK;KACN,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAGnC;IACC,QAAQ,MAAM,CAAC,YAAY,EAAE,CAAC;QAC5B,KAAK,SAAS;YACZ,OAAO,IAAI,YAAY,CAAC,SAAS,EAAE,wBAAwB,EAAE;gBAC3D,QAAQ,EAAE,0EAA0E;aACrF,CAAC,CAAC;QACL,KAAK,iBAAiB;YACpB,OAAO,IAAI,YAAY,CAAC,cAAc,EAAE,8CAA8C,EAAE;gBACtF,QAAQ,EAAE,iEAAiE;aAC5E,CAAC,CAAC;QACL,KAAK,cAAc;YACjB,OAAO,IAAI,YAAY,CAAC,cAAc,EAAE,gDAAgD,EAAE;gBACxF,QAAQ,EAAE,4DAA4D;aACvE,CAAC,CAAC;QACL,KAAK,YAAY;YACf,OAAO,IAAI,YAAY,CACrB,WAAW,EACX,gEAAgE,EAChE;gBACE,QAAQ,EAAE,2DAA2D,WAAW,EAAE;aACnF,CACF,CAAC;IACN,CAAC;IAED,4EAA4E;IAC5E,gDAAgD;IAChD,QAAQ,MAAM,CAAC,UAAU,EAAE,CAAC;QAC1B,KAAK,aAAa;YAChB,OAAO,IAAI,YAAY,CAAC,gBAAgB,EAAE,uCAAuC,EAAE;gBACjF,QAAQ,EAAE,qEAAqE;aAChF,CAAC,CAAC;QACL,KAAK,WAAW;YACd,OAAO,IAAI,YAAY,CACrB,gBAAgB,EAChB,qDAAqD,EACrD;gBACE,QAAQ,EAAE,+DAA+D;gBACzE,SAAS,EAAE,IAAI;aAChB,CACF,CAAC;IACN,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,IAAI,KAAK,YAAY,YAAY;QAAE,OAAO,KAAK,CAAC;IAEhD,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,IAAI,YAAY,CAAC,cAAc,EAAE,sCAAsC,EAAE;QAC9E,QAAQ,EAAE,0EAA0E;QACpF,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC;KAC3D,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,MAAM,YAAY,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAC3C,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,YAAY,CAAC,IAAI,KAAK,YAAY,CAAC,aAAa,EAAE,EAAE,EAAE,CAAC;KAC5F,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { YouTubeError } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Exclusive access to a directory's contents while a long job writes them.
|
|
5
|
+
*
|
|
6
|
+
* Two jobs on one channel would interleave their checkpoints, and the later
|
|
7
|
+
* write would silently drop whatever the other had recorded since it last
|
|
8
|
+
* read. A lock file rather than an in-process mutex because a second server
|
|
9
|
+
* process — a second editor window, a stray `npm start` — is the case that
|
|
10
|
+
* actually happens.
|
|
11
|
+
*/
|
|
12
|
+
/** Long enough that a slow job is never mistaken for a dead one. */
|
|
13
|
+
export declare const LOCK_STALE_MS: number;
|
|
14
|
+
export declare const lockRecordSchema: z.ZodObject<{
|
|
15
|
+
pid: z.ZodNumber;
|
|
16
|
+
startedAt: z.ZodString;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
startedAt: string;
|
|
19
|
+
pid: number;
|
|
20
|
+
}, {
|
|
21
|
+
startedAt: string;
|
|
22
|
+
pid: number;
|
|
23
|
+
}>;
|
|
24
|
+
export type LockRecord = z.infer<typeof lockRecordSchema>;
|
|
25
|
+
/**
|
|
26
|
+
* Run `work` holding the lock at `path`.
|
|
27
|
+
*
|
|
28
|
+
* It is released on any exit, including cancellation. A process killed outright
|
|
29
|
+
* leaves one behind, which is why a lock is also broken when its owner is gone
|
|
30
|
+
* or when it is simply too old to be real. `held` builds the error to throw
|
|
31
|
+
* when someone else genuinely has it, so each caller can name its own tool.
|
|
32
|
+
*/
|
|
33
|
+
export declare function withFileLock<T>(path: string, work: () => Promise<T>, held: (existing: LockRecord | undefined) => YouTubeError): Promise<T>;
|
|
34
|
+
//# sourceMappingURL=file-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock.d.ts","sourceRoot":"","sources":["../../src/utils/file-lock.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;;;;;;;GAQG;AAEH,oEAAoE;AACpE,eAAO,MAAM,aAAa,QAAiB,CAAC;AAE5C,eAAO,MAAM,gBAAgB;;;;;;;;;EAG3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACtB,IAAI,EAAE,CAAC,QAAQ,EAAE,UAAU,GAAG,SAAS,KAAK,YAAY,GACvD,OAAO,CAAC,CAAC,CAAC,CAoBZ"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readJsonFile } from './json-file.js';
|
|
4
|
+
/**
|
|
5
|
+
* Exclusive access to a directory's contents while a long job writes them.
|
|
6
|
+
*
|
|
7
|
+
* Two jobs on one channel would interleave their checkpoints, and the later
|
|
8
|
+
* write would silently drop whatever the other had recorded since it last
|
|
9
|
+
* read. A lock file rather than an in-process mutex because a second server
|
|
10
|
+
* process — a second editor window, a stray `npm start` — is the case that
|
|
11
|
+
* actually happens.
|
|
12
|
+
*/
|
|
13
|
+
/** Long enough that a slow job is never mistaken for a dead one. */
|
|
14
|
+
export const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
15
|
+
export const lockRecordSchema = z.object({
|
|
16
|
+
pid: z.number().int(),
|
|
17
|
+
startedAt: z.string(),
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Run `work` holding the lock at `path`.
|
|
21
|
+
*
|
|
22
|
+
* It is released on any exit, including cancellation. A process killed outright
|
|
23
|
+
* leaves one behind, which is why a lock is also broken when its owner is gone
|
|
24
|
+
* or when it is simply too old to be real. `held` builds the error to throw
|
|
25
|
+
* when someone else genuinely has it, so each caller can name its own tool.
|
|
26
|
+
*/
|
|
27
|
+
export async function withFileLock(path, work, held) {
|
|
28
|
+
if (!(await acquire(path))) {
|
|
29
|
+
const existing = await readJsonFile(path, lockRecordSchema);
|
|
30
|
+
// An unreadable lock is one being written this instant, which is as held as
|
|
31
|
+
// it gets. Only a lock whose owner is demonstrably gone gets broken.
|
|
32
|
+
if (existing === undefined || !isStale(existing))
|
|
33
|
+
throw held(existing);
|
|
34
|
+
await rm(path, { force: true });
|
|
35
|
+
// Losing this second race means another caller broke the same stale lock
|
|
36
|
+
// first, and is now the legitimate owner.
|
|
37
|
+
if (!(await acquire(path)))
|
|
38
|
+
throw held(undefined);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return await work();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await rm(path, { force: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create the lock file, or report that someone else already has.
|
|
49
|
+
*
|
|
50
|
+
* `wx` fails when the file exists, and that check and the creation are one
|
|
51
|
+
* operation in the kernel. Reading first and writing after is not a lock: two
|
|
52
|
+
* jobs starting together both see nothing and both proceed.
|
|
53
|
+
*/
|
|
54
|
+
async function acquire(path) {
|
|
55
|
+
const lock = { pid: process.pid, startedAt: new Date().toISOString() };
|
|
56
|
+
try {
|
|
57
|
+
await writeFile(path, JSON.stringify(lock), { encoding: 'utf-8', flag: 'wx' });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (errorCode(error) === 'EEXIST')
|
|
62
|
+
return false;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function isStale({ pid, startedAt }) {
|
|
67
|
+
const started = Date.parse(startedAt);
|
|
68
|
+
if (Number.isNaN(started) || Date.now() - started > LOCK_STALE_MS)
|
|
69
|
+
return true;
|
|
70
|
+
return !isProcessAlive(pid);
|
|
71
|
+
}
|
|
72
|
+
function isProcessAlive(pid) {
|
|
73
|
+
try {
|
|
74
|
+
// Signal 0 performs the permission and existence checks without delivering
|
|
75
|
+
// anything.
|
|
76
|
+
process.kill(pid, 0);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
// EPERM means the process exists but belongs to another user, which for
|
|
81
|
+
// this purpose is very much alive.
|
|
82
|
+
return errorCode(error) === 'EPERM';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** The `code` a Node system error carries, when it is one. */
|
|
86
|
+
function errorCode(error) {
|
|
87
|
+
return typeof error === 'object' && error !== null && 'code' in error
|
|
88
|
+
? String(error.code)
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=file-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock.js","sourceRoot":"","sources":["../../src/utils/file-lock.ts"],"names":[],"mappings":"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"}
|