@hed-hog/lms 0.0.364 → 0.0.366

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 (290) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/class-group/class-group.controller.d.ts +1 -0
  6. package/dist/class-group/class-group.controller.d.ts.map +1 -1
  7. package/dist/class-group/class-group.service.d.ts +1 -0
  8. package/dist/class-group/class-group.service.d.ts.map +1 -1
  9. package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
  10. package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
  11. package/dist/course/course-export-scorm12-worker.service.js +109 -0
  12. package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
  13. package/dist/course/course-export-scorm12.service.d.ts +42 -0
  14. package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
  15. package/dist/course/course-export-scorm12.service.js +628 -0
  16. package/dist/course/course-export-scorm12.service.js.map +1 -0
  17. package/dist/course/course-export.service.d.ts +84 -0
  18. package/dist/course/course-export.service.d.ts.map +1 -0
  19. package/dist/course/course-export.service.js +237 -0
  20. package/dist/course/course-export.service.js.map +1 -0
  21. package/dist/course/course-structure.controller.d.ts +20 -10
  22. package/dist/course/course-structure.controller.d.ts.map +1 -1
  23. package/dist/course/course-structure.controller.js +20 -4
  24. package/dist/course/course-structure.controller.js.map +1 -1
  25. package/dist/course/course-structure.service.d.ts +12 -4
  26. package/dist/course/course-structure.service.d.ts.map +1 -1
  27. package/dist/course/course-structure.service.js +98 -23
  28. package/dist/course/course-structure.service.js.map +1 -1
  29. package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
  30. package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
  31. package/dist/course/course-video-agent-pipeline.service.js +398 -0
  32. package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
  33. package/dist/course/course-video-hls.service.d.ts +71 -0
  34. package/dist/course/course-video-hls.service.d.ts.map +1 -0
  35. package/dist/course/course-video-hls.service.js +784 -0
  36. package/dist/course/course-video-hls.service.js.map +1 -0
  37. package/dist/course/course.controller.d.ts +47 -13
  38. package/dist/course/course.controller.d.ts.map +1 -1
  39. package/dist/course/course.controller.js +40 -26
  40. package/dist/course/course.controller.js.map +1 -1
  41. package/dist/course/course.mcp-tools.js +1 -1
  42. package/dist/course/course.mcp-tools.js.map +1 -1
  43. package/dist/course/course.module.d.ts.map +1 -1
  44. package/dist/course/course.module.js +16 -0
  45. package/dist/course/course.module.js.map +1 -1
  46. package/dist/course/course.service.d.ts +8 -9
  47. package/dist/course/course.service.d.ts.map +1 -1
  48. package/dist/course/course.service.js +93 -50
  49. package/dist/course/course.service.js.map +1 -1
  50. package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
  51. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
  52. package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
  53. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
  54. package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
  55. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
  56. package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
  57. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -1
  58. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  59. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  60. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  61. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  62. package/dist/course/dto/create-course-export.dto.d.ts +14 -0
  63. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
  64. package/dist/course/dto/create-course-export.dto.js +71 -0
  65. package/dist/course/dto/create-course-export.dto.js.map +1 -0
  66. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
  67. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  68. package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
  69. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  70. package/dist/course/ffmpeg.util.d.ts +10 -0
  71. package/dist/course/ffmpeg.util.d.ts.map +1 -0
  72. package/dist/course/ffmpeg.util.js +79 -0
  73. package/dist/course/ffmpeg.util.js.map +1 -0
  74. package/dist/course/lms-bulk-upload-automation.service.d.ts +18 -1
  75. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  76. package/dist/course/lms-bulk-upload-automation.service.js +106 -8
  77. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  78. package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
  79. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
  80. package/dist/course/lms-bulk-upload-infra.service.js +32 -8
  81. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
  82. package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
  83. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  84. package/dist/course/lms-bulk-upload.controller.js +43 -2
  85. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  86. package/dist/course/lms-bulk-upload.service.d.ts +11 -0
  87. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  88. package/dist/course/lms-bulk-upload.service.js +59 -6
  89. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  90. package/dist/course/lms-setting.controller.d.ts +2 -1
  91. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  92. package/dist/course/lms-setting.controller.js +4 -2
  93. package/dist/course/lms-setting.controller.js.map +1 -1
  94. package/dist/course/scorm12-schemas.d.ts +4 -0
  95. package/dist/course/scorm12-schemas.d.ts.map +1 -0
  96. package/dist/course/scorm12-schemas.js +9 -0
  97. package/dist/course/scorm12-schemas.js.map +1 -0
  98. package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
  99. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  100. package/dist/enterprise/training/training-admin.service.d.ts +2 -0
  101. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  102. package/dist/enterprise/training/training-student.service.d.ts +51 -0
  103. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  104. package/dist/enterprise/training/training-student.service.js +217 -4
  105. package/dist/enterprise/training/training-student.service.js.map +1 -1
  106. package/dist/evaluation/evaluation.service.d.ts +18 -0
  107. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  108. package/dist/evaluation/evaluation.service.js +125 -0
  109. package/dist/evaluation/evaluation.service.js.map +1 -1
  110. package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
  111. package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
  112. package/dist/exam/dto/create-standalone-question.dto.js +70 -0
  113. package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
  114. package/dist/exam/exam.module.d.ts.map +1 -1
  115. package/dist/exam/exam.module.js +2 -1
  116. package/dist/exam/exam.module.js.map +1 -1
  117. package/dist/exam/exam.service.d.ts +21 -0
  118. package/dist/exam/exam.service.d.ts.map +1 -1
  119. package/dist/exam/exam.service.js +80 -0
  120. package/dist/exam/exam.service.js.map +1 -1
  121. package/dist/exam/question.controller.d.ts +27 -0
  122. package/dist/exam/question.controller.d.ts.map +1 -0
  123. package/dist/exam/question.controller.js +53 -0
  124. package/dist/exam/question.controller.js.map +1 -0
  125. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
  126. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  127. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +161 -25
  128. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  129. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  130. package/dist/lms-commerce-access.subscriber.d.ts +11 -0
  131. package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
  132. package/dist/lms-commerce-access.subscriber.js +74 -0
  133. package/dist/lms-commerce-access.subscriber.js.map +1 -0
  134. package/dist/lms.module.d.ts.map +1 -1
  135. package/dist/lms.module.js +16 -5
  136. package/dist/lms.module.js.map +1 -1
  137. package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
  138. package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
  139. package/dist/platforma/dto/heartbeat.dto.js +50 -0
  140. package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
  141. package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
  142. package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
  143. package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
  144. package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
  145. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
  146. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
  147. package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
  148. package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
  149. package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
  150. package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
  151. package/dist/platforma/platforma-heartbeat.service.js +50 -0
  152. package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
  153. package/dist/platforma/platforma-performance.service.d.ts +121 -0
  154. package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
  155. package/dist/platforma/platforma-performance.service.js +500 -0
  156. package/dist/platforma/platforma-performance.service.js.map +1 -0
  157. package/dist/platforma/platforma-search.service.d.ts +21 -0
  158. package/dist/platforma/platforma-search.service.d.ts.map +1 -0
  159. package/dist/platforma/platforma-search.service.js +64 -0
  160. package/dist/platforma/platforma-search.service.js.map +1 -0
  161. package/dist/platforma/platforma-video.service.d.ts +39 -0
  162. package/dist/platforma/platforma-video.service.d.ts.map +1 -0
  163. package/dist/platforma/platforma-video.service.js +301 -0
  164. package/dist/platforma/platforma-video.service.js.map +1 -0
  165. package/dist/platforma/platforma.controller.d.ts +209 -1
  166. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  167. package/dist/platforma/platforma.controller.js +208 -2
  168. package/dist/platforma/platforma.controller.js.map +1 -1
  169. package/dist/realtime/lms-realtime.controller.d.ts +2 -0
  170. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
  171. package/dist/realtime/lms-realtime.controller.js +31 -0
  172. package/dist/realtime/lms-realtime.controller.js.map +1 -1
  173. package/dist/realtime/lms-realtime.service.d.ts +1 -1
  174. package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
  175. package/dist/realtime/lms-realtime.service.js.map +1 -1
  176. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
  177. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
  178. package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
  179. package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
  180. package/dist/student-xp/student-xp.controller.d.ts +15 -0
  181. package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
  182. package/dist/student-xp/student-xp.controller.js +24 -0
  183. package/dist/student-xp/student-xp.controller.js.map +1 -1
  184. package/dist/student-xp/student-xp.service.d.ts +16 -0
  185. package/dist/student-xp/student-xp.service.d.ts.map +1 -1
  186. package/dist/student-xp/student-xp.service.js +51 -1
  187. package/dist/student-xp/student-xp.service.js.map +1 -1
  188. package/hedhog/data/evaluation_topic.yaml +17 -0
  189. package/hedhog/data/menu.yaml +0 -17
  190. package/hedhog/data/queue_definition.yaml +48 -0
  191. package/hedhog/data/route.yaml +94 -124
  192. package/hedhog/data/setting_group.yaml +19 -19
  193. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
  194. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
  195. package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
  196. package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
  197. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
  198. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
  199. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
  200. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +158 -45
  201. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
  202. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
  203. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
  204. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
  205. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
  206. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
  207. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
  208. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
  209. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
  210. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
  211. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
  212. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
  213. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
  214. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
  215. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
  216. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -1
  217. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
  218. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
  219. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
  220. package/hedhog/frontend/app/courses/page.tsx.ejs +85 -9
  221. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
  222. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
  223. package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
  224. package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
  225. package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
  226. package/hedhog/frontend/app/paths/page.tsx.ejs +38 -4
  227. package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
  228. package/hedhog/frontend/messages/en.json +44 -28
  229. package/hedhog/frontend/messages/pt.json +47 -29
  230. package/hedhog/table/course_enrollment.yaml +3 -0
  231. package/hedhog/table/course_export.yaml +62 -0
  232. package/hedhog/table/lesson_view_event.yaml +66 -0
  233. package/package.json +14 -9
  234. package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
  235. package/src/course/course-export-scorm12-worker.service.ts +124 -0
  236. package/src/course/course-export-scorm12.service.ts +668 -0
  237. package/src/course/course-export.service.ts +280 -0
  238. package/src/course/course-structure.controller.ts +16 -2
  239. package/src/course/course-structure.service.ts +100 -7
  240. package/src/course/course-video-agent-pipeline.service.ts +471 -0
  241. package/src/course/course-video-hls.service.ts +966 -0
  242. package/src/course/course.controller.ts +33 -19
  243. package/src/course/course.mcp-tools.ts +1 -1
  244. package/src/course/course.module.ts +16 -0
  245. package/src/course/course.service.ts +119 -61
  246. package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
  247. package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
  248. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  249. package/src/course/dto/create-course-export.dto.ts +56 -0
  250. package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
  251. package/src/course/ffmpeg.util.ts +65 -0
  252. package/src/course/lms-bulk-upload-automation.service.ts +156 -6
  253. package/src/course/lms-bulk-upload-infra.service.ts +39 -6
  254. package/src/course/lms-bulk-upload.controller.ts +32 -2
  255. package/src/course/lms-bulk-upload.service.ts +70 -7
  256. package/src/course/lms-setting.controller.ts +4 -2
  257. package/src/course/scorm12-schemas.ts +9 -0
  258. package/src/enterprise/training/training-student.service.ts +221 -2
  259. package/src/evaluation/evaluation.service.ts +123 -0
  260. package/src/exam/dto/create-standalone-question.dto.ts +66 -0
  261. package/src/exam/exam.module.ts +2 -1
  262. package/src/exam/exam.service.ts +86 -0
  263. package/src/exam/question.controller.ts +28 -0
  264. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
  265. package/src/lms-commerce-access.subscriber.ts +88 -0
  266. package/src/lms.module.ts +16 -5
  267. package/src/platforma/dto/heartbeat.dto.ts +30 -0
  268. package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
  269. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
  270. package/src/platforma/platforma-heartbeat.service.ts +33 -0
  271. package/src/platforma/platforma-performance.service.ts +606 -0
  272. package/src/platforma/platforma-search.service.ts +48 -0
  273. package/src/platforma/platforma-video.service.ts +346 -0
  274. package/src/platforma/platforma.controller.ts +137 -1
  275. package/src/platforma/platforma.service.ts +268 -268
  276. package/src/realtime/lms-realtime.controller.ts +27 -1
  277. package/src/realtime/lms-realtime.service.ts +2 -1
  278. package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
  279. package/src/student-xp/student-xp.controller.ts +18 -2
  280. package/src/student-xp/student-xp.service.ts +84 -2
  281. package/hedhog/data/video_resolution_profile.yaml +0 -7
  282. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
  283. package/hedhog/table/course_video_resolution_profile.yaml +0 -22
  284. package/hedhog/table/video_resolution_profile.yaml +0 -18
  285. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
  286. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
  287. package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
  288. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
  289. package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
  290. package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
