youtube-knowledge-mcp 2.2.0 → 3.0.1

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.
Files changed (407) hide show
  1. package/README.md +126 -36
  2. package/dist/brain-schemas.d.ts +10 -0
  3. package/dist/brain-schemas.d.ts.map +1 -1
  4. package/dist/harvest-schemas.d.ts +75 -0
  5. package/dist/harvest-schemas.d.ts.map +1 -0
  6. package/dist/harvest-schemas.js +111 -0
  7. package/dist/harvest-schemas.js.map +1 -0
  8. package/dist/registry/brain-tools.d.ts.map +1 -1
  9. package/dist/registry/brain-tools.js +16 -0
  10. package/dist/registry/brain-tools.js.map +1 -1
  11. package/dist/registry/channel-tools.d.ts.map +1 -1
  12. package/dist/registry/channel-tools.js +16 -0
  13. package/dist/registry/channel-tools.js.map +1 -1
  14. package/dist/registry/harvest-tools.d.ts +10 -0
  15. package/dist/registry/harvest-tools.d.ts.map +1 -0
  16. package/dist/registry/harvest-tools.js +109 -0
  17. package/dist/registry/harvest-tools.js.map +1 -0
  18. package/dist/registry/health-tools.d.ts.map +1 -1
  19. package/dist/registry/health-tools.js +18 -0
  20. package/dist/registry/health-tools.js.map +1 -1
  21. package/dist/registry/index.d.ts.map +1 -1
  22. package/dist/registry/index.js +6 -0
  23. package/dist/registry/index.js.map +1 -1
  24. package/dist/registry/media-tools.d.ts.map +1 -1
  25. package/dist/registry/media-tools.js +31 -0
  26. package/dist/registry/media-tools.js.map +1 -1
  27. package/dist/registry/store-tools.d.ts +9 -0
  28. package/dist/registry/store-tools.d.ts.map +1 -0
  29. package/dist/registry/store-tools.js +59 -0
  30. package/dist/registry/store-tools.js.map +1 -0
  31. package/dist/registry/transcript-tools.d.ts +4 -1
  32. package/dist/registry/transcript-tools.d.ts.map +1 -1
  33. package/dist/registry/transcript-tools.js +20 -1
  34. package/dist/registry/transcript-tools.js.map +1 -1
  35. package/dist/registry/video-tools.d.ts.map +1 -1
  36. package/dist/registry/video-tools.js +49 -1
  37. package/dist/registry/video-tools.js.map +1 -1
  38. package/dist/registry/vision-tools.d.ts +7 -0
  39. package/dist/registry/vision-tools.d.ts.map +1 -0
  40. package/dist/registry/vision-tools.js +55 -0
  41. package/dist/registry/vision-tools.js.map +1 -0
  42. package/dist/schemas.d.ts +245 -0
  43. package/dist/schemas.d.ts.map +1 -1
  44. package/dist/schemas.js +58 -0
  45. package/dist/schemas.js.map +1 -1
  46. package/dist/thumbnail-schemas.d.ts +38 -28
  47. package/dist/thumbnail-schemas.d.ts.map +1 -1
  48. package/dist/tools/ask-brain.js +1 -1
  49. package/dist/tools/ask-brain.js.map +1 -1
  50. package/dist/tools/ask-brains.d.ts +66 -0
  51. package/dist/tools/ask-brains.d.ts.map +1 -0
  52. package/dist/tools/ask-brains.js +111 -0
  53. package/dist/tools/ask-brains.js.map +1 -0
  54. package/dist/tools/brain-info.js +1 -1
  55. package/dist/tools/brain-info.js.map +1 -1
  56. package/dist/tools/check-health.d.ts +75 -6
  57. package/dist/tools/check-health.d.ts.map +1 -1
  58. package/dist/tools/check-health.js +114 -6
  59. package/dist/tools/check-health.js.map +1 -1
  60. package/dist/tools/check-new-uploads.d.ts +55 -0
  61. package/dist/tools/check-new-uploads.d.ts.map +1 -0
  62. package/dist/tools/check-new-uploads.js +74 -0
  63. package/dist/tools/check-new-uploads.js.map +1 -0
  64. package/dist/tools/clips.d.ts +15 -0
  65. package/dist/tools/clips.d.ts.map +1 -1
  66. package/dist/tools/clips.js +5 -5
  67. package/dist/tools/clips.js.map +1 -1
  68. package/dist/tools/detect-scenes.d.ts +38 -0
  69. package/dist/tools/detect-scenes.d.ts.map +1 -0
  70. package/dist/tools/detect-scenes.js +66 -0
  71. package/dist/tools/detect-scenes.js.map +1 -0
  72. package/dist/tools/export-data.d.ts +85 -0
  73. package/dist/tools/export-data.d.ts.map +1 -0
  74. package/dist/tools/export-data.js +99 -0
  75. package/dist/tools/export-data.js.map +1 -0
  76. package/dist/tools/fetch-channel-thumbnails.d.ts +8 -8
  77. package/dist/tools/fetch-videos.d.ts +15 -4
  78. package/dist/tools/fetch-videos.d.ts.map +1 -1
  79. package/dist/tools/fetch-videos.js +63 -22
  80. package/dist/tools/fetch-videos.js.map +1 -1
  81. package/dist/tools/frames-subtitles.d.ts.map +1 -1
  82. package/dist/tools/frames-subtitles.js +16 -8
  83. package/dist/tools/frames-subtitles.js.map +1 -1
  84. package/dist/tools/frames.d.ts +73 -0
  85. package/dist/tools/frames.d.ts.map +1 -0
  86. package/dist/tools/frames.js +136 -0
  87. package/dist/tools/frames.js.map +1 -0
  88. package/dist/tools/get-channel-info.d.ts +2 -0
  89. package/dist/tools/get-channel-info.d.ts.map +1 -1
  90. package/dist/tools/get-channel-info.js +4 -0
  91. package/dist/tools/get-channel-info.js.map +1 -1
  92. package/dist/tools/get-comments.d.ts +238 -6
  93. package/dist/tools/get-comments.d.ts.map +1 -1
  94. package/dist/tools/get-comments.js +85 -21
  95. package/dist/tools/get-comments.js.map +1 -1
  96. package/dist/tools/get-coverage.d.ts +122 -0
  97. package/dist/tools/get-coverage.d.ts.map +1 -0
  98. package/dist/tools/get-coverage.js +105 -0
  99. package/dist/tools/get-coverage.js.map +1 -0
  100. package/dist/tools/get-sponsor-segments.d.ts +49 -0
  101. package/dist/tools/get-sponsor-segments.d.ts.map +1 -0
  102. package/dist/tools/get-sponsor-segments.js +61 -0
  103. package/dist/tools/get-sponsor-segments.js.map +1 -0
  104. package/dist/tools/get-thumbnail.d.ts.map +1 -1
  105. package/dist/tools/get-thumbnail.js +2 -1
  106. package/dist/tools/get-thumbnail.js.map +1 -1
  107. package/dist/tools/get-transcript.d.ts +1 -0
  108. package/dist/tools/get-transcript.d.ts.map +1 -1
  109. package/dist/tools/get-transcript.js +11 -4
  110. package/dist/tools/get-transcript.js.map +1 -1
  111. package/dist/tools/get-video-details.d.ts +118 -0
  112. package/dist/tools/get-video-details.d.ts.map +1 -0
  113. package/dist/tools/get-video-details.js +147 -0
  114. package/dist/tools/get-video-details.js.map +1 -0
  115. package/dist/tools/gif-concat.d.ts +49 -0
  116. package/dist/tools/gif-concat.d.ts.map +1 -0
  117. package/dist/tools/gif-concat.js +96 -0
  118. package/dist/tools/gif-concat.js.map +1 -0
  119. package/dist/tools/harvest-channel.d.ts +92 -0
  120. package/dist/tools/harvest-channel.d.ts.map +1 -0
  121. package/dist/tools/harvest-channel.js +62 -0
  122. package/dist/tools/harvest-channel.js.map +1 -0
  123. package/dist/tools/harvest-comments.d.ts +81 -0
  124. package/dist/tools/harvest-comments.d.ts.map +1 -0
  125. package/dist/tools/harvest-comments.js +115 -0
  126. package/dist/tools/harvest-comments.js.map +1 -0
  127. package/dist/tools/harvest-details.d.ts +84 -0
  128. package/dist/tools/harvest-details.d.ts.map +1 -0
  129. package/dist/tools/harvest-details.js +71 -0
  130. package/dist/tools/harvest-details.js.map +1 -0
  131. package/dist/tools/library.js +1 -1
  132. package/dist/tools/library.js.map +1 -1
  133. package/dist/tools/list-captions.d.ts +17 -0
  134. package/dist/tools/list-captions.d.ts.map +1 -0
  135. package/dist/tools/list-captions.js +60 -0
  136. package/dist/tools/list-captions.js.map +1 -0
  137. package/dist/tools/list-channel-playlists.d.ts +30 -0
  138. package/dist/tools/list-channel-playlists.d.ts.map +1 -0
  139. package/dist/tools/list-channel-playlists.js +44 -0
  140. package/dist/tools/list-channel-playlists.js.map +1 -0
  141. package/dist/tools/list-channel-thumbnails.d.ts +10 -10
  142. package/dist/tools/list-channel-thumbnails.js +1 -1
  143. package/dist/tools/list-channel-thumbnails.js.map +1 -1
  144. package/dist/tools/list-library.js +1 -1
  145. package/dist/tools/list-library.js.map +1 -1
  146. package/dist/tools/prune-harvest.d.ts +59 -0
  147. package/dist/tools/prune-harvest.d.ts.map +1 -0
  148. package/dist/tools/prune-harvest.js +108 -0
  149. package/dist/tools/prune-harvest.js.map +1 -0
  150. package/dist/tools/query-comments.d.ts +125 -0
  151. package/dist/tools/query-comments.d.ts.map +1 -0
  152. package/dist/tools/query-comments.js +106 -0
  153. package/dist/tools/query-comments.js.map +1 -0
  154. package/dist/tools/query-videos.d.ts +118 -0
  155. package/dist/tools/query-videos.d.ts.map +1 -0
  156. package/dist/tools/query-videos.js +97 -0
  157. package/dist/tools/query-videos.js.map +1 -0
  158. package/dist/tools/repair-store.d.ts +34 -0
  159. package/dist/tools/repair-store.d.ts.map +1 -0
  160. package/dist/tools/repair-store.js +58 -0
  161. package/dist/tools/repair-store.js.map +1 -0
  162. package/dist/tools/search-channels.d.ts +6 -0
  163. package/dist/tools/search-channels.d.ts.map +1 -1
  164. package/dist/tools/search-channels.js +5 -1
  165. package/dist/tools/search-channels.js.map +1 -1
  166. package/dist/tools/search-videos.js +1 -1
  167. package/dist/tools/search-videos.js.map +1 -1
  168. package/dist/tools/transcribe-video.d.ts +28 -0
  169. package/dist/tools/transcribe-video.d.ts.map +1 -0
  170. package/dist/tools/transcribe-video.js +52 -0
  171. package/dist/tools/transcribe-video.js.map +1 -0
  172. package/dist/utils/brain-fusion.d.ts +24 -0
  173. package/dist/utils/brain-fusion.d.ts.map +1 -0
  174. package/dist/utils/brain-fusion.js +18 -0
  175. package/dist/utils/brain-fusion.js.map +1 -0
  176. package/dist/utils/channel-playlists.d.ts +21 -0
  177. package/dist/utils/channel-playlists.d.ts.map +1 -0
  178. package/dist/utils/channel-playlists.js +56 -0
  179. package/dist/utils/channel-playlists.js.map +1 -0
  180. package/dist/utils/clips.d.ts.map +1 -1
  181. package/dist/utils/clips.js +3 -17
  182. package/dist/utils/clips.js.map +1 -1
  183. package/dist/utils/comment-store.d.ts +71 -0
  184. package/dist/utils/comment-store.d.ts.map +1 -0
  185. package/dist/utils/comment-store.js +129 -0
  186. package/dist/utils/comment-store.js.map +1 -0
  187. package/dist/utils/comment-threads.d.ts +51 -0
  188. package/dist/utils/comment-threads.d.ts.map +1 -0
  189. package/dist/utils/comment-threads.js +76 -0
  190. package/dist/utils/comment-threads.js.map +1 -0
  191. package/dist/utils/concat.d.ts +19 -0
  192. package/dist/utils/concat.d.ts.map +1 -0
  193. package/dist/utils/concat.js +135 -0
  194. package/dist/utils/concat.js.map +1 -0
  195. package/dist/utils/contact-sheet.d.ts +28 -0
  196. package/dist/utils/contact-sheet.d.ts.map +1 -0
  197. package/dist/utils/contact-sheet.js +65 -0
  198. package/dist/utils/contact-sheet.js.map +1 -0
  199. package/dist/utils/coverage-text.d.ts +3 -0
  200. package/dist/utils/coverage-text.d.ts.map +1 -0
  201. package/dist/utils/coverage-text.js +62 -0
  202. package/dist/utils/coverage-text.js.map +1 -0
  203. package/dist/utils/coverage.d.ts +57 -0
  204. package/dist/utils/coverage.d.ts.map +1 -0
  205. package/dist/utils/coverage.js +142 -0
  206. package/dist/utils/coverage.js.map +1 -0
  207. package/dist/utils/detail-batch.d.ts +25 -0
  208. package/dist/utils/detail-batch.d.ts.map +1 -0
  209. package/dist/utils/detail-batch.js +58 -0
  210. package/dist/utils/detail-batch.js.map +1 -0
  211. package/dist/utils/errors.d.ts +1 -1
  212. package/dist/utils/errors.d.ts.map +1 -1
  213. package/dist/utils/errors.js +19 -1
  214. package/dist/utils/errors.js.map +1 -1
  215. package/dist/utils/export-data.d.ts +41 -0
  216. package/dist/utils/export-data.d.ts.map +1 -0
  217. package/dist/utils/export-data.js +114 -0
  218. package/dist/utils/export-data.js.map +1 -0
  219. package/dist/utils/export-format.d.ts +23 -0
  220. package/dist/utils/export-format.d.ts.map +1 -0
  221. package/dist/utils/export-format.js +59 -0
  222. package/dist/utils/export-format.js.map +1 -0
  223. package/dist/utils/export-load.d.ts +28 -0
  224. package/dist/utils/export-load.d.ts.map +1 -0
  225. package/dist/utils/export-load.js +70 -0
  226. package/dist/utils/export-load.js.map +1 -0
  227. package/dist/utils/export-rows.d.ts +35 -0
  228. package/dist/utils/export-rows.d.ts.map +1 -0
  229. package/dist/utils/export-rows.js +69 -0
  230. package/dist/utils/export-rows.js.map +1 -0
  231. package/dist/utils/ffmpeg.d.ts +26 -0
  232. package/dist/utils/ffmpeg.d.ts.map +1 -0
  233. package/dist/utils/ffmpeg.js +28 -0
  234. package/dist/utils/ffmpeg.js.map +1 -0
  235. package/dist/utils/format.d.ts +23 -1
  236. package/dist/utils/format.d.ts.map +1 -1
  237. package/dist/utils/format.js +5 -1
  238. package/dist/utils/format.js.map +1 -1
  239. package/dist/utils/frame-plan.d.ts +24 -0
  240. package/dist/utils/frame-plan.d.ts.map +1 -0
  241. package/dist/utils/frame-plan.js +58 -0
  242. package/dist/utils/frame-plan.js.map +1 -0
  243. package/dist/utils/frames.d.ts +32 -0
  244. package/dist/utils/frames.d.ts.map +1 -0
  245. package/dist/utils/frames.js +60 -0
  246. package/dist/utils/frames.js.map +1 -0
  247. package/dist/utils/gif.d.ts +31 -0
  248. package/dist/utils/gif.d.ts.map +1 -0
  249. package/dist/utils/gif.js +77 -0
  250. package/dist/utils/gif.js.map +1 -0
  251. package/dist/utils/harvest-catalog.d.ts +54 -0
  252. package/dist/utils/harvest-catalog.d.ts.map +1 -0
  253. package/dist/utils/harvest-catalog.js +173 -0
  254. package/dist/utils/harvest-catalog.js.map +1 -0
  255. package/dist/utils/harvest-details.d.ts +30 -0
  256. package/dist/utils/harvest-details.d.ts.map +1 -0
  257. package/dist/utils/harvest-details.js +98 -0
  258. package/dist/utils/harvest-details.js.map +1 -0
  259. package/dist/utils/harvest-lock.d.ts +3 -0
  260. package/dist/utils/harvest-lock.d.ts.map +1 -0
  261. package/dist/utils/harvest-lock.js +26 -0
  262. package/dist/utils/harvest-lock.js.map +1 -0
  263. package/dist/utils/harvest-receipts.d.ts +14 -0
  264. package/dist/utils/harvest-receipts.d.ts.map +1 -0
  265. package/dist/utils/harvest-receipts.js +110 -0
  266. package/dist/utils/harvest-receipts.js.map +1 -0
  267. package/dist/utils/listing-cursor.d.ts +31 -0
  268. package/dist/utils/listing-cursor.d.ts.map +1 -0
  269. package/dist/utils/listing-cursor.js +39 -0
  270. package/dist/utils/listing-cursor.js.map +1 -0
  271. package/dist/utils/local-transcription.d.ts +14 -0
  272. package/dist/utils/local-transcription.d.ts.map +1 -0
  273. package/dist/utils/local-transcription.js +85 -0
  274. package/dist/utils/local-transcription.js.map +1 -0
  275. package/dist/utils/moment.d.ts +24 -0
  276. package/dist/utils/moment.d.ts.map +1 -0
  277. package/dist/utils/moment.js +22 -0
  278. package/dist/utils/moment.js.map +1 -0
  279. package/dist/utils/most-replayed.d.ts +15 -0
  280. package/dist/utils/most-replayed.d.ts.map +1 -0
  281. package/dist/utils/most-replayed.js +25 -0
  282. package/dist/utils/most-replayed.js.map +1 -0
  283. package/dist/utils/pot-preflight.d.ts +53 -0
  284. package/dist/utils/pot-preflight.d.ts.map +1 -0
  285. package/dist/utils/pot-preflight.js +146 -0
  286. package/dist/utils/pot-preflight.js.map +1 -0
  287. package/dist/utils/scenes.d.ts +39 -0
  288. package/dist/utils/scenes.d.ts.map +1 -0
  289. package/dist/utils/scenes.js +83 -0
  290. package/dist/utils/scenes.js.map +1 -0
  291. package/dist/utils/seconds.d.ts +5 -0
  292. package/dist/utils/seconds.d.ts.map +1 -0
  293. package/dist/utils/seconds.js +7 -0
  294. package/dist/utils/seconds.js.map +1 -0
  295. package/dist/utils/sleep.d.ts +9 -0
  296. package/dist/utils/sleep.d.ts.map +1 -0
  297. package/dist/utils/sleep.js +25 -0
  298. package/dist/utils/sleep.js.map +1 -0
  299. package/dist/utils/sponsorblock.d.ts +24 -0
  300. package/dist/utils/sponsorblock.d.ts.map +1 -0
  301. package/dist/utils/sponsorblock.js +153 -0
  302. package/dist/utils/sponsorblock.js.map +1 -0
  303. package/dist/utils/store-health.d.ts +37 -0
  304. package/dist/utils/store-health.d.ts.map +1 -0
  305. package/dist/utils/store-health.js +92 -0
  306. package/dist/utils/store-health.js.map +1 -0
  307. package/dist/utils/store-migrations.d.ts +33 -0
  308. package/dist/utils/store-migrations.d.ts.map +1 -0
  309. package/dist/utils/store-migrations.js +41 -0
  310. package/dist/utils/store-migrations.js.map +1 -0
  311. package/dist/utils/store-paths.d.ts +33 -0
  312. package/dist/utils/store-paths.d.ts.map +1 -0
  313. package/dist/utils/store-paths.js +48 -0
  314. package/dist/utils/store-paths.js.map +1 -0
  315. package/dist/utils/store-rows.d.ts +22 -0
  316. package/dist/utils/store-rows.d.ts.map +1 -0
  317. package/dist/utils/store-rows.js +62 -0
  318. package/dist/utils/store-rows.js.map +1 -0
  319. package/dist/utils/store-schema.d.ts +16 -0
  320. package/dist/utils/store-schema.d.ts.map +1 -0
  321. package/dist/utils/store-schema.js +168 -0
  322. package/dist/utils/store-schema.js.map +1 -0
  323. package/dist/utils/store.d.ts +34 -0
  324. package/dist/utils/store.d.ts.map +1 -0
  325. package/dist/utils/store.js +157 -0
  326. package/dist/utils/store.js.map +1 -0
  327. package/dist/utils/transcript-cache.d.ts +5 -1
  328. package/dist/utils/transcript-cache.d.ts.map +1 -1
  329. package/dist/utils/transcript-cache.js +31 -50
  330. package/dist/utils/transcript-cache.js.map +1 -1
  331. package/dist/utils/transcript-files.d.ts +24 -0
  332. package/dist/utils/transcript-files.d.ts.map +1 -0
  333. package/dist/utils/transcript-files.js +61 -0
  334. package/dist/utils/transcript-files.js.map +1 -0
  335. package/dist/utils/transcript.d.ts +3 -0
  336. package/dist/utils/transcript.d.ts.map +1 -1
  337. package/dist/utils/transcript.js +2 -0
  338. package/dist/utils/transcript.js.map +1 -1
  339. package/dist/utils/upload-check.d.ts +38 -0
  340. package/dist/utils/upload-check.d.ts.map +1 -0
  341. package/dist/utils/upload-check.js +93 -0
  342. package/dist/utils/upload-check.js.map +1 -0
  343. package/dist/utils/validate.d.ts +2 -0
  344. package/dist/utils/validate.d.ts.map +1 -1
  345. package/dist/utils/validate.js +4 -0
  346. package/dist/utils/validate.js.map +1 -1
  347. package/dist/utils/video-detail-store.d.ts +47 -0
  348. package/dist/utils/video-detail-store.d.ts.map +1 -0
  349. package/dist/utils/video-detail-store.js +89 -0
  350. package/dist/utils/video-detail-store.js.map +1 -0
  351. package/dist/utils/video-stats-cache.d.ts +16 -0
  352. package/dist/utils/video-stats-cache.d.ts.map +1 -0
  353. package/dist/utils/video-stats-cache.js +81 -0
  354. package/dist/utils/video-stats-cache.js.map +1 -0
  355. package/dist/utils/video-stats-schema.d.ts +187 -0
  356. package/dist/utils/video-stats-schema.d.ts.map +1 -0
  357. package/dist/utils/video-stats-schema.js +50 -0
  358. package/dist/utils/video-stats-schema.js.map +1 -0
  359. package/dist/utils/video-stats-storage.d.ts +16 -0
  360. package/dist/utils/video-stats-storage.d.ts.map +1 -0
  361. package/dist/utils/video-stats-storage.js +37 -0
  362. package/dist/utils/video-stats-storage.js.map +1 -0
  363. package/dist/utils/video-stats.d.ts +82 -0
  364. package/dist/utils/video-stats.d.ts.map +1 -0
  365. package/dist/utils/video-stats.js +108 -0
  366. package/dist/utils/video-stats.js.map +1 -0
  367. package/dist/utils/video-store.d.ts +69 -0
  368. package/dist/utils/video-store.d.ts.map +1 -0
  369. package/dist/utils/video-store.js +77 -0
  370. package/dist/utils/video-store.js.map +1 -0
  371. package/dist/utils/video-stream.d.ts +21 -0
  372. package/dist/utils/video-stream.d.ts.map +1 -0
  373. package/dist/utils/video-stream.js +36 -0
  374. package/dist/utils/video-stream.js.map +1 -0
  375. package/dist/utils/whisper.d.ts +39 -0
  376. package/dist/utils/whisper.d.ts.map +1 -0
  377. package/dist/utils/whisper.js +137 -0
  378. package/dist/utils/whisper.js.map +1 -0
  379. package/dist/utils/youtube-channel.d.ts +13 -2
  380. package/dist/utils/youtube-channel.d.ts.map +1 -1
  381. package/dist/utils/youtube-channel.js +39 -3
  382. package/dist/utils/youtube-channel.js.map +1 -1
  383. package/dist/utils/youtube-schemas.d.ts +104 -0
  384. package/dist/utils/youtube-schemas.d.ts.map +1 -1
  385. package/dist/utils/youtube-schemas.js +32 -0
  386. package/dist/utils/youtube-schemas.js.map +1 -1
  387. package/dist/utils/youtube-video.d.ts +22 -1
  388. package/dist/utils/youtube-video.d.ts.map +1 -1
  389. package/dist/utils/youtube-video.js +40 -28
  390. package/dist/utils/youtube-video.js.map +1 -1
  391. package/dist/utils/ytdlp-limiter.d.ts +3 -0
  392. package/dist/utils/ytdlp-limiter.d.ts.map +1 -1
  393. package/dist/utils/ytdlp-limiter.js +19 -3
  394. package/dist/utils/ytdlp-limiter.js.map +1 -1
  395. package/dist/utils/ytdlp-pacer.d.ts +64 -0
  396. package/dist/utils/ytdlp-pacer.d.ts.map +1 -0
  397. package/dist/utils/ytdlp-pacer.js +165 -0
  398. package/dist/utils/ytdlp-pacer.js.map +1 -0
  399. package/dist/utils/ytdlp-stream.d.ts +32 -0
  400. package/dist/utils/ytdlp-stream.d.ts.map +1 -0
  401. package/dist/utils/ytdlp-stream.js +61 -0
  402. package/dist/utils/ytdlp-stream.js.map +1 -0
  403. package/dist/utils/ytdlp.d.ts +10 -0
  404. package/dist/utils/ytdlp.d.ts.map +1 -1
  405. package/dist/utils/ytdlp.js +36 -25
  406. package/dist/utils/ytdlp.js.map +1 -1
  407. package/package.json +10 -4
