youtube-knowledge-mcp 2.2.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 (192) hide show
  1. package/README.md +62 -18
  2. package/dist/harvest-schemas.d.ts +75 -0
  3. package/dist/harvest-schemas.d.ts.map +1 -0
  4. package/dist/harvest-schemas.js +106 -0
  5. package/dist/harvest-schemas.js.map +1 -0
  6. package/dist/registry/harvest-tools.d.ts +10 -0
  7. package/dist/registry/harvest-tools.d.ts.map +1 -0
  8. package/dist/registry/harvest-tools.js +109 -0
  9. package/dist/registry/harvest-tools.js.map +1 -0
  10. package/dist/registry/health-tools.d.ts.map +1 -1
  11. package/dist/registry/health-tools.js +18 -0
  12. package/dist/registry/health-tools.js.map +1 -1
  13. package/dist/registry/index.d.ts.map +1 -1
  14. package/dist/registry/index.js +2 -0
  15. package/dist/registry/index.js.map +1 -1
  16. package/dist/registry/video-tools.js +1 -1
  17. package/dist/registry/video-tools.js.map +1 -1
  18. package/dist/schemas.d.ts +239 -0
  19. package/dist/schemas.d.ts.map +1 -1
  20. package/dist/schemas.js +56 -0
  21. package/dist/schemas.js.map +1 -1
  22. package/dist/thumbnail-schemas.d.ts +28 -28
  23. package/dist/tools/ask-brain.js +1 -1
  24. package/dist/tools/ask-brain.js.map +1 -1
  25. package/dist/tools/brain-info.js +1 -1
  26. package/dist/tools/brain-info.js.map +1 -1
  27. package/dist/tools/check-health.d.ts +59 -0
  28. package/dist/tools/check-health.d.ts.map +1 -1
  29. package/dist/tools/check-health.js +85 -6
  30. package/dist/tools/check-health.js.map +1 -1
  31. package/dist/tools/fetch-channel-thumbnails.d.ts +8 -8
  32. package/dist/tools/fetch-videos.d.ts +15 -4
  33. package/dist/tools/fetch-videos.d.ts.map +1 -1
  34. package/dist/tools/fetch-videos.js +63 -22
  35. package/dist/tools/fetch-videos.js.map +1 -1
  36. package/dist/tools/get-comments.d.ts +238 -6
  37. package/dist/tools/get-comments.d.ts.map +1 -1
  38. package/dist/tools/get-comments.js +85 -21
  39. package/dist/tools/get-comments.js.map +1 -1
  40. package/dist/tools/get-coverage.d.ts +122 -0
  41. package/dist/tools/get-coverage.d.ts.map +1 -0
  42. package/dist/tools/get-coverage.js +105 -0
  43. package/dist/tools/get-coverage.js.map +1 -0
  44. package/dist/tools/harvest-channel.d.ts +92 -0
  45. package/dist/tools/harvest-channel.d.ts.map +1 -0
  46. package/dist/tools/harvest-channel.js +62 -0
  47. package/dist/tools/harvest-channel.js.map +1 -0
  48. package/dist/tools/harvest-comments.d.ts +81 -0
  49. package/dist/tools/harvest-comments.d.ts.map +1 -0
  50. package/dist/tools/harvest-comments.js +115 -0
  51. package/dist/tools/harvest-comments.js.map +1 -0
  52. package/dist/tools/library.js +1 -1
  53. package/dist/tools/library.js.map +1 -1
  54. package/dist/tools/list-channel-thumbnails.d.ts +10 -10
  55. package/dist/tools/list-channel-thumbnails.js +1 -1
  56. package/dist/tools/list-channel-thumbnails.js.map +1 -1
  57. package/dist/tools/list-library.js +1 -1
  58. package/dist/tools/list-library.js.map +1 -1
  59. package/dist/tools/prune-harvest.d.ts +59 -0
  60. package/dist/tools/prune-harvest.d.ts.map +1 -0
  61. package/dist/tools/prune-harvest.js +108 -0
  62. package/dist/tools/prune-harvest.js.map +1 -0
  63. package/dist/tools/query-comments.d.ts +125 -0
  64. package/dist/tools/query-comments.d.ts.map +1 -0
  65. package/dist/tools/query-comments.js +106 -0
  66. package/dist/tools/query-comments.js.map +1 -0
  67. package/dist/tools/query-videos.d.ts +118 -0
  68. package/dist/tools/query-videos.d.ts.map +1 -0
  69. package/dist/tools/query-videos.js +97 -0
  70. package/dist/tools/query-videos.js.map +1 -0
  71. package/dist/tools/repair-store.d.ts +34 -0
  72. package/dist/tools/repair-store.d.ts.map +1 -0
  73. package/dist/tools/repair-store.js +58 -0
  74. package/dist/tools/repair-store.js.map +1 -0
  75. package/dist/tools/search-channels.d.ts.map +1 -1
  76. package/dist/tools/search-channels.js +5 -1
  77. package/dist/tools/search-channels.js.map +1 -1
  78. package/dist/tools/search-videos.js +1 -1
  79. package/dist/tools/search-videos.js.map +1 -1
  80. package/dist/utils/comment-store.d.ts +71 -0
  81. package/dist/utils/comment-store.d.ts.map +1 -0
  82. package/dist/utils/comment-store.js +129 -0
  83. package/dist/utils/comment-store.js.map +1 -0
  84. package/dist/utils/comment-threads.d.ts +51 -0
  85. package/dist/utils/comment-threads.d.ts.map +1 -0
  86. package/dist/utils/comment-threads.js +76 -0
  87. package/dist/utils/comment-threads.js.map +1 -0
  88. package/dist/utils/coverage-text.d.ts +3 -0
  89. package/dist/utils/coverage-text.d.ts.map +1 -0
  90. package/dist/utils/coverage-text.js +62 -0
  91. package/dist/utils/coverage-text.js.map +1 -0
  92. package/dist/utils/coverage.d.ts +57 -0
  93. package/dist/utils/coverage.d.ts.map +1 -0
  94. package/dist/utils/coverage.js +142 -0
  95. package/dist/utils/coverage.js.map +1 -0
  96. package/dist/utils/errors.d.ts +1 -1
  97. package/dist/utils/errors.d.ts.map +1 -1
  98. package/dist/utils/errors.js +9 -1
  99. package/dist/utils/errors.js.map +1 -1
  100. package/dist/utils/format.d.ts +17 -1
  101. package/dist/utils/format.d.ts.map +1 -1
  102. package/dist/utils/format.js +1 -1
  103. package/dist/utils/format.js.map +1 -1
  104. package/dist/utils/harvest-catalog.d.ts +44 -0
  105. package/dist/utils/harvest-catalog.d.ts.map +1 -0
  106. package/dist/utils/harvest-catalog.js +164 -0
  107. package/dist/utils/harvest-catalog.js.map +1 -0
  108. package/dist/utils/harvest-lock.d.ts +3 -0
  109. package/dist/utils/harvest-lock.d.ts.map +1 -0
  110. package/dist/utils/harvest-lock.js +26 -0
  111. package/dist/utils/harvest-lock.js.map +1 -0
  112. package/dist/utils/harvest-receipts.d.ts +14 -0
  113. package/dist/utils/harvest-receipts.d.ts.map +1 -0
  114. package/dist/utils/harvest-receipts.js +110 -0
  115. package/dist/utils/harvest-receipts.js.map +1 -0
  116. package/dist/utils/listing-cursor.d.ts +31 -0
  117. package/dist/utils/listing-cursor.d.ts.map +1 -0
  118. package/dist/utils/listing-cursor.js +39 -0
  119. package/dist/utils/listing-cursor.js.map +1 -0
  120. package/dist/utils/pot-preflight.d.ts +53 -0
  121. package/dist/utils/pot-preflight.d.ts.map +1 -0
  122. package/dist/utils/pot-preflight.js +146 -0
  123. package/dist/utils/pot-preflight.js.map +1 -0
  124. package/dist/utils/sleep.d.ts +9 -0
  125. package/dist/utils/sleep.d.ts.map +1 -0
  126. package/dist/utils/sleep.js +25 -0
  127. package/dist/utils/sleep.js.map +1 -0
  128. package/dist/utils/store-health.d.ts +37 -0
  129. package/dist/utils/store-health.d.ts.map +1 -0
  130. package/dist/utils/store-health.js +92 -0
  131. package/dist/utils/store-health.js.map +1 -0
  132. package/dist/utils/store-migrations.d.ts +33 -0
  133. package/dist/utils/store-migrations.d.ts.map +1 -0
  134. package/dist/utils/store-migrations.js +41 -0
  135. package/dist/utils/store-migrations.js.map +1 -0
  136. package/dist/utils/store-paths.d.ts +33 -0
  137. package/dist/utils/store-paths.d.ts.map +1 -0
  138. package/dist/utils/store-paths.js +48 -0
  139. package/dist/utils/store-paths.js.map +1 -0
  140. package/dist/utils/store-rows.d.ts +22 -0
  141. package/dist/utils/store-rows.d.ts.map +1 -0
  142. package/dist/utils/store-rows.js +62 -0
  143. package/dist/utils/store-rows.js.map +1 -0
  144. package/dist/utils/store-schema.d.ts +16 -0
  145. package/dist/utils/store-schema.d.ts.map +1 -0
  146. package/dist/utils/store-schema.js +168 -0
  147. package/dist/utils/store-schema.js.map +1 -0
  148. package/dist/utils/store.d.ts +34 -0
  149. package/dist/utils/store.d.ts.map +1 -0
  150. package/dist/utils/store.js +157 -0
  151. package/dist/utils/store.js.map +1 -0
  152. package/dist/utils/video-stats-cache.d.ts +16 -0
  153. package/dist/utils/video-stats-cache.d.ts.map +1 -0
  154. package/dist/utils/video-stats-cache.js +81 -0
  155. package/dist/utils/video-stats-cache.js.map +1 -0
  156. package/dist/utils/video-stats.d.ts +186 -0
  157. package/dist/utils/video-stats.d.ts.map +1 -0
  158. package/dist/utils/video-stats.js +168 -0
  159. package/dist/utils/video-stats.js.map +1 -0
  160. package/dist/utils/video-store.d.ts +69 -0
  161. package/dist/utils/video-store.d.ts.map +1 -0
  162. package/dist/utils/video-store.js +77 -0
  163. package/dist/utils/video-store.js.map +1 -0
  164. package/dist/utils/youtube-channel.d.ts +9 -2
  165. package/dist/utils/youtube-channel.d.ts.map +1 -1
  166. package/dist/utils/youtube-channel.js +33 -3
  167. package/dist/utils/youtube-channel.js.map +1 -1
  168. package/dist/utils/youtube-schemas.d.ts +98 -0
  169. package/dist/utils/youtube-schemas.d.ts.map +1 -1
  170. package/dist/utils/youtube-schemas.js +30 -0
  171. package/dist/utils/youtube-schemas.js.map +1 -1
  172. package/dist/utils/youtube-video.d.ts +22 -1
  173. package/dist/utils/youtube-video.d.ts.map +1 -1
  174. package/dist/utils/youtube-video.js +40 -28
  175. package/dist/utils/youtube-video.js.map +1 -1
  176. package/dist/utils/ytdlp-limiter.d.ts +3 -0
  177. package/dist/utils/ytdlp-limiter.d.ts.map +1 -1
  178. package/dist/utils/ytdlp-limiter.js +19 -3
  179. package/dist/utils/ytdlp-limiter.js.map +1 -1
  180. package/dist/utils/ytdlp-pacer.d.ts +64 -0
  181. package/dist/utils/ytdlp-pacer.d.ts.map +1 -0
  182. package/dist/utils/ytdlp-pacer.js +165 -0
  183. package/dist/utils/ytdlp-pacer.js.map +1 -0
  184. package/dist/utils/ytdlp-stream.d.ts +32 -0
  185. package/dist/utils/ytdlp-stream.d.ts.map +1 -0
  186. package/dist/utils/ytdlp-stream.js +61 -0
  187. package/dist/utils/ytdlp-stream.js.map +1 -0
  188. package/dist/utils/ytdlp.d.ts +10 -0
  189. package/dist/utils/ytdlp.d.ts.map +1 -1
  190. package/dist/utils/ytdlp.js +36 -25
  191. package/dist/utils/ytdlp.js.map +1 -1
  192. package/package.json +2 -2
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ import { toolResult } from '../utils/format.js';
3
+ import { closeStore, getStore, quarantineStore } from '../utils/store.js';
4
+ import { readStoreHealth } from '../utils/store-health.js';
5
+ export const repairStoreSchema = {
6
+ confirm: z
7
+ .literal(true)
8
+ .describe('Must be true. If the store is damaged this moves it aside and starts an empty one; ' +
9
+ 'harvested comments cost hours of network and cannot be recovered from any local cache.'),
10
+ };
11
+ export const repairStoreOutputSchema = {
12
+ wasCorrupt: z.boolean(),
13
+ integrity: z.enum(['ok', 'failed', 'unchecked']),
14
+ movedTo: z
15
+ .string()
16
+ .optional()
17
+ .describe('Where the damaged file was moved. Absent if it was healthy.'),
18
+ lost: z.object({
19
+ channels: z.number().int(),
20
+ videos: z.number().int(),
21
+ comments: z.number().int(),
22
+ }),
23
+ storePath: z.string(),
24
+ };
25
+ /**
26
+ * Moves a damaged store aside and starts a fresh one.
27
+ *
28
+ * Deliberately not automatic. `search-index.json` is rebuilt from disk without
29
+ * asking because it is derivable; this is not — it is the only copy of data
30
+ * that took hours of network to collect. So the damaged file is renamed rather
31
+ * than deleted, and the report says what was in it.
32
+ */
33
+ export async function repairStoreHandler() {
34
+ const before = await readStoreHealth();
35
+ if (before.integrity === 'ok') {
36
+ return toolResult(`Store is healthy (quick_check ok): ${String(before.videos)} videos, ${String(before.comments)} comments. Nothing to repair.`, {
37
+ wasCorrupt: false,
38
+ integrity: before.integrity,
39
+ lost: { channels: before.channels, videos: before.videos, comments: before.comments },
40
+ storePath: before.path,
41
+ });
42
+ }
43
+ const movedTo = await quarantineStore();
44
+ closeStore();
45
+ // Re-opening recreates the schema, so the next harvest starts cleanly.
46
+ await getStore();
47
+ const lost = {
48
+ channels: before.channels,
49
+ videos: before.videos,
50
+ comments: before.comments,
51
+ };
52
+ return toolResult([
53
+ `Store was ${before.integrity === 'failed' ? 'corrupt' : 'unreadable'}. Moved it to ${movedTo} and created a fresh one.`,
54
+ `Lost from the working store: ${String(lost.videos)} videos, ${String(lost.comments)} comments, ${String(lost.channels)} channels.`,
55
+ 'The damaged file was kept, not deleted. Re-run the harvests you need.',
56
+ ].join('\n'), { wasCorrupt: true, integrity: before.integrity, movedTo, lost, storePath: before.path });
57
+ }
58
+ //# sourceMappingURL=repair-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repair-store.js","sourceRoot":"","sources":["../../src/tools/repair-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE3D,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,OAAO,EAAE,CAAC;SACP,OAAO,CAAC,IAAI,CAAC;SACb,QAAQ,CACP,qFAAqF;QACnF,wFAAwF,CAC3F;CACJ,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE;IACvB,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;IAChD,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,6DAA6D,CAAC;IAC1E,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QAC1B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;QACxB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;KAC3B,CAAC;IACF,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,MAAM,GAAG,MAAM,eAAe,EAAE,CAAC;IAEvC,IAAI,MAAM,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;QAC9B,OAAO,UAAU,CACf,sCAAsC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,+BAA+B,EAC7H;YACE,UAAU,EAAE,KAAK;YACjB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE;YACrF,SAAS,EAAE,MAAM,CAAC,IAAI;SACvB,CACF,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,eAAe,EAAE,CAAC;IACxC,UAAU,EAAE,CAAC;IACb,uEAAuE;IACvE,MAAM,QAAQ,EAAE,CAAC;IAEjB,MAAM,IAAI,GAAG;QACX,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;KAC1B,CAAC;IAEF,OAAO,UAAU,CACf;QACE,aAAa,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,iBAAiB,OAAO,2BAA2B;QACxH,gCAAgC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY;QACnI,uEAAuE;KACxE,CAAC,IAAI,CAAC,IAAI,CAAC,EACZ,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,IAAI,EAAE,CACzF,CAAC;AACJ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"search-channels.d.ts","sourceRoot":"","sources":["../../src/tools/search-channels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,eAAO,MAAM,oBAAoB;;;CAUhC,CAAC;AAEF,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAItC,CAAC;AAEF,wBAAsB,qBAAqB,CAAC,EAC1C,KAAK,EACL,KAAK,GACN,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,cAAc,CAAC,CAwB1B"}
1
+ {"version":3,"file":"search-channels.d.ts","sourceRoot":"","sources":["../../src/tools/search-channels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,eAAO,MAAM,oBAAoB;;;CAUhC,CAAC;AAEF,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAItC,CAAC;AAEF,wBAAsB,qBAAqB,CAAC,EAC1C,KAAK,EACL,KAAK,GACN,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,cAAc,CAAC,CA4B1B"}
@@ -20,7 +20,11 @@ export const searchChannelsOutputSchema = {
20
20
  };
21
21
  export async function searchChannelsHandler({ query, limit, }) {
22
22
  const channels = await searchChannels(query, limit);
23
- const structured = { query, channels, ...pageInfo(channels.length, channels.length) };
23
+ const structured = {
24
+ query,
25
+ channels,
26
+ ...pageInfo({ total: channels.length, count: channels.length }),
27
+ };
24
28
  if (channels.length === 0) {
25
29
  return toolResult(`No channels found for "${query}".`, structured);
26
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search-channels.js","sourceRoot":"","sources":["../../src/tools/search-channels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGnE,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,QAAQ,CAAC,iFAAiF,CAAC;IAC9F,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,EAAE,CAAC;SACP,OAAO,CAAC,CAAC,CAAC;SACV,QAAQ,CAAC,yDAAyD,CAAC;CACvE,CAAC;AAEF,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC;IACpC,GAAG,eAAe;CACnB,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,EAC1C,KAAK,EACL,KAAK,GAIN;IACC,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACpD,MAAM,UAAU,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;IAEtF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,UAAU,CAAC,0BAA0B,KAAK,IAAI,EAAE,UAAU,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,KAAK,GAAa;QACtB,SAAS,QAAQ,CAAC,MAAM,WAAW,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,SAAS,KAAK,GAAG;QACpF,EAAE;KACH,CAAC;IAEF,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;QACzB,MAAM,IAAI,GAAG,EAAE,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,EAAE,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/F,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACxE,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,UAAU,GAAG,IAAI,EAAE,CAAC,CAAC;QACzC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACnB,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9F,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,OAAO,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,CAAC;AAClD,CAAC"}
1
+ {"version":3,"file":"search-channels.js","sourceRoot":"","sources":["../../src/tools/search-channels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGnE,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,QAAQ,CAAC,iFAAiF,CAAC;IAC9F,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,EAAE,CAAC;SACP,OAAO,CAAC,CAAC,CAAC;SACV,QAAQ,CAAC,yDAAyD,CAAC;CACvE,CAAC;AAEF,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC;IACpC,GAAG,eAAe;CACnB,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,EAC1C,KAAK,EACL,KAAK,GAIN;IACC,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACpD,MAAM,UAAU,GAAG;QACjB,KAAK;QACL,QAAQ;QACR,GAAG,QAAQ,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;KAChE,CAAC;IAEF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,UAAU,CAAC,0BAA0B,KAAK,IAAI,EAAE,UAAU,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,KAAK,GAAa;QACtB,SAAS,QAAQ,CAAC,MAAM,WAAW,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,SAAS,KAAK,GAAG;QACpF,EAAE;KACH,CAAC;IAEF,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;QACzB,MAAM,IAAI,GAAG,EAAE,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,EAAE,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/F,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACxE,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,UAAU,GAAG,IAAI,EAAE,CAAC,CAAC;QACzC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACnB,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9F,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,OAAO,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,CAAC;AAClD,CAAC"}
@@ -42,7 +42,7 @@ export async function searchVideosHandler({ query, limit, }) {
42
42
  channel: v.channel,
43
43
  viewCount: v.viewCount,
44
44
  })),
45
- ...pageInfo(results.length, results.length),
45
+ ...pageInfo({ total: results.length, count: results.length }),
46
46
  });
