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.
Files changed (478) hide show
  1. package/README.md +153 -38
  2. package/dist/brain-schemas.d.ts +10 -10
  3. package/dist/brain-schemas.d.ts.map +1 -1
  4. package/dist/brain-schemas.js +0 -4
  5. package/dist/brain-schemas.js.map +1 -1
  6. package/dist/harvest-schemas.d.ts +75 -0
  7. package/dist/harvest-schemas.d.ts.map +1 -0
  8. package/dist/harvest-schemas.js +106 -0
  9. package/dist/harvest-schemas.js.map +1 -0
  10. package/dist/http/app.d.ts +16 -0
  11. package/dist/http/app.d.ts.map +1 -0
  12. package/dist/http/app.js +176 -0
  13. package/dist/http/app.js.map +1 -0
  14. package/dist/http/auth.d.ts +38 -0
  15. package/dist/http/auth.d.ts.map +1 -0
  16. package/dist/http/auth.js +79 -0
  17. package/dist/http/auth.js.map +1 -0
  18. package/dist/http/config.d.ts +41 -0
  19. package/dist/http/config.d.ts.map +1 -0
  20. package/dist/http/config.js +65 -0
  21. package/dist/http/config.js.map +1 -0
  22. package/dist/http/rate-limiter.d.ts +24 -0
  23. package/dist/http/rate-limiter.d.ts.map +1 -0
  24. package/dist/http/rate-limiter.js +55 -0
  25. package/dist/http/rate-limiter.js.map +1 -0
  26. package/dist/http/sessions.d.ts +34 -0
  27. package/dist/http/sessions.d.ts.map +1 -0
  28. package/dist/http/sessions.js +62 -0
  29. package/dist/http/sessions.js.map +1 -0
  30. package/dist/http.d.ts +6 -48
  31. package/dist/http.d.ts.map +1 -1
  32. package/dist/http.js +35 -252
  33. package/dist/http.js.map +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +11 -517
  36. package/dist/index.js.map +1 -1
  37. package/dist/prompts-brain.d.ts +4 -0
  38. package/dist/prompts-brain.d.ts.map +1 -0
  39. package/dist/prompts-brain.js +48 -0
  40. package/dist/prompts-brain.js.map +1 -0
  41. package/dist/prompts-research.d.ts +4 -0
  42. package/dist/prompts-research.d.ts.map +1 -0
  43. package/dist/prompts-research.js +74 -0
  44. package/dist/prompts-research.js.map +1 -0
  45. package/dist/prompts-shared.d.ts +13 -0
  46. package/dist/prompts-shared.d.ts.map +1 -0
  47. package/dist/prompts-shared.js +7 -0
  48. package/dist/prompts-shared.js.map +1 -0
  49. package/dist/prompts.d.ts.map +1 -1
  50. package/dist/prompts.js +5 -100
  51. package/dist/prompts.js.map +1 -1
  52. package/dist/registry/brain-tools.d.ts +4 -0
  53. package/dist/registry/brain-tools.d.ts.map +1 -0
  54. package/dist/registry/brain-tools.js +104 -0
  55. package/dist/registry/brain-tools.js.map +1 -0
  56. package/dist/registry/channel-tools.d.ts +4 -0
  57. package/dist/registry/channel-tools.d.ts.map +1 -0
  58. package/dist/registry/channel-tools.js +53 -0
  59. package/dist/registry/channel-tools.js.map +1 -0
  60. package/dist/registry/harvest-tools.d.ts +10 -0
  61. package/dist/registry/harvest-tools.d.ts.map +1 -0
  62. package/dist/registry/harvest-tools.js +109 -0
  63. package/dist/registry/harvest-tools.js.map +1 -0
  64. package/dist/registry/health-tools.d.ts +4 -0
  65. package/dist/registry/health-tools.d.ts.map +1 -0
  66. package/dist/registry/health-tools.js +39 -0
  67. package/dist/registry/health-tools.js.map +1 -0
  68. package/dist/registry/index.d.ts +8 -0
  69. package/dist/registry/index.d.ts.map +1 -0
  70. package/dist/registry/index.js +29 -0
  71. package/dist/registry/index.js.map +1 -0
  72. package/dist/registry/library-tools.d.ts +4 -0
  73. package/dist/registry/library-tools.d.ts.map +1 -0
  74. package/dist/registry/library-tools.js +116 -0
  75. package/dist/registry/library-tools.js.map +1 -0
  76. package/dist/registry/media-tools.d.ts +4 -0
  77. package/dist/registry/media-tools.d.ts.map +1 -0
  78. package/dist/registry/media-tools.js +100 -0
  79. package/dist/registry/media-tools.js.map +1 -0
  80. package/dist/registry/thumbnail-tools.d.ts +7 -0
  81. package/dist/registry/thumbnail-tools.d.ts.map +1 -0
  82. package/dist/registry/thumbnail-tools.js +72 -0
  83. package/dist/registry/thumbnail-tools.js.map +1 -0
  84. package/dist/registry/transcript-tools.d.ts +4 -0
  85. package/dist/registry/transcript-tools.d.ts.map +1 -0
  86. package/dist/registry/transcript-tools.js +68 -0
  87. package/dist/registry/transcript-tools.js.map +1 -0
  88. package/dist/registry/types.d.ts +41 -0
  89. package/dist/registry/types.d.ts.map +1 -0
  90. package/dist/registry/types.js +17 -0
  91. package/dist/registry/types.js.map +1 -0
  92. package/dist/registry/video-tools.d.ts +4 -0
  93. package/dist/registry/video-tools.d.ts.map +1 -0
  94. package/dist/registry/video-tools.js +101 -0
  95. package/dist/registry/video-tools.js.map +1 -0
  96. package/dist/resources.d.ts.map +1 -1
  97. package/dist/resources.js +13 -10
  98. package/dist/resources.js.map +1 -1
  99. package/dist/schemas.d.ts +250 -2
  100. package/dist/schemas.d.ts.map +1 -1
  101. package/dist/schemas.js +61 -0
  102. package/dist/schemas.js.map +1 -1
  103. package/dist/thumbnail-resource.d.ts +8 -0
  104. package/dist/thumbnail-resource.d.ts.map +1 -0
  105. package/dist/thumbnail-resource.js +38 -0
  106. package/dist/thumbnail-resource.js.map +1 -0
  107. package/dist/thumbnail-schemas.d.ts +512 -0
  108. package/dist/thumbnail-schemas.d.ts.map +1 -0
  109. package/dist/thumbnail-schemas.js +95 -0
  110. package/dist/thumbnail-schemas.js.map +1 -0
  111. package/dist/tools/ask-brain.js +1 -1
  112. package/dist/tools/ask-brain.js.map +1 -1
  113. package/dist/tools/batch.js +1 -1
  114. package/dist/tools/batch.js.map +1 -1
  115. package/dist/tools/brain-info.js +1 -1
  116. package/dist/tools/brain-info.js.map +1 -1
  117. package/dist/tools/build-brain.js +1 -1
  118. package/dist/tools/build-brain.js.map +1 -1
  119. package/dist/tools/check-health.d.ts +61 -0
  120. package/dist/tools/check-health.d.ts.map +1 -1
  121. package/dist/tools/check-health.js +94 -6
  122. package/dist/tools/check-health.js.map +1 -1
  123. package/dist/tools/clips.d.ts +9 -41
  124. package/dist/tools/clips.d.ts.map +1 -1
  125. package/dist/tools/clips.js +52 -128
  126. package/dist/tools/clips.js.map +1 -1
  127. package/dist/tools/fetch-channel-thumbnails.d.ts +179 -0
  128. package/dist/tools/fetch-channel-thumbnails.d.ts.map +1 -0
  129. package/dist/tools/fetch-channel-thumbnails.js +123 -0
  130. package/dist/tools/fetch-channel-thumbnails.js.map +1 -0
  131. package/dist/tools/fetch-videos.d.ts +18 -4
  132. package/dist/tools/fetch-videos.d.ts.map +1 -1
  133. package/dist/tools/fetch-videos.js +64 -20
  134. package/dist/tools/fetch-videos.js.map +1 -1
  135. package/dist/tools/frames-subtitles.d.ts +44 -0
  136. package/dist/tools/frames-subtitles.d.ts.map +1 -0
  137. package/dist/tools/frames-subtitles.js +104 -0
  138. package/dist/tools/frames-subtitles.js.map +1 -0
  139. package/dist/tools/get-channel-info.d.ts +2 -0
  140. package/dist/tools/get-channel-info.d.ts.map +1 -1
  141. package/dist/tools/get-channel-info.js +8 -1
  142. package/dist/tools/get-channel-info.js.map +1 -1
  143. package/dist/tools/get-comments.d.ts +238 -6
  144. package/dist/tools/get-comments.d.ts.map +1 -1
  145. package/dist/tools/get-comments.js +85 -21
  146. package/dist/tools/get-comments.js.map +1 -1
  147. package/dist/tools/get-coverage.d.ts +122 -0
  148. package/dist/tools/get-coverage.d.ts.map +1 -0
  149. package/dist/tools/get-coverage.js +105 -0
  150. package/dist/tools/get-coverage.js.map +1 -0
  151. package/dist/tools/get-playlist-info.d.ts.map +1 -1
  152. package/dist/tools/get-playlist-info.js +1 -0
  153. package/dist/tools/get-playlist-info.js.map +1 -1
  154. package/dist/tools/get-thumbnail.d.ts +38 -0
  155. package/dist/tools/get-thumbnail.d.ts.map +1 -0
  156. package/dist/tools/get-thumbnail.js +152 -0
  157. package/dist/tools/get-thumbnail.js.map +1 -0
  158. package/dist/tools/get-transcript.d.ts.map +1 -1
  159. package/dist/tools/get-transcript.js +2 -14
  160. package/dist/tools/get-transcript.js.map +1 -1
  161. package/dist/tools/harvest-channel.d.ts +92 -0
  162. package/dist/tools/harvest-channel.d.ts.map +1 -0
  163. package/dist/tools/harvest-channel.js +62 -0
  164. package/dist/tools/harvest-channel.js.map +1 -0
  165. package/dist/tools/harvest-comments.d.ts +81 -0
  166. package/dist/tools/harvest-comments.d.ts.map +1 -0
  167. package/dist/tools/harvest-comments.js +115 -0
  168. package/dist/tools/harvest-comments.js.map +1 -0
  169. package/dist/tools/library.d.ts.map +1 -1
  170. package/dist/tools/library.js +14 -4
  171. package/dist/tools/library.js.map +1 -1
  172. package/dist/tools/list-channel-thumbnails.d.ts +235 -0
  173. package/dist/tools/list-channel-thumbnails.d.ts.map +1 -0
  174. package/dist/tools/list-channel-thumbnails.js +79 -0
  175. package/dist/tools/list-channel-thumbnails.js.map +1 -0
  176. package/dist/tools/list-library.js +1 -1
  177. package/dist/tools/list-library.js.map +1 -1
  178. package/dist/tools/prune-harvest.d.ts +59 -0
  179. package/dist/tools/prune-harvest.d.ts.map +1 -0
  180. package/dist/tools/prune-harvest.js +108 -0
  181. package/dist/tools/prune-harvest.js.map +1 -0
  182. package/dist/tools/query-comments.d.ts +125 -0
  183. package/dist/tools/query-comments.d.ts.map +1 -0
  184. package/dist/tools/query-comments.js +106 -0
  185. package/dist/tools/query-comments.js.map +1 -0
  186. package/dist/tools/query-videos.d.ts +118 -0
  187. package/dist/tools/query-videos.d.ts.map +1 -0
  188. package/dist/tools/query-videos.js +97 -0
  189. package/dist/tools/query-videos.js.map +1 -0
  190. package/dist/tools/repair-store.d.ts +34 -0
  191. package/dist/tools/repair-store.d.ts.map +1 -0
  192. package/dist/tools/repair-store.js +58 -0
  193. package/dist/tools/repair-store.js.map +1 -0
  194. package/dist/tools/save-to-library.d.ts.map +1 -1
  195. package/dist/tools/save-to-library.js +5 -1
  196. package/dist/tools/save-to-library.js.map +1 -1
  197. package/dist/tools/search-channels.d.ts +6 -0
  198. package/dist/tools/search-channels.d.ts.map +1 -1
  199. package/dist/tools/search-channels.js +5 -1
  200. package/dist/tools/search-channels.js.map +1 -1
  201. package/dist/tools/search-transcript.d.ts.map +1 -1
  202. package/dist/tools/search-transcript.js +6 -13
  203. package/dist/tools/search-transcript.js.map +1 -1
  204. package/dist/tools/search-videos.d.ts +3 -0
  205. package/dist/tools/search-videos.d.ts.map +1 -1
  206. package/dist/tools/search-videos.js +1 -1
  207. package/dist/tools/search-videos.js.map +1 -1
  208. package/dist/utils/batches.d.ts +15 -0
  209. package/dist/utils/batches.d.ts.map +1 -0
  210. package/dist/utils/batches.js +31 -0
  211. package/dist/utils/batches.js.map +1 -0
  212. package/dist/utils/brain-build.d.ts.map +1 -1
  213. package/dist/utils/brain-build.js +1 -12
  214. package/dist/utils/brain-build.js.map +1 -1
  215. package/dist/utils/brain-index.d.ts.map +1 -1
  216. package/dist/utils/brain-index.js +3 -2
  217. package/dist/utils/brain-index.js.map +1 -1
  218. package/dist/utils/brain-lock.d.ts +2 -18
  219. package/dist/utils/brain-lock.d.ts.map +1 -1
  220. package/dist/utils/brain-lock.js +4 -80
  221. package/dist/utils/brain-lock.js.map +1 -1
  222. package/dist/utils/brain-lookup.d.ts +8 -0
  223. package/dist/utils/brain-lookup.d.ts.map +1 -1
  224. package/dist/utils/brain-lookup.js +5 -31
  225. package/dist/utils/brain-lookup.js.map +1 -1
  226. package/dist/utils/brain-storage.d.ts.map +1 -1
  227. package/dist/utils/brain-storage.js +5 -5
  228. package/dist/utils/brain-storage.js.map +1 -1
  229. package/dist/utils/caption-probe.d.ts +7 -0
  230. package/dist/utils/caption-probe.d.ts.map +1 -0
  231. package/dist/utils/caption-probe.js +36 -0
  232. package/dist/utils/caption-probe.js.map +1 -0
  233. package/dist/utils/channel-images.d.ts +16 -0
  234. package/dist/utils/channel-images.d.ts.map +1 -0
  235. package/dist/utils/channel-images.js +35 -0
  236. package/dist/utils/channel-images.js.map +1 -0
  237. package/dist/utils/channel-lookup.d.ts +5 -0
  238. package/dist/utils/channel-lookup.d.ts.map +1 -0
  239. package/dist/utils/channel-lookup.js +37 -0
  240. package/dist/utils/channel-lookup.js.map +1 -0
  241. package/dist/utils/chapters.d.ts +11 -0
  242. package/dist/utils/chapters.d.ts.map +1 -0
  243. package/dist/utils/chapters.js +24 -0
  244. package/dist/utils/chapters.js.map +1 -0
  245. package/dist/utils/clips.d.ts.map +1 -1
  246. package/dist/utils/clips.js +18 -26
  247. package/dist/utils/clips.js.map +1 -1
  248. package/dist/utils/comment-store.d.ts +71 -0
  249. package/dist/utils/comment-store.d.ts.map +1 -0
  250. package/dist/utils/comment-store.js +129 -0
  251. package/dist/utils/comment-store.js.map +1 -0
  252. package/dist/utils/comment-threads.d.ts +51 -0
  253. package/dist/utils/comment-threads.d.ts.map +1 -0
  254. package/dist/utils/comment-threads.js +76 -0
  255. package/dist/utils/comment-threads.js.map +1 -0
  256. package/dist/utils/coverage-text.d.ts +3 -0
  257. package/dist/utils/coverage-text.d.ts.map +1 -0
  258. package/dist/utils/coverage-text.js +62 -0
  259. package/dist/utils/coverage-text.js.map +1 -0
  260. package/dist/utils/coverage.d.ts +57 -0
  261. package/dist/utils/coverage.d.ts.map +1 -0
  262. package/dist/utils/coverage.js +142 -0
  263. package/dist/utils/coverage.js.map +1 -0
  264. package/dist/utils/env.d.ts +26 -0
  265. package/dist/utils/env.d.ts.map +1 -0
  266. package/dist/utils/env.js +72 -0
  267. package/dist/utils/env.js.map +1 -0
  268. package/dist/utils/errors.d.ts +1 -1
  269. package/dist/utils/errors.d.ts.map +1 -1
  270. package/dist/utils/errors.js +28 -2
  271. package/dist/utils/errors.js.map +1 -1
  272. package/dist/utils/file-lock.d.ts +34 -0
  273. package/dist/utils/file-lock.d.ts.map +1 -0
  274. package/dist/utils/file-lock.js +91 -0
  275. package/dist/utils/file-lock.js.map +1 -0
  276. package/dist/utils/flat-listing.d.ts +89 -0
  277. package/dist/utils/flat-listing.d.ts.map +1 -0
  278. package/dist/utils/flat-listing.js +68 -0
  279. package/dist/utils/flat-listing.js.map +1 -0
  280. package/dist/utils/format.d.ts +18 -4
  281. package/dist/utils/format.d.ts.map +1 -1
  282. package/dist/utils/format.js +2 -6
  283. package/dist/utils/format.js.map +1 -1
  284. package/dist/utils/guard.d.ts +21 -0
  285. package/dist/utils/guard.d.ts.map +1 -0
  286. package/dist/utils/guard.js +105 -0
  287. package/dist/utils/guard.js.map +1 -0
  288. package/dist/utils/harvest-catalog.d.ts +44 -0
  289. package/dist/utils/harvest-catalog.d.ts.map +1 -0
  290. package/dist/utils/harvest-catalog.js +164 -0
  291. package/dist/utils/harvest-catalog.js.map +1 -0
  292. package/dist/utils/harvest-lock.d.ts +3 -0
  293. package/dist/utils/harvest-lock.d.ts.map +1 -0
  294. package/dist/utils/harvest-lock.js +26 -0
  295. package/dist/utils/harvest-lock.js.map +1 -0
  296. package/dist/utils/harvest-receipts.d.ts +14 -0
  297. package/dist/utils/harvest-receipts.d.ts.map +1 -0
  298. package/dist/utils/harvest-receipts.js +110 -0
  299. package/dist/utils/harvest-receipts.js.map +1 -0
  300. package/dist/utils/image-dimensions.d.ts +19 -0
  301. package/dist/utils/image-dimensions.d.ts.map +1 -0
  302. package/dist/utils/image-dimensions.js +93 -0
  303. package/dist/utils/image-dimensions.js.map +1 -0
  304. package/dist/utils/image-fetch.d.ts +31 -0
  305. package/dist/utils/image-fetch.d.ts.map +1 -0
  306. package/dist/utils/image-fetch.js +141 -0
  307. package/dist/utils/image-fetch.js.map +1 -0
  308. package/dist/utils/json-file.d.ts +14 -5
  309. package/dist/utils/json-file.d.ts.map +1 -1
  310. package/dist/utils/json-file.js +24 -9
  311. package/dist/utils/json-file.js.map +1 -1
  312. package/dist/utils/listing-cursor.d.ts +31 -0
  313. package/dist/utils/listing-cursor.d.ts.map +1 -0
  314. package/dist/utils/listing-cursor.js +39 -0
  315. package/dist/utils/listing-cursor.js.map +1 -0
  316. package/dist/utils/paths.d.ts +10 -0
  317. package/dist/utils/paths.d.ts.map +1 -1
  318. package/dist/utils/paths.js +14 -0
  319. package/dist/utils/paths.js.map +1 -1
  320. package/dist/utils/pattern.d.ts +30 -0
  321. package/dist/utils/pattern.d.ts.map +1 -0
  322. package/dist/utils/pattern.js +130 -0
  323. package/dist/utils/pattern.js.map +1 -0
  324. package/dist/utils/pot-preflight.d.ts +53 -0
  325. package/dist/utils/pot-preflight.d.ts.map +1 -0
  326. package/dist/utils/pot-preflight.js +146 -0
  327. package/dist/utils/pot-preflight.js.map +1 -0
  328. package/dist/utils/search-index.d.ts.map +1 -1
  329. package/dist/utils/search-index.js +9 -4
  330. package/dist/utils/search-index.js.map +1 -1
  331. package/dist/utils/sleep.d.ts +9 -0
  332. package/dist/utils/sleep.d.ts.map +1 -0
  333. package/dist/utils/sleep.js +25 -0
  334. package/dist/utils/sleep.js.map +1 -0
  335. package/dist/utils/storage.d.ts.map +1 -1
  336. package/dist/utils/storage.js +14 -8
  337. package/dist/utils/storage.js.map +1 -1
  338. package/dist/utils/store-health.d.ts +37 -0
  339. package/dist/utils/store-health.d.ts.map +1 -0
  340. package/dist/utils/store-health.js +92 -0
  341. package/dist/utils/store-health.js.map +1 -0
  342. package/dist/utils/store-migrations.d.ts +33 -0
  343. package/dist/utils/store-migrations.d.ts.map +1 -0
  344. package/dist/utils/store-migrations.js +41 -0
  345. package/dist/utils/store-migrations.js.map +1 -0
  346. package/dist/utils/store-paths.d.ts +33 -0
  347. package/dist/utils/store-paths.d.ts.map +1 -0
  348. package/dist/utils/store-paths.js +48 -0
  349. package/dist/utils/store-paths.js.map +1 -0
  350. package/dist/utils/store-rows.d.ts +22 -0
  351. package/dist/utils/store-rows.d.ts.map +1 -0
  352. package/dist/utils/store-rows.js +62 -0
  353. package/dist/utils/store-rows.js.map +1 -0
  354. package/dist/utils/store-schema.d.ts +16 -0
  355. package/dist/utils/store-schema.d.ts.map +1 -0
  356. package/dist/utils/store-schema.js +168 -0
  357. package/dist/utils/store-schema.js.map +1 -0
  358. package/dist/utils/store.d.ts +34 -0
  359. package/dist/utils/store.d.ts.map +1 -0
  360. package/dist/utils/store.js +157 -0
  361. package/dist/utils/store.js.map +1 -0
  362. package/dist/utils/subtitles.d.ts +7 -0
  363. package/dist/utils/subtitles.d.ts.map +1 -0
  364. package/dist/utils/subtitles.js +30 -0
  365. package/dist/utils/subtitles.js.map +1 -0
  366. package/dist/utils/thumbnail-entry.d.ts +19 -0
  367. package/dist/utils/thumbnail-entry.d.ts.map +1 -0
  368. package/dist/utils/thumbnail-entry.js +119 -0
  369. package/dist/utils/thumbnail-entry.js.map +1 -0
  370. package/dist/utils/thumbnail-fetch.d.ts +37 -0
  371. package/dist/utils/thumbnail-fetch.d.ts.map +1 -0
  372. package/dist/utils/thumbnail-fetch.js +89 -0
  373. package/dist/utils/thumbnail-fetch.js.map +1 -0
  374. package/dist/utils/thumbnail-ladder.d.ts +44 -0
  375. package/dist/utils/thumbnail-ladder.d.ts.map +1 -0
  376. package/dist/utils/thumbnail-ladder.js +68 -0
  377. package/dist/utils/thumbnail-ladder.js.map +1 -0
  378. package/dist/utils/thumbnail-listing.d.ts +16 -0
  379. package/dist/utils/thumbnail-listing.d.ts.map +1 -0
  380. package/dist/utils/thumbnail-listing.js +26 -0
  381. package/dist/utils/thumbnail-listing.js.map +1 -0
  382. package/dist/utils/thumbnail-paths.d.ts +20 -0
  383. package/dist/utils/thumbnail-paths.d.ts.map +1 -0
  384. package/dist/utils/thumbnail-paths.js +35 -0
  385. package/dist/utils/thumbnail-paths.js.map +1 -0
  386. package/dist/utils/thumbnail-store.d.ts +19 -0
  387. package/dist/utils/thumbnail-store.d.ts.map +1 -0
  388. package/dist/utils/thumbnail-store.js +125 -0
  389. package/dist/utils/thumbnail-store.js.map +1 -0
  390. package/dist/utils/transcript-cache.d.ts +20 -0
  391. package/dist/utils/transcript-cache.d.ts.map +1 -0
  392. package/dist/utils/transcript-cache.js +127 -0
  393. package/dist/utils/transcript-cache.js.map +1 -0
  394. package/dist/utils/transcript-search.d.ts +20 -0
  395. package/dist/utils/transcript-search.d.ts.map +1 -0
  396. package/dist/utils/transcript-search.js +35 -0
  397. package/dist/utils/transcript-search.js.map +1 -0
  398. package/dist/utils/transcript.d.ts +10 -31
  399. package/dist/utils/transcript.d.ts.map +1 -1
  400. package/dist/utils/transcript.js +2 -64
  401. package/dist/utils/transcript.js.map +1 -1
  402. package/dist/utils/validate-youtube.d.ts +35 -0
  403. package/dist/utils/validate-youtube.d.ts.map +1 -0
  404. package/dist/utils/validate-youtube.js +87 -0
  405. package/dist/utils/validate-youtube.js.map +1 -0
  406. package/dist/utils/validate.d.ts +11 -4
  407. package/dist/utils/validate.d.ts.map +1 -1
  408. package/dist/utils/validate.js +43 -36
  409. package/dist/utils/validate.js.map +1 -1
  410. package/dist/utils/version.d.ts +2 -0
  411. package/dist/utils/version.d.ts.map +1 -0
  412. package/dist/utils/version.js +19 -0
  413. package/dist/utils/version.js.map +1 -0
  414. package/dist/utils/video-stats-cache.d.ts +16 -0
  415. package/dist/utils/video-stats-cache.d.ts.map +1 -0
  416. package/dist/utils/video-stats-cache.js +81 -0
  417. package/dist/utils/video-stats-cache.js.map +1 -0
  418. package/dist/utils/video-stats.d.ts +186 -0
  419. package/dist/utils/video-stats.d.ts.map +1 -0
  420. package/dist/utils/video-stats.js +168 -0
  421. package/dist/utils/video-stats.js.map +1 -0
  422. package/dist/utils/video-store.d.ts +69 -0
  423. package/dist/utils/video-store.d.ts.map +1 -0
  424. package/dist/utils/video-store.js +77 -0
  425. package/dist/utils/video-store.js.map +1 -0
  426. package/dist/utils/youtube-channel.d.ts +51 -0
  427. package/dist/utils/youtube-channel.d.ts.map +1 -0
  428. package/dist/utils/youtube-channel.js +91 -0
  429. package/dist/utils/youtube-channel.js.map +1 -0
  430. package/dist/utils/youtube-download.d.ts +27 -0
  431. package/dist/utils/youtube-download.d.ts.map +1 -0
  432. package/dist/utils/youtube-download.js +118 -0
  433. package/dist/utils/youtube-download.js.map +1 -0
  434. package/dist/utils/youtube-schemas.d.ts +492 -0
  435. package/dist/utils/youtube-schemas.d.ts.map +1 -0
  436. package/dist/utils/youtube-schemas.js +148 -0
  437. package/dist/utils/youtube-schemas.js.map +1 -0
  438. package/dist/utils/youtube-search.d.ts +14 -0
  439. package/dist/utils/youtube-search.d.ts.map +1 -0
  440. package/dist/utils/youtube-search.js +39 -0
  441. package/dist/utils/youtube-search.js.map +1 -0
  442. package/dist/utils/youtube-url.d.ts +25 -0
  443. package/dist/utils/youtube-url.d.ts.map +1 -0
  444. package/dist/utils/youtube-url.js +75 -0
  445. package/dist/utils/youtube-url.js.map +1 -0
  446. package/dist/utils/youtube-video.d.ts +68 -0
  447. package/dist/utils/youtube-video.d.ts.map +1 -0
  448. package/dist/utils/youtube-video.js +110 -0
  449. package/dist/utils/youtube-video.js.map +1 -0
  450. package/dist/utils/youtube.d.ts +10 -122
  451. package/dist/utils/youtube.d.ts.map +1 -1
  452. package/dist/utils/youtube.js +11 -486
  453. package/dist/utils/youtube.js.map +1 -1
  454. package/dist/utils/ytdlp-env.d.ts +33 -0
  455. package/dist/utils/ytdlp-env.d.ts.map +1 -0
  456. package/dist/utils/ytdlp-env.js +121 -0
  457. package/dist/utils/ytdlp-env.js.map +1 -0
  458. package/dist/utils/ytdlp-limiter.d.ts +27 -0
  459. package/dist/utils/ytdlp-limiter.d.ts.map +1 -0
  460. package/dist/utils/ytdlp-limiter.js +85 -0
  461. package/dist/utils/ytdlp-limiter.js.map +1 -0
  462. package/dist/utils/ytdlp-pacer.d.ts +64 -0
  463. package/dist/utils/ytdlp-pacer.d.ts.map +1 -0
  464. package/dist/utils/ytdlp-pacer.js +165 -0
  465. package/dist/utils/ytdlp-pacer.js.map +1 -0
  466. package/dist/utils/ytdlp-parse.d.ts +13 -0
  467. package/dist/utils/ytdlp-parse.d.ts.map +1 -0
  468. package/dist/utils/ytdlp-parse.js +48 -0
  469. package/dist/utils/ytdlp-parse.js.map +1 -0
  470. package/dist/utils/ytdlp-stream.d.ts +32 -0
  471. package/dist/utils/ytdlp-stream.d.ts.map +1 -0
  472. package/dist/utils/ytdlp-stream.js +61 -0
  473. package/dist/utils/ytdlp-stream.js.map +1 -0
  474. package/dist/utils/ytdlp.d.ts +23 -18
  475. package/dist/utils/ytdlp.d.ts.map +1 -1
  476. package/dist/utils/ytdlp.js +56 -134
  477. package/dist/utils/ytdlp.js.map +1 -1
  478. package/package.json +3 -2