@@ -0,0 +1,60 @@
1
+ import { inBatches } from './batches.js';
2
+ import { YouTubeError } from './errors.js';
3
+ import { FFMPEG_JPEG_QUALITY, runFfmpeg } from './ffmpeg.js';
4
+ import { planTimestamps } from './frame-plan.js';
5
+ import { requireFfmpeg } from './preflight.js';
6
+ import { FFMPEG_NETWORK_INPUT_ARGS, PREVIEW_STREAM_SELECTOR, resolveStreamUrl, } from './video-stream.js';
7
+ import { TIMEOUTS } from './ytdlp.js';
8
+ import { getVideoInfo } from './youtube.js';
9
+ /**
10
+ * Looking at a video: stills captured into memory, never onto disk.
11
+ *
12
+ * A picture returned inline is something the model can read, which is what makes
13
+ * "what is on screen at 3:20" answerable without a human in the loop. Nothing is
14
+ * written, so the same code serves a contact sheet and a handful of stills.
15
+ */
16
+ /** Each frame is an inline image, so a call is bounded by what a reply can carry. */
17
+ export const MAX_FRAMES_PER_CALL = 12;
18
+ /** Wide enough to read slide text, small enough to be about 100 KB a frame. */
19
+ export const DEFAULT_FRAME_WIDTH = 768;
20
+ /** Concurrent ffmpeg processes. Each is one ranged request against the same stream. */
21
+ const CAPTURE_CONCURRENCY = 4;
22
+ async function captureOne(mediaUrl, timestamp, width) {
23
+ const failure = `ffmpeg could not capture the frame at ${String(timestamp)}s.`;
24
+ const nextStep = 'Try a different timestamp, or check that ffmpeg works with network input.';
25
+ const { stdout } = await runFfmpeg([
26
+ ...FFMPEG_NETWORK_INPUT_ARGS,
27
+ // -ss before -i seeks without decoding everything up to that point.
28
+ '-ss',
29
+ String(timestamp),
30
+ '-i',
31
+ mediaUrl,
32
+ '-frames:v',
33
+ '1',
34
+ '-vf',
35
+ `scale=min(${String(width)}\\,iw):-2`,
36
+ '-q:v',
37
+ FFMPEG_JPEG_QUALITY,
38
+ '-f',
39
+ 'image2pipe',
40
+ '-c:v',
41
+ 'mjpeg',
42
+ 'pipe:1',
43
+ ], { failure, nextStep, timeoutMs: TIMEOUTS.transcript });
44
+ if (stdout.byteLength === 0)
45
+ throw new YouTubeError('YTDLP_FAILED', failure, { nextStep });
46
+ return { timestamp, bytes: stdout };
47
+ }
48
+ /** Capture the frames a request describes, in timestamp order. */
49
+ export async function captureFrames(video, request, options) {
50
+ const { width, maxFrames, label } = options;
51
+ // Cheap local checks first: a missing ffmpeg or an impossible request should
52
+ // fail before a single network round trip.
53
+ await requireFfmpeg(label);
54
+ const info = await getVideoInfo(video);
55
+ const timestamps = planTimestamps(request, info.duration, maxFrames);
56
+ const mediaUrl = await resolveStreamUrl(info.id, PREVIEW_STREAM_SELECTOR, label);
57
+ const frames = await inBatches(timestamps, CAPTURE_CONCURRENCY, (timestamp) => captureOne(mediaUrl, timestamp, width));
58
+ return { videoId: info.id, title: info.title, duration: info.duration, frames };
59
+ }
60
+ //# sourceMappingURL=frames.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames.js","sourceRoot":"","sources":["../../src/utils/frames.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAyB,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;GAMG;AAEH,qFAAqF;AACrF,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AACtC,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEvC,uFAAuF;AACvF,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAsB9B,KAAK,UAAU,UAAU,CACvB,QAAgB,EAChB,SAAiB,EACjB,KAAa;IAEb,MAAM,OAAO,GAAG,yCAAyC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC;IAC/E,MAAM,QAAQ,GAAG,2EAA2E,CAAC;IAE7F,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAChC;QACE,GAAG,yBAAyB;QAC5B,oEAAoE;QACpE,KAAK;QACL,MAAM,CAAC,SAAS,CAAC;QACjB,IAAI;QACJ,QAAQ;QACR,WAAW;QACX,GAAG;QACH,KAAK;QACL,aAAa,MAAM,CAAC,KAAK,CAAC,WAAW;QACrC,MAAM;QACN,mBAAmB;QACnB,IAAI;QACJ,YAAY;QACZ,MAAM;QACN,OAAO;QACP,QAAQ;KACT,EACD,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,UAAU,EAAE,CACtD,CAAC;IAEF,IAAI,MAAM,CAAC,UAAU,KAAK,CAAC;QAAE,MAAM,IAAI,YAAY,CAAC,cAAc,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC3F,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AACtC,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAa,EACb,OAAyB,EACzB,OAAuB;IAEvB,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAE5C,6EAA6E;IAC7E,2CAA2C;IAC3C,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;IAC3B,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,KAAK,CAAC,CAAC;IACvC,MAAM,UAAU,GAAG,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IAErE,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,EAAE,EAAE,uBAAuB,EAAE,KAAK,CAAC,CAAC;IACjF,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,UAAU,EAAE,mBAAmB,EAAE,CAAC,SAAS,EAAE,EAAE,CAC5E,UAAU,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CACvC,CAAC;IAEF,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;AAClF,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A short animated GIF from a moment of a video.
3
+ *
4
+ * A GIF stores every frame whole, so its size grows with length, width and frame
5
+ * rate far faster than a video's does. The length is therefore capped: past it the
6
+ * file is one nobody can share, and a clip is the right tool.
7
+ */
8
+ export declare const MAX_GIF_SECONDS = 15;
9
+ export declare const DEFAULT_GIF_WIDTH = 480;
10
+ export declare const DEFAULT_GIF_FPS = 12;
11
+ export interface GifResult {
12
+ videoId: string;
13
+ title: string;
14
+ filePath: string;
15
+ start: number;
16
+ end: number;
17
+ duration: number;
18
+ width: number;
19
+ fps: number;
20
+ bytes: number;
21
+ }
22
+ export declare function extractGif(video: string, rangeOptions: {
23
+ start?: number;
24
+ end?: number;
25
+ chapter?: string;
26
+ }, options: {
27
+ width: number;
28
+ fps: number;
29
+ outputDir?: string;
30
+ }): Promise<GifResult>;
31
+ //# sourceMappingURL=gif.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gif.d.ts","sourceRoot":"","sources":["../../src/utils/gif.ts"],"names":[],"mappings":"AAeA;;;;;;GAMG;AAEH,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,eAAO,MAAM,eAAe,KAAK,CAAC;AAIlC,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,UAAU,CAC9B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAChE,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1D,OAAO,CAAC,SAAS,CAAC,CAoEpB"}
@@ -0,0 +1,77 @@
1
+ import { mkdir, rm, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { resolveRange, safeStem } from './clips.js';
4
+ import { YouTubeError } from './errors.js';
5
+ import { runFfmpeg } from './ffmpeg.js';
6
+ import { dataDir } from './paths.js';
7
+ import { requireFfmpeg } from './preflight.js';
8
+ import { resolveOutputDir } from './validate.js';
9
+ import { FFMPEG_NETWORK_INPUT_ARGS, PREVIEW_STREAM_SELECTOR, resolveStreamUrl, } from './video-stream.js';
10
+ import { TIMEOUTS } from './ytdlp.js';
11
+ /**
12
+ * A short animated GIF from a moment of a video.
13
+ *
14
+ * A GIF stores every frame whole, so its size grows with length, width and frame
15
+ * rate far faster than a video's does. The length is therefore capped: past it the
16
+ * file is one nobody can share, and a clip is the right tool.
17
+ */
18
+ export const MAX_GIF_SECONDS = 15;
19
+ export const DEFAULT_GIF_WIDTH = 480;
20
+ export const DEFAULT_GIF_FPS = 12;
21
+ const GIFS_DIR = dataDir('gifs');
22
+ export async function extractGif(video, rangeOptions, options) {
23
+ const { width, fps } = options;
24
+ // Cheap local checks first: a missing ffmpeg, an unwritable destination or an
25
+ // over-long range should fail now, not after a network round trip.
26
+ await requireFfmpeg('GIF extraction');
27
+ const targetDir = resolveOutputDir(options.outputDir, GIFS_DIR);
28
+ const { range, title, videoId } = await resolveRange(video, rangeOptions);
29
+ const duration = range.end - range.start;
30
+ if (duration > MAX_GIF_SECONDS) {
31
+ throw new YouTubeError('INVALID_INPUT', `A GIF can be at most ${String(MAX_GIF_SECONDS)} seconds, and this range is ${String(Math.round(duration))}.`, { nextStep: 'Narrow the range, or use extract_clip for a longer excerpt.' });
32
+ }
33
+ await mkdir(targetDir, { recursive: true });
34
+ const mediaUrl = await resolveStreamUrl(videoId, PREVIEW_STREAM_SELECTOR, 'extract_gif');
35
+ const filePath = join(targetDir, `${safeStem(title, videoId)} [${String(Math.round(range.start))}-${String(Math.round(range.end))}].gif`);
36
+ try {
37
+ await runFfmpeg([
38
+ ...FFMPEG_NETWORK_INPUT_ARGS,
39
+ // Both before -i: seek there, then read only the range.
40
+ '-ss',
41
+ String(range.start),
42
+ '-t',
43
+ String(duration),
44
+ '-i',
45
+ mediaUrl,
46
+ '-vf',
47
+ // The palette is built from this clip's own frames, which is what keeps the
48
+ // colours faithful within a GIF's 256.
49
+ `fps=${String(fps)},scale=${String(width)}:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse`,
50
+ '-loop',
51
+ '0',
52
+ '-y',
53
+ filePath,
54
+ ], {
55
+ failure: 'ffmpeg could not make that GIF.',
56
+ nextStep: 'Try a shorter range, or check that ffmpeg works with network input.',
57
+ timeoutMs: TIMEOUTS.transcript,
58
+ });
59
+ }
60
+ catch (error) {
61
+ // ffmpeg writes as it goes, so a failed or cancelled run leaves half a GIF behind.
62
+ await rm(filePath, { force: true });
63
+ throw error;
64
+ }
65
+ return {
66
+ videoId,
67
+ title,
68
+ filePath,
69
+ start: range.start,
70
+ end: range.end,
71
+ duration,
72
+ width,
73
+ fps,
74
+ bytes: (await stat(filePath)).size,
75
+ };
76
+ }
77
+ //# sourceMappingURL=gif.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gif.js","sourceRoot":"","sources":["../../src/utils/gif.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { Coverage } from '../harvest-schemas.js';
3
+ /**
4
+ * Reading a channel's whole catalogue.
5
+ *
6
+ * The listing plane is cheap and is not bot-checked: measured 1,716 videos in
7
+ * 31 seconds from one spawn, so a catalogue costs about a minute per channel
8
+ * and the old 100-video cap was self-imposed rather than forced.
9
+ *
10
+ * Every tab is walked because a channel's videos are not all under /videos —
11
+ * shorts, streams, releases and podcasts each have their own. The first tab
12
+ * that lists a video owns it; later tabs record themselves in `also_in_tabs`.
13
+ * Last-writer-wins would mislabel most of a music channel's catalogue.
14
+ */
15
+ export declare const HARVEST_TABS: readonly ["videos", "shorts", "streams", "releases", "podcasts"];
16
+ export type HarvestTab = (typeof HARVEST_TABS)[number];
17
+ export declare const MAX_VIDEOS_PER_CATALOG = 10000;
18
+ /**
19
+ * Whether a listing failed only because the channel has no such tab.
20
+ *
21
+ * Recognised by the error's code, which the classifier assigns from yt-dlp's own words.
22
+ * It used to look for those words in the message, which never carries them, so no
23
+ * channel lacking a tab was ever catalogued as complete.
24
+ */
25
+ export declare function isAbsentTab(error: unknown): boolean;
26
+ export interface TabResult {
27
+ tab: HarvestTab;
28
+ listed: number;
29
+ added: number;
30
+ complete: boolean;
31
+ error?: string;
32
+ }
33
+ export interface CatalogResult {
34
+ channelId: string;
35
+ name: string;
36
+ handle: string;
37
+ tabs: TabResult[];
38
+ videosSeen: number;
39
+ videosAdded: number;
40
+ coverage: Coverage;
41
+ }
42
+ /** A channel tab as yt-dlp's flat listing, one JSON object per line, newest first. */
43
+ export declare function listTab(channelUrl: string, tab: HarvestTab, limit: number, label?: string): Promise<string>;
44
+ /**
45
+ * Walks every tab of a channel into the store.
46
+ *
47
+ * A tab the channel simply does not have is not a failure — yt-dlp says so in
48
+ * words, and treating that as an error would make every channel without a
49
+ * podcasts tab look broken.
50
+ */
51
+ export declare function harvestCatalog(database: DatabaseSync, channel: string, options?: {
52
+ maxVideos?: number;
53
+ }): Promise<CatalogResult>;
54
+ //# sourceMappingURL=harvest-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-catalog.d.ts","sourceRoot":"","sources":["../../src/utils/harvest-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAahD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,YAAY,kEAAmE,CAAC;AAC7F,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEnD;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,UAAU,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAsBD,sFAAsF;AACtF,wBAAsB,OAAO,CAC3B,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,MAAM,EACb,KAAK,SAA2B,GAC/B,OAAO,CAAC,MAAM,CAAC,CAiBjB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACnC,OAAO,CAAC,aAAa,CAAC,CAQxB"}
@@ -0,0 +1,173 @@
1
+ import { runYtDlp, TIMEOUTS } from './ytdlp.js';
2
+ import { FLAT_PRINT_TEMPLATE, flatEntrySchema, toVideoListItem } from './flat-listing.js';
3
+ import { parseYtDlpJsonLines } from './ytdlp-parse.js';
4
+ import { resolveListTarget } from './youtube-url.js';
5
+ import { getChannelInfo } from './youtube-channel.js';
6
+ import { coverageOf } from './coverage.js';
7
+ import { saveReceipt } from './harvest-receipts.js';
8
+ import { inTransaction } from './store.js';
9
+ import { throwIfAborted, reportProgress } from './context.js';
10
+ import { countRows } from './store-rows.js';
11
+ import { withChannelHarvestLock } from './harvest-lock.js';
12
+ import { YouTubeError } from './errors.js';
13
+ /**
14
+ * Reading a channel's whole catalogue.
15
+ *
16
+ * The listing plane is cheap and is not bot-checked: measured 1,716 videos in
17
+ * 31 seconds from one spawn, so a catalogue costs about a minute per channel
18
+ * and the old 100-video cap was self-imposed rather than forced.
19
+ *
20
+ * Every tab is walked because a channel's videos are not all under /videos —
21
+ * shorts, streams, releases and podcasts each have their own. The first tab
22
+ * that lists a video owns it; later tabs record themselves in `also_in_tabs`.
23
+ * Last-writer-wins would mislabel most of a music channel's catalogue.
24
+ */
25
+ export const HARVEST_TABS = ['videos', 'shorts', 'streams', 'releases', 'podcasts'];
26
+ export const MAX_VIDEOS_PER_CATALOG = 10_000;
27
+ /**
28
+ * Whether a listing failed only because the channel has no such tab.
29
+ *
30
+ * Recognised by the error's code, which the classifier assigns from yt-dlp's own words.
31
+ * It used to look for those words in the message, which never carries them, so no
32
+ * channel lacking a tab was ever catalogued as complete.
33
+ */
34
+ export function isAbsentTab(error) {
35
+ return error instanceof YouTubeError && error.code === 'NOT_FOUND';
36
+ }
37
+ const UPSERT_VIDEO = `
38
+ INSERT INTO video (
39
+ video_id, channel_id, title, duration_s, upload_date, view_count,
40
+ live_status, thumbnail_url, tab, also_in_tabs, catalog_rank, listed_at
41
+ ) VALUES (?,?,?,?,?,?,?,?,?,'[]',?,?)
42
+ ON CONFLICT(video_id) DO UPDATE SET
43
+ title = excluded.title,
44
+ duration_s = excluded.duration_s,
45
+ view_count = excluded.view_count,
46
+ live_status = excluded.live_status,
47
+ thumbnail_url = excluded.thumbnail_url,
48
+ listed_at = excluded.listed_at,
49
+ -- The owning tab is kept; a later tab only records that it also lists this
50
+ -- video, so a re-walk in a different order cannot relabel the catalogue.
51
+ also_in_tabs = CASE
52
+ WHEN video.tab = excluded.tab THEN video.also_in_tabs
53
+ WHEN instr(video.also_in_tabs, excluded.tab) > 0 THEN video.also_in_tabs
54
+ ELSE json_insert(video.also_in_tabs, '$[#]', excluded.tab)
55
+ END`;
56
+ /** A channel tab as yt-dlp's flat listing, one JSON object per line, newest first. */
57
+ export async function listTab(channelUrl, tab, limit, label = `harvest_channel:${tab}`) {
58
+ return runYtDlp([
59
+ '--skip-download',
60
+ '--flat-playlist',
61
+ '--lazy-playlist',
62
+ '--print',
63
+ FLAT_PRINT_TEMPLATE,
64
+ '--playlist-end',
65
+ String(limit),
66
+ ], {
67
+ label,
68
+ timeoutMs: TIMEOUTS.transcript,
69
+ target: resolveListTarget(`${channelUrl}/${tab}`),
70
+ });
71
+ }
72
+ /**
73
+ * Walks every tab of a channel into the store.
74
+ *
75
+ * A tab the channel simply does not have is not a failure — yt-dlp says so in
76
+ * words, and treating that as an error would make every channel without a
77
+ * podcasts tab look broken.
78
+ */
79
+ export async function harvestCatalog(database, channel, options = {}) {
80
+ const maxVideos = Math.min(options.maxVideos ?? 500, MAX_VIDEOS_PER_CATALOG);
81
+ // Resolving the channel comes before the lock because the lock is keyed on
82
+ // the channel id, and only YouTube can turn a handle into one.
83
+ const info = await getChannelInfo(channel);
84
+ return withChannelHarvestLock(info.channelId, () => walkTabs(database, info, maxVideos));
85
+ }
86
+ async function walkTabs(database, info, maxVideos) {
87
+ const now = Date.now();
88
+ database
89
+ .prepare(`INSERT INTO channel (channel_id, handle, name, channel_url, subscriber_count, description, first_seen_at, updated_at)
90
+ VALUES (?,?,?,?,?,?,?,?)
91
+ ON CONFLICT(channel_id) DO UPDATE SET
92
+ handle = excluded.handle, name = excluded.name, channel_url = excluded.channel_url,
93
+ subscriber_count = excluded.subscriber_count, description = excluded.description,
94
+ updated_at = excluded.updated_at`)
95
+ .run(info.channelId, info.handle, info.name, info.channelUrl, info.subscriberCount, info.description, now, now);
96
+ const tabs = [];
97
+ const seen = new Set();
98
+ let rank = 0;
99
+ // Counted from the store, not from `seen`: on a second run every video is
100
+ // new to the walk and none is new to the catalogue, and reporting the former
101
+ // as "added" would make a re-run look like a fresh harvest.
102
+ const countVideos = () => countRows(database.prepare('SELECT COUNT(*) FROM video WHERE channel_id = ?'), info.channelId);
103
+ const before = countVideos();
104
+ for (const tab of HARVEST_TABS) {
105
+ throwIfAborted();
106
+ reportProgress(tabs.length, HARVEST_TABS.length, `harvest_channel: ${tab}`);
107
+ let stdout;
108
+ try {
109
+ stdout = await listTab(info.channelUrl, tab, maxVideos);
110
+ }
111
+ catch (error) {
112
+ const message = error instanceof YouTubeError ? error.message : String(error);
113
+ const absent = isAbsentTab(error);
114
+ tabs.push({
115
+ tab,
116
+ listed: 0,
117
+ added: 0,
118
+ // A tab the channel does not have is a complete answer, not a gap.
119
+ complete: absent,
120
+ ...(absent ? {} : { error: message }),
121
+ });
122
+ continue;
123
+ }
124
+ const entries = parseYtDlpJsonLines(stdout, flatEntrySchema).map(toVideoListItem);
125
+ const insert = database.prepare(UPSERT_VIDEO);
126
+ const tabBefore = countVideos();
127
+ inTransaction(database, () => {
128
+ for (const entry of entries) {
129
+ if (seen.size >= maxVideos)
130
+ break;
131
+ seen.add(entry.id);
132
+ insert.run(entry.id, info.channelId, entry.title, entry.duration, entry.uploadDate === '' ? null : entry.uploadDate, entry.viewCount ?? null, entry.liveStatus ?? null, entry.thumbnailUrl ?? null, tab, rank, now);
133
+ rank += 1;
134
+ }
135
+ });
136
+ tabs.push({
137
+ tab,
138
+ listed: entries.length,
139
+ added: countVideos() - tabBefore,
140
+ complete: entries.length < maxVideos,
141
+ });
142
+ }
143
+ const everyTabComplete = tabs.every((tab) => tab.complete);
144
+ const coverage = coverageOf({
145
+ scope: 'channel-catalog',
146
+ targetId: info.channelId,
147
+ have: seen.size,
148
+ source: `yt-dlp --flat-playlist over ${HARVEST_TABS.join(', ')}`,
149
+ // Only an unbroken walk of every tab proves the catalogue is whole. A tab
150
+ // that errored, or one that filled to the cap, leaves it unprovable.
151
+ ...(everyTabComplete
152
+ ? {
153
+ expected: { value: seen.size, source: 'source-exhausted' },
154
+ ranToExhaustion: true,
155
+ }
156
+ : {}),
157
+ ...(seen.size >= maxVideos ? { limitApplied: maxVideos } : {}),
158
+ ...(everyTabComplete ? {} : { resumeToken: `catalog:${info.channelId}` }),
159
+ });
160
+ inTransaction(database, () => {
161
+ saveReceipt(database, coverage, { startedAt: now });
162
+ });
163
+ return {
164
+ channelId: info.channelId,
165
+ name: info.name,
166
+ handle: info.handle,
167
+ tabs,
168
+ videosSeen: seen.size,
169
+ videosAdded: countVideos() - before,
170
+ coverage,
171
+ };
172
+ }
173
+ //# sourceMappingURL=harvest-catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-catalog.js","sourceRoot":"","sources":["../../src/utils/harvest-catalog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { Coverage } from '../harvest-schemas.js';
3
+ import { type DetailFailure } from './detail-batch.js';
4
+ import { type YouTubeErrorCode } from './errors.js';
5
+ /**
6
+ * Reading every catalogued video of a channel, and keeping what each said.
7
+ *
8
+ * Resumable by construction: a video is read once, its row says so, and the next run
9
+ * takes the next unread one. What came back before a refusal is kept, because every
10
+ * batch commits on its own.
11
+ */
12
+ export declare const DEFAULT_DETAIL_BATCH = 50;
13
+ export declare const MAX_DETAIL_BATCH = 500;
14
+ export interface DetailsResult {
15
+ channelId: string;
16
+ name: string;
17
+ attempted: number;
18
+ stored: number;
19
+ failures: DetailFailure[];
20
+ /** Videos still unread and still readable. */
21
+ remaining: number;
22
+ /** The error that made the run stop early, when one did. */
23
+ stoppedBecause?: YouTubeErrorCode;
24
+ coverage: Coverage;
25
+ }
26
+ export declare function harvestDetails(database: DatabaseSync, channel: string, options: {
27
+ maxVideos: number;
28
+ retryUnreadable?: boolean;
29
+ }): Promise<DetailsResult>;
30
+ //# sourceMappingURL=harvest-details.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-details.d.ts","sourceRoot":"","sources":["../../src/utils/harvest-details.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGtD,OAAO,EAAyC,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC9F,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAQlE;;;;;;GAMG;AAEH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAQpC,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,wBAAsB,cAAc,CAClC,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GACxD,OAAO,CAAC,aAAa,CAAC,CAmFxB"}
@@ -0,0 +1,98 @@
1
+ import { reportProgress, throwIfAborted } from './context.js';
2
+ import { coverageOf } from './coverage.js';
3
+ import { STOPPING, persist, read, stoppingCode } from './detail-batch.js';
4
+ import { YouTubeError } from './errors.js';
5
+ import { withChannelHarvestLock } from './harvest-lock.js';
6
+ import { readReceipt, saveReceipt } from './harvest-receipts.js';
7
+ import { inTransaction } from './store.js';
8
+ import { catalogCounts, pendingVideoIds, unreadableCount } from './video-detail-store.js';
9
+ import { getChannelInfo } from './youtube-channel.js';
10
+ import { concurrencyState } from './ytdlp.js';
11
+ /**
12
+ * Reading every catalogued video of a channel, and keeping what each said.
13
+ *
14
+ * Resumable by construction: a video is read once, its row says so, and the next run
15
+ * takes the next unread one. What came back before a refusal is kept, because every
16
+ * batch commits on its own.
17
+ */
18
+ export const DEFAULT_DETAIL_BATCH = 50;
19
+ export const MAX_DETAIL_BATCH = 500;
20
+ /** Concurrent reads. Each is a full yt-dlp extraction, the call YouTube throttles hardest. */
21
+ const READ_CONCURRENCY = 4;
22
+ function plural(count, noun) {
23
+ return `${String(count)} ${noun}${count === 1 ? '' : 's'}`;
24
+ }
25
+ export async function harvestDetails(database, channel, options) {
26
+ const info = await getChannelInfo(channel);
27
+ if (catalogCounts(database, info.channelId).videos === 0) {
28
+ throw new YouTubeError('NOT_FOUND', `${info.name} has not been catalogued yet.`, {
29
+ nextStep: 'Call harvest_channel first, so there is a catalogue of videos to read.',
30
+ });
31
+ }
32
+ return withChannelHarvestLock(info.channelId, async () => {
33
+ const startedAt = Date.now();
34
+ const { channelId } = info;
35
+ const queue = pendingVideoIds(database, channelId, options.maxVideos, {
36
+ includeUnreadable: options.retryUnreadable === true,
37
+ });
38
+ const width = Math.max(1, Math.min(concurrencyState().limit, READ_CONCURRENCY));
39
+ const failures = [];
40
+ let stored = 0;
41
+ let attempted = 0;
42
+ let stoppedBecause;
43
+ for (let start = 0; start < queue.length && stoppedBecause === undefined; start += width) {
44
+ throwIfAborted();
45
+ reportProgress(start, queue.length, 'harvest_details');
46
+ const batch = await Promise.all(queue.slice(start, start + width).map(read));
47
+ attempted += batch.length;
48
+ const saved = persist(database, batch);
49
+ stored += saved.stored;
50
+ failures.push(...saved.failures);
51
+ stoppedBecause = stoppingCode(batch);
52
+ }
53
+ const counts = catalogCounts(database, channelId);
54
+ const remaining = pendingVideoIds(database, channelId, counts.videos).length;
55
+ const unreadable = unreadableCount(database, channelId);
56
+ const capBound = attempted >= options.maxVideos && remaining > 0;
57
+ const catalog = readReceipt(database, 'channel-catalog', channelId);
58
+ const stopReason = stoppedBecause === undefined ? undefined : STOPPING[stoppedBecause];
59
+ const coverage = coverageOf({
60
+ scope: 'video-details',
61
+ targetId: channelId,
62
+ have: counts.detailed,
63
+ source: 'yt-dlp -j per catalogued video',
64
+ // The catalogue's size is a fair total only if the catalogue itself is whole.
65
+ ...(catalog?.complete === true
66
+ ? { expected: { value: counts.videos, source: 'store:catalog' } }
67
+ : {}),
68
+ ...(capBound ? { limitApplied: options.maxVideos } : {}),
69
+ ...(stopReason !== undefined
70
+ ? { reason: stopReason }
71
+ : remaining === 0 && (failures.length > 0 || unreadable > 0)
72
+ ? { reason: 'PARTIAL_ERROR' }
73
+ : {}),
74
+ ...(remaining > 0 || stoppedBecause !== undefined
75
+ ? { resumeToken: `details:${channelId}` }
76
+ : {}),
77
+ ...(unreadable > 0
78
+ ? {
79
+ note: `${plural(unreadable, 'video')} cannot be read (private, members-only, age-gated, removed or failing), so this stays incomplete.`,
80
+ }
81
+ : {}),
82
+ });
83
+ inTransaction(database, () => {
84
+ saveReceipt(database, coverage, { startedAt });
85
+ });
86
+ return {
87
+ channelId,
88
+ name: info.name,
89
+ attempted,
90
+ stored,
91
+ failures,
92
+ remaining,
93
+ ...(stoppedBecause === undefined ? {} : { stoppedBecause }),
94
+ coverage,
95
+ };
96
+ });
97
+ }
98
+ //# sourceMappingURL=harvest-details.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-details.js","sourceRoot":"","sources":["../../src/utils/harvest-details.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare function withChannelHarvestLock<T>(channelId: string, work: () => Promise<T>): Promise<T>;
2
+ export declare function withVideoHarvestLock<T>(videoId: string, work: () => Promise<T>): Promise<T>;
3
+ //# sourceMappingURL=harvest-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-lock.d.ts","sourceRoot":"","sources":["../../src/utils/harvest-lock.ts"],"names":[],"mappings":"AAsBA,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACrB,OAAO,CAAC,CAAC,CAAC,CAKZ;AAED,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAKjG"}
@@ -0,0 +1,26 @@
1
+ import { YouTubeError } from './errors.js';
2
+ import { ensurePrivateDir } from './paths.js';
3
+ import { withFileLock } from './file-lock.js';
4
+ import { channelHarvestLockPath, storeDir, videoHarvestLockPath } from './store-paths.js';
5
+ /**
6
+ * Exclusive access to one harvest target.
7
+ *
8
+ * WAL already keeps concurrent writes consistent, so this is not about the
9
+ * rows. It is about the requests: two harvests of the same channel would each
10
+ * spawn yt-dlp and double the rate at the one resource that throttles us.
11
+ */
12
+ function alreadyRunning(what, id, existing) {
13
+ const owner = existing === undefined ? '' : `Started at ${existing.startedAt} by process ${existing.pid}. `;
14
+ return new YouTubeError('INVALID_INPUT', `A harvest of ${what} ${id} is already running.`, {
15
+ nextStep: `${owner}Wait for it to finish, then call the tool again to continue where it stopped.`,
16
+ });
17
+ }
18
+ export async function withChannelHarvestLock(channelId, work) {
19
+ await ensurePrivateDir(storeDir());
20
+ return withFileLock(channelHarvestLockPath(channelId), work, (existing) => alreadyRunning('channel', channelId, existing));
21
+ }
22
+ export async function withVideoHarvestLock(videoId, work) {
23
+ await ensurePrivateDir(storeDir());
24
+ return withFileLock(videoHarvestLockPath(videoId), work, (existing) => alreadyRunning('video', videoId, existing));
25
+ }
26
+ //# sourceMappingURL=harvest-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-lock.js","sourceRoot":"","sources":["../../src/utils/harvest-lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAmB,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE1F;;;;;;GAMG;AAEH,SAAS,cAAc,CAAC,IAAY,EAAE,EAAU,EAAE,QAAgC;IAChF,MAAM,KAAK,GACT,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,QAAQ,CAAC,SAAS,eAAe,QAAQ,CAAC,GAAG,IAAI,CAAC;IAEhG,OAAO,IAAI,YAAY,CAAC,eAAe,EAAE,gBAAgB,IAAI,IAAI,EAAE,sBAAsB,EAAE;QACzF,QAAQ,EAAE,GAAG,KAAK,+EAA+E;KAClG,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,SAAiB,EACjB,IAAsB;IAEtB,MAAM,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC;IACnC,OAAO,YAAY,CAAC,sBAAsB,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,EAAE,CACxE,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,CAAC,CAC/C,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAI,OAAe,EAAE,IAAsB;IACnF,MAAM,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC;IACnC,OAAO,YAAY,CAAC,oBAAoB,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,EAAE,CACpE,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,CAC3C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import { type Coverage } from '../harvest-schemas.js';
3
+ export declare function saveReceipt(database: DatabaseSync, coverage: Coverage, options?: {
4
+ startedAt?: number;
5
+ errorCode?: string;
6
+ }): void;
7
+ export declare function readReceipt(database: DatabaseSync, scope: Coverage['scope'], targetId: string): Coverage | undefined;
8
+ export declare function listReceipts(database: DatabaseSync, options?: {
9
+ scope?: Coverage['scope'];
10
+ limit?: number;
11
+ offset?: number;
12
+ }): Coverage[];
13
+ export declare function countReceipts(database: DatabaseSync, scope?: Coverage['scope']): number;
14
+ //# sourceMappingURL=harvest-receipts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-receipts.d.ts","sourceRoot":"","sources":["../../src/utils/harvest-receipts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAsC,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAgE1F,wBAAgB,WAAW,CACzB,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACvD,IAAI,CAsCN;AAuBD,wBAAgB,WAAW,CACzB,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,EACxB,QAAQ,EAAE,MAAM,GACf,QAAQ,GAAG,SAAS,CAQtB;AAED,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,YAAY,EACtB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3E,QAAQ,EAAE,CAwBZ;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,MAAM,CAavF"}