47
47
  }
48
48
  //# sourceMappingURL=search-videos.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"search-videos.js","sourceRoot":"","sources":["../../src/tools/search-videos.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGpE,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,QAAQ,CAAC,2EAA2E,CAAC;IACxF,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,EAAE,CAAC;SACP,OAAO,CAAC,CAAC,CAAC;SACV,QAAQ,CAAC,wDAAwD,CAAC;CACtE,CAAC;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC;IACnC,GAAG,eAAe;CACnB,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EACxC,KAAK,EACL,KAAK,GAIN;IACC,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAEjD,MAAM,KAAK,GAAa;QACtB,SAAS,OAAO,CAAC,MAAM,UAAU,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,SAAS,KAAK,GAAG;QACjF,EAAE;KACH,CAAC;IAEF,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,iBAAiB,MAAM,CAAC,CAAC,OAAO,GAAG,KAAK,EAAE,CAAC,CAAC;QAC/D,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,OAAO,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAClC,KAAK;QACL,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC1B,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,eAAe,EAAE,CAAC,CAAC,QAAQ;YAC3B,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;YACtC,GAAG,EAAE,CAAC,CAAC,GAAG;YACV,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,SAAS,EAAE,CAAC,CAAC,SAAS;SACvB,CAAC,CAAC;QACH,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC;KAC5C,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"search-videos.js","sourceRoot":"","sources":["../../src/tools/search-videos.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGpE,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,QAAQ,CAAC,2EAA2E,CAAC;IACxF,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,EAAE,CAAC;SACP,OAAO,CAAC,CAAC,CAAC;SACV,QAAQ,CAAC,wDAAwD,CAAC;CACtE,CAAC;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC;IACnC,GAAG,eAAe;CACnB,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EACxC,KAAK,EACL,KAAK,GAIN;IACC,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAEjD,MAAM,KAAK,GAAa;QACtB,SAAS,OAAO,CAAC,MAAM,UAAU,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,SAAS,KAAK,GAAG;QACjF,EAAE;KACH,CAAC;IAEF,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,iBAAiB,MAAM,CAAC,CAAC,OAAO,GAAG,KAAK,EAAE,CAAC,CAAC;QAC/D,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,OAAO,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAClC,KAAK;QACL,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC1B,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,eAAe,EAAE,CAAC,CAAC,QAAQ;YAC3B,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;YACtC,GAAG,EAAE,CAAC,CAAC,GAAG;YACV,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,SAAS,EAAE,CAAC,CAAC,SAAS;SACvB,CAAC,CAAC;QACH,GAAG,QAAQ,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;KAC9D,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,71 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import { z } from 'zod';