@@ -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"}
@@ -0,0 +1,110 @@
1
+ import { z } from 'zod';
2
+ import { coverageSchema, harvestScopeSchema } from '../harvest-schemas.js';
3
+ import { assertCoverageConsistent } from './coverage.js';
4
+ import { queryRow, queryRows, sqliteBoolean } from './store-rows.js';
5
+ /**
6
+ * Persisting completeness receipts.
7
+ *
8
+ * Written in the same transaction as the rows they describe. That is the whole
9
+ * reason this store is a database: there is no interval in which a receipt can
10
+ * claim something the store does not hold, so the reconciliation dance that
11
+ * `brain-build.ts` performs between its manifest and its chunks has no
12
+ * analogue here.
13
+ */
14
+ const receiptRowSchema = z.object({
15
+ scope: harvestScopeSchema,
16
+ target_id: z.string(),
17
+ state: z.string(),
18
+ reason: z.string(),
19
+ have: z.number(),
20
+ expected: z.number().nullable(),
21
+ expected_source: z.string().nullable(),
22
+ source: z.string(),
23
+ limit_applied: z.number().nullable(),
24
+ sort_applied: z.string().nullable(),
25
+ ran_to_exhaustion: sqliteBoolean.nullable(),
26
+ resume_token: z.string().nullable(),
27
+ started_at: z.number(),
28
+ finished_at: z.number().nullable(),
29
+ attempts: z.number(),
30
+ error_code: z.string().nullable(),
31
+ note: z.string().nullable(),
32
+ });
33
+ const UPSERT = `
34
+ INSERT INTO harvest_receipt (
35
+ scope, target_id, state, reason, have, expected, expected_source, source,
36
+ limit_applied, sort_applied, ran_to_exhaustion, resume_token,
37
+ started_at, finished_at, attempts, error_code, note
38
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
39
+ ON CONFLICT(scope, target_id) DO UPDATE SET
40
+ state = excluded.state, reason = excluded.reason, have = excluded.have,
41
+ expected = excluded.expected, expected_source = excluded.expected_source,
42
+ source = excluded.source, limit_applied = excluded.limit_applied,
43
+ sort_applied = excluded.sort_applied, ran_to_exhaustion = excluded.ran_to_exhaustion,
44
+ resume_token = excluded.resume_token, finished_at = excluded.finished_at,
45
+ attempts = harvest_receipt.attempts + 1, error_code = excluded.error_code,
46
+ note = excluded.note`;
47
+ /** `complete` is the receipt's own word; the rest describe how it ended. */
48
+ function stateOf(coverage) {
49
+ if (coverage.complete)
50
+ return 'complete';
51
+ if (coverage.reason === 'SOURCE_REFUSED' || coverage.reason === 'TIMEOUT')
52
+ return 'failed';
53
+ if (coverage.reason === 'NOT_ATTEMPTED')
54
+ return 'running';
55
+ return 'partial';
56
+ }
57
+ function toRow(coverage) {
58
+ // Re-checked on the way to disk as well as on the way out of coverageOf: a
59
+ // receipt that reaches storage wrong is one that will be believed later.
60
+ assertCoverageConsistent(coverage);
61
+ return coverage;
62
+ }
63
+ export function saveReceipt(database, coverage, options = {}) {
64
+ const checked = toRow(coverage);
65
+ const finishedAt = Date.parse(checked.harvestedAt);
66
+ database
67
+ .prepare(UPSERT)
68
+ .run(checked.scope, checked.targetId, stateOf(checked), checked.reason, checked.have, checked.expected ?? null, checked.expectedSource ?? null, checked.source, checked.limitApplied ?? null, checked.sortApplied ?? null, checked.ranToExhaustion === undefined ? null : Number(checked.ranToExhaustion), checked.resumeToken ?? null, options.startedAt ?? finishedAt, finishedAt, 0, options.errorCode ?? null, checked.note ?? null);
69
+ database
70
+ .prepare('INSERT INTO harvest_event (scope, target_id, at, state, delta, note) VALUES (?,?,?,?,?,?)')
71
+ .run(checked.scope, checked.targetId, finishedAt, stateOf(checked), checked.have, checked.reason);
72
+ }
73
+ function fromRow(row) {
74
+ const expected = row.expected;
75
+ const expectedSource = row.expected_source;
76
+ return coverageSchema.parse({
77
+ scope: row.scope,
78
+ targetId: row.target_id,
79
+ complete: row.state === 'complete',
80
+ reason: row.reason,
81
+ have: row.have,
82
+ ...(expected === null || expectedSource === null ? {} : { expected, expectedSource }),
83
+ ...(row.ran_to_exhaustion === null ? {} : { ranToExhaustion: row.ran_to_exhaustion }),
84
+ source: row.source,
85
+ harvestedAt: new Date(row.finished_at ?? row.started_at).toISOString(),
86
+ ...(row.limit_applied === null ? {} : { limitApplied: row.limit_applied }),
87
+ ...(row.sort_applied === null ? {} : { sortApplied: row.sort_applied }),
88
+ ...(row.resume_token === null ? {} : { resumeToken: row.resume_token }),
89
+ ...(row.note === null ? {} : { note: row.note }),
90
+ });
91
+ }
92
+ export function readReceipt(database, scope, targetId) {
93
+ const row = queryRow(database.prepare('SELECT * FROM harvest_receipt WHERE scope = ? AND target_id = ?'), receiptRowSchema, scope, targetId);
94
+ return row === undefined ? undefined : fromRow(row);
95
+ }
96
+ export function listReceipts(database, options = {}) {
97
+ const limit = options.limit ?? 50;
98
+ const offset = options.offset ?? 0;
99
+ const rows = options.scope === undefined
100
+ ? queryRows(database.prepare('SELECT * FROM harvest_receipt ORDER BY finished_at DESC, target_id LIMIT ? OFFSET ?'), receiptRowSchema, limit, offset)
101
+ : queryRows(database.prepare('SELECT * FROM harvest_receipt WHERE scope = ? ORDER BY finished_at DESC, target_id LIMIT ? OFFSET ?'), receiptRowSchema, options.scope, limit, offset);
102
+ return rows.map(fromRow);
103
+ }
104
+ export function countReceipts(database, scope) {
105
+ const row = scope === undefined
106
+ ? queryRow(database.prepare('SELECT COUNT(*) AS n FROM harvest_receipt'), z.object({ n: z.number() }))
107
+ : queryRow(database.prepare('SELECT COUNT(*) AS n FROM harvest_receipt WHERE scope = ?'), z.object({ n: z.number() }), scope);
108
+ return row?.n ?? 0;
109
+ }
110
+ //# sourceMappingURL=harvest-receipts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-receipts.js","sourceRoot":"","sources":["../../src/utils/harvest-receipts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Reading an image's format and size from its first bytes.
3
+ *
4
+ * The manifest a thumbnail job writes records what was actually saved — a
5
+ * JPEG of 1280 by 720, say — and it must not take that from the URL's name or
6
+ * the server's Content-Type, both of which describe what was asked for rather
7
+ * than what arrived. Three container formats cover what YouTube's image hosts
8
+ * serve; anything else is reported as unknown rather than guessed.
9
+ */
10
+ export type ImageFormat = 'jpg' | 'png' | 'webp';
11
+ export interface ImageProbe {
12
+ format: ImageFormat;
13
+ width: number;
14
+ height: number;
15
+ mimeType: string;
16
+ }
17
+ /** The format and pixel size of an image, or `undefined` when the bytes are not one this reads. */
18
+ export declare function probeImage(bytes: Uint8Array): ImageProbe | undefined;
19
+ //# sourceMappingURL=image-dimensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-dimensions.d.ts","sourceRoot":"","sources":["../../src/utils/image-dimensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AA4FD,mGAAmG;AACnG,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAKpE"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Reading an image's format and size from its first bytes.
3
+ *
4
+ * The manifest a thumbnail job writes records what was actually saved — a
5
+ * JPEG of 1280 by 720, say — and it must not take that from the URL's name or
6
+ * the server's Content-Type, both of which describe what was asked for rather
7
+ * than what arrived. Three container formats cover what YouTube's image hosts
8
+ * serve; anything else is reported as unknown rather than guessed.
9
+ */
10
+ const JPEG_SIGNATURE = [0xff, 0xd8, 0xff];
11
+ const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
12
+ function startsWith(bytes, signature, at = 0) {
13
+ return signature.every((byte, index) => bytes[at + index] === byte);
14
+ }
15
+ function ascii(bytes, from, length) {
16
+ return String.fromCharCode(...bytes.subarray(from, from + length));
17
+ }
18
+ /** Every caller checks the length first, so a read here is always in range. */
19
+ function view(bytes) {
20
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
21
+ }
22
+ function be16(bytes, at) {
23
+ return view(bytes).getUint16(at);
24
+ }
25
+ function be32(bytes, at) {
26
+ return view(bytes).getUint32(at);
27
+ }
28
+ function le16(bytes, at) {
29
+ return view(bytes).getUint16(at, true);
30
+ }
31
+ function le24(bytes, at) {
32
+ return le16(bytes, at) | (view(bytes).getUint8(at + 2) << 16);
33
+ }
34
+ /** Start-of-frame markers carry the dimensions; C4, C8 and CC are other things. */
35
+ function isStartOfFrame(marker) {
36
+ return marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
37
+ }
38
+ function probeJpeg(bytes) {
39
+ let at = 2;
40
+ while (at + 9 < bytes.length) {
41
+ if (bytes[at] !== 0xff)
42
+ return undefined;
43
+ const marker = view(bytes).getUint8(at + 1);
44
+ // Standalone markers have no length field.
45
+ if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
46
+ at += 2;
47
+ continue;
48
+ }
49
+ // Scan data or end of image: no frame header was found before it.
50
+ if (marker === 0xda || marker === 0xd9)
51
+ return undefined;
52
+ if (isStartOfFrame(marker)) {
53
+ return sized('jpg', 'image/jpeg', be16(bytes, at + 7), be16(bytes, at + 5));
54
+ }
55
+ at += 2 + be16(bytes, at + 2);
56
+ }
57
+ return undefined;
58
+ }
59
+ function probePng(bytes) {
60
+ if (bytes.length < 24 || ascii(bytes, 12, 4) !== 'IHDR')
61
+ return undefined;
62
+ return sized('png', 'image/png', be32(bytes, 16), be32(bytes, 20));
63
+ }
64
+ function probeWebp(bytes) {
65
+ if (bytes.length < 30 || ascii(bytes, 8, 4) !== 'WEBP')
66
+ return undefined;
67
+ switch (ascii(bytes, 12, 4)) {
68
+ case 'VP8 ':
69
+ return sized('webp', 'image/webp', le16(bytes, 26) & 0x3fff, le16(bytes, 28) & 0x3fff);
70
+ case 'VP8L': {
71
+ const bits = view(bytes).getUint32(21, true);
72
+ return sized('webp', 'image/webp', (bits & 0x3fff) + 1, ((bits >>> 14) & 0x3fff) + 1);
73
+ }
74
+ case 'VP8X':
75
+ return sized('webp', 'image/webp', le24(bytes, 24) + 1, le24(bytes, 27) + 1);
76
+ default:
77
+ return undefined;
78
+ }
79
+ }
80
+ function sized(format, mimeType, width, height) {
81
+ return width > 0 && height > 0 ? { format, mimeType, width, height } : undefined;
82
+ }
83
+ /** The format and pixel size of an image, or `undefined` when the bytes are not one this reads. */
84
+ export function probeImage(bytes) {
85
+ if (startsWith(bytes, JPEG_SIGNATURE))
86
+ return probeJpeg(bytes);
87
+ if (startsWith(bytes, PNG_SIGNATURE))
88
+ return probePng(bytes);
89
+ if (bytes.length >= 12 && ascii(bytes, 0, 4) === 'RIFF')
90
+ return probeWebp(bytes);
91
+ return undefined;
92
+ }
93
+ //# sourceMappingURL=image-dimensions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-dimensions.js","sourceRoot":"","sources":["../../src/utils/image-dimensions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The one place this server fetches anything over HTTP itself.
3
+ *
4
+ * Everything else goes through yt-dlp. Thumbnails are the exception because
5
+ * their URLs are known from a single channel listing, and asking yt-dlp to
6
+ * extract each video just to download its image would multiply the requests
7
+ * YouTube sees by the size of the channel. The fetch is deliberately narrow:
8
+ * an allowlist of image hosts, https only, redirects re-checked hop by hop, a
9
+ * timeout, a byte cap enforced while reading, and an image content type.
10
+ */
11
+ export declare const IMAGE_HOSTS: readonly ["i.ytimg.com", "yt3.googleusercontent.com", "yt3.ggpht.com"];
12
+ export declare const IMAGE_TIMEOUT_MS = 20000;
13
+ /** Cap for images written to disk. */
14
+ export declare const MAX_IMAGE_BYTES: number;
15
+ /** Cap for images returned inline as base64; a 1280x720 JPEG is about 140 KB. */
16
+ export declare const MAX_INLINE_IMAGE_BYTES: number;
17
+ export interface FetchImageOptions {
18
+ maxBytes?: number;
19
+ timeoutMs?: number;
20
+ }
21
+ export interface FetchedImage {
22
+ bytes: Buffer;
23
+ contentType: string;
24
+ /** The URL the bytes came from, after any redirects. */
25
+ url: string;
26
+ }
27
+ /** Validate an image URL against the allowlist, returning it parsed. */
28
+ export declare function assertImageUrl(input: string): URL;
29
+ /** Fetch one image into memory, or throw a typed error saying why not. */
30
+ export declare function fetchImage(input: string, options?: FetchImageOptions): Promise<FetchedImage>;
31
+ //# sourceMappingURL=image-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-fetch.d.ts","sourceRoot":"","sources":["../../src/utils/image-fetch.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AAEH,eAAO,MAAM,WAAW,wEAAyE,CAAC;AAClG,eAAO,MAAM,gBAAgB,QAAS,CAAC;AACvC,sCAAsC;AACtC,eAAO,MAAM,eAAe,QAAkB,CAAC;AAC/C,iFAAiF;AACjF,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AAKtD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAcjD;AAgBD,0EAA0E;AAC1E,wBAAsB,UAAU,CAC9B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,YAAY,CAAC,CA0CvB"}
@@ -0,0 +1,141 @@
1
+ import { currentContext } from './context.js';
2
+ import { YouTubeError } from './errors.js';
3
+ /**
4
+ * The one place this server fetches anything over HTTP itself.
5
+ *
6
+ * Everything else goes through yt-dlp. Thumbnails are the exception because
7
+ * their URLs are known from a single channel listing, and asking yt-dlp to
8
+ * extract each video just to download its image would multiply the requests
9
+ * YouTube sees by the size of the channel. The fetch is deliberately narrow:
10
+ * an allowlist of image hosts, https only, redirects re-checked hop by hop, a
11
+ * timeout, a byte cap enforced while reading, and an image content type.
12
+ */
13
+ export const IMAGE_HOSTS = ['i.ytimg.com', 'yt3.googleusercontent.com', 'yt3.ggpht.com'];
14
+ export const IMAGE_TIMEOUT_MS = 20_000;
15
+ /** Cap for images written to disk. */
16
+ export const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
17
+ /** Cap for images returned inline as base64; a 1280x720 JPEG is about 140 KB. */
18
+ export const MAX_INLINE_IMAGE_BYTES = 2 * 1024 * 1024;
19
+ const MAX_REDIRECTS = 3;
20
+ const HOST_SET = new Set(IMAGE_HOSTS);
21
+ /** Validate an image URL against the allowlist, returning it parsed. */
22
+ export function assertImageUrl(input) {
23
+ let url;
24
+ try {
25
+ url = new URL(input);
26
+ }
27
+ catch {
28
+ throw notAllowed(input);
29
+ }
30
+ const allowed = url.protocol === 'https:' &&
31
+ HOST_SET.has(url.hostname.toLowerCase()) &&
32
+ url.username === '' &&
33
+ url.password === '';
34
+ if (!allowed)
35
+ throw notAllowed(input);
36
+ return url;
37
+ }
38
+ function notAllowed(input) {
39
+ const shown = input.length > 96 ? `${input.slice(0, 93)}...` : input;
40
+ return new YouTubeError('INVALID_INPUT', `"${shown}" is not a YouTube image URL.`, {
41
+ nextStep: `Images are only fetched over https from ${IMAGE_HOSTS.join(', ')}.`,
42
+ });
43
+ }
44
+ function failed(message, cause) {
45
+ return new YouTubeError('FETCH_FAILED', message, {
46
+ nextStep: 'Retry later; if it keeps failing, the image may have been removed.',
47
+ cause,
48
+ });
49
+ }
50
+ /** Fetch one image into memory, or throw a typed error saying why not. */
51
+ export async function fetchImage(input, options = {}) {
52
+ const { maxBytes = MAX_IMAGE_BYTES, timeoutMs = IMAGE_TIMEOUT_MS } = options;
53
+ const request = currentContext().signal;
54
+ const timeout = AbortSignal.timeout(timeoutMs);
55
+ const signal = request === undefined ? timeout : AbortSignal.any([timeout, request]);
56
+ let url = assertImageUrl(input);
57
+ for (let hop = 0;; hop++) {
58
+ const response = await send(url, signal, request);
59
+ if (response.status >= 300 && response.status < 400) {
60
+ const location = response.headers.get('location');
61
+ if (location === null || hop >= MAX_REDIRECTS)
62
+ throw failed('The image URL redirected too far.');
63
+ // Re-validated: a redirect is a URL the server chose, not the caller.
64
+ url = assertImageUrl(new URL(location, url).toString());
65
+ continue;
66
+ }
67
+ if (response.status === 404)
68
+ throw new YouTubeError('NOT_FOUND', 'There is no image at that size.');
69
+ if (response.status === 429) {
70
+ throw new YouTubeError('RATE_LIMITED', 'YouTube is rate limiting image downloads.', {
71
+ nextStep: 'Wait a minute before retrying.',
72
+ retryable: true,
73
+ });
74
+ }
75
+ if (!response.ok)
76
+ throw failed(`The image host answered ${response.status}.`);
77
+ const contentType = mediaType(response.headers.get('content-type') ?? '');
78
+ if (!contentType.startsWith('image/'))
79
+ throw failed('The response was not an image.');
80
+ const declared = Number(response.headers.get('content-length') ?? '0');
81
+ if (declared > maxBytes)
82
+ throw failed(`The image is larger than ${maxBytes} bytes.`);
83
+ return {
84
+ bytes: await readCapped(response, maxBytes, signal, request),
85
+ contentType,
86
+ url: url.toString(),
87
+ };
88
+ }
89
+ }
90
+ async function send(url, signal, request) {
91
+ try {
92
+ return await fetch(url, { redirect: 'manual', signal });
93
+ }
94
+ catch (error) {
95
+ throw translate(error, request);
96
+ }
97
+ }
98
+ /** The media type of a Content-Type header, without its parameters. */
99
+ function mediaType(header) {
100
+ const semicolon = header.indexOf(';');
101
+ return header.slice(0, semicolon === -1 ? header.length : semicolon).trim();
102
+ }
103
+ /** Read the body, refusing to go past the cap whatever Content-Length claimed. */
104
+ async function readCapped(response, maxBytes, signal, request) {
105
+ if (response.body === null)
106
+ return Buffer.alloc(0);
107
+ // A reader rather than `for await`: the cap has to cancel the stream from
108
+ // inside the loop, and the async iterator holds the lock that cancel needs.
109
+ const reader = response.body.getReader();
110
+ const chunks = [];
111
+ let total = 0;
112
+ try {
113
+ for (let next = await reader.read(); !next.done; next = await reader.read()) {
114
+ total += next.value.byteLength;
115
+ if (total > maxBytes) {
116
+ await reader.cancel();
117
+ throw failed(`The image is larger than ${maxBytes} bytes.`);
118
+ }
119
+ chunks.push(next.value);
120
+ }
121
+ }
122
+ catch (error) {
123
+ if (error instanceof YouTubeError)
124
+ throw error;
125
+ throw translate(error, request, signal);
126
+ }
127
+ return Buffer.concat(chunks);
128
+ }
129
+ function translate(error, request, signal) {
130
+ if (request?.aborted === true)
131
+ return new YouTubeError('CANCELLED', 'The request was cancelled.');
132
+ const timedOut = (error instanceof Error && error.name === 'TimeoutError') || signal?.aborted === true;
133
+ if (timedOut) {
134
+ return new YouTubeError('TIMEOUT', 'The image host did not respond in time.', {
135
+ nextStep: 'Retry; the host may be slow.',
136
+ retryable: true,
137
+ });
138
+ }
139
+ return failed('The image could not be downloaded.', error);
140
+ }
141
+ //# sourceMappingURL=image-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-fetch.js","sourceRoot":"","sources":["../../src/utils/image-fetch.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import type { z } from 'zod';
2
2
  /**
3
- * Reading and writing the JSON documents this server keeps on disk.
3
+ * Reading and writing the documents this server keeps on disk.
4
4
  *
5
5
  * Both halves exist because the library and the brains make the same two
6
6
  * mistakes otherwise. Writing straight to the destination means an interrupted
@@ -10,20 +10,29 @@ import type { z } from 'zod';
10
10
  * file edited by hand, or truncated by that same crash, propagates `undefined`
11
11
  * into code that assumed a shape.
12
12
  */
