@hasna/microservices 0.0.16 → 0.0.18

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 (293) hide show
  1. package/README.md +143 -23
  2. package/bin/index.js +784 -13987
  3. package/bin/mcp.js +298 -10973
  4. package/dist/index.js +251 -10056
  5. package/package.json +36 -26
  6. package/microservices/microservice-ads/package.json +0 -28
  7. package/microservices/microservice-ads/src/cli/index.ts +0 -605
  8. package/microservices/microservice-ads/src/db/campaigns.ts +0 -797
  9. package/microservices/microservice-ads/src/db/database.ts +0 -91
  10. package/microservices/microservice-ads/src/db/migrations.ts +0 -60
  11. package/microservices/microservice-ads/src/index.ts +0 -39
  12. package/microservices/microservice-ads/src/mcp/index.ts +0 -480
  13. package/microservices/microservice-analytics/package.json +0 -28
  14. package/microservices/microservice-analytics/src/cli/index.ts +0 -373
  15. package/microservices/microservice-analytics/src/db/analytics.ts +0 -564
  16. package/microservices/microservice-analytics/src/db/database.ts +0 -91
  17. package/microservices/microservice-analytics/src/db/migrations.ts +0 -50
  18. package/microservices/microservice-analytics/src/index.ts +0 -37
  19. package/microservices/microservice-analytics/src/mcp/index.ts +0 -334
  20. package/microservices/microservice-assets/package.json +0 -28
  21. package/microservices/microservice-assets/src/cli/index.ts +0 -375
  22. package/microservices/microservice-assets/src/db/assets.ts +0 -370
  23. package/microservices/microservice-assets/src/db/database.ts +0 -91
  24. package/microservices/microservice-assets/src/db/migrations.ts +0 -51
  25. package/microservices/microservice-assets/src/index.ts +0 -32
  26. package/microservices/microservice-assets/src/mcp/index.ts +0 -346
  27. package/microservices/microservice-bookkeeping/package.json +0 -28
  28. package/microservices/microservice-bookkeeping/src/cli/index.ts +0 -386
  29. package/microservices/microservice-bookkeeping/src/db/bookkeeping.ts +0 -591
  30. package/microservices/microservice-bookkeeping/src/db/database.ts +0 -91
  31. package/microservices/microservice-bookkeeping/src/db/migrations.ts +0 -52
  32. package/microservices/microservice-bookkeeping/src/index.ts +0 -32
  33. package/microservices/microservice-bookkeeping/src/mcp/index.ts +0 -284
  34. package/microservices/microservice-calendar/package.json +0 -28
  35. package/microservices/microservice-calendar/src/cli/index.ts +0 -287
  36. package/microservices/microservice-calendar/src/db/calendar.ts +0 -328
  37. package/microservices/microservice-calendar/src/db/database.ts +0 -91
  38. package/microservices/microservice-calendar/src/db/migrations.ts +0 -47
  39. package/microservices/microservice-calendar/src/index.ts +0 -24
  40. package/microservices/microservice-calendar/src/mcp/index.ts +0 -226
  41. package/microservices/microservice-company/package.json +0 -28
  42. package/microservices/microservice-company/src/cli/index.ts +0 -1126
  43. package/microservices/microservice-company/src/db/company.ts +0 -854
  44. package/microservices/microservice-company/src/db/database.ts +0 -91
  45. package/microservices/microservice-company/src/db/migrations.ts +0 -214
  46. package/microservices/microservice-company/src/db/workflow-migrations.ts +0 -44
  47. package/microservices/microservice-company/src/index.ts +0 -60
  48. package/microservices/microservice-company/src/lib/audit.ts +0 -168
  49. package/microservices/microservice-company/src/lib/finance.ts +0 -299
  50. package/microservices/microservice-company/src/lib/settings.ts +0 -85
  51. package/microservices/microservice-company/src/lib/workflows.ts +0 -698
  52. package/microservices/microservice-company/src/mcp/index.ts +0 -991
  53. package/microservices/microservice-compliance/package.json +0 -28
  54. package/microservices/microservice-compliance/src/cli/index.ts +0 -467
  55. package/microservices/microservice-compliance/src/db/compliance.ts +0 -633
  56. package/microservices/microservice-compliance/src/db/database.ts +0 -91
  57. package/microservices/microservice-compliance/src/db/migrations.ts +0 -63
  58. package/microservices/microservice-compliance/src/index.ts +0 -46
  59. package/microservices/microservice-compliance/src/mcp/index.ts +0 -438
  60. package/microservices/microservice-contacts/package.json +0 -28
  61. package/microservices/microservice-contacts/src/cli/index.ts +0 -393
  62. package/microservices/microservice-contacts/src/db/companies.ts +0 -167
  63. package/microservices/microservice-contacts/src/db/contacts.ts +0 -249
  64. package/microservices/microservice-contacts/src/db/database.ts +0 -91
  65. package/microservices/microservice-contacts/src/db/migrations.ts +0 -71
  66. package/microservices/microservice-contacts/src/db/relationships.ts +0 -53
  67. package/microservices/microservice-contacts/src/index.ts +0 -42
  68. package/microservices/microservice-contacts/src/mcp/index.ts +0 -303
  69. package/microservices/microservice-contracts/package.json +0 -28
  70. package/microservices/microservice-contracts/src/cli/index.ts +0 -770
  71. package/microservices/microservice-contracts/src/db/contracts.ts +0 -925
  72. package/microservices/microservice-contracts/src/db/database.ts +0 -91
  73. package/microservices/microservice-contracts/src/db/migrations.ts +0 -141
  74. package/microservices/microservice-contracts/src/index.ts +0 -43
  75. package/microservices/microservice-contracts/src/mcp/index.ts +0 -617
  76. package/microservices/microservice-crm/package.json +0 -28
  77. package/microservices/microservice-crm/src/cli/index.ts +0 -396
  78. package/microservices/microservice-crm/src/db/database.ts +0 -91
  79. package/microservices/microservice-crm/src/db/migrations.ts +0 -66
  80. package/microservices/microservice-crm/src/db/pipeline.ts +0 -397
  81. package/microservices/microservice-crm/src/index.ts +0 -34
  82. package/microservices/microservice-crm/src/mcp/index.ts +0 -294
  83. package/microservices/microservice-documents/package.json +0 -28
  84. package/microservices/microservice-documents/src/cli/index.ts +0 -246
  85. package/microservices/microservice-documents/src/db/database.ts +0 -91
  86. package/microservices/microservice-documents/src/db/documents.ts +0 -316
  87. package/microservices/microservice-documents/src/db/migrations.ts +0 -49
  88. package/microservices/microservice-documents/src/index.ts +0 -24
  89. package/microservices/microservice-documents/src/mcp/index.ts +0 -202
  90. package/microservices/microservice-domains/package.json +0 -28
  91. package/microservices/microservice-domains/src/cli/index.ts +0 -1111
  92. package/microservices/microservice-domains/src/db/database.ts +0 -91
  93. package/microservices/microservice-domains/src/db/domains.ts +0 -1164
  94. package/microservices/microservice-domains/src/db/migrations.ts +0 -60
  95. package/microservices/microservice-domains/src/index.ts +0 -65
  96. package/microservices/microservice-domains/src/lib/brandsight.ts +0 -350
  97. package/microservices/microservice-domains/src/lib/godaddy.ts +0 -338
  98. package/microservices/microservice-domains/src/lib/namecheap.ts +0 -262
  99. package/microservices/microservice-domains/src/lib/registrar.ts +0 -355
  100. package/microservices/microservice-domains/src/mcp/index.ts +0 -781
  101. package/microservices/microservice-expenses/package.json +0 -28
  102. package/microservices/microservice-expenses/src/cli/index.ts +0 -267
  103. package/microservices/microservice-expenses/src/db/database.ts +0 -91
  104. package/microservices/microservice-expenses/src/db/expenses.ts +0 -345
  105. package/microservices/microservice-expenses/src/db/migrations.ts +0 -45
  106. package/microservices/microservice-expenses/src/index.ts +0 -25
  107. package/microservices/microservice-expenses/src/mcp/index.ts +0 -196
  108. package/microservices/microservice-habits/package.json +0 -28
  109. package/microservices/microservice-habits/src/cli/index.ts +0 -315
  110. package/microservices/microservice-habits/src/db/database.ts +0 -91
  111. package/microservices/microservice-habits/src/db/habits.ts +0 -451
  112. package/microservices/microservice-habits/src/db/migrations.ts +0 -46
  113. package/microservices/microservice-habits/src/index.ts +0 -31
  114. package/microservices/microservice-habits/src/mcp/index.ts +0 -313
  115. package/microservices/microservice-health/package.json +0 -28
  116. package/microservices/microservice-health/src/cli/index.ts +0 -484
  117. package/microservices/microservice-health/src/db/database.ts +0 -91
  118. package/microservices/microservice-health/src/db/health.ts +0 -708
  119. package/microservices/microservice-health/src/db/migrations.ts +0 -70
  120. package/microservices/microservice-health/src/index.ts +0 -63
  121. package/microservices/microservice-health/src/mcp/index.ts +0 -437
  122. package/microservices/microservice-hiring/package.json +0 -28
  123. package/microservices/microservice-hiring/src/cli/index.ts +0 -741
  124. package/microservices/microservice-hiring/src/db/database.ts +0 -91
  125. package/microservices/microservice-hiring/src/db/hiring.ts +0 -1085
  126. package/microservices/microservice-hiring/src/db/migrations.ts +0 -89
  127. package/microservices/microservice-hiring/src/index.ts +0 -80
  128. package/microservices/microservice-hiring/src/lib/scoring.ts +0 -206
  129. package/microservices/microservice-hiring/src/mcp/index.ts +0 -709
  130. package/microservices/microservice-inventory/package.json +0 -28
  131. package/microservices/microservice-inventory/src/cli/index.ts +0 -365
  132. package/microservices/microservice-inventory/src/db/database.ts +0 -91
  133. package/microservices/microservice-inventory/src/db/inventory.ts +0 -393
  134. package/microservices/microservice-inventory/src/db/migrations.ts +0 -54
  135. package/microservices/microservice-inventory/src/index.ts +0 -28
  136. package/microservices/microservice-inventory/src/mcp/index.ts +0 -250
  137. package/microservices/microservice-invoices/dashboard/index.html +0 -12
  138. package/microservices/microservice-invoices/dashboard/package.json +0 -29
  139. package/microservices/microservice-invoices/dashboard/tsconfig.json +0 -14
  140. package/microservices/microservice-invoices/dashboard/vite.config.ts +0 -15
  141. package/microservices/microservice-invoices/package.json +0 -31
  142. package/microservices/microservice-invoices/src/cli/index.ts +0 -308
  143. package/microservices/microservice-invoices/src/db/business.ts +0 -241
  144. package/microservices/microservice-invoices/src/db/clients.ts +0 -127
  145. package/microservices/microservice-invoices/src/db/database.ts +0 -91
  146. package/microservices/microservice-invoices/src/db/invoices.ts +0 -345
  147. package/microservices/microservice-invoices/src/db/migrations.ts +0 -184
  148. package/microservices/microservice-invoices/src/index.ts +0 -56
  149. package/microservices/microservice-invoices/src/mcp/index.ts +0 -242
  150. package/microservices/microservice-invoices/src/server/index.ts +0 -162
  151. package/microservices/microservice-leads/package.json +0 -28
  152. package/microservices/microservice-leads/src/cli/index.ts +0 -596
  153. package/microservices/microservice-leads/src/db/database.ts +0 -91
  154. package/microservices/microservice-leads/src/db/leads.ts +0 -520
  155. package/microservices/microservice-leads/src/db/lists.ts +0 -151
  156. package/microservices/microservice-leads/src/db/migrations.ts +0 -93
  157. package/microservices/microservice-leads/src/index.ts +0 -65
  158. package/microservices/microservice-leads/src/lib/enrichment.ts +0 -202
  159. package/microservices/microservice-leads/src/lib/scoring.ts +0 -134
  160. package/microservices/microservice-leads/src/mcp/index.ts +0 -533
  161. package/microservices/microservice-notes/package.json +0 -28
  162. package/microservices/microservice-notes/src/cli/index.ts +0 -63
  163. package/microservices/microservice-notes/src/db/database.ts +0 -91
  164. package/microservices/microservice-notes/src/db/migrations.ts +0 -40
  165. package/microservices/microservice-notes/src/db/notes.ts +0 -114
  166. package/microservices/microservice-notes/src/index.ts +0 -2
  167. package/microservices/microservice-notes/src/mcp/index.ts +0 -37
  168. package/microservices/microservice-notifications/package.json +0 -28
  169. package/microservices/microservice-notifications/src/cli/index.ts +0 -349
  170. package/microservices/microservice-notifications/src/db/database.ts +0 -91
  171. package/microservices/microservice-notifications/src/db/migrations.ts +0 -62
  172. package/microservices/microservice-notifications/src/db/notifications.ts +0 -509
  173. package/microservices/microservice-notifications/src/index.ts +0 -41
  174. package/microservices/microservice-notifications/src/mcp/index.ts +0 -422
  175. package/microservices/microservice-payments/package.json +0 -28
  176. package/microservices/microservice-payments/src/cli/index.ts +0 -609
  177. package/microservices/microservice-payments/src/db/database.ts +0 -91
  178. package/microservices/microservice-payments/src/db/migrations.ts +0 -81
  179. package/microservices/microservice-payments/src/db/payments.ts +0 -1204
  180. package/microservices/microservice-payments/src/index.ts +0 -51
  181. package/microservices/microservice-payments/src/mcp/index.ts +0 -683
  182. package/microservices/microservice-payroll/package.json +0 -28
  183. package/microservices/microservice-payroll/src/cli/index.ts +0 -643
  184. package/microservices/microservice-payroll/src/db/database.ts +0 -91
  185. package/microservices/microservice-payroll/src/db/migrations.ts +0 -95
  186. package/microservices/microservice-payroll/src/db/payroll.ts +0 -1377
  187. package/microservices/microservice-payroll/src/index.ts +0 -48
  188. package/microservices/microservice-payroll/src/mcp/index.ts +0 -666
  189. package/microservices/microservice-products/package.json +0 -28
  190. package/microservices/microservice-products/src/cli/index.ts +0 -416
  191. package/microservices/microservice-products/src/db/categories.ts +0 -154
  192. package/microservices/microservice-products/src/db/database.ts +0 -91
  193. package/microservices/microservice-products/src/db/migrations.ts +0 -58
  194. package/microservices/microservice-products/src/db/pricing-tiers.ts +0 -66
  195. package/microservices/microservice-products/src/db/products.ts +0 -452
  196. package/microservices/microservice-products/src/index.ts +0 -53
  197. package/microservices/microservice-products/src/mcp/index.ts +0 -453
  198. package/microservices/microservice-projects/package.json +0 -28
  199. package/microservices/microservice-projects/src/cli/index.ts +0 -480
  200. package/microservices/microservice-projects/src/db/database.ts +0 -91
  201. package/microservices/microservice-projects/src/db/migrations.ts +0 -65
  202. package/microservices/microservice-projects/src/db/projects.ts +0 -715
  203. package/microservices/microservice-projects/src/index.ts +0 -57
  204. package/microservices/microservice-projects/src/mcp/index.ts +0 -501
  205. package/microservices/microservice-proposals/package.json +0 -28
  206. package/microservices/microservice-proposals/src/cli/index.ts +0 -400
  207. package/microservices/microservice-proposals/src/db/database.ts +0 -91
  208. package/microservices/microservice-proposals/src/db/migrations.ts +0 -52
  209. package/microservices/microservice-proposals/src/db/proposals.ts +0 -532
  210. package/microservices/microservice-proposals/src/index.ts +0 -37
  211. package/microservices/microservice-proposals/src/mcp/index.ts +0 -375
  212. package/microservices/microservice-reading/package.json +0 -28
  213. package/microservices/microservice-reading/src/cli/index.ts +0 -464
  214. package/microservices/microservice-reading/src/db/database.ts +0 -91
  215. package/microservices/microservice-reading/src/db/migrations.ts +0 -59
  216. package/microservices/microservice-reading/src/db/reading.ts +0 -524
  217. package/microservices/microservice-reading/src/index.ts +0 -51
  218. package/microservices/microservice-reading/src/mcp/index.ts +0 -368
  219. package/microservices/microservice-shipping/package.json +0 -28
  220. package/microservices/microservice-shipping/src/cli/index.ts +0 -606
  221. package/microservices/microservice-shipping/src/db/database.ts +0 -91
  222. package/microservices/microservice-shipping/src/db/migrations.ts +0 -69
  223. package/microservices/microservice-shipping/src/db/shipping.ts +0 -1093
  224. package/microservices/microservice-shipping/src/index.ts +0 -53
  225. package/microservices/microservice-shipping/src/mcp/index.ts +0 -533
  226. package/microservices/microservice-social/package.json +0 -29
  227. package/microservices/microservice-social/src/cli/index.ts +0 -1583
  228. package/microservices/microservice-social/src/db/database.ts +0 -91
  229. package/microservices/microservice-social/src/db/migrations.ts +0 -160
  230. package/microservices/microservice-social/src/db/social.ts +0 -1076
  231. package/microservices/microservice-social/src/index.ts +0 -46
  232. package/microservices/microservice-social/src/lib/audience.ts +0 -353
  233. package/microservices/microservice-social/src/lib/content-ai.ts +0 -278
  234. package/microservices/microservice-social/src/lib/media.ts +0 -311
  235. package/microservices/microservice-social/src/lib/mentions.ts +0 -434
  236. package/microservices/microservice-social/src/lib/metrics-sync.ts +0 -264
  237. package/microservices/microservice-social/src/lib/publisher.ts +0 -377
  238. package/microservices/microservice-social/src/lib/scheduler.ts +0 -229
  239. package/microservices/microservice-social/src/lib/sentiment.ts +0 -256
  240. package/microservices/microservice-social/src/lib/threads.ts +0 -291
  241. package/microservices/microservice-social/src/mcp/index.ts +0 -1425
  242. package/microservices/microservice-social/src/server/index.ts +0 -441
  243. package/microservices/microservice-subscriptions/package.json +0 -28
  244. package/microservices/microservice-subscriptions/src/cli/index.ts +0 -715
  245. package/microservices/microservice-subscriptions/src/db/database.ts +0 -91
  246. package/microservices/microservice-subscriptions/src/db/migrations.ts +0 -125
  247. package/microservices/microservice-subscriptions/src/db/subscriptions.ts +0 -1256
  248. package/microservices/microservice-subscriptions/src/index.ts +0 -41
  249. package/microservices/microservice-subscriptions/src/mcp/index.ts +0 -631
  250. package/microservices/microservice-timesheets/package.json +0 -28
  251. package/microservices/microservice-timesheets/src/cli/index.ts +0 -373
  252. package/microservices/microservice-timesheets/src/db/database.ts +0 -91
  253. package/microservices/microservice-timesheets/src/db/locale.ts +0 -217
  254. package/microservices/microservice-timesheets/src/db/migrations.ts +0 -74
  255. package/microservices/microservice-timesheets/src/db/timesheets.ts +0 -447
  256. package/microservices/microservice-timesheets/src/index.ts +0 -44
  257. package/microservices/microservice-timesheets/src/mcp/index.ts +0 -269
  258. package/microservices/microservice-transcriber/package.json +0 -29
  259. package/microservices/microservice-transcriber/src/cli/index.ts +0 -1593
  260. package/microservices/microservice-transcriber/src/db/annotations.ts +0 -37
  261. package/microservices/microservice-transcriber/src/db/comments.ts +0 -166
  262. package/microservices/microservice-transcriber/src/db/database.ts +0 -91
  263. package/microservices/microservice-transcriber/src/db/migrations.ts +0 -118
  264. package/microservices/microservice-transcriber/src/db/proofread.ts +0 -119
  265. package/microservices/microservice-transcriber/src/db/transcripts.ts +0 -395
  266. package/microservices/microservice-transcriber/src/index.ts +0 -43
  267. package/microservices/microservice-transcriber/src/lib/config.ts +0 -77
  268. package/microservices/microservice-transcriber/src/lib/diff.ts +0 -91
  269. package/microservices/microservice-transcriber/src/lib/downloader.ts +0 -638
  270. package/microservices/microservice-transcriber/src/lib/feeds.ts +0 -62
  271. package/microservices/microservice-transcriber/src/lib/live.ts +0 -94
  272. package/microservices/microservice-transcriber/src/lib/notion.ts +0 -129
  273. package/microservices/microservice-transcriber/src/lib/proofread.ts +0 -296
  274. package/microservices/microservice-transcriber/src/lib/providers.ts +0 -713
  275. package/microservices/microservice-transcriber/src/lib/summarizer.ts +0 -147
  276. package/microservices/microservice-transcriber/src/lib/translator.ts +0 -75
  277. package/microservices/microservice-transcriber/src/lib/webhook.ts +0 -37
  278. package/microservices/microservice-transcriber/src/mcp/index.ts +0 -1330
  279. package/microservices/microservice-transcriber/src/server/index.ts +0 -199
  280. package/microservices/microservice-travel/package.json +0 -28
  281. package/microservices/microservice-travel/src/cli/index.ts +0 -505
  282. package/microservices/microservice-travel/src/db/database.ts +0 -91
  283. package/microservices/microservice-travel/src/db/migrations.ts +0 -77
  284. package/microservices/microservice-travel/src/db/travel.ts +0 -802
  285. package/microservices/microservice-travel/src/index.ts +0 -60
  286. package/microservices/microservice-travel/src/mcp/index.ts +0 -495
  287. package/microservices/microservice-wiki/package.json +0 -28
  288. package/microservices/microservice-wiki/src/cli/index.ts +0 -345
  289. package/microservices/microservice-wiki/src/db/database.ts +0 -91
  290. package/microservices/microservice-wiki/src/db/migrations.ts +0 -55
  291. package/microservices/microservice-wiki/src/db/wiki.ts +0 -395
  292. package/microservices/microservice-wiki/src/index.ts +0 -32
  293. package/microservices/microservice-wiki/src/mcp/index.ts +0 -344