@@ -5,23 +5,26 @@ import {
5
5
  Controller,
6
6
  Delete,
7
7
  Get,
8
- HttpCode,
9
- HttpStatus,
10
8
  Param,
11
9
  ParseIntPipe,
12
10
  Patch,
13
11
  Post,
14
12
  Query,
15
13
  } from '@nestjs/common';
14
+ import { CourseExportService } from './course-export.service';
16
15
  import { CourseService } from './course.service';
17
- import { CreateCourseDto } from './dto/create-course.dto';
18
16
  import { CleanupCourseStorageDto } from './dto/cleanup-course-storage.dto';
17
+ import { CreateCourseDto } from './dto/create-course.dto';
18
+ import { CreateCourseExportDto } from './dto/create-course-export.dto';
19
19
  import { UpdateCourseDto } from './dto/update-course.dto';
20
20
 
21
21
  @Role()
22
22
  @Controller('lms/courses')
23
23
  export class CourseController {
24
- constructor(private readonly courseService: CourseService) {}
24
+ constructor(
25
+ private readonly courseService: CourseService,
26
+ private readonly courseExportService: CourseExportService,
27
+ ) {}
25
28
 
26
29
  @Get()
27
30
  list(
@@ -83,6 +86,32 @@ export class CourseController {
83
86
  );
84
87
  }
85
88
 
89
+ @Get(':id/exports')
90
+ listExports(@Param('id', ParseIntPipe) id: number) {
91
+ return this.courseExportService.listExports(id);
92
+ }
93
+
94
+ @Post(':id/exports')
95
+ createExport(
96
+ @Param('id', ParseIntPipe) id: number,
97
+ @Body() dto: CreateCourseExportDto,
98
+ @User() user: any,
99
+ ) {
100
+ return this.courseExportService.enqueueScorm12Export(
101
+ id,
102
+ Number(user?.id || 0),
103
+ dto,
104
+ );
105
+ }
106
+
107
+ @Delete(':id/exports/:exportId')
108
+ deleteExport(
109
+ @Param('id', ParseIntPipe) id: number,
110
+ @Param('exportId', ParseIntPipe) exportId: number,
111
+ ) {
112
+ return this.courseExportService.deleteExport(id, exportId);
113
+ }
114
+
86
115
  @Get(':id')
87
116
  getById(@Param('id', ParseIntPipe) id: number) {
88
117
  return this.courseService.getById(id);
@@ -106,19 +135,4 @@ export class CourseController {
106
135
  return this.courseService.remove(id, Number(user?.id || 0) || null);
107
136
  }
108
137
 
109
- @Get(':courseId/video-resolution-profiles')
110
- getCourseVideoProfiles(
111
- @Param('courseId', ParseIntPipe) courseId: number,
112
- ) {
113
- return this.courseService.getCourseVideoProfiles(courseId);
114
- }
115
-
116
- @Post(':courseId/video-resolution-profiles/sync')
117
- @HttpCode(HttpStatus.OK)
118
- syncCourseVideoProfiles(
119
- @Param('courseId', ParseIntPipe) courseId: number,
120
- @Body() body: { profileIds: number[] },
121
- ) {
122
- return this.courseService.syncCourseVideoProfiles(courseId, body.profileIds ?? []);
123
- }
124
138
  }
@@ -244,7 +244,7 @@ export class LmsCoursesMcpTools {
244
244
  courseId: { type: 'number', description: 'Course ID' },
245
245
  sessionId: { type: 'number', description: 'Session ID' },
246
246
  titulo: { type: 'string', description: 'Lesson title (required, max 255)' },
247
- tipo: { type: 'string', description: 'Lesson type (required): video|questao|post|exercicio' },
247
+ tipo: { type: 'string', description: 'Lesson type (required): video|questao|post' },
248
248
  descricaoPublica: { type: 'string', description: 'Public description' },
249
249
  descricaoPrivada: { type: 'string', description: 'Private description' },
250
250
  duracao: { type: 'number', description: 'Duration in seconds' },
@@ -1,3 +1,4 @@
1
+ import { AgentModule } from '@hed-hog/agent';
1
2
  import { PrismaModule } from '@hed-hog/api-prisma';
2
3
  import { CoreModule } from '@hed-hog/core';
3
4
  import { QueueModule } from '@hed-hog/queue';
@@ -8,11 +9,17 @@ import { CourseOperationsIntegrationService } from './course-operations-integrat
8
9
  import { CourseOperationsController } from './course-operations.controller';
9
10
  import { CourseStructureController } from './course-structure.controller';
10
11
  import { CourseStructureService } from './course-structure.service';
12
+ import { CourseVideoAgentPipelineService } from './course-video-agent-pipeline.service';
11
13
  import { CourseVideoConversionService } from './course-video-conversion.service';
14
+ import { CourseVideoHlsService } from './course-video-hls.service';
15
+ import { CourseExportScorm12Service } from './course-export-scorm12.service';
16
+ import { CourseExportScorm12WorkerService } from './course-export-scorm12-worker.service';
17
+ import { CourseExportService } from './course-export.service';
12
18
  import { CourseController } from './course.controller';
13
19
  import { LmsCoursesMcpTools } from './course.mcp-tools';
14
20
  import { LmsBulkUploadInfraService } from './lms-bulk-upload-infra.service';
15
21
  import { CourseService } from './course.service';
22
+ import { LmsBulkUploadAutomationService } from './lms-bulk-upload-automation.service';
16
23
  import { LmsBulkUploadController } from './lms-bulk-upload.controller';
17
24
  import { LmsBulkUploadService } from './lms-bulk-upload.service';
18
25
  import { LmsOperationsTaskSubscriber } from './lms-operations-task.subscriber';
@@ -24,6 +31,7 @@ import { LmsSettingController } from './lms-setting.controller';
24
31
  forwardRef(() => InstructorModule),
25
32
  forwardRef(() => CoreModule),
26
33
  forwardRef(() => QueueModule),
34
+ forwardRef(() => AgentModule),
27
35
  ],
28
36
  controllers: [
29
37
  CourseController,
@@ -34,10 +42,16 @@ import { LmsSettingController } from './lms-setting.controller';
34
42
  LmsBulkUploadController,
35
43
  ],
36
44
  providers: [
45
+ CourseExportService,
46
+ CourseExportScorm12Service,
47
+ CourseExportScorm12WorkerService,
37
48
  CourseOperationsIntegrationService,
38
49
  CourseService,
39
50
  CourseStructureService,
51
+ CourseVideoAgentPipelineService,
40
52
  CourseVideoConversionService,
53
+ CourseVideoHlsService,
54
+ LmsBulkUploadAutomationService,
41
55
  LmsBulkUploadInfraService,
42
56
  LmsBulkUploadService,
43
57
  LmsCoursesMcpTools,
@@ -47,6 +61,8 @@ import { LmsSettingController } from './lms-setting.controller';
47
61
  forwardRef(() => CourseService),
48
62
  forwardRef(() => CourseStructureService),
49
63
  forwardRef(() => CourseVideoConversionService),
64
+ forwardRef(() => CourseVideoHlsService),
65
+ CourseVideoAgentPipelineService,
50
66
  ],
51
67
  })