13
- export interface WriteJsonOptions {
13
+ export interface WriteFileOptions {
14
+ /** Permission bits for the new file. Owner-only by default: these are the user's notes and caches. */
15
+ mode?: number;
16
+ }
17
+ export interface WriteJsonOptions extends WriteFileOptions {
14
18
  /**
15
19
  * Indent the output. Worth it for files a human may open; not for a search
16
20
  * index, where indentation is most of the bytes.
17
21
  */
18
22
  pretty?: boolean;
19
23
  }
24
+ export declare const PRIVATE_FILE_MODE = 384;
20
25
  /**
21
- * Write JSON through a temporary file and rename it into place.
26
+ * Write a file through a temporary name and rename it into place.
22
27
  *
28
+ * The data is flushed to disk before the rename: without `fsync` a power loss
29
+ * after the rename could leave a zero-length file under the final name, which
30
+ * is exactly the half-written document the rename was meant to rule out.
23
31
  * `rename` within a directory is atomic on POSIX and on Windows via
24
- * `MoveFileEx`, so a reader sees either the previous document or the new one,
25
- * never a partial write.
32
+ * `MoveFileEx`, so a reader sees either the previous document or the new one.
26
33
  */
34
+ export declare function writeFileAtomic(path: string, data: string | Uint8Array, options?: WriteFileOptions): Promise<void>;
35
+ /** Serialise and write a JSON document atomically. */
27
36
  export declare function writeJsonAtomic(path: string, value: unknown, options?: WriteJsonOptions): Promise<void>;
28
37
  /**
29
38
  * Read and validate a JSON document, or `undefined` if it is missing,
@@ -1 +1 @@
1
- {"version":3,"file":"json-file.d.ts","sourceRoot":"","sources":["../../src/utils/json-file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAgBf;AAID;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,EAAE,OAAO,CAAC,GAC1C,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CASxB"}
1
+ {"version":3,"file":"json-file.d.ts","sourceRoot":"","sources":["../../src/utils/json-file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,sGAAsG;IACtG,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;IACxD;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,iBAAiB,MAAQ,CAAC;AAIvC;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED,sDAAsD;AACtD,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAIf;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,EAAE,OAAO,CAAC,GAC1C,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CASxB"}
@@ -1,21 +1,31 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { open, readFile, rename, rm } from 'node:fs/promises';
3
+ export const PRIVATE_FILE_MODE = 0o600;
4
+ let writeSequence = 0;
3
5
  /**
4
- * Write JSON through a temporary file and rename it into place.
6
+ * Write a file through a temporary name and rename it into place.
5
7
  *
8
+ * The data is flushed to disk before the rename: without `fsync` a power loss
9
+ * after the rename could leave a zero-length file under the final name, which
10
+ * is exactly the half-written document the rename was meant to rule out.
6
11
  * `rename` within a directory is atomic on POSIX and on Windows via
7
- * `MoveFileEx`, so a reader sees either the previous document or the new one,
8
- * never a partial write.
12
+ * `MoveFileEx`, so a reader sees either the previous document or the new one.
9
13
  */
10
- export async function writeJsonAtomic(path, value, options = {}) {
11
- const { pretty = true } = options;
12
- const serialised = JSON.stringify(value, null, pretty ? 2 : undefined);
14
+ export async function writeFileAtomic(path, data, options = {}) {
15
+ const { mode = PRIVATE_FILE_MODE } = options;
13
16
  // A shared temporary name would let two writers of the same document destroy
14
17
  // each other's staging file, and the loser's rename would fail on a path that
15
18
  // the winner had already moved away.
16
19
  const temporaryPath = `${path}.${process.pid}.${writeSequence++}.tmp`;
17
20
  try {
18
- await writeFile(temporaryPath, serialised, 'utf-8');
21
+ const handle = await open(temporaryPath, 'w', mode);
22
+ try {
23
+ await handle.writeFile(data);
24
+ await handle.sync();
25
+ }
26
+ finally {
27
+ await handle.close();
28
+ }
19
29
  await rename(temporaryPath, path);
20
30
  }
21
31
  catch (error) {
@@ -23,7 +33,12 @@ export async function writeJsonAtomic(path, value, options = {}) {
23
33
  throw error;
24
34
  }
25
35
  }
26
- let writeSequence = 0;
36
+ /** Serialise and write a JSON document atomically. */
37
+ export async function writeJsonAtomic(path, value, options = {}) {
38
+ const { pretty = true, ...fileOptions } = options;
39
+ const serialised = JSON.stringify(value, null, pretty ? 2 : undefined);
40
+ await writeFileAtomic(path, serialised, fileOptions);
41
+ }
27
42
  /**
28
43
  * Read and validate a JSON document, or `undefined` if it is missing,
29
44
  * unparseable or does not match.
@@ -1 +1 @@
1
- {"version":3,"file":"json-file.js","sourceRoot":"","sources":["../../src/utils/json-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAuBnE;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAY,EACZ,KAAc,EACd,UAA4B,EAAE;IAE9B,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAClC,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAEvE,6EAA6E;IAC7E,8EAA8E;IAC9E,qCAAqC;IACrC,MAAM,aAAa,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,aAAa,EAAE,MAAM,CAAC;IAEtE,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,aAAa,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;QACpD,MAAM,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,EAAE,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,IAAI,aAAa,GAAG,CAAC,CAAC;AAEtB;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAY,EACZ,MAA2C;IAE3C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAExC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAC3E,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"json-file.js","sourceRoot":"","sources":["../../src/utils/json-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AA4B9D,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEvC,IAAI,aAAa,GAAG,CAAC,CAAC;AAEtB;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAY,EACZ,IAAyB,EACzB,UAA4B,EAAE;IAE9B,MAAM,EAAE,IAAI,GAAG,iBAAiB,EAAE,GAAG,OAAO,CAAC;IAE7C,6EAA6E;IAC7E,8EAA8E;IAC9E,qCAAqC;IACrC,MAAM,aAAa,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,aAAa,EAAE,MAAM,CAAC;IAEtE,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YAC7B,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACtB,CAAC;gBAAS,CAAC;YACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;QACD,MAAM,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,EAAE,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,sDAAsD;AACtD,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAY,EACZ,KAAc,EACd,UAA4B,EAAE;IAE9B,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,CAAC;IAClD,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACvE,MAAM,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;AACvD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAY,EACZ,MAA2C;IAE3C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAExC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QAC3E,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The cursor `fetch_videos` hands back.
4
+ *
5
+ * YouTube's listings are continuation-paginated, so `--playlist-items 101:120`
6
+ * still walks from the start: paging server-side is O(n) per page and O(n^2)
7
+ * overall. It is also not a cursor in any useful sense — a new upload shifts
8
+ * every index by one, so an offset silently re-reads or skips at the seam.
9
+ *
10
+ * So the cursor carries the next index *and* the id of the last item already
11
+ * returned. If that id reappears on the next page the listing moved under us,
12
+ * and the duplicate is dropped rather than handed over twice.
13
+ */
14
+ declare const cursorSchema: z.ZodObject<{
15
+ v: z.ZodLiteral<1>;
16
+ start: z.ZodNumber;
17
+ anchor: z.ZodString;
18
+ }, "strip", z.ZodTypeAny, {
19
+ anchor: string;
20
+ start: number;
21
+ v: 1;
22
+ }, {
23
+ anchor: string;
24
+ start: number;
25
+ v: 1;
26
+ }>;
27
+ export type ListingCursor = z.infer<typeof cursorSchema>;
28
+ export declare function encodeCursor(cursor: ListingCursor): string;
29
+ export declare function decodeCursor(token: string): ListingCursor;
30
+ export {};
31
+ //# sourceMappingURL=listing-cursor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listing-cursor.d.ts","sourceRoot":"","sources":["../../src/utils/listing-cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;GAWG;AAEH,QAAA,MAAM,YAAY;;;;;;;;;;;;EAIhB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAEzD,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAE1D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAezD"}