@@ -1,638 +0,0 @@
1
- /**
2
- * Audio downloader — detects source type and extracts audio for transcription.
3
- * Uses yt-dlp for YouTube, Vimeo, Wistia, and other URL-based sources.
4
- * Uses ffmpeg for local file trimming.
5
- */
6
-
7
- import { existsSync, mkdirSync, unlinkSync } from "node:fs";
8
- import { dirname, join, resolve } from "node:path";
9
- import { homedir, tmpdir } from "node:os";
10
- import type { TranscriptSourceType } from "../db/transcripts.js";
11
-
12
- export interface VideoChapter {
13
- title: string;
14
- start_time: number;
15
- end_time: number;
16
- }
17
-
18
- export interface DownloadResult {
19
- filePath: string;
20
- sourceType: TranscriptSourceType;
21
- videoTitle: string | null;
22
- chapters: VideoChapter[];
23
- cleanup: () => void;
24
- }
25
-
26
- export interface TrimOptions {
27
- start?: number; // seconds
28
- end?: number; // seconds
29
- }
30
-
31
- /**
32
- * Detect source type from a URL or file path.
33
- */
34
- export function detectSourceType(source: string): TranscriptSourceType {
35
- if (!source.startsWith("http://") && !source.startsWith("https://")) {
36
- return "file";
37
- }
38
-
39
- const lower = source.toLowerCase();
40
- if (lower.includes("youtube.com") || lower.includes("youtu.be")) return "youtube";
41
- if (lower.includes("vimeo.com")) return "vimeo";
42
- if (lower.includes("wistia.com") || lower.includes("wi.st") || lower.includes("wistia.net")) return "wistia";
43
- return "url";
44
- }
45
-
46
- /**
47
- * Download or locate audio from a source (URL or file path).
48
- * Optionally trims to a time range using --start / --end (seconds).
49
- *
50
- * For URLs: yt-dlp --download-sections handles the trim (avoids downloading unused parts).
51
- * For local files: ffmpeg -ss / -to handles the trim.
52
- */
53
- export async function prepareAudio(source: string, trim?: TrimOptions): Promise<DownloadResult> {
54
- const sourceType = detectSourceType(source);
55
-
56
- if (sourceType === "file") {
57
- if (!existsSync(source)) {
58
- throw new Error(`File not found: ${source}`);
59
- }
60
- if (trim?.start !== undefined || trim?.end !== undefined) {
61
- const trimmed = await trimLocalFile(source, trim);
62
- return {
63
- filePath: trimmed,
64
- sourceType,
65
- videoTitle: null,
66
- chapters: [],
67
- cleanup: () => {
68
- try { if (existsSync(trimmed)) unlinkSync(trimmed); } catch {}
69
- },
70
- };
71
- }
72
- return {
73
- filePath: source,
74
- sourceType,
75
- videoTitle: null,
76
- chapters: [],
77
- cleanup: () => {},
78
- };
79
- }
80
-
81
- // Remote source — download full audio and fetch metadata in parallel
82
- const tempId = crypto.randomUUID();
83
- const outputTemplate = join(tmpdir(), `transcriber-${tempId}.%(ext)s`);
84
-
85
- const [, info] = await Promise.all([
86
- runYtDlp(source, outputTemplate),
87
- getVideoInfo(source).catch(() => null),
88
- ]);
89
-
90
- const downloadedPath = findDownloadedFile(tmpdir(), `transcriber-${tempId}`);
91
- if (!downloadedPath) {
92
- throw new Error(`yt-dlp did not produce an output file for: ${source}`);
93
- }
94
-
95
- // Trim locally with ffmpeg if start/end provided (more reliable than --download-sections)
96
- if (trim?.start !== undefined || trim?.end !== undefined) {
97
- const trimmedPath = await trimLocalFile(downloadedPath, trim);
98
- try { unlinkSync(downloadedPath); } catch {} // clean up full download
99
- return {
100
- filePath: trimmedPath,
101
- sourceType,
102
- videoTitle: info?.title ?? null,
103
- chapters: info?.chapters ?? [],
104
- cleanup: () => {
105
- try { if (existsSync(trimmedPath)) unlinkSync(trimmedPath); } catch {}
106
- },
107
- };
108
- }
109
-
110
- return {
111
- filePath: downloadedPath,
112
- sourceType,
113
- videoTitle: info?.title ?? null,
114
- chapters: info?.chapters ?? [],
115
- cleanup: () => {
116
- try { if (existsSync(downloadedPath)) unlinkSync(downloadedPath); } catch {}
117
- },
118
- };
119
- }
120
-
121
- /**
122
- * Quick title fetch using yt-dlp --print — no download, very fast.
123
- */
124
- async function fetchVideoTitle(url: string): Promise<string | null> {
125
- try {
126
- const proc = Bun.spawn(
127
- [ytdlp(), "--print", "%(title)s", "--no-download", url],
128
- { stdout: "pipe", stderr: "pipe" }
129
- );
130
- const [exitCode, stdout] = await Promise.all([
131
- proc.exited,
132
- new Response(proc.stdout).text(),
133
- ]);
134
- if (exitCode !== 0) return null;
135
- const title = stdout.trim();
136
- return title.length > 0 && title !== "NA" ? title : null;
137
- } catch {
138
- return null;
139
- }
140
- }
141
-
142
- /**
143
- * Resolve yt-dlp binary — prefers homebrew version (usually newer) over pip.
144
- */
145
- function getYtDlpBinary(): string {
146
- const home = process.env["HOME"] ?? "";
147
- const candidates = [`${home}/.local/bin/yt-dlp-nightly`, "/opt/homebrew/bin/yt-dlp", "yt-dlp"];
148
- for (const bin of candidates) {
149
- try {
150
- const proc = Bun.spawnSync([bin, "--version"], { stdout: "pipe", stderr: "pipe" });
151
- if (proc.exitCode === 0) return bin;
152
- } catch {}
153
- }
154
- return "yt-dlp";
155
- }
156
-
157
- let _ytdlpBin: string | null = null;
158
- function ytdlp(): string {
159
- if (!_ytdlpBin) _ytdlpBin = getYtDlpBinary();
160
- return _ytdlpBin;
161
- }
162
-
163
- async function runYtDlp(url: string, outputTemplate: string): Promise<void> {
164
- const args = [ytdlp(), "-x", "--audio-format", "mp3", "--audio-quality", "0", "-o", outputTemplate, url];
165
-
166
- const proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
167
- const exitCode = await proc.exited;
168
- if (exitCode !== 0) {
169
- const stderr = await new Response(proc.stderr).text();
170
- throw new Error(`yt-dlp failed (exit ${exitCode}): ${stderr.trim()}`);
171
- }
172
- }
173
-
174
- async function trimLocalFile(filePath: string, trim: TrimOptions): Promise<string> {
175
- const tempId = crypto.randomUUID();
176
- const ext = filePath.split(".").pop() ?? "mp3";
177
- const outPath = join(tmpdir(), `transcriber-trim-${tempId}.${ext}`);
178
-
179
- const args = ["ffmpeg", "-y", "-i", filePath];
180
- if (trim.start !== undefined) args.push("-ss", String(trim.start));
181
- if (trim.end !== undefined) args.push("-to", String(trim.end));
182
- args.push("-c", "copy", outPath);
183
-
184
- const proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
185
- const exitCode = await proc.exited;
186
- if (exitCode !== 0) {
187
- const stderr = await new Response(proc.stderr).text();
188
- throw new Error(`ffmpeg trim failed (exit ${exitCode}): ${stderr.trim()}`);
189
- }
190
-
191
- return outPath;
192
- }
193
-
194
- function findDownloadedFile(dir: string, prefix: string): string | null {
195
- const extensions = ["mp3", "m4a", "ogg", "opus", "wav", "webm", "flac"];
196
- for (const ext of extensions) {
197
- const candidate = join(dir, `${prefix}.${ext}`);
198
- if (existsSync(candidate)) return candidate;
199
- }
200
- return null;
201
- }
202
-
203
- export interface VideoInfo {
204
- title: string | null;
205
- duration: number | null; // seconds
206
- uploader: string | null;
207
- platform: string | null;
208
- description: string | null;
209
- thumbnail: string | null;
210
- upload_date: string | null; // YYYYMMDD
211
- view_count: number | null;
212
- chapters: Array<{ title: string; start_time: number; end_time: number }>;
213
- formats: Array<{ format_id: string; ext: string; resolution: string | null }>;
214
- }
215
-
216
- /**
217
- * Fetch video metadata without downloading. Uses yt-dlp --dump-json.
218
- * Only works for URLs — returns null for local files.
219
- */
220
- export async function getVideoInfo(url: string): Promise<VideoInfo> {
221
- const proc = Bun.spawn(
222
- [ytdlp(), "--dump-json", "--no-download", url],
223
- { stdout: "pipe", stderr: "pipe" }
224
- );
225
-
226
- const [exitCode, stdout, stderr] = await Promise.all([
227
- proc.exited,
228
- new Response(proc.stdout).text(),
229
- new Response(proc.stderr).text(),
230
- ]);
231
-
232
- if (exitCode !== 0) {
233
- throw new Error(`yt-dlp info failed (exit ${exitCode}): ${stderr.trim()}`);
234
- }
235
-
236
- let raw: Record<string, unknown>;
237
- try {
238
- raw = JSON.parse(stdout);
239
- } catch {
240
- throw new Error(`yt-dlp returned invalid JSON: ${stdout.slice(0, 200)}`);
241
- }
242
-
243
- const chapters = Array.isArray(raw["chapters"])
244
- ? (raw["chapters"] as Array<{ title: string; start_time: number; end_time: number }>).map((c) => ({
245
- title: c.title,
246
- start_time: c.start_time,
247
- end_time: c.end_time,
248
- }))
249
- : [];
250
-
251
- const formats = Array.isArray(raw["formats"])
252
- ? (raw["formats"] as Array<{ format_id: string; ext: string; resolution?: string | null }>)
253
- .slice(-10) // last 10 formats (usually best quality last)
254
- .map((f) => ({ format_id: f.format_id, ext: f.ext, resolution: f.resolution ?? null }))
255
- : [];
256
-
257
- return {
258
- title: (raw["title"] as string) ?? null,
259
- duration: typeof raw["duration"] === "number" ? raw["duration"] : null,
260
- uploader: (raw["uploader"] as string) ?? (raw["channel"] as string) ?? null,
261
- platform: (raw["extractor_key"] as string) ?? (raw["ie_key"] as string) ?? null,
262
- description: typeof raw["description"] === "string" ? raw["description"].slice(0, 500) : null,
263
- thumbnail: (raw["thumbnail"] as string) ?? null,
264
- upload_date: (raw["upload_date"] as string) ?? null,
265
- view_count: typeof raw["view_count"] === "number" ? raw["view_count"] : null,
266
- chapters,
267
- formats,
268
- };
269
- }
270
-
271
- export interface DownloadAudioOptions {
272
- format?: "mp3" | "m4a" | "wav";
273
- outputPath?: string; // explicit output file path (overrides auto-naming)
274
- trim?: TrimOptions;
275
- }
276
-
277
- export interface DownloadAudioResult {
278
- filePath: string;
279
- sourceType: TranscriptSourceType;
280
- title: string | null;
281
- duration: number | null;
282
- }
283
-
284
- /**
285
- * Resolve the base audio output directory:
286
- * .microservices/microservice-transcriber/audio/ (walks up from cwd, or falls back to home).
287
- */
288
- export function getAudioOutputDir(): string {
289
- if (process.env["MICROSERVICES_DIR"]) {
290
- return join(process.env["MICROSERVICES_DIR"], "microservice-transcriber", "audio");
291
- }
292
- let dir = resolve(process.cwd());
293
- while (true) {
294
- const msDir = join(dir, ".microservices");
295
- if (existsSync(msDir)) return join(msDir, "microservice-transcriber", "audio");
296
- const parent = dirname(dir);
297
- if (parent === dir) break;
298
- dir = parent;
299
- }
300
- return join(homedir(), ".microservices", "microservice-transcriber", "audio");
301
- }
302
-
303
- /**
304
- * Download video (not just audio) from a URL for clip extraction.
305
- * Returns path to temp video file.
306
- */
307
- export async function downloadVideo(url: string): Promise<{ path: string; cleanup: () => void }> {
308
- const tempId = crypto.randomUUID();
309
- const outTemplate = join(tmpdir(), `transcriber-vid-${tempId}.%(ext)s`);
310
-
311
- const proc = Bun.spawn(
312
- [ytdlp(), "-f", "best[ext=mp4]/best", "-o", outTemplate, url],
313
- { stdout: "pipe", stderr: "pipe" }
314
- );
315
- const exitCode = await proc.exited;
316
- if (exitCode !== 0) {
317
- const stderr = await new Response(proc.stderr).text();
318
- throw new Error(`yt-dlp video download failed: ${stderr.trim()}`);
319
- }
320
-
321
- const extensions = ["mp4", "webm", "mkv", "avi"];
322
- for (const ext of extensions) {
323
- const candidate = join(tmpdir(), `transcriber-vid-${tempId}.${ext}`);
324
- if (existsSync(candidate)) {
325
- return { path: candidate, cleanup: () => { try { unlinkSync(candidate); } catch {} } };
326
- }
327
- }
328
- throw new Error("yt-dlp did not produce a video file");
329
- }
330
-
331
- /**
332
- * Create a video/audio clip with optional burned-in subtitles using ffmpeg.
333
- */
334
- export async function createClip(options: {
335
- videoPath: string;
336
- start: number;
337
- end: number;
338
- subtitlePath?: string; // ASS file path to burn in
339
- outputPath: string;
340
- }): Promise<void> {
341
- const args = ["ffmpeg", "-y", "-i", options.videoPath, "-ss", String(options.start), "-to", String(options.end)];
342
-
343
- if (options.subtitlePath) {
344
- // Burn in subtitles — need to escape path for ffmpeg filter
345
- const escaped = options.subtitlePath.replace(/[\\:]/g, "\\$&");
346
- args.push("-vf", `subtitles=${escaped}`);
347
- }
348
-
349
- args.push("-c:a", "aac", options.outputPath);
350
-
351
- const proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
352
- const exitCode = await proc.exited;
353
- if (exitCode !== 0) {
354
- const stderr = await new Response(proc.stderr).text();
355
- throw new Error(`ffmpeg clip failed: ${stderr.trim().slice(-200)}`);
356
- }
357
- }
358
-
359
- /**
360
- * Detect whether a URL is a playlist (YouTube, etc.).
361
- */
362
- export function isPlaylistUrl(url: string): boolean {
363
- return url.includes("list=") || url.includes("/playlist");
364
- }
365
-
366
- /**
367
- * Extract individual video URLs from a playlist using yt-dlp --flat-playlist.
368
- */
369
- export async function getPlaylistUrls(url: string): Promise<Array<{ url: string; title: string | null }>> {
370
- const proc = Bun.spawn(
371
- [ytdlp(), "--flat-playlist", "--dump-json", "--no-download", url],
372
- { stdout: "pipe", stderr: "pipe" }
373
- );
374
-
375
- const [exitCode, stdout] = await Promise.all([
376
- proc.exited,
377
- new Response(proc.stdout).text(),
378
- ]);
379
-
380
- if (exitCode !== 0) {
381
- const stderr = await new Response(proc.stderr).text();
382
- throw new Error(`yt-dlp playlist failed (exit ${exitCode}): ${stderr.trim()}`);
383
- }
384
-
385
- // Each line is a JSON object for one video
386
- return stdout
387
- .trim()
388
- .split("\n")
389
- .filter(Boolean)
390
- .map((line) => {
391
- try {
392
- const entry = JSON.parse(line);
393
- const videoUrl = entry.url
394
- ? (entry.url.startsWith("http") ? entry.url : `https://www.youtube.com/watch?v=${entry.id || entry.url}`)
395
- : `https://www.youtube.com/watch?v=${entry.id}`;
396
- return { url: videoUrl, title: entry.title ?? null };
397
- } catch {
398
- return null;
399
- }
400
- })
401
- .filter(Boolean) as Array<{ url: string; title: string | null }>;
402
- }
403
-
404
- /**
405
- * Generate a 6-character random alphanumeric suffix for collision avoidance.
406
- */
407
- function nanoSuffix(): string {
408
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
409
- return Array.from(crypto.getRandomValues(new Uint8Array(6)))
410
- .map((b) => chars[b % chars.length])
411
- .join("");
412
- }
413
-
414
- /**
415
- * Normalize a title into a safe, lowercase, hyphenated filename stem.
416
- *
417
- * Rules:
418
- * - Lowercase
419
- * - & → "and"
420
- * - Spaces and underscores → hyphens
421
- * - Strip everything except alphanumeric, hyphens, dots
422
- * - Collapse consecutive hyphens
423
- * - Strip leading/trailing hyphens
424
- * - Max 80 chars
425
- * - Append 6-char nanoid suffix for collision avoidance
426
- *
427
- * Examples:
428
- * "My Awesome Video! (2024)" → "my-awesome-video-2024-a3k9mz"
429
- * "C++ Tutorial: Part 1/3" → "c-tutorial-part-1-3-b7xq2p"
430
- * "Cats & Dogs Forever" → "cats-and-dogs-forever-m4nk8r"
431
- */
432
- export function normalizeFilename(title: string): string {
433
- let s = title.toLowerCase();
434
- s = s.replace(/&/g, "and"); // & → and
435
- s = s.replace(/[/_:,;|.]+/g, "-"); // separators → hyphens (/, :, _, ., etc.)
436
- s = s.replace(/[_\s]+/g, "-"); // spaces/underscores → hyphens
437
- s = s.replace(/[^a-z0-9-]/g, ""); // strip everything else (!, ?, +, (, ), etc.)
438
- s = s.replace(/-{2,}/g, "-"); // collapse multiple hyphens
439
- s = s.replace(/^-+|-+$/g, ""); // strip leading/trailing hyphens
440
- s = s.slice(0, 80); // max 80 chars
441
- s = s.replace(/-+$/, ""); // clean trailing hyphens after slice
442
- const suffix = nanoSuffix();
443
- return s.length > 0 ? `${s}-${suffix}` : suffix;
444
- }
445
-
446
- /**
447
- * Download audio from a URL and save to the audio library.
448
- * Does NOT transcribe — just extracts audio.
449
- */
450
- export async function downloadAudio(
451
- url: string,
452
- options: DownloadAudioOptions = {}
453
- ): Promise<DownloadAudioResult> {
454
- const sourceType = detectSourceType(url);
455
- if (sourceType === "file") {
456
- throw new Error("Use a URL for download. Local files don't need downloading.");
457
- }
458
-
459
- const format = options.format ?? "mp3";
460
-
461
- // Fetch title and duration in parallel before downloading
462
- const info = await getVideoInfo(url).catch(() => null);
463
- const title = info?.title ?? null;
464
- const duration = info?.duration ?? null;
465
-
466
- // Determine output path
467
- let outPath: string;
468
- if (options.outputPath) {
469
- outPath = options.outputPath;
470
- mkdirSync(dirname(outPath), { recursive: true });
471
- } else {
472
- const platform = sourceType; // already lowercase alphanum (file/youtube/vimeo/etc.)
473
- const fileName = title ? normalizeFilename(title) : nanoSuffix();
474
- const audioDir = join(getAudioOutputDir(), platform);
475
- mkdirSync(audioDir, { recursive: true });
476
- outPath = join(audioDir, `${fileName}.${format}`);
477
- }
478
-
479
- const args = [ytdlp(), "-x", "--audio-format", format, "--audio-quality", "0", "-o", outPath, url];
480
-
481
- const proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
482
- const exitCode = await proc.exited;
483
- if (exitCode !== 0) {
484
- const stderr = await new Response(proc.stderr).text();
485
- throw new Error(`yt-dlp failed (exit ${exitCode}): ${stderr.trim()}`);
486
- }
487
-
488
- // yt-dlp may adjust extension; find actual file
489
- let actualPath = existsSync(outPath) ? outPath : findDownloadedFile(dirname(outPath), outPath.replace(/\.[^.]+$/, "").split("/").pop()!) ?? outPath;
490
-
491
- // Trim locally after download (more reliable than --download-sections)
492
- if (options.trim?.start !== undefined || options.trim?.end !== undefined) {
493
- const trimmedPath = await trimLocalFile(actualPath, options.trim);
494
- try { unlinkSync(actualPath); } catch {}
495
- actualPath = trimmedPath;
496
- }
497
-
498
- return { filePath: actualPath, sourceType, title, duration };
499
- }
500
-
501
- /**
502
- * Get audio file duration in seconds using ffprobe.
503
- */
504
- export async function getAudioDuration(filePath: string): Promise<number> {
505
- const proc = Bun.spawn(
506
- ["ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "csv=p=0", filePath],
507
- { stdout: "pipe", stderr: "pipe" }
508
- );
509
- const [exitCode, stdout] = await Promise.all([
510
- proc.exited,
511
- new Response(proc.stdout).text(),
512
- ]);
513
- if (exitCode !== 0) throw new Error("ffprobe failed to get audio duration");
514
- const duration = parseFloat(stdout.trim());
515
- return isNaN(duration) ? 0 : duration;
516
- }
517
-
518
- /**
519
- * Split an audio file into chunks of `chunkDurationSec` seconds.
520
- * Returns array of temp file paths + their start offsets.
521
- */
522
- export async function splitAudioIntoChunks(
523
- filePath: string,
524
- chunkDurationSec = 600 // 10 minutes default
525
- ): Promise<Array<{ path: string; startOffset: number }>> {
526
- const totalDuration = await getAudioDuration(filePath);
527
- if (totalDuration <= chunkDurationSec) {
528
- return [{ path: filePath, startOffset: 0 }];
529
- }
530
-
531
- const chunks: Array<{ path: string; startOffset: number }> = [];
532
- const ext = filePath.split(".").pop() ?? "mp3";
533
- let offset = 0;
534
-
535
- while (offset < totalDuration) {
536
- const chunkId = crypto.randomUUID();
537
- const chunkPath = join(tmpdir(), `transcriber-chunk-${chunkId}.${ext}`);
538
- const args = [
539
- "ffmpeg", "-y", "-i", filePath,
540
- "-ss", String(offset),
541
- "-t", String(chunkDurationSec),
542
- "-c", "copy", chunkPath,
543
- ];
544
-
545
- const proc = Bun.spawn(args, { stdout: "pipe", stderr: "pipe" });
546
- const exitCode = await proc.exited;
547
- if (exitCode !== 0) {
548
- const stderr = await new Response(proc.stderr).text();
549
- throw new Error(`ffmpeg chunk split failed at ${offset}s: ${stderr.trim()}`);
550
- }
551
-
552
- chunks.push({ path: chunkPath, startOffset: offset });
553
- offset += chunkDurationSec;
554
- }
555
-
556
- return chunks;
557
- }
558
-
559
- /**
560
- * Raw comment from yt-dlp .info.json comments array.
561
- */
562
- export interface RawComment {
563
- author: string | null;
564
- author_id: string | null;
565
- text: string;
566
- like_count: number;
567
- timestamp: number | null;
568
- parent: string | null; // "root" for top-level, comment id for replies
569
- id: string;
570
- }
571
-
572
- /**
573
- * Fetch comments for a video URL using yt-dlp --write-comments.
574
- * Downloads only the .info.json (no media) and parses the comments array.
575
- */
576
- export async function fetchComments(url: string): Promise<RawComment[]> {
577
- const tempId = crypto.randomUUID();
578
- const outputTemplate = join(tmpdir(), `comments-${tempId}`);
579
-
580
- const proc = Bun.spawn(
581
- [ytdlp(), "--write-comments", "--skip-download", "--no-write-thumbnail", "-o", outputTemplate, url],
582
- { stdout: "pipe", stderr: "pipe" }
583
- );
584
-
585
- const [exitCode, , stderr] = await Promise.all([
586
- proc.exited,
587
- new Response(proc.stdout).text(),
588
- new Response(proc.stderr).text(),
589
- ]);
590
-
591
- if (exitCode !== 0) {
592
- throw new Error(`yt-dlp comment fetch failed (exit ${exitCode}): ${stderr.trim()}`);
593
- }
594
-
595
- // yt-dlp writes <output>.info.json
596
- const infoPath = `${outputTemplate}.info.json`;
597
- const { readFileSync, unlinkSync: unlinkFile, existsSync: fileExists } = await import("node:fs");
598
-
599
- if (!fileExists(infoPath)) {
600
- throw new Error("yt-dlp did not produce an info.json file for comments");
601
- }
602
-
603
- try {
604
- const raw = JSON.parse(readFileSync(infoPath, "utf8"));
605
- const comments: RawComment[] = [];
606
-
607
- if (Array.isArray(raw.comments)) {
608
- for (const c of raw.comments) {
609
- comments.push({
610
- author: c.author ?? null,
611
- author_id: c.author_id ?? null,
612
- text: typeof c.text === "string" ? c.text : String(c.text ?? ""),
613
- like_count: typeof c.like_count === "number" ? c.like_count : 0,
614
- timestamp: typeof c.timestamp === "number" ? c.timestamp : null,
615
- parent: c.parent === "root" ? null : (c.parent ?? null),
616
- id: c.id ?? crypto.randomUUID(),
617
- });
618
- }
619
- }
620
-
621
- return comments;
622
- } finally {
623
- try { unlinkFile(infoPath); } catch {}
624
- }
625
- }
626
-
627
- /**
628
- * Check whether yt-dlp is available on the system.
629
- */
630
- export async function checkYtDlp(): Promise<boolean> {
631
- try {
632
- const proc = Bun.spawn([ytdlp(), "--version"], { stdout: "pipe", stderr: "pipe" });
633
- const exitCode = await proc.exited;
634
- return exitCode === 0;
635
- } catch {
636
- return false;
637
- }
638
- }
@@ -1,62 +0,0 @@
1
- /**
2
- * RSS feed parsing for podcast auto-transcription.
3
- * Fetches RSS XML, extracts audio enclosure URLs for new episodes.
4
- */
5
-
6
- export interface FeedEpisode {
7
- title: string | null;
8
- url: string;
9
- published: string | null;
10
- duration: string | null;
11
- }
12
-
13
- export interface Feed {
14
- url: string;
15
- title: string | null;
16
- lastChecked: string | null;
17
- }
18
-
19
- /**
20
- * Fetch and parse an RSS feed, returning audio episodes.
21
- */
22
- export async function fetchFeedEpisodes(feedUrl: string): Promise<{ feedTitle: string | null; episodes: FeedEpisode[] }> {
23
- const res = await fetch(feedUrl);
24
- if (!res.ok) throw new Error(`Failed to fetch feed: ${res.status} ${res.statusText}`);
25
- const xml = await res.text();
26
- return parseRss(xml);
27
- }
28
-
29
- /**
30
- * Simple RSS XML parser — extracts items with audio enclosures.
31
- * No XML library needed — uses regex for the simple RSS structure.
32
- */
33
- function parseRss(xml: string): { feedTitle: string | null; episodes: FeedEpisode[] } {
34
- // Feed title
35
- const channelTitleMatch = xml.match(/<channel>[\s\S]*?<title>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/title>/);
36
- const feedTitle = channelTitleMatch?.[1]?.trim() ?? null;
37
-
38
- // Extract items
39
- const items = xml.match(/<item>[\s\S]*?<\/item>/g) ?? [];
40
- const episodes: FeedEpisode[] = [];
41
-
42
- for (const item of items) {
43
- // Find audio enclosure
44
- const enclosureMatch = item.match(/<enclosure[^>]+url=["']([^"']+)["'][^>]*type=["']audio\/[^"']+["']/i)
45
- || item.match(/<enclosure[^>]+type=["']audio\/[^"']+["'][^>]*url=["']([^"']+)["']/i);
46
- if (!enclosureMatch) continue;
47
-
48
- const url = enclosureMatch[1];
49
- const titleMatch = item.match(/<title>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/title>/);
50
- const pubDateMatch = item.match(/<pubDate>(.*?)<\/pubDate>/);
51
- const durationMatch = item.match(/<itunes:duration>(.*?)<\/itunes:duration>/);
52
-
53
- episodes.push({
54
- title: titleMatch?.[1]?.trim() ?? null,
55
- url,
56
- published: pubDateMatch?.[1]?.trim() ?? null,
57
- duration: durationMatch?.[1]?.trim() ?? null,
58
- });
59
- }
60
-
61
- return { feedTitle, episodes };
62
- }