52
68
  export class CourseModule {}
@@ -16,8 +16,8 @@ import {
16
16
  } from '@nestjs/common';
17
17
  import { CourseOperationsIntegrationService } from './course-operations-integration.service';
18
18
  import {
19
- COURSE_STORAGE_CLEANUP_CATEGORIES,
20
- type CourseStorageCleanupCategory,
19
+ COURSE_STORAGE_CLEANUP_CATEGORIES,
20
+ type CourseStorageCleanupCategory,
21
21
  } from './dto/cleanup-course-storage.dto';
22
22
  import { CreateCourseDto } from './dto/create-course.dto';
23
23
  import { UpdateCourseDto } from './dto/update-course.dto';
@@ -480,10 +480,12 @@ export class CourseService implements OnModuleInit, IJobHandler {
480
480
  ]);
481
481
 
482
482
  const courseIds = courses.map((course) => course.id);
483
- const [extrasById, projectLinksById] = await Promise.all([
484
- this.getCourseExtras(courseIds),
485
- this.operationsIntegration.getCourseProjectLinks(courseIds),
486
- ]);
483
+ const [extrasById, projectLinksById, coursesWithRunningJobs] =
484
+ await Promise.all([
485
+ this.getCourseExtras(courseIds),
486
+ this.operationsIntegration.getCourseProjectLinks(courseIds),
487
+ this.getCoursesWithRunningJobs(courseIds),
488
+ ]);
487
489
 