3
+ import type { CommentThread } from './comment-threads.js';
4
+ /**
5
+ * Comments on disk.
6
+ *
7
+ * Upserted on the comment id, never INSERT OR REPLACE: replace deletes and
8
+ * reinserts, which changes the rowid the external-content FTS index is keyed
9
+ * on. Upsert keeps it and fires the update trigger, so a re-harvest costs
10
+ * network and never data.
11
+ */
12
+ export declare const COMMENT_COMMIT_BATCH = 2000;
13
+ declare const commentRowSchema: z.ZodObject<{
14
+ comment_id: z.ZodString;
15
+ video_id: z.ZodString;
16
+ parent_id: z.ZodNullable<z.ZodString>;
17
+ text: z.ZodString;
18
+ like_count: z.ZodNumber;
19
+ author: z.ZodString;
20
+ author_id: z.ZodNullable<z.ZodString>;
21
+ author_is_uploader: z.ZodEffects<z.ZodUnion<[z.ZodLiteral<0>, z.ZodLiteral<1>]>, boolean, 0 | 1>;
22
+ author_is_verified: z.ZodEffects<z.ZodUnion<[z.ZodLiteral<0>, z.ZodLiteral<1>]>, boolean, 0 | 1>;
23
+ is_pinned: z.ZodEffects<z.ZodUnion<[z.ZodLiteral<0>, z.ZodLiteral<1>]>, boolean, 0 | 1>;
24
+ is_favorited: z.ZodEffects<z.ZodUnion<[z.ZodLiteral<0>, z.ZodLiteral<1>]>, boolean, 0 | 1>;
25
+ published_at: z.ZodNullable<z.ZodNumber>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ author: string;
28
+ text: string;
29
+ like_count: number;
30
+ author_id: string | null;
31
+ author_is_uploader: boolean;
32
+ author_is_verified: boolean;
33
+ is_pinned: boolean;
34
+ is_favorited: boolean;
35
+ comment_id: string;
36
+ video_id: string;
37
+ parent_id: string | null;
38
+ published_at: number | null;
39
+ }, {
40
+ author: string;
41
+ text: string;
42
+ like_count: number;
43
+ author_id: string | null;
44
+ author_is_uploader: 0 | 1;
45
+ author_is_verified: 0 | 1;
46
+ is_pinned: 0 | 1;
47
+ is_favorited: 0 | 1;
48
+ comment_id: string;
49
+ video_id: string;
50
+ parent_id: string | null;
51
+ published_at: number | null;
52
+ }>;
53
+ export type StoredComment = z.infer<typeof commentRowSchema>;
54
+ /** Writes threads and their replies. The caller owns the transaction. */
55
+ export declare function saveThreads(database: DatabaseSync, videoId: string, threads: CommentThread[], now?: number): number;
56
+ export interface CommentQuery {
57
+ videoId?: string;
58
+ channelId?: string;
59
+ match?: string;
60
+ authorId?: string;
61
+ minLikes?: number;
62
+ topLevelOnly?: boolean;
63
+ threadOf?: string;
64
+ order?: 'likes' | 'newest' | 'oldest' | 'relevance';
65
+ limit?: number;
66
+ offset?: number;
67
+ }
68
+ export declare function queryComments(database: DatabaseSync, query: CommentQuery): StoredComment[];
69
+ export declare function countComments(database: DatabaseSync, query: CommentQuery): number;
70
+ export {};
71
+ //# sourceMappingURL=comment-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment-store.d.ts","sourceRoot":"","sources":["../../src/utils/comment-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG3E;;;;;;;GAOG;AAEH,eAAO,MAAM,oBAAoB,OAAQ,CAAC;AAe1C,QAAA,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAapB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAuB7D,yEAAyE;AACzE,wBAAgB,WAAW,CACzB,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,aAAa,EAAE,EACxB,GAAG,SAAa,GACf,MAAM,CAcR;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA8CD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,GAAG,aAAa,EAAE,CAoB1F;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,CAQjF"}
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ import { queryRows, sqliteBoolean } from './store-rows.js';
3
+ /**
4
+ * Comments on disk.
5
+ *
6
+ * Upserted on the comment id, never INSERT OR REPLACE: replace deletes and
7
+ * reinserts, which changes the rowid the external-content FTS index is keyed
8
+ * on. Upsert keeps it and fires the update trigger, so a re-harvest costs
9
+ * network and never data.
10
+ */
11
+ export const COMMENT_COMMIT_BATCH = 2_000;
12
+ const UPSERT = `
13
+ INSERT INTO comment (
14
+ comment_id, video_id, parent_id, text, like_count, author, author_id, author_url,
15
+ author_thumbnail, author_is_uploader, author_is_verified, is_pinned, is_favorited,
16
+ time_text, published_at, harvested_at
17
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
18
+ ON CONFLICT(comment_id) DO UPDATE SET
19
+ text = excluded.text,
20
+ like_count = excluded.like_count,
21
+ is_pinned = excluded.is_pinned,
22
+ is_favorited = excluded.is_favorited,
23
+ harvested_at = excluded.harvested_at`;
24
+ const commentRowSchema = z.object({
25
+ comment_id: z.string(),
26
+ video_id: z.string(),
27
+ parent_id: z.string().nullable(),
28
+ text: z.string(),
29
+ like_count: z.number(),
30
+ author: z.string(),
31
+ author_id: z.string().nullable(),
32
+ author_is_uploader: sqliteBoolean,
33
+ author_is_verified: sqliteBoolean,
34
+ is_pinned: sqliteBoolean,
35
+ is_favorited: sqliteBoolean,
36
+ published_at: z.number().nullable(),
37
+ });
38
+ function bind(comment, videoId, now) {
39
+ return [
40
+ comment.id,
41
+ videoId,
42
+ comment.parentId,
43
+ comment.text,
44
+ comment.likeCount,
45
+ comment.author,
46
+ comment.authorId ?? null,
47
+ comment.authorUrl ?? null,
48
+ comment.authorThumbnailUrl ?? null,
49
+ Number(comment.authorIsUploader),
50
+ Number(comment.authorIsVerified),
51
+ Number(comment.isPinned),
52
+ Number(comment.isFavorited),
53
+ comment.timeText ?? null,
54
+ comment.timestamp ?? null,
55
+ now,
56
+ ];
57
+ }
58
+ /** Writes threads and their replies. The caller owns the transaction. */
59
+ export function saveThreads(database, videoId, threads, now = Date.now()) {
60
+ const statement = database.prepare(UPSERT);
61
+ let written = 0;
62
+ for (const thread of threads) {
63
+ statement.run(...bind(thread.comment, videoId, now));
64
+ written += 1;
65
+ for (const reply of thread.replies) {
66
+ statement.run(...bind(reply, videoId, now));
67
+ written += 1;
68
+ }
69
+ }
70
+ return written;
71
+ }
72
+ function buildWhere(query) {
73
+ const clauses = [];
74
+ const params = [];
75
+ if (query.videoId !== undefined) {
76
+ clauses.push('c.video_id = ?');
77
+ params.push(query.videoId);
78
+ }
79
+ if (query.channelId !== undefined) {
80
+ clauses.push('c.video_id IN (SELECT video_id FROM video WHERE channel_id = ?)');
81
+ params.push(query.channelId);
82
+ }
83
+ if (query.authorId !== undefined) {
84
+ clauses.push('(c.author_id = ? OR c.author = ?)');
85
+ params.push(query.authorId, query.authorId);
86
+ }
87
+ if (query.threadOf !== undefined) {
88
+ clauses.push('(c.comment_id = ? OR c.parent_id = ?)');
89
+ params.push(query.threadOf, query.threadOf);
90
+ }
91
+ if (query.topLevelOnly === true)
92
+ clauses.push('c.parent_id IS NULL');
93
+ if (query.minLikes !== undefined && query.minLikes > 0) {
94
+ clauses.push('c.like_count >= ?');
95
+ params.push(query.minLikes);
96
+ }
97
+ if (query.match !== undefined && query.match !== '') {
98
+ // Bound, never concatenated: `match` is caller text and FTS5 has its own
99
+ // expression syntax that must not reach the SQL parser.
100
+ clauses.push('c.rowid IN (SELECT rowid FROM comment_fts WHERE comment_fts MATCH ?)');
101
+ params.push(query.match);
102
+ }
103
+ return { sql: clauses.length === 0 ? '' : ` WHERE ${clauses.join(' AND ')}`, params };
104
+ }
105
+ const ORDER = {
106
+ likes: 'c.like_count DESC, c.comment_id',
107
+ newest: 'c.published_at DESC, c.comment_id',
108
+ oldest: 'c.published_at ASC, c.comment_id',
109
+ // Threads read in reply order; relevance needs the FTS rank, which only
110
+ // exists when a match was given, so it falls back to likes.
111
+ relevance: 'c.like_count DESC, c.comment_id',
112
+ };
113
+ export function queryComments(database, query) {
114
+ const where = buildWhere(query);
115
+ const order = query.threadOf === undefined
116
+ ? ORDER[query.order ?? 'likes']
117
+ : 'c.parent_id NULLS FIRST, c.published_at';
118
+ return queryRows(database.prepare(`SELECT c.comment_id, c.video_id, c.parent_id, c.text, c.like_count, c.author,
119
+ c.author_id, c.author_is_uploader, c.author_is_verified, c.is_pinned,
120
+ c.is_favorited, c.published_at
121
+ FROM comment c${where.sql}
122
+ ORDER BY ${order} LIMIT ? OFFSET ?`), commentRowSchema, ...where.params, query.limit ?? 25, query.offset ?? 0);
123
+ }
124
+ export function countComments(database, query) {
125
+ const where = buildWhere(query);
126
+ const rows = queryRows(database.prepare(`SELECT COUNT(*) AS n FROM comment c${where.sql}`), z.object({ n: z.number() }), ...where.params);
127
+ return rows[0]?.n ?? 0;
128
+ }
129
+ //# sourceMappingURL=comment-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment-store.js","sourceRoot":"","sources":["../../src/utils/comment-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import type { z } from 'zod';
2
+ import type { commentRowSchema } from './youtube-schemas.js';
3
+ /**
4
+ * Rebuilding threads from yt-dlp's flat comment array.
5
+ *
6
+ * yt-dlp returns replies alongside their parents and marks each with a
7
+ * `parent` of `root` or the parent's id. The previous implementation filtered
8
+ * the replies away *after* paying to fetch them, which is the most expensive
9
+ * possible way to have no replies.
10
+ */
11
+ export type CommentRow = z.infer<typeof commentRowSchema>;
12
+ export interface ThreadedComment {
13
+ id: string;
14
+ parentId: string | null;
15
+ author: string;
16
+ authorId?: string;
17
+ authorUrl?: string;
18
+ authorThumbnailUrl?: string;
19
+ authorIsUploader: boolean;
20
+ authorIsVerified: boolean;
21
+ text: string;
22
+ likeCount: number;
23
+ isPinned: boolean;
24
+ isFavorited: boolean;
25
+ timestamp?: number;
26
+ publishedAt?: string;
27
+ timeText?: string;
28
+ }
29
+ export interface CommentThread {
30
+ comment: ThreadedComment;
31
+ replies: ThreadedComment[];
32
+ /** Replies present in THIS extraction, not YouTube's own figure. */
33
+ replyCount: number;
34
+ }
35
+ export interface ThreadedComments {
36
+ threads: CommentThread[];
37
+ rootCount: number;
38
+ replyCount: number;
39
+ /** Replies whose parent was cut by a cap. Kept, and counted, never dropped. */
40
+ orphanCount: number;
41
+ }
42
+ /**
43
+ * Groups rows into threads, in yt-dlp's own order.
44
+ *
45
+ * A reply whose parent is missing — because `max_parents` truncated the set —
46
+ * is promoted to a root rather than discarded, keeping its `parentId` so the
47
+ * gap stays visible. Silently dropping data is the bug this whole release is
48
+ * about.
49
+ */
50
+ export declare function toThreads(rows: CommentRow[]): ThreadedComments;
51
+ //# sourceMappingURL=comment-threads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment-threads.d.ts","sourceRoot":"","sources":["../../src/utils/comment-threads.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE7D;;;;;;;GAOG;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,eAAe,CAAC;IACzB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,WAAW,EAAE,MAAM,CAAC;CACrB;AAiCD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,gBAAgB,CA0C9D"}
@@ -0,0 +1,76 @@
1
+ function toComment(row, index) {
2
+ const parent = row.parent ?? 'root';
3
+ const timestamp = row.timestamp ?? undefined;
4
+ // yt-dlp writes null for an absent field; normalised to undefined once here
5
+ // so every spread below is a plain `=== undefined` check.
6
+ const authorId = row.author_id ?? undefined;
7
+ const authorUrl = row.author_url ?? undefined;
8
+ const authorThumbnail = row.author_thumbnail ?? undefined;
9
+ const timeText = row._time_text ?? undefined;
10
+ return {
11
+ // yt-dlp always supplies an id; the fallback keeps a malformed row
12
+ // addressable rather than silently merging it with another.
13
+ id: row.id ?? `unknown-${String(index)}`,
14
+ parentId: parent === 'root' ? null : parent,
15
+ author: row.author ?? 'Unknown',
16
+ ...(authorId === undefined ? {} : { authorId }),
17
+ ...(authorUrl === undefined ? {} : { authorUrl }),
18
+ ...(authorThumbnail === undefined ? {} : { authorThumbnailUrl: authorThumbnail }),
19
+ authorIsUploader: row.author_is_uploader ?? false,
20
+ authorIsVerified: row.author_is_verified ?? false,
21
+ text: row.text ?? '',
22
+ likeCount: row.like_count ?? 0,
23
+ isPinned: row.is_pinned ?? false,
24
+ isFavorited: row.is_favorited ?? false,
25
+ ...(timestamp === undefined ? {} : { timestamp }),
26
+ ...(timestamp === undefined ? {} : { publishedAt: new Date(timestamp * 1000).toISOString() }),
27
+ ...(timeText === undefined ? {} : { timeText }),
28
+ };
29
+ }
30
+ /**
31
+ * Groups rows into threads, in yt-dlp's own order.
32
+ *
33
+ * A reply whose parent is missing — because `max_parents` truncated the set —
34
+ * is promoted to a root rather than discarded, keeping its `parentId` so the
35
+ * gap stays visible. Silently dropping data is the bug this whole release is
36
+ * about.
37
+ */
38
+ export function toThreads(rows) {
39
+ const comments = rows.map(toComment);
40
+ const threads = [];
41
+ const threadById = new Map();
42
+ let replyCount = 0;
43
+ let orphanCount = 0;
44
+ for (const comment of comments) {
45
+ if (comment.parentId === null) {
46
+ const thread = { comment, replies: [], replyCount: 0 };
47
+ threads.push(thread);
48
+ threadById.set(comment.id, thread);
49
+ }
50
+ }
51
+ for (const comment of comments) {
52
+ if (comment.parentId === null)
53
+ continue;
54
+ const thread = threadById.get(comment.parentId);
55
+ if (thread === undefined) {
56
+ // The parent was cut by a cap. Keep the reply as its own thread so the
57
+ // text survives, and count it so the caller knows threads are partial.
58
+ orphanCount += 1;
59
+ threads.push({ comment, replies: [], replyCount: 0 });
60
+ continue;
61
+ }
62
+ thread.replies.push(comment);
63
+ thread.replyCount += 1;
64
+ replyCount += 1;
65
+ }
66
+ for (const thread of threads) {
67
+ thread.replies.sort((left, right) => (left.timestamp ?? 0) - (right.timestamp ?? 0));
68
+ }
69
+ return {
70
+ threads,
71
+ rootCount: threads.length - orphanCount,
72
+ replyCount,
73
+ orphanCount,
74
+ };
75
+ }
76
+ //# sourceMappingURL=comment-threads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment-threads.js","sourceRoot":"","sources":["../../src/utils/comment-threads.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Coverage } from '../harvest-schemas.js';
2
+ export declare function renderCoverage(coverage: Coverage): string;
3
+ //# sourceMappingURL=coverage-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage-text.d.ts","sourceRoot":"","sources":["../../src/utils/coverage-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AA8CtD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CA6BzD"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The human- and model-readable form of a receipt.
3
+ *
4
+ * `structuredContent` carries the machine-readable truth, but a model reads
5
+ * the text block, and that is where an overclaim actually happens. So every
6
+ * extraction tool prepends this, from one function, rather than each writing
7
+ * its own wording and drifting.
8
+ *
9
+ * The incomplete form says three things on purpose: the numbers, why it
10
+ * stopped, and an explicit instruction not to describe it as everything. The
11
+ * last one reads as belt-and-braces next to the `complete: false` field, and
12
+ * it is: the field is for programs, the sentence is for the reader.
13
+ */
14
+ const UNITS = {
15
+ 'channel-catalog': 'videos',
16
+ 'video-details': 'videos',
17
+ 'video-comments': 'comments',
18
+ };
19
+ const WHY = {
20
+ COMPLETE: 'complete',
21
+ CAP_REACHED: 'stopped by the requested cap',
22
+ BUDGET_SPENT: 'stopped when the time budget ran out',
23
+ RATE_LIMITED: 'stopped because YouTube throttled this client',
24
+ CANCELLED: 'cancelled by the client',
25
+ TIMEOUT: 'timed out',
26
+ SOURCE_REFUSED: 'YouTube refused this content to the current session',
27
+ COMMENTS_DISABLED: 'comments are turned off for this video',
28
+ SOURCE_SILENT: 'YouTube states no total, so exhaustion cannot be proven',
29
+ PARTIAL_ERROR: 'some items failed',
30
+ NOT_ATTEMPTED: 'not attempted yet',
31
+ };
32
+ function count(value) {
33
+ return value.toLocaleString('en-US');
34
+ }
35
+ function ratio(coverage) {
36
+ if (coverage.expected === undefined || coverage.expected === 0)
37
+ return '';
38
+ const percent = (coverage.have / coverage.expected) * 100;
39
+ return ` (${percent < 0.1 && percent > 0 ? '<0.1' : percent.toFixed(1)}%)`;
40
+ }
41
+ export function renderCoverage(coverage) {
42
+ const unit = UNITS[coverage.scope];
43
+ const of = coverage.expected === undefined
44
+ ? `${count(coverage.have)} ${unit}`
45
+ : `${count(coverage.have)} of ${coverage.expectedSource === 'youtube:comment_count' ? 'about ' : ''}${count(coverage.expected)} ${unit}${ratio(coverage)}`;
46
+ const lines = [`Coverage: ${of} for ${coverage.targetId}.`];
47
+ if (coverage.complete) {
48
+ lines.push(`COMPLETE — ${WHY[coverage.reason]}, harvested ${coverage.harvestedAt}.`);
49
+ if (coverage.staleAfter !== undefined) {
50
+ lines.push(`Re-check after ${coverage.staleAfter} (this can change upstream).`);
51
+ }
52
+ return lines.join('\n');
53
+ }
54
+ const bias = coverage.sortApplied === 'top' && coverage.limitApplied !== undefined
55
+ ? ', sorted by "top" (a biased prefix, not a random sample)'
56
+ : '';
57
+ lines.push(`INCOMPLETE — ${WHY[coverage.reason]}${bias}.`, `Do not describe this as the full ${unit === 'comments' ? 'comment history' : 'video list'}.`);
58
+ if (coverage.note !== undefined)
59
+ lines.push(coverage.note);
60
+ return lines.join('\n');
61
+ }
62
+ //# sourceMappingURL=coverage-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage-text.js","sourceRoot":"","sources":["../../src/utils/coverage-text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import { type Coverage, type CompletenessReason, type ExpectedSource, type HarvestScope } from '../harvest-schemas.js';
2
+ /**
3
+ * The one constructor for a completeness receipt.
4
+ *
5
+ * A wrong `complete: true` is the worst thing this server can emit: it is the
6
+ * difference between a consumer saying "here is a sample of the comments" and
7
+ * "here is the video's full comment history". So `complete` is not a field a
8
+ * caller sets — it is derived here, and the derivation is guarded by
9
+ * invariants that are asserted in code rather than merely documented.
10
+ *
11
+ * The rule that does the most work: `expected` is what the SOURCE said, and is
12
+ * never derived from `have`. Copying the number we collected into the field
13
+ * that means "how many there are" would turn "we stopped" into "there was
14
+ * nothing more", silently, and every check downstream would agree with it.
15
+ */
16
+ /** A complete receipt is still only as fresh as its harvest; comments accrue. */
17
+ export declare const RECEIPT_STALE_MS: number;
18
+ /** Beyond this, receipts are summarised rather than listed in a tool result. */
19
+ export declare const COVERAGE_IN_RESULT_LIMIT = 100;
20
+ export interface ExpectedTotal {
21
+ value: number;
22
+ source: ExpectedSource;
23
+ }
24
+ export interface CoverageInput {
25
+ scope: HarvestScope;
26
+ targetId: string;
27
+ have: number;
28
+ source: string;
29
+ /**
30
+ * Only ever an explicit pair or nothing. There is deliberately no code path
31
+ * that accepts a bare number, because the number would have to come from
32
+ * somewhere and `have` is the nearest thing to hand.
33
+ */
34
+ expected?: ExpectedTotal;
35
+ ranToExhaustion?: boolean;
36
+ reason?: CompletenessReason;
37
+ limitApplied?: number;
38
+ sortApplied?: 'top' | 'new';
39
+ resumeToken?: string;
40
+ harvestedAt?: Date;
41
+ note?: string;
42
+ }
43
+ /** Thrown for a receipt that contradicts itself. Not a YouTubeError: no caller can fix it. */
44
+ export declare class CoverageInvariantError extends Error {
45
+ constructor(message: string);
46
+ }
47
+ /**
48
+ * Checks a receipt against every invariant.
49
+ *
50
+ * Called inside `coverageOf` and again by handlers before a result goes out,
51
+ * because the cost of the second call is nothing and the cost of a wrong
52
+ * `complete: true` reaching a consumer is the whole point of this module.
53
+ */
54
+ export declare function assertCoverageConsistent(coverage: Coverage): void;
55
+ /** Builds a validated receipt. The only way to make one. */
56
+ export declare function coverageOf(input: CoverageInput): Coverage;
57
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../src/utils/coverage.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;;;;GAaG;AAEH,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,QAAiB,CAAC;AAE/C,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,8FAA8F;AAC9F,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAwCD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CA0CjE;AAED,4DAA4D;AAC5D,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,CA8BzD"}