@hasna/microservices 0.0.15 → 0.0.17

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 (296) hide show
  1. package/README.md +120 -138
  2. package/bin/index.js +703 -12728
  3. package/bin/mcp.js +250 -9726
  4. package/dist/index.js +170 -9407
  5. package/package.json +25 -25
  6. package/microservices/microservice-ads/package.json +0 -27
  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 -93
  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 -27
  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 -93
  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 -27
  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 -93
  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 -27
  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 -93
  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 -27
  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 -82
  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 -27
  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 -93
  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 -27
  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 -93
  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 -27
  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 -93
  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 -27
  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 -93
  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 -27
  77. package/microservices/microservice-crm/src/cli/index.ts +0 -396
  78. package/microservices/microservice-crm/src/db/database.ts +0 -82
  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 -27
  84. package/microservices/microservice-documents/src/cli/index.ts +0 -246
  85. package/microservices/microservice-documents/src/db/database.ts +0 -82
  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 -27
  91. package/microservices/microservice-domains/src/cli/index.ts +0 -1111
  92. package/microservices/microservice-domains/src/db/database.ts +0 -93
  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 -27
  102. package/microservices/microservice-expenses/src/cli/index.ts +0 -267
  103. package/microservices/microservice-expenses/src/db/database.ts +0 -82
  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 -27
  109. package/microservices/microservice-habits/src/cli/index.ts +0 -315
  110. package/microservices/microservice-habits/src/db/database.ts +0 -93
  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 -27
  116. package/microservices/microservice-health/src/cli/index.ts +0 -484
  117. package/microservices/microservice-health/src/db/database.ts +0 -93
  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 -27
  123. package/microservices/microservice-hiring/src/cli/index.ts +0 -741
  124. package/microservices/microservice-hiring/src/db/database.ts +0 -93
  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 -27
  131. package/microservices/microservice-inventory/src/cli/index.ts +0 -365
  132. package/microservices/microservice-inventory/src/db/database.ts +0 -82
  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/dist/assets/index-Bngq7FNM.css +0 -1
  138. package/microservices/microservice-invoices/dashboard/dist/assets/index-aHW4ARZR.js +0 -124
  139. package/microservices/microservice-invoices/dashboard/dist/index.html +0 -13
  140. package/microservices/microservice-invoices/dashboard/index.html +0 -12
  141. package/microservices/microservice-invoices/dashboard/package.json +0 -29
  142. package/microservices/microservice-invoices/dashboard/tsconfig.json +0 -14
  143. package/microservices/microservice-invoices/dashboard/vite.config.ts +0 -15
  144. package/microservices/microservice-invoices/package.json +0 -30
  145. package/microservices/microservice-invoices/src/cli/index.ts +0 -308
  146. package/microservices/microservice-invoices/src/db/business.ts +0 -241
  147. package/microservices/microservice-invoices/src/db/clients.ts +0 -127
  148. package/microservices/microservice-invoices/src/db/database.ts +0 -82
  149. package/microservices/microservice-invoices/src/db/invoices.ts +0 -345
  150. package/microservices/microservice-invoices/src/db/migrations.ts +0 -184
  151. package/microservices/microservice-invoices/src/index.ts +0 -56
  152. package/microservices/microservice-invoices/src/mcp/index.ts +0 -242
  153. package/microservices/microservice-invoices/src/server/index.ts +0 -162
  154. package/microservices/microservice-leads/package.json +0 -27
  155. package/microservices/microservice-leads/src/cli/index.ts +0 -596
  156. package/microservices/microservice-leads/src/db/database.ts +0 -93
  157. package/microservices/microservice-leads/src/db/leads.ts +0 -520
  158. package/microservices/microservice-leads/src/db/lists.ts +0 -151
  159. package/microservices/microservice-leads/src/db/migrations.ts +0 -93
  160. package/microservices/microservice-leads/src/index.ts +0 -65
  161. package/microservices/microservice-leads/src/lib/enrichment.ts +0 -202
  162. package/microservices/microservice-leads/src/lib/scoring.ts +0 -134
  163. package/microservices/microservice-leads/src/mcp/index.ts +0 -533
  164. package/microservices/microservice-notes/package.json +0 -27
  165. package/microservices/microservice-notes/src/cli/index.ts +0 -63
  166. package/microservices/microservice-notes/src/db/database.ts +0 -93
  167. package/microservices/microservice-notes/src/db/migrations.ts +0 -40
  168. package/microservices/microservice-notes/src/db/notes.ts +0 -114
  169. package/microservices/microservice-notes/src/index.ts +0 -2
  170. package/microservices/microservice-notes/src/mcp/index.ts +0 -37
  171. package/microservices/microservice-notifications/package.json +0 -27
  172. package/microservices/microservice-notifications/src/cli/index.ts +0 -349
  173. package/microservices/microservice-notifications/src/db/database.ts +0 -93
  174. package/microservices/microservice-notifications/src/db/migrations.ts +0 -62
  175. package/microservices/microservice-notifications/src/db/notifications.ts +0 -509
  176. package/microservices/microservice-notifications/src/index.ts +0 -41
  177. package/microservices/microservice-notifications/src/mcp/index.ts +0 -422
  178. package/microservices/microservice-payments/package.json +0 -27
  179. package/microservices/microservice-payments/src/cli/index.ts +0 -609
  180. package/microservices/microservice-payments/src/db/database.ts +0 -93
  181. package/microservices/microservice-payments/src/db/migrations.ts +0 -81
  182. package/microservices/microservice-payments/src/db/payments.ts +0 -1204
  183. package/microservices/microservice-payments/src/index.ts +0 -51
  184. package/microservices/microservice-payments/src/mcp/index.ts +0 -683
  185. package/microservices/microservice-payroll/package.json +0 -27
  186. package/microservices/microservice-payroll/src/cli/index.ts +0 -643
  187. package/microservices/microservice-payroll/src/db/database.ts +0 -93
  188. package/microservices/microservice-payroll/src/db/migrations.ts +0 -95
  189. package/microservices/microservice-payroll/src/db/payroll.ts +0 -1377
  190. package/microservices/microservice-payroll/src/index.ts +0 -48
  191. package/microservices/microservice-payroll/src/mcp/index.ts +0 -666
  192. package/microservices/microservice-products/package.json +0 -27
  193. package/microservices/microservice-products/src/cli/index.ts +0 -416
  194. package/microservices/microservice-products/src/db/categories.ts +0 -154
  195. package/microservices/microservice-products/src/db/database.ts +0 -93
  196. package/microservices/microservice-products/src/db/migrations.ts +0 -58
  197. package/microservices/microservice-products/src/db/pricing-tiers.ts +0 -66
  198. package/microservices/microservice-products/src/db/products.ts +0 -452
  199. package/microservices/microservice-products/src/index.ts +0 -53
  200. package/microservices/microservice-products/src/mcp/index.ts +0 -453
  201. package/microservices/microservice-projects/package.json +0 -27
  202. package/microservices/microservice-projects/src/cli/index.ts +0 -480
  203. package/microservices/microservice-projects/src/db/database.ts +0 -93
  204. package/microservices/microservice-projects/src/db/migrations.ts +0 -65
  205. package/microservices/microservice-projects/src/db/projects.ts +0 -715
  206. package/microservices/microservice-projects/src/index.ts +0 -57
  207. package/microservices/microservice-projects/src/mcp/index.ts +0 -501
  208. package/microservices/microservice-proposals/package.json +0 -27
  209. package/microservices/microservice-proposals/src/cli/index.ts +0 -400
  210. package/microservices/microservice-proposals/src/db/database.ts +0 -93
  211. package/microservices/microservice-proposals/src/db/migrations.ts +0 -52
  212. package/microservices/microservice-proposals/src/db/proposals.ts +0 -532
  213. package/microservices/microservice-proposals/src/index.ts +0 -37
  214. package/microservices/microservice-proposals/src/mcp/index.ts +0 -375
  215. package/microservices/microservice-reading/package.json +0 -27
  216. package/microservices/microservice-reading/src/cli/index.ts +0 -464
  217. package/microservices/microservice-reading/src/db/database.ts +0 -93
  218. package/microservices/microservice-reading/src/db/migrations.ts +0 -59
  219. package/microservices/microservice-reading/src/db/reading.ts +0 -524
  220. package/microservices/microservice-reading/src/index.ts +0 -51
  221. package/microservices/microservice-reading/src/mcp/index.ts +0 -368
  222. package/microservices/microservice-shipping/package.json +0 -27
  223. package/microservices/microservice-shipping/src/cli/index.ts +0 -606
  224. package/microservices/microservice-shipping/src/db/database.ts +0 -93
  225. package/microservices/microservice-shipping/src/db/migrations.ts +0 -69
  226. package/microservices/microservice-shipping/src/db/shipping.ts +0 -1093
  227. package/microservices/microservice-shipping/src/index.ts +0 -53
  228. package/microservices/microservice-shipping/src/mcp/index.ts +0 -533
  229. package/microservices/microservice-social/package.json +0 -28
  230. package/microservices/microservice-social/src/cli/index.ts +0 -1583
  231. package/microservices/microservice-social/src/db/database.ts +0 -93
  232. package/microservices/microservice-social/src/db/migrations.ts +0 -160
  233. package/microservices/microservice-social/src/db/social.ts +0 -1076
  234. package/microservices/microservice-social/src/index.ts +0 -46
  235. package/microservices/microservice-social/src/lib/audience.ts +0 -353
  236. package/microservices/microservice-social/src/lib/content-ai.ts +0 -278
  237. package/microservices/microservice-social/src/lib/media.ts +0 -311
  238. package/microservices/microservice-social/src/lib/mentions.ts +0 -434
  239. package/microservices/microservice-social/src/lib/metrics-sync.ts +0 -264
  240. package/microservices/microservice-social/src/lib/publisher.ts +0 -377
  241. package/microservices/microservice-social/src/lib/scheduler.ts +0 -229
  242. package/microservices/microservice-social/src/lib/sentiment.ts +0 -256
  243. package/microservices/microservice-social/src/lib/threads.ts +0 -291
  244. package/microservices/microservice-social/src/mcp/index.ts +0 -1425
  245. package/microservices/microservice-social/src/server/index.ts +0 -441
  246. package/microservices/microservice-subscriptions/package.json +0 -27
  247. package/microservices/microservice-subscriptions/src/cli/index.ts +0 -715
  248. package/microservices/microservice-subscriptions/src/db/database.ts +0 -93
  249. package/microservices/microservice-subscriptions/src/db/migrations.ts +0 -125
  250. package/microservices/microservice-subscriptions/src/db/subscriptions.ts +0 -1256
  251. package/microservices/microservice-subscriptions/src/index.ts +0 -41
  252. package/microservices/microservice-subscriptions/src/mcp/index.ts +0 -631
  253. package/microservices/microservice-timesheets/package.json +0 -27
  254. package/microservices/microservice-timesheets/src/cli/index.ts +0 -373
  255. package/microservices/microservice-timesheets/src/db/database.ts +0 -82
  256. package/microservices/microservice-timesheets/src/db/locale.ts +0 -217
  257. package/microservices/microservice-timesheets/src/db/migrations.ts +0 -74
  258. package/microservices/microservice-timesheets/src/db/timesheets.ts +0 -447
  259. package/microservices/microservice-timesheets/src/index.ts +0 -44
  260. package/microservices/microservice-timesheets/src/mcp/index.ts +0 -269
  261. package/microservices/microservice-transcriber/package.json +0 -28
  262. package/microservices/microservice-transcriber/src/cli/index.ts +0 -1593
  263. package/microservices/microservice-transcriber/src/db/annotations.ts +0 -37
  264. package/microservices/microservice-transcriber/src/db/comments.ts +0 -166
  265. package/microservices/microservice-transcriber/src/db/database.ts +0 -82
  266. package/microservices/microservice-transcriber/src/db/migrations.ts +0 -118
  267. package/microservices/microservice-transcriber/src/db/proofread.ts +0 -119
  268. package/microservices/microservice-transcriber/src/db/transcripts.ts +0 -395
  269. package/microservices/microservice-transcriber/src/index.ts +0 -43
  270. package/microservices/microservice-transcriber/src/lib/config.ts +0 -77
  271. package/microservices/microservice-transcriber/src/lib/diff.ts +0 -91
  272. package/microservices/microservice-transcriber/src/lib/downloader.ts +0 -638
  273. package/microservices/microservice-transcriber/src/lib/feeds.ts +0 -62
  274. package/microservices/microservice-transcriber/src/lib/live.ts +0 -94
  275. package/microservices/microservice-transcriber/src/lib/notion.ts +0 -129
  276. package/microservices/microservice-transcriber/src/lib/proofread.ts +0 -296
  277. package/microservices/microservice-transcriber/src/lib/providers.ts +0 -713
  278. package/microservices/microservice-transcriber/src/lib/summarizer.ts +0 -147
  279. package/microservices/microservice-transcriber/src/lib/translator.ts +0 -75
  280. package/microservices/microservice-transcriber/src/lib/webhook.ts +0 -37
  281. package/microservices/microservice-transcriber/src/mcp/index.ts +0 -1330
  282. package/microservices/microservice-transcriber/src/server/index.ts +0 -199
  283. package/microservices/microservice-travel/package.json +0 -27
  284. package/microservices/microservice-travel/src/cli/index.ts +0 -505
  285. package/microservices/microservice-travel/src/db/database.ts +0 -93
  286. package/microservices/microservice-travel/src/db/migrations.ts +0 -77
  287. package/microservices/microservice-travel/src/db/travel.ts +0 -802
  288. package/microservices/microservice-travel/src/index.ts +0 -60
  289. package/microservices/microservice-travel/src/mcp/index.ts +0 -495
  290. package/microservices/microservice-wiki/package.json +0 -27
  291. package/microservices/microservice-wiki/src/cli/index.ts +0 -345
  292. package/microservices/microservice-wiki/src/db/database.ts +0 -93
  293. package/microservices/microservice-wiki/src/db/migrations.ts +0 -55
  294. package/microservices/microservice-wiki/src/db/wiki.ts +0 -395
  295. package/microservices/microservice-wiki/src/index.ts +0 -32
  296. 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
- }