488
490
  return {
489
491
  total,
@@ -496,6 +498,8 @@ export class CourseService implements OnModuleInit, IJobHandler {
496
498
  undefined,
497
499
  extrasById.get(c.id),
498
500
  projectLinksById.get(c.id),
501
+ undefined,
502
+ coursesWithRunningJobs.has(c.id),
499
503
  ),
500
504
  ),
501
505
  };
@@ -1169,6 +1173,7 @@ export class CourseService implements OnModuleInit, IJobHandler {
1169
1173
  course_image: 'Imagens do curso',
1170
1174
  course_file: 'Arquivos do curso',
1171
1175
  other_lesson_file: 'Outros arquivos',
1176
+ course_export: 'Arquivos de exportação',
1172
1177
  };
1173
1178
 
1174
1179
  return labels[category];
@@ -1265,6 +1270,30 @@ export class CourseService implements OnModuleInit, IJobHandler {
1265
1270
  return;
1266
1271
  }
1267
1272
 
1273
+ if (category === 'course_export') {
1274
+ const exports = await this.prisma.course_export.findMany({
1275
+ where: {
1276
+ course_id: courseId,
1277
+ file_id: { not: null },
1278
+ status: { in: ['completed', 'failed'] },
1279
+ },
1280
+ select: { id: true, file_id: true },
1281
+ });
1282
+
1283
+ const fileIds = exports
1284
+ .map((e) => e.file_id)
1285
+ .filter((id): id is number => id != null);
1286
+
1287
+ await this.prisma.course_export.deleteMany({
1288
+ where: { id: { in: exports.map((e) => e.id) } },
1289
+ });
1290
+
1291
+ if (fileIds.length > 0) {
1292
+ await this.fileService.delete('pt', { ids: fileIds }).catch(() => undefined);
1293
+ }
1294
+ return;
1295
+ }
1296
+
1268
1297
  await this.prisma.course_lesson_file.deleteMany({
1269
1298
  where: {
1270
1299
  course_lesson: { course_module: { course_id: courseId } },
@@ -1315,6 +1344,15 @@ export class CourseService implements OnModuleInit, IJobHandler {
1315
1344
  INNER JOIN course_lesson cl ON cl.id = clvf.course_lesson_id
1316
1345
  INNER JOIN course_module cm ON cm.id = cl.course_module_id
1317
1346
  WHERE cm.course_id = ${courseId}
1347
+
1348
+ UNION ALL
1349
+
1350
+ SELECT ce.file_id, f.size AS file_size, 'course_export' AS storage_category
1351
+ FROM course_export ce
1352
+ INNER JOIN file f ON f.id = ce.file_id
1353
+ WHERE ce.course_id = ${courseId}
1354
+ AND ce.status = 'completed'
1355
+ AND ce.file_id IS NOT NULL
1318
1356
  ) AS files
1319
1357
  WHERE files.file_id IS NOT NULL
1320
1358
  `;
@@ -1463,6 +1501,7 @@ export class CourseService implements OnModuleInit, IJobHandler {
1463
1501
  } | null;
1464
1502
  } | null;
1465
1503
  }>,
1504
+ hasRunningJobs = false,
1466
1505
  ) {
1467
1506
  const resolvedName = extras?.name ?? c.name ?? null;
1468
1507
  const resolvedTitle = this.normalizeOptionalText(c.title) ?? c.slug;
@@ -1549,6 +1588,7 @@ export class CourseService implements OnModuleInit, IJobHandler {
1549
1588
  (c.certificate_template_id
1550
1589
  ? String(c.certificate_template_id)
1551
1590
  : null),
1591
+ hasRunningJobs,
1552
1592
  progressByModule: metrics?.progressByModule ?? [],
1553
1593
  instructors: Array.from(
1554
1594
  new Map(
@@ -1624,6 +1664,25 @@ export class CourseService implements OnModuleInit, IJobHandler {
1624
1664
  return extrasById;
1625
1665
  }
1626
1666
 
1667
+ private async getCoursesWithRunningJobs(
1668
+ courseIds: number[],
1669
+ ): Promise<Set<number>> {
1670
+ if (courseIds.length === 0) return new Set();
1671
+
1672
+ const rows = (await this.prisma.$queryRawUnsafe(`
1673
+ SELECT DISTINCT cm.course_id
1674
+ FROM queue_job qj
1675
+ INNER JOIN course_lesson cl ON cl.id = CAST(qj.source_entity_id AS INTEGER)
1676
+ INNER JOIN course_module cm ON cm.id = cl.course_module_id
1677
+ WHERE qj.source_entity = 'course_lesson'
1678
+ AND qj.source_module = 'lms'
1679
+ AND qj.status IN ('pending', 'scheduled', 'processing', 'retrying')
1680
+ AND cm.course_id IN (${courseIds.join(',')})
1681
+ `)) as Array<{ course_id: number }>;
1682
+
1683
+ return new Set(rows.map((r) => Number(r.course_id)));
1684
+ }
1685
+
1627
1686
  private async persistCourseExtras(
1628
1687
  id: number,
1629
1688
  data: PersistCourseExtrasInput,
@@ -1653,7 +1712,8 @@ export class CourseService implements OnModuleInit, IJobHandler {
1653
1712
  }
1654
1713
 
1655
1714
  if (data.offeringType !== undefined) {
1656
- pushSetClause('offering_type', data.offeringType);
1715
+ values.push(data.offeringType);
1716
+ setClauses.push(`offering_type = $${values.length}::course_offering_type_8af4d2739d_enum`);
1657
1717
  }
1658
1718
 
1659
1719
  if (setClauses.length === 0) {
@@ -1670,8 +1730,8 @@ export class CourseService implements OnModuleInit, IJobHandler {
1670
1730
  `,
1671
1731
  ...values,
1672
1732
  );
1673
- } catch {
1674
- // Some environments may still be behind the current course schema.
1733
+ } catch (err) {
1734
+ console.error('[persistCourseExtras] failed:', err);
1675
1735
  }
1676
1736
  }
1677
1737
 
@@ -1836,53 +1896,6 @@ export class CourseService implements OnModuleInit, IJobHandler {
1836
1896
  });
1837
1897
  }
1838
1898
 
1839
- async getCourseVideoProfiles(courseId: number) {
1840
- const rows = await (this.prisma as any).course_video_resolution_profile.findMany({
1841
- where: { course_id: courseId },
1842
- include: { video_resolution_profile: true },
1843
- orderBy: { video_resolution_profile: { name: 'asc' } },
1844
- });
1845
-
1846
- return (rows as any[]).map((row) => ({
1847
- id: row.video_resolution_profile.id,
1848
- name: row.video_resolution_profile.name,
1849
- ffmpeg_params: row.video_resolution_profile.ffmpeg_params,
1850
- status: row.video_resolution_profile.status,
1851
- }));
1852
- }
1853
-
1854
- async syncCourseVideoProfiles(courseId: number, profileIds: number[]) {
1855
- const linkClient = (this.prisma as any).course_video_resolution_profile;
1856
-
1857
- await this.prisma.$transaction(async (tx: any) => {
1858
- await tx.course_video_resolution_profile.deleteMany({
1859
- where: { course_id: courseId },
1860
- });
1861
-
1862
- if (profileIds.length > 0) {
1863
- await tx.course_video_resolution_profile.createMany({
1864
- data: profileIds.map((id) => ({
1865
- course_id: courseId,
1866
- video_resolution_profile_id: id,
1867
- })),
1868
- });
1869
- }
1870
- });
1871
-
1872
- return linkClient.findMany({
1873
- where: { course_id: courseId },
1874
- include: { video_resolution_profile: true },
1875
- orderBy: { video_resolution_profile: { name: 'asc' } },
1876
- }).then((rows: any[]) =>
1877
- rows.map((row: any) => ({
1878
- id: row.video_resolution_profile.id,
1879
- name: row.video_resolution_profile.name,
1880
- ffmpeg_params: row.video_resolution_profile.ffmpeg_params,
1881
- status: row.video_resolution_profile.status,
1882
- })),
1883
- );
1884
- }
1885
-
1886
1899
  async getCourseContentOverview(courseId: number) {
1887
1900
  const [
1888
1901
  moduleCount,
@@ -1892,6 +1905,7 @@ export class CourseService implements OnModuleInit, IJobHandler {
1892
1905
  extractedImageCount,
1893
1906
  resourceFileCount,
1894
1907
  storageRows,
1908
+ videoFileRows,
1895
1909
  ] = await Promise.all([
1896
1910
  this.prisma.course_module.count({ where: { course_id: courseId } }),
1897
1911
  this.prisma.course_lesson.findMany({
@@ -1917,6 +1931,17 @@ export class CourseService implements OnModuleInit, IJobHandler {
1917
1931
  },
1918
1932
  }),
1919
1933
  this.getCourseStorageRows(courseId),
1934
+ this.prisma.course_lesson_file.findMany({
1935
+ where: {
1936
+ course_lesson: { course_module: { course_id: courseId } },
1937
+ OR: [
1938
+ { type: 'video_original' },
1939
+ { type: { startsWith: 'video_profile:' } },
1940
+ ],
1941
+ file_id: { not: null },
1942
+ },
1943
+ select: { course_lesson_id: true, type: true },
1944
+ }),
1920
1945
  ]);
1921
1946
 
1922
1947
  const transcriptionLessonIds = new Set(
@@ -1924,10 +1949,23 @@ export class CourseService implements OnModuleInit, IJobHandler {
1924
1949
  );
1925
1950
  const xpLessonIds = new Set(xpMapRows.map((r) => r.course_lesson_id));
1926
1951
 
1927
- const lessonsByType = { video: 0, questao: 0, post: 0, exercicio: 0 };
1952
+ const videoOriginalLessonIds = new Set(
1953
+ videoFileRows
1954
+ .filter((r) => r.type === 'video_original')
1955
+ .map((r) => r.course_lesson_id),
1956
+ );
1957
+ const videoProfileLessonIds = new Set(
1958
+ videoFileRows
1959
+ .filter((r) => r.type?.startsWith('video_profile:'))
1960
+ .map((r) => r.course_lesson_id),
1961
+ );
1962
+
1963
+ const lessonsByType = { video: 0, questao: 0, post: 0 };
1928
1964
  let publishedLessonCount = 0;
1929
1965
  let videoWithTranscription = 0;
1930
1966
  let videoWithXp = 0;
1967
+ let videoWithVideo = 0;
1968
+ let videoWithProcessedVideo = 0;
1931
1969
 
1932
1970
  const categoryOrder = [
1933
1971
  'video_original',
@@ -1939,6 +1977,7 @@ export class CourseService implements OnModuleInit, IJobHandler {
1939
1977
  'course_image',
1940
1978
  'course_file',
1941
1979
  'other_lesson_file',
1980
+ 'course_export',
1942
1981
  ];
1943
1982
 
1944
1983
  const uniqueStorageFiles = new Map<number, number>();
@@ -1995,8 +2034,9 @@ export class CourseService implements OnModuleInit, IJobHandler {
1995
2034
  if (lesson.published) publishedLessonCount++;
1996
2035
 
1997
2036
  let sourceType: string | undefined;
2037
+ let parsed: Record<string, unknown> | null = null;
1998
2038
  try {
1999
- const parsed = lesson.content
2039
+ parsed = lesson.content
2000
2040
  ? (JSON.parse(lesson.content as string) as Record<string, unknown>)
2001
2041
  : null;
2002
2042
  sourceType =
@@ -2007,12 +2047,11 @@ export class CourseService implements OnModuleInit, IJobHandler {
2007
2047
  // ignore malformed content
2008
2048
  }
2009
2049
 
2010
- let uiType: 'video' | 'questao' | 'post' | 'exercicio';
2050
+ let uiType: 'video' | 'questao' | 'post';
2011
2051
  if (
2012
2052
  sourceType === 'video' ||
2013
2053
  sourceType === 'questao' ||
2014
- sourceType === 'post' ||
2015
- sourceType === 'exercicio'
2054
+ sourceType === 'post'
2016
2055
  ) {
2017
2056
  uiType = sourceType;
2018
2057
  } else if (lesson.type === 'video') {
@@ -2028,6 +2067,23 @@ export class CourseService implements OnModuleInit, IJobHandler {
2028
2067
  if (uiType === 'video') {
2029
2068
  if (transcriptionLessonIds.has(lesson.id)) videoWithTranscription++;
2030
2069
  if (xpLessonIds.has(lesson.id)) videoWithXp++;
2070
+
2071
+ const videoUrl =
2072
+ typeof parsed?.videoUrl === 'string' ? parsed.videoUrl : '';
2073
+ const videoProvedor =
2074
+ typeof parsed?.videoProvedor === 'string'
2075
+ ? parsed.videoProvedor
2076
+ : 'file_storage';
2077
+
2078
+ const hasVideo =
2079
+ videoUrl.length > 0 || videoOriginalLessonIds.has(lesson.id);
2080
+ const isProcessed =
2081
+ videoProvedor !== 'file_storage'
2082
+ ? videoUrl.length > 0
2083
+ : videoProfileLessonIds.has(lesson.id);
2084
+
2085
+ if (hasVideo) videoWithVideo++;
2086
+ if (isProcessed) videoWithProcessedVideo++;
2031
2087
  }
2032
2088
  }
2033
2089
 
@@ -2042,6 +2098,8 @@ export class CourseService implements OnModuleInit, IJobHandler {
2042
2098
  lessonCount: lessonsByType.video,
2043
2099
  withTranscription: videoWithTranscription,
2044
2100
  withXp: videoWithXp,
2101
+ withVideo: videoWithVideo,
2102
+ withProcessedVideo: videoWithProcessedVideo,
2045
2103
  },
2046
2104
  media: { extractedImageCount },
2047
2105
  resources: { fileCount: resourceFileCount },
@@ -10,6 +10,7 @@ export const COURSE_STORAGE_CLEANUP_CATEGORIES = [
10
10
  'course_image',
11
11
  'course_file',
12
12
  'other_lesson_file',
13
+ 'course_export',
13
14
  ] as const;
14
15
 
15
16
  export type CourseStorageCleanupCategory =
@@ -1,6 +1,6 @@
1
1
  import { ArrayNotEmpty, IsArray, IsIn, IsOptional } from 'class-validator';
2
2
 
3
- export const BULK_UPLOAD_CLEANUP_STATUSES = ['done', 'error', 'cancelled'] as const;
3
+ export const BULK_UPLOAD_CLEANUP_STATUSES = ['received', 'done', 'error', 'cancelled'] as const;
4
4
  export const BULK_UPLOAD_CLEANUP_WINDOWS = [
5
5
  'last_hour',
6
6
  'last_day',
@@ -1,6 +1,10 @@
1
- import { IsIn } from 'class-validator';
1
+ import { IsBoolean, IsIn, IsOptional } from 'class-validator';
2
2
 
3
3
  export class CreateCourseBulkJobDto {
4
- @IsIn(['transcription', 'xp_recalculation'])
5
- jobType: 'transcription' | 'xp_recalculation';
4
+ @IsIn(['transcription', 'xp_recalculation', 'video_processing'])
5
+ jobType: 'transcription' | 'xp_recalculation' | 'video_processing';
6
+
7
+ @IsBoolean()
8
+ @IsOptional()
9
+ reprocessAlreadyProcessed?: boolean;
6
10
  }
@@ -0,0 +1,56 @@
1
+ import { Type } from 'class-transformer';
2
+ import {
3
+ IsIn,
4
+ IsInt,
5
+ IsOptional,
6
+ IsString,
7
+ Matches,
8
+ Max,
9
+ Min,
10
+ ValidateNested,
11
+ } from 'class-validator';
12
+
13
+ export class ScormVisualSettingsDto {
14
+ @IsOptional()
15
+ @IsString()
16
+ @Matches(/^#[0-9a-fA-F]{6}$/, {
17
+ message: 'primaryColor deve ser uma cor hex válida (ex: #2563eb)',
18
+ })
19
+ primaryColor?: string;
20
+
21
+ @IsOptional()
22
+ @IsIn(['system', 'humanist', 'serif', 'mono'])
23
+ fontFamily?: string;
24
+
25
+ @IsOptional()
26
+ @IsIn(['sm', 'md', 'lg'])
27
+ fontSize?: string;
28
+
29
+ @IsOptional()
30
+ @IsInt()
31
+ @Min(200)
32
+ @Max(420)
33
+ sidebarWidth?: number;
34
+
35
+ @IsOptional()
36
+ @IsIn(['left', 'right'])
37
+ sidebarPosition?: string;
38
+
39
+ @IsOptional()
40
+ @IsIn(['bar', 'pie'])
41
+ progressStyle?: string;
42
+
43
+ @IsOptional()
44
+ @IsIn(['light', 'dark'])
45
+ sidebarTheme?: string;
46
+ }
47
+
48
+ export class CreateCourseExportDto {
49
+ @IsIn(['scorm_1_2'])
50
+ format: 'scorm_1_2';
51
+
52
+ @IsOptional()
53
+ @ValidateNested()
54
+ @Type(() => ScormVisualSettingsDto)
55
+ visualSettings?: ScormVisualSettingsDto;
56
+ }
@@ -18,10 +18,11 @@ const COURSE_LESSON_FILE_TYPES = [
18
18
  'student_download',
19
19
  'supplementary_material',
20
20
  'video_original',
21
+ 'video_hls',
21
22
  ] as const;
22
23
 
23
24
  const COURSE_LESSON_FILE_TYPE_REGEX =
24
- /^(lesson_audio|student_download|supplementary_material|video_original|video_profile:\d+)$/;
25
+ /^(lesson_audio|student_download|supplementary_material|video_original|video_hls|video_profile:\d+)$/;
25
26
 
26
27
  export type CourseLessonFileType =
27
28
  | (typeof COURSE_LESSON_FILE_TYPES)[number]
@@ -74,8 +75,8 @@ export class CreateCourseStructureLessonDto {
74
75
  @IsOptional()
75
76
  descricaoPrivada?: string;
76
77
 
77
- @IsEnum(['video', 'questao', 'post', 'exercicio'])
78
- tipo: 'video' | 'questao' | 'post' | 'exercicio';
78
+ @IsEnum(['video', 'questao', 'post'])
79
+ tipo: 'video' | 'questao' | 'post';
79
80
 
80
81
  @IsInt()
81
82
  @Min(0)
@@ -0,0 +1,65 @@
1
+ import { existsSync, readdirSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ /**
5
+ * Resolves the ffmpeg/ffprobe binaries across environments: explicit env override first,
6
+ * then a WinGet (Gyan.FFmpeg) lookup on Windows, finally the bare command on PATH.
7
+ * Mirrors the resolution baked into CourseVideoHlsService so the decomposed pipeline jobs
8
+ * (extract / split) behave identically.
9
+ */
10
+ export function getFfmpegCommand(): string {
11
+ const fromEnv = process.env.FFMPEG_PATH?.trim();
12
+ if (fromEnv) return fromEnv;
13
+ if (process.platform === 'win32') {
14
+ const found = findWindowsBinary('ffmpeg');
15
+ if (found) return found;
16
+ }
17
+ return 'ffmpeg';
18
+ }
19
+
20
+ export function getFfprobeCommand(): string {
21
+ const fromEnv = process.env.FFPROBE_PATH?.trim();
22
+ if (fromEnv) return fromEnv;
23
+ const ffmpegEnv = process.env.FFMPEG_PATH?.trim();
24
+ if (ffmpegEnv) {
25
+ const candidate = ffmpegEnv.replace(/ffmpeg(\.exe)?$/i, (_, ext) => `ffprobe${ext ?? ''}`);
26
+ if (existsSync(candidate)) return candidate;
27
+ }
28
+ if (process.platform === 'win32') {
29
+ const found = findWindowsBinary('ffprobe');
30
+ if (found) return found;
31
+ }
32
+ return 'ffprobe';
33
+ }
34
+
35
+ export function findWindowsBinary(name: 'ffmpeg' | 'ffprobe'): string | null {
36
+ const localAppData = process.env.LOCALAPPDATA;
37
+ if (!localAppData) return null;
38
+ const packagesRoot = join(localAppData, 'Microsoft', 'WinGet', 'Packages');
39
+ try {
40
+ const packageDirs = readdirSync(packagesRoot, { withFileTypes: true })
41
+ .filter((e) => e.isDirectory() && e.name.startsWith('Gyan.FFmpeg'))
42
+ .map((e) => join(packagesRoot, e.name))
43
+ .sort((a, b) => b.localeCompare(a));
44
+
45
+ for (const dir of packageDirs) {
46
+ const direct = join(dir, 'bin', `${name}.exe`);
47
+ if (existsSync(direct)) return direct;
48
+ try {
49
+ const versionDirs = readdirSync(dir, { withFileTypes: true })
50
+ .filter((e) => e.isDirectory() && e.name.toLowerCase().startsWith('ffmpeg-'))
51
+ .map((e) => join(dir, e.name))
52
+ .sort((a, b) => b.localeCompare(a));
53
+ for (const vd of versionDirs) {
54
+ const candidate = join(vd, 'bin', `${name}.exe`);
55
+ if (existsSync(candidate)) return candidate;
56
+ }
57
+ } catch {
58
+ /* skip */
59
+ }
60
+ }
61
+ } catch {
62
+ /* skip */
63
+ }
64
+ return null;
65
+ }