@gentbajko/slopify 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. package/LICENSE +201 -21
  2. package/NOTICE +12 -0
  3. package/README.md +21 -40
  4. package/SUBTITLES.md +1 -1
  5. package/dist/adapter-registry.js +11 -1
  6. package/dist/adapters/alignment/cache.js +4 -1
  7. package/dist/adapters/alignment/ctc.js +25 -23
  8. package/dist/adapters/alignment/index.js +37 -6
  9. package/dist/adapters/alignment/multilingual.js +267 -0
  10. package/dist/adapters/alignment/protocol.js +11 -0
  11. package/dist/adapters/alignment/quality.js +4 -3
  12. package/dist/adapters/alignment/sentences.js +95 -0
  13. package/dist/adapters/alignment/spec.js +24 -0
  14. package/dist/adapters/alignment/spell.js +769 -0
  15. package/dist/adapters/alignment/text.js +56 -9
  16. package/dist/adapters/alignment/window.js +34 -30
  17. package/dist/adapters/alignment/worker.js +23 -21
  18. package/dist/adapters/explain.js +11 -11
  19. package/dist/adapters/host-cli/index.js +55 -5
  20. package/dist/adapters/host-cli/transport.js +13 -6
  21. package/dist/adapters/image/bytes.js +2 -2
  22. package/dist/adapters/image/codex-output.js +60 -18
  23. package/dist/adapters/image/codex.js +166 -25
  24. package/dist/adapters/image/fal.js +31 -3
  25. package/dist/adapters/image/google.js +17 -2
  26. package/dist/adapters/image/openai.js +45 -16
  27. package/dist/adapters/image/reference.js +41 -0
  28. package/dist/adapters/image/replicate.js +14 -2
  29. package/dist/adapters/image/video.js +2 -2
  30. package/dist/adapters/key-probes.js +36 -0
  31. package/dist/adapters/llm/claude-code.js +168 -32
  32. package/dist/adapters/llm/cli-login-error.js +1 -1
  33. package/dist/adapters/llm/codex-limits.js +157 -0
  34. package/dist/adapters/llm/codex-models.js +2 -1
  35. package/dist/adapters/llm/codex-server-models.js +2 -1
  36. package/dist/adapters/llm/codex.js +99 -12
  37. package/dist/adapters/llm/document-workspace.js +2 -2
  38. package/dist/adapters/llm/gemini-workspace.js +1 -1
  39. package/dist/adapters/llm/gemini.js +16 -3
  40. package/dist/adapters/llm/image-workspace.js +58 -0
  41. package/dist/adapters/llm/openrouter.js +11 -3
  42. package/dist/adapters/llm/run-cli.js +26 -9
  43. package/dist/adapters/tts/cartesia.js +10 -0
  44. package/dist/adapters/tts/elevenlabs.js +31 -9
  45. package/dist/adapters/tts/inworld-async.js +9 -5
  46. package/dist/adapters/tts/inworld.js +17 -3
  47. package/dist/adapters/tts/openai.js +3 -0
  48. package/dist/adapters/tts/voice-languages.js +80 -0
  49. package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
  50. package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
  51. package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
  52. package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
  53. package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
  54. package/dist/assets/fonts/noto/OFL.txt +93 -0
  55. package/dist/assets/fonts/noto/SOURCE.txt +18 -0
  56. package/dist/assets/models.yaml +7 -0
  57. package/dist/assets/sample/sample-audiobook.tar +0 -0
  58. package/dist/assets/sample/sample-podcast.tar +0 -0
  59. package/dist/assets/sample/sample-project.tar +0 -0
  60. package/dist/catalog/merge.js +132 -0
  61. package/dist/catalog/registry.js +33 -12
  62. package/dist/catalog/runtime-models.js +1 -1
  63. package/dist/catalog/schema.js +12 -1
  64. package/dist/catalog/store.js +147 -8
  65. package/dist/catalog/validate.js +17 -2
  66. package/dist/compose.yaml +71 -0
  67. package/dist/edge/autostart/docker-record.js +32 -0
  68. package/dist/edge/autostart/docker.js +50 -0
  69. package/dist/edge/autostart/index.js +78 -0
  70. package/dist/edge/autostart/launcher.js +176 -0
  71. package/dist/edge/autostart/model.js +3 -0
  72. package/dist/edge/autostart/native.js +210 -0
  73. package/dist/edge/autostart/prompt.js +66 -0
  74. package/dist/edge/autostart/service.js +81 -0
  75. package/dist/edge/cli.js +57 -51
  76. package/dist/edge/docker-install/activation.js +56 -0
  77. package/dist/edge/docker-install/apply.js +458 -0
  78. package/dist/edge/docker-install/engine.js +362 -0
  79. package/dist/edge/docker-install/run.js +154 -0
  80. package/dist/edge/docker-install/state.js +146 -0
  81. package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
  82. package/dist/edge/docker.js +14 -22
  83. package/dist/edge/events/hub.js +11 -0
  84. package/dist/edge/host-cli.js +1 -1
  85. package/dist/edge/http/actions.js +32 -10
  86. package/dist/edge/http/app.js +60 -2
  87. package/dist/edge/http/auditions.js +70 -0
  88. package/dist/edge/http/autostart.js +37 -0
  89. package/dist/edge/http/backups.js +40 -0
  90. package/dist/edge/http/channel-memory.js +92 -0
  91. package/dist/edge/http/channels.js +128 -0
  92. package/dist/edge/http/entries.js +22 -3
  93. package/dist/edge/http/files.js +1 -1
  94. package/dist/edge/http/folder-location.js +1 -1
  95. package/dist/edge/http/home.js +39 -0
  96. package/dist/edge/http/host-cli.js +45 -6
  97. package/dist/edge/http/narration-peaks.js +76 -0
  98. package/dist/edge/http/onboarding.js +152 -0
  99. package/dist/edge/http/open-folder.js +1 -1
  100. package/dist/edge/http/patch-notes.js +45 -0
  101. package/dist/edge/http/planning.js +4 -2
  102. package/dist/edge/http/problem.js +1 -0
  103. package/dist/edge/http/project-create.js +87 -0
  104. package/dist/edge/http/projects.js +31 -99
  105. package/dist/edge/http/prompts.js +40 -3
  106. package/dist/edge/http/pronunciations.js +23 -4
  107. package/dist/edge/http/providers.js +86 -0
  108. package/dist/edge/http/reviews.js +121 -0
  109. package/dist/edge/http/revision-files.js +1 -1
  110. package/dist/edge/http/revisions.js +1 -1
  111. package/dist/edge/http/run-cost.js +25 -0
  112. package/dist/edge/http/schedules.js +111 -6
  113. package/dist/edge/http/settings.js +128 -6
  114. package/dist/edge/http/storage-files.js +50 -0
  115. package/dist/edge/http/storage.js +29 -2
  116. package/dist/edge/http/studio.js +207 -0
  117. package/dist/edge/http/style-preview.js +100 -0
  118. package/dist/edge/http/subtitles.js +1 -1
  119. package/dist/edge/http/trash.js +69 -0
  120. package/dist/edge/http/update.js +24 -3
  121. package/dist/edge/http/whats-new.js +15 -0
  122. package/dist/edge/http/youtube-edits.js +83 -0
  123. package/dist/edge/native-update.js +62 -0
  124. package/dist/host-cli/open-folder.js +10 -10
  125. package/dist/host-cli/runtime.js +12 -3
  126. package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
  127. package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
  128. package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
  129. package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
  130. package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
  131. package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
  132. package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
  133. package/dist/kernel/db/migrations/0032-channels.sql +65 -0
  134. package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
  135. package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
  136. package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
  137. package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
  138. package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
  139. package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
  140. package/dist/kernel/paths.js +27 -6
  141. package/dist/kernel/pipeline.js +4 -1
  142. package/dist/kernel/ports/host-cli.js +139 -16
  143. package/dist/kernel/ports/image.js +3 -1
  144. package/dist/kernel/ports/key-probe.js +1 -0
  145. package/dist/kernel/ports/languages.js +157 -0
  146. package/dist/kernel/ports/llm.js +3 -1
  147. package/dist/kernel/ports/model.js +5 -2
  148. package/dist/kernel/ports/narration-aliases.js +60 -0
  149. package/dist/kernel/ports/plan-limits.js +36 -0
  150. package/dist/kernel/runner/attempt.js +18 -9
  151. package/dist/kernel/runner/audition.js +36 -0
  152. package/dist/kernel/runner/graph.js +24 -5
  153. package/dist/kernel/runner/index.js +35 -5
  154. package/dist/kernel/runner/meter.js +1 -0
  155. package/dist/kernel/runner/providers.js +164 -9
  156. package/dist/kernel/runner/retry-policy.js +43 -0
  157. package/dist/kernel/runner/standalone.js +134 -0
  158. package/dist/kernel/runner/work-authority.js +28 -2
  159. package/dist/main.js +321 -20
  160. package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
  161. package/dist/patch-notes/1.0-to-2.0.md +74 -0
  162. package/dist/patch-notes/2.0-to-3.0.md +257 -0
  163. package/dist/patch-notes/3.0.0.md +70 -0
  164. package/dist/patch-notes/index.json +26 -0
  165. package/dist/slices/admission/model.js +35 -0
  166. package/dist/slices/admission/repo.js +24 -4
  167. package/dist/slices/admission/rules.js +126 -1
  168. package/dist/slices/admission/schema.js +68 -1
  169. package/dist/slices/admission/short-mode.js +47 -0
  170. package/dist/slices/admission/start.js +90 -16
  171. package/dist/slices/article/continuation.js +2 -2
  172. package/dist/slices/article/provided-entries.js +2 -2
  173. package/dist/slices/article/run.js +6 -2
  174. package/dist/slices/article/segments.js +3 -3
  175. package/dist/slices/backups/files.js +135 -0
  176. package/dist/slices/backups/folder.js +43 -0
  177. package/dist/slices/backups/model.js +65 -0
  178. package/dist/slices/backups/repo.js +28 -0
  179. package/dist/slices/backups/schedule.js +60 -0
  180. package/dist/slices/backups/service.js +311 -0
  181. package/dist/slices/batch/index.js +9 -2
  182. package/dist/slices/cancel/index.js +1 -1
  183. package/dist/slices/channels/cast-images.js +110 -0
  184. package/dist/slices/channels/cast-match.js +32 -0
  185. package/dist/slices/channels/images.js +19 -0
  186. package/dist/slices/channels/model.js +5 -0
  187. package/dist/slices/channels/rebrand.js +100 -0
  188. package/dist/slices/channels/repo.js +218 -0
  189. package/dist/slices/channels/runs.js +129 -0
  190. package/dist/slices/channels/schema.js +129 -0
  191. package/dist/slices/channels/service.js +180 -0
  192. package/dist/slices/channels/studio-csv.js +110 -0
  193. package/dist/slices/channels/videos.js +120 -0
  194. package/dist/slices/checkpoints/recovery.js +10 -0
  195. package/dist/slices/control/index.js +2 -1
  196. package/dist/slices/document/fonts.js +1 -1
  197. package/dist/slices/document/render.js +1 -1
  198. package/dist/slices/episodes/related.js +90 -0
  199. package/dist/slices/episodes/repo.js +60 -0
  200. package/dist/slices/episodes/service.js +58 -0
  201. package/dist/slices/episodes/summarize.js +170 -0
  202. package/dist/slices/estimate/index.js +113 -24
  203. package/dist/slices/estimate/requests.js +60 -2
  204. package/dist/slices/fixes/rules.js +73 -0
  205. package/dist/slices/fonts/coverage.js +82 -0
  206. package/dist/slices/fonts/sfnt.js +74 -0
  207. package/dist/slices/images/run.js +8 -5
  208. package/dist/slices/images/scale.js +84 -0
  209. package/dist/slices/library/diff.js +122 -0
  210. package/dist/slices/library/history.js +100 -0
  211. package/dist/slices/library/model.js +3 -0
  212. package/dist/slices/library/photorealistic.js +49 -0
  213. package/dist/slices/library/repo.js +19 -14
  214. package/dist/slices/library/save.js +62 -12
  215. package/dist/slices/library/slots.js +15 -3
  216. package/dist/slices/library/snapshot.js +8 -2
  217. package/dist/slices/library/used-by.js +153 -0
  218. package/dist/slices/loudness/level-pieces.js +41 -0
  219. package/dist/slices/loudness/loudnorm.js +164 -0
  220. package/dist/slices/loudness/model.js +153 -0
  221. package/dist/slices/model-upkeep/model.js +10 -0
  222. package/dist/slices/model-upkeep/switch.js +217 -0
  223. package/dist/slices/model-upkeep/usage.js +183 -0
  224. package/dist/slices/narration/aliases-library.js +31 -0
  225. package/dist/slices/narration/aliases-schema.js +53 -0
  226. package/dist/slices/narration/aliases.js +49 -0
  227. package/dist/slices/narration/blocks.js +352 -0
  228. package/dist/slices/narration/chunk.js +19 -7
  229. package/dist/slices/narration/concat.js +2 -2
  230. package/dist/slices/narration/describe.js +45 -0
  231. package/dist/slices/narration/pauses-model.js +63 -0
  232. package/dist/slices/narration/pauses.js +269 -0
  233. package/dist/slices/narration/peaks-model.js +1 -0
  234. package/dist/slices/narration/peaks.js +94 -0
  235. package/dist/slices/narration/preparation.js +21 -3
  236. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  237. package/dist/slices/narration/pronunciation.js +72 -25
  238. package/dist/slices/narration/run.js +6 -6
  239. package/dist/slices/narration/shared-glossary.js +1 -1
  240. package/dist/slices/narration/steering.js +15 -4
  241. package/dist/slices/notifications/notifier.js +94 -0
  242. package/dist/slices/notifications/rules.js +102 -0
  243. package/dist/slices/notifications/send.js +40 -0
  244. package/dist/slices/notifications/settings.js +34 -0
  245. package/dist/slices/onboarding/copy.js +143 -0
  246. package/dist/slices/onboarding/first-run.js +46 -0
  247. package/dist/slices/onboarding/install.js +119 -0
  248. package/dist/slices/onboarding/model.js +64 -0
  249. package/dist/slices/onboarding/packs.js +491 -0
  250. package/dist/slices/onboarding/quick-short.js +129 -0
  251. package/dist/slices/onboarding/sample.js +132 -0
  252. package/dist/slices/onboarding/state.js +68 -0
  253. package/dist/slices/onboarding/template.js +73 -0
  254. package/dist/slices/patch-notes/library.js +73 -0
  255. package/dist/slices/patch-notes/seen.js +77 -0
  256. package/dist/slices/play-drafts/convert.js +160 -9
  257. package/dist/slices/play-drafts/readiness.js +3 -0
  258. package/dist/slices/play-drafts/repo.js +9 -1
  259. package/dist/slices/play-drafts/review-inputs.js +22 -3
  260. package/dist/slices/play-drafts/schema.js +109 -2
  261. package/dist/slices/play-drafts/service.js +10 -1
  262. package/dist/slices/play-drafts/start.js +43 -18
  263. package/dist/slices/play-drafts/uploads.js +2 -1
  264. package/dist/slices/project-templates/from-project.js +70 -4
  265. package/dist/slices/project-templates/one-off.js +26 -0
  266. package/dist/slices/project-templates/repo.js +2 -1
  267. package/dist/slices/project-templates/service.js +15 -4
  268. package/dist/slices/project-templates/setup.js +29 -2
  269. package/dist/slices/rebuild/admission-repo.js +1 -1
  270. package/dist/slices/rebuild/model.js +6 -0
  271. package/dist/slices/rebuild/preview-details.js +67 -2
  272. package/dist/slices/rebuild/preview-plan.js +13 -6
  273. package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
  274. package/dist/slices/rebuild/recipe-audio.js +66 -12
  275. package/dist/slices/rebuild/recipe-build.js +15 -5
  276. package/dist/slices/rebuild/recipe-cast.js +18 -0
  277. package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
  278. package/dist/slices/rebuild/recipe-describe.js +93 -0
  279. package/dist/slices/rebuild/recipe-edit.js +13 -1
  280. package/dist/slices/rebuild/recipe-exports.js +62 -8
  281. package/dist/slices/rebuild/recipe-input-schema.js +43 -1
  282. package/dist/slices/rebuild/recipe-legacy.js +12 -0
  283. package/dist/slices/rebuild/recipe-loudness.js +45 -0
  284. package/dist/slices/rebuild/recipe-model.js +25 -6
  285. package/dist/slices/rebuild/recipe-pauses.js +7 -0
  286. package/dist/slices/rebuild/recipe-preparation.js +3 -3
  287. package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
  288. package/dist/slices/rebuild/recipe-reference.js +58 -0
  289. package/dist/slices/rebuild/recipe-reviews.js +117 -0
  290. package/dist/slices/rebuild/recipe-save.js +4 -1
  291. package/dist/slices/rebuild/recipe-shorts.js +32 -5
  292. package/dist/slices/rebuild/recipe-text.js +114 -19
  293. package/dist/slices/rebuild/recipe-validation.js +29 -3
  294. package/dist/slices/rebuild/recipe-visual.js +104 -29
  295. package/dist/slices/rebuild/recipe-voices.js +286 -0
  296. package/dist/slices/rebuild/recipe-work.js +77 -6
  297. package/dist/slices/rebuild/recipe-youtube.js +2 -0
  298. package/dist/slices/rebuild/recovery-conflict.js +7 -3
  299. package/dist/slices/rebuild/recovery-model.js +3 -0
  300. package/dist/slices/rebuild/recovery-selection.js +28 -2
  301. package/dist/slices/rebuild/recovery.js +35 -17
  302. package/dist/slices/rebuild/repo.js +3 -3
  303. package/dist/slices/rebuild/retry.js +18 -0
  304. package/dist/slices/rebuild/review-redo.js +110 -0
  305. package/dist/slices/rebuild/runtime-actions.js +14 -2
  306. package/dist/slices/rebuild/runtime-admission.js +4 -1
  307. package/dist/slices/rebuild/runtime-animate.js +4 -4
  308. package/dist/slices/rebuild/runtime-article.js +5 -5
  309. package/dist/slices/rebuild/runtime-document.js +4 -4
  310. package/dist/slices/rebuild/runtime-export-bed.js +43 -0
  311. package/dist/slices/rebuild/runtime-export-edit.js +70 -11
  312. package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
  313. package/dist/slices/rebuild/runtime-export-short.js +85 -0
  314. package/dist/slices/rebuild/runtime-export.js +84 -19
  315. package/dist/slices/rebuild/runtime-figure-card.js +87 -0
  316. package/dist/slices/rebuild/runtime-image.js +69 -0
  317. package/dist/slices/rebuild/runtime-local.js +154 -27
  318. package/dist/slices/rebuild/runtime-materialize.js +8 -0
  319. package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
  320. package/dist/slices/rebuild/runtime-narration-text.js +7 -7
  321. package/dist/slices/rebuild/runtime-pictures.js +28 -0
  322. package/dist/slices/rebuild/runtime-piece-label.js +29 -0
  323. package/dist/slices/rebuild/runtime-provider.js +134 -19
  324. package/dist/slices/rebuild/runtime-publication.js +21 -5
  325. package/dist/slices/rebuild/runtime-review.js +237 -0
  326. package/dist/slices/rebuild/runtime-run.js +17 -11
  327. package/dist/slices/rebuild/runtime-shorts.js +48 -23
  328. package/dist/slices/rebuild/runtime-store.js +39 -10
  329. package/dist/slices/rebuild/runtime-subtitles.js +72 -19
  330. package/dist/slices/rebuild/runtime-voices.js +103 -0
  331. package/dist/slices/rebuild/runtime-youtube.js +7 -6
  332. package/dist/slices/rebuild/service-readiness.js +15 -1
  333. package/dist/slices/rebuild/soften.js +49 -0
  334. package/dist/slices/rebuild/transition-repo.js +10 -3
  335. package/dist/slices/research/run.js +4 -4
  336. package/dist/slices/research/synthesis.js +2 -2
  337. package/dist/slices/reviews/model.js +105 -0
  338. package/dist/slices/reviews/outcome.js +22 -0
  339. package/dist/slices/reviews/repo.js +106 -0
  340. package/dist/slices/reviews/rules.js +63 -0
  341. package/dist/slices/reviews/verdict.js +71 -0
  342. package/dist/slices/reviews/view.js +30 -0
  343. package/dist/slices/revisions/adopt-content.js +12 -2
  344. package/dist/slices/revisions/adopt.js +9 -2
  345. package/dist/slices/revisions/downloads.js +7 -1
  346. package/dist/slices/revisions/image-plan.js +183 -0
  347. package/dist/slices/revisions/mutation-assets.js +43 -9
  348. package/dist/slices/revisions/mutation-cues.js +71 -0
  349. package/dist/slices/revisions/mutation-prepare.js +8 -10
  350. package/dist/slices/revisions/mutation-request.js +2 -1
  351. package/dist/slices/revisions/mutations.js +97 -3
  352. package/dist/slices/revisions/schema.js +16 -2
  353. package/dist/slices/run-cost/limits.js +128 -0
  354. package/dist/slices/run-cost/meter.js +47 -0
  355. package/dist/slices/run-cost/panel.js +234 -0
  356. package/dist/slices/run-cost/pricing.js +92 -0
  357. package/dist/slices/run-cost/week.js +108 -0
  358. package/dist/slices/schedules/agenda.js +158 -0
  359. package/dist/slices/schedules/repo.js +58 -18
  360. package/dist/slices/schedules/scheduler.js +67 -16
  361. package/dist/slices/schedules/schema.js +138 -1
  362. package/dist/slices/schedules/service.js +48 -4
  363. package/dist/slices/schedules/similar.js +90 -0
  364. package/dist/slices/schedules/topic-list.js +163 -0
  365. package/dist/slices/schedules/topics.js +422 -0
  366. package/dist/slices/settings/first-run.js +102 -0
  367. package/dist/slices/settings/health.js +190 -0
  368. package/dist/slices/settings/key-guides.js +128 -0
  369. package/dist/slices/settings/key-test.js +85 -0
  370. package/dist/slices/settings/model.js +11 -1
  371. package/dist/slices/settings/playback.js +15 -0
  372. package/dist/slices/settings/repo.js +17 -3
  373. package/dist/slices/settings/voices.js +64 -4
  374. package/dist/slices/settings/whats-new.js +64 -0
  375. package/dist/slices/shorts/captions.js +9 -0
  376. package/dist/slices/shorts/pick.js +3 -3
  377. package/dist/slices/shorts/prompts.js +1 -1
  378. package/dist/slices/shorts/render.js +54 -5
  379. package/dist/slices/storage/asset-name.js +3 -0
  380. package/dist/slices/storage/backup-export.js +48 -9
  381. package/dist/slices/storage/backup-format.js +38 -1
  382. package/dist/slices/storage/backup-import.js +191 -19
  383. package/dist/slices/storage/delete-project.js +7 -4
  384. package/dist/slices/storage/documents.js +79 -0
  385. package/dist/slices/storage/files-location.js +531 -0
  386. package/dist/slices/storage/layout.js +20 -0
  387. package/dist/slices/storage/model.js +14 -0
  388. package/dist/slices/storage/portable.js +95 -12
  389. package/dist/slices/storage/reconcile.js +19 -2
  390. package/dist/slices/storage/repo.js +4 -1
  391. package/dist/slices/storage/schema.js +11 -0
  392. package/dist/slices/storage/trim.js +146 -0
  393. package/dist/slices/studio/disclosure.js +74 -0
  394. package/dist/slices/studio/model.js +28 -0
  395. package/dist/slices/studio/pack.js +209 -0
  396. package/dist/slices/studio/settings.js +133 -0
  397. package/dist/slices/style-preview/images.js +61 -0
  398. package/dist/slices/style-preview/render.js +161 -0
  399. package/dist/slices/style-preview/schema.js +45 -0
  400. package/dist/slices/style-preview/service.js +93 -0
  401. package/dist/slices/style-preview/settings.js +89 -0
  402. package/dist/slices/subtitles/captions.js +42 -9
  403. package/dist/slices/subtitles/model.js +22 -0
  404. package/dist/slices/subtitles/prepare.js +27 -16
  405. package/dist/slices/subtitles/transcript.js +1 -1
  406. package/dist/slices/thumbnail/run.js +7 -7
  407. package/dist/slices/trash/model.js +25 -0
  408. package/dist/slices/trash/service.js +273 -0
  409. package/dist/slices/uploads/repo.js +20 -0
  410. package/dist/slices/video/ambient-bed-schema.js +15 -0
  411. package/dist/slices/video/ambient-bed.js +95 -0
  412. package/dist/slices/video/ambient-mix.js +72 -0
  413. package/dist/slices/video/audio-inputs.js +2 -2
  414. package/dist/slices/video/cards.js +5 -2
  415. package/dist/slices/video/edit-list.js +23 -2
  416. package/dist/slices/video/edit-settings.js +13 -3
  417. package/dist/slices/video/ffmpeg.js +63 -9
  418. package/dist/slices/video/figure-card.js +492 -0
  419. package/dist/slices/video/figure-spans.js +51 -0
  420. package/dist/slices/video/plan.js +125 -13
  421. package/dist/slices/video/run.js +14 -12
  422. package/dist/slices/video/slideshow.js +39 -4
  423. package/dist/slices/voices/attribution.js +69 -0
  424. package/dist/slices/voices/audio-files.js +82 -0
  425. package/dist/slices/voices/audition.js +25 -0
  426. package/dist/slices/voices/cast.js +56 -0
  427. package/dist/slices/voices/grouping.js +49 -0
  428. package/dist/slices/voices/join.js +35 -0
  429. package/dist/slices/voices/languages.js +25 -0
  430. package/dist/slices/voices/model.js +179 -0
  431. package/dist/slices/voices/palette.js +26 -0
  432. package/dist/slices/voices/panel.js +91 -0
  433. package/dist/slices/voices/portraits.js +55 -0
  434. package/dist/slices/voices/script.js +136 -0
  435. package/dist/slices/voices/timing.js +58 -0
  436. package/dist/slices/youtube/answer.js +1 -1
  437. package/dist/slices/youtube/chapters.js +126 -0
  438. package/dist/slices/youtube/edits-repo.js +69 -0
  439. package/dist/slices/youtube/edits.js +89 -0
  440. package/dist/slices/youtube/placeholders.js +98 -0
  441. package/dist/slices/youtube/transcript.js +11 -4
  442. package/dist/updater/service.js +66 -21
  443. package/dist/web/assets/index-CUA2sUxU.css +1 -0
  444. package/dist/web/assets/index-CX590kE-.js +303 -0
  445. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  446. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  447. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  448. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  449. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  450. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  451. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  452. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  453. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  454. package/dist/web/assets/{pdf-CAJNT254.js → pdf-BG8n_uc5.js} +1 -1
  455. package/dist/web/index.html +2 -2
  456. package/package.json +4 -4
  457. package/dist/edge/docker-launch.js +0 -81
  458. package/dist/edge/docker-projects/activation.js +0 -35
  459. package/dist/edge/docker-projects/claims.js +0 -85
  460. package/dist/edge/docker-projects/committed.js +0 -40
  461. package/dist/edge/docker-projects/engine.js +0 -461
  462. package/dist/edge/docker-projects/install.js +0 -254
  463. package/dist/edge/docker-projects/prune.js +0 -66
  464. package/dist/edge/docker-projects/recover.js +0 -100
  465. package/dist/edge/docker-projects/state.js +0 -225
  466. package/dist/web/assets/index-BGgYMm2u.css +0 -1
  467. package/dist/web/assets/index-BVVgtntL.js +0 -171
  468. package/scripts/docker-run.sh +0 -3
  469. /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
@@ -0,0 +1,207 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { Readable } from "node:stream";
3
+ import { zValidator } from "@hono/zod-validator";
4
+ import { Hono } from "hono";
5
+ import { z } from "zod";
6
+ import { findDownload } from "../../slices/storage/downloads.js";
7
+ import { studioPlaylistMax } from "../../slices/studio/model.js";
8
+ import { packItem, uploadPack } from "../../slices/studio/pack.js";
9
+ import { bearerToken, isExtensionOrigin, pairStudioExtension, readStudioPlaylist, resetStudioPairing, saveRealFootage, saveStudioPlaylist, studioPairing, studioPlaylistProblem, studioRequestAllowed, } from "../../slices/studio/settings.js";
10
+ import { onInvalid, problem, titleOf } from "./problem.js";
11
+ const id = z
12
+ .string()
13
+ .min(1)
14
+ .max(64)
15
+ .regex(/^[0-9A-Za-z_-]+$/);
16
+ const projectParam = z.object({ projectId: id });
17
+ const fileParam = z.object({
18
+ projectId: id,
19
+ asset: z
20
+ .string()
21
+ .max(64)
22
+ .regex(/^[a-z0-9-]+$/),
23
+ });
24
+ const chooseBody = z.object({ short: z.number().int().min(1).max(99).optional() });
25
+ const playlistBody = z.object({ playlist: z.string().max(studioPlaylistMax * 2) });
26
+ const realFootageBody = z.object({ realFootage: z.boolean() });
27
+ // How long a "Fill in YouTube Studio" choice stays the one the extension fills in.
28
+ const choiceMs = 6 * 60 * 60 * 1000;
29
+ // The Studio routes. The ones under `/ext` are the only routes of the app a page from another
30
+ // origin may read, and only the paired browser extension: every request carries the pairing
31
+ // token, and the CORS headers name the paired extension's origin, never a web page's and never
32
+ // `*`. The rest are the Slopify page's own, refused from any other origin.
33
+ export function studioRoutes(deps) {
34
+ // What the person chose to fill in last; kept in memory, so a restart asks them to choose
35
+ // again rather than filling in an old choice.
36
+ let chosen;
37
+ const samePage = (c) => {
38
+ const origin = c.req.header("origin");
39
+ if (origin === undefined || origin === new URL(c.req.url).origin)
40
+ return undefined;
41
+ return problem(c, {
42
+ status: 403,
43
+ title: titleOf(403),
44
+ detail: "For your safety, YouTube Studio settings can only be changed from the Slopify page itself. Open Slopify and try again there.",
45
+ });
46
+ };
47
+ // The CORS headers for the extension: its own origin, echoed only when it is allowed.
48
+ const allowOrigin = (c, pairing) => {
49
+ const origin = c.req.header("origin");
50
+ const paired = studioPairing(deps.db).origin;
51
+ c.header("Vary", "Origin");
52
+ if (!isExtensionOrigin(origin))
53
+ return;
54
+ if (origin === paired || pairing) {
55
+ c.header("Access-Control-Allow-Origin", origin);
56
+ c.header("Access-Control-Allow-Headers", "authorization, content-type");
57
+ c.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
58
+ c.header("Access-Control-Max-Age", "600");
59
+ }
60
+ };
61
+ const refused = (c) => problem(c, {
62
+ status: 401,
63
+ title: "Unauthorized",
64
+ detail: "The Slopify Studio extension isn't paired with this Slopify. Copy the pairing token from Slopify's Settings → YouTube Studio into the extension's options and press Pair.",
65
+ });
66
+ return (new Hono()
67
+ .get("/settings", (c) => {
68
+ // The token is read only by the Slopify page, never by another origin's.
69
+ const denied = samePage(c);
70
+ if (denied !== undefined)
71
+ return denied;
72
+ return c.json({ playlist: readStudioPlaylist(deps.db), pairing: studioPairing(deps.db) });
73
+ })
74
+ .put("/settings/playlist", zValidator("json", playlistBody, onInvalid), (c) => {
75
+ const denied = samePage(c);
76
+ if (denied !== undefined)
77
+ return denied;
78
+ const raw = c.req.valid("json").playlist;
79
+ const invalid = studioPlaylistProblem(raw);
80
+ if (invalid !== undefined)
81
+ return problem(c, {
82
+ status: 400,
83
+ title: titleOf(400),
84
+ detail: invalid,
85
+ extensions: { fields: [{ field: "playlist", message: invalid }] },
86
+ });
87
+ return c.json({ playlist: saveStudioPlaylist(deps.db, raw) });
88
+ })
89
+ .post("/settings/pairing", (c) => {
90
+ const denied = samePage(c);
91
+ if (denied !== undefined)
92
+ return denied;
93
+ return c.json({ pairing: resetStudioPairing(deps.db) });
94
+ })
95
+ .get("/packs/:projectId", zValidator("param", projectParam, onInvalid), (c) => {
96
+ const result = uploadPack(deps, c.req.valid("param").projectId);
97
+ if (!result.ok)
98
+ return unknownProject(c);
99
+ return c.json(result.pack);
100
+ })
101
+ // Prepare upload's AI use tick: the project's uploaded clips are real footage.
102
+ .put("/packs/:projectId/real-footage", zValidator("param", projectParam, onInvalid), zValidator("json", realFootageBody, onInvalid), (c) => {
103
+ const denied = samePage(c);
104
+ if (denied !== undefined)
105
+ return denied;
106
+ const { projectId } = c.req.valid("param");
107
+ if (!uploadPack(deps, projectId).ok)
108
+ return unknownProject(c);
109
+ saveRealFootage(deps.db, projectId, c.req.valid("json").realFootage);
110
+ const result = uploadPack(deps, projectId);
111
+ return result.ok ? c.json(result.pack) : unknownProject(c);
112
+ })
113
+ .post("/packs/:projectId/choose", zValidator("param", projectParam, onInvalid), zValidator("json", chooseBody, onInvalid), (c) => {
114
+ const denied = samePage(c);
115
+ if (denied !== undefined)
116
+ return denied;
117
+ const { projectId } = c.req.valid("param");
118
+ const { short } = c.req.valid("json");
119
+ const result = uploadPack(deps, projectId);
120
+ if (!result.ok)
121
+ return unknownProject(c);
122
+ if (packItem(result.pack, short) === undefined)
123
+ return problem(c, {
124
+ status: 404,
125
+ title: titleOf(404),
126
+ detail: `This project has no short ${String(short)}. Reload the project page and press Prepare upload again.`,
127
+ });
128
+ chosen = { projectId, short, at: deps.clock.now().getTime() };
129
+ return c.json({ chosen: { projectId, short: short ?? null } });
130
+ })
131
+ // The extension's routes.
132
+ .options("/ext/pair", (c) => {
133
+ allowOrigin(c, true);
134
+ return c.body(null, 204);
135
+ })
136
+ .options("/ext/*", (c) => {
137
+ allowOrigin(c, false);
138
+ return c.body(null, 204);
139
+ })
140
+ .post("/ext/pair", (c) => {
141
+ allowOrigin(c, true);
142
+ const result = pairStudioExtension(deps.db, bearerToken(c.req.header("authorization")), c.req.header("origin"), deps.clock.now());
143
+ if (!result.ok)
144
+ return result.reason === "bad-token"
145
+ ? refused(c)
146
+ : problem(c, {
147
+ status: 403,
148
+ title: titleOf(403),
149
+ detail: "Only the Slopify Studio browser extension can pair with Slopify. Install it (see Settings → YouTube Studio) and pair from its options page.",
150
+ });
151
+ // The origin is paired now; the response carries its headers.
152
+ allowOrigin(c, false);
153
+ return c.json({ paired: true, origin: result.pairing.origin });
154
+ })
155
+ .get("/ext/pack", (c) => {
156
+ allowOrigin(c, false);
157
+ if (!studioRequestAllowed(deps.db, bearerToken(c.req.header("authorization")), c.req.header("origin")))
158
+ return refused(c);
159
+ const fresh = chosen !== undefined && deps.clock.now().getTime() - chosen.at <= choiceMs
160
+ ? chosen
161
+ : undefined;
162
+ if (fresh === undefined)
163
+ return problem(c, {
164
+ status: 404,
165
+ title: titleOf(404),
166
+ detail: "Nothing is chosen to fill in. In Slopify, open the project, press Prepare upload, then Fill in YouTube Studio.",
167
+ });
168
+ const result = uploadPack(deps, fresh.projectId);
169
+ const item = result.ok ? packItem(result.pack, fresh.short) : undefined;
170
+ if (!result.ok || item === undefined)
171
+ return problem(c, {
172
+ status: 404,
173
+ title: titleOf(404),
174
+ detail: "The project chosen to fill in is gone. In Slopify, open the project again and press Fill in YouTube Studio.",
175
+ });
176
+ return c.json({ pack: result.pack, item });
177
+ })
178
+ .get("/ext/files/:projectId/:asset", zValidator("param", fileParam, onInvalid), (c) => {
179
+ allowOrigin(c, false);
180
+ if (!studioRequestAllowed(deps.db, bearerToken(c.req.header("authorization")), c.req.header("origin")))
181
+ return refused(c);
182
+ const { projectId, asset } = c.req.valid("param");
183
+ // Only the thumbnails of a pack: the extension needs nothing else from the disk.
184
+ const result = uploadPack(deps, projectId);
185
+ const listed = result.ok &&
186
+ result.pack.items.some((item) => item.thumbnails.some((thumbnail) => thumbnail.asset === asset));
187
+ const found = listed ? findDownload(deps, projectId, asset) : undefined;
188
+ if (found === undefined || !found.ok)
189
+ return problem(c, {
190
+ status: 404,
191
+ title: titleOf(404),
192
+ detail: "That thumbnail isn't in the project's upload pack any more. Reload the project in Slopify and press Fill in YouTube Studio again.",
193
+ });
194
+ return c.body(Readable.toWeb(createReadStream(found.download.path)), 200, {
195
+ "content-type": found.download.contentType,
196
+ "content-length": String(found.download.bytes),
197
+ "x-content-type-options": "nosniff",
198
+ });
199
+ }));
200
+ }
201
+ function unknownProject(c) {
202
+ return problem(c, {
203
+ status: 404,
204
+ title: titleOf(404),
205
+ detail: "This project no longer exists. Go back to Projects and open it again.",
206
+ });
207
+ }
@@ -0,0 +1,100 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { Readable } from "node:stream";
3
+ import { zValidator } from "@hono/zod-validator";
4
+ import { Hono } from "hono";
5
+ import { z } from "zod";
6
+ import { StylePreviewError } from "../../slices/style-preview/render.js";
7
+ import { stylePreviewRequestSchema } from "../../slices/style-preview/schema.js";
8
+ import { onInvalid, problem, titleOf } from "./problem.js";
9
+ // POST renders (or finds) the preview for a set of caption and Look settings and answers where
10
+ // to play it; GET streams the mp4, with byte ranges so the browser's player can seek.
11
+ const fileParam = z.object({ file: z.string().regex(/^[a-f0-9]{64}\.mp4$/) });
12
+ export function stylePreviewRoutes(deps) {
13
+ return new Hono()
14
+ .post("/", zValidator("json", stylePreviewRequestSchema, onInvalid), async (c) => {
15
+ const previews = deps.stylePreviews;
16
+ if (previews === undefined)
17
+ return unavailable(c);
18
+ try {
19
+ const result = await previews.render(c.req.valid("json"));
20
+ c.header("Cache-Control", "no-store");
21
+ return c.json({
22
+ hash: result.hash,
23
+ url: `/api/style-preview/${result.hash}.mp4?v=${String(result.version)}`,
24
+ cached: result.cached,
25
+ seconds: result.seconds,
26
+ });
27
+ }
28
+ catch (error) {
29
+ if (!(error instanceof StylePreviewError))
30
+ throw error;
31
+ const status = error.reason === "font-missing" ? 409 : error.reason === "ffmpeg-missing" ? 503 : 500;
32
+ return problem(c, {
33
+ status,
34
+ title: titleOf(status),
35
+ detail: error.message,
36
+ extensions: { reason: error.reason },
37
+ });
38
+ }
39
+ })
40
+ .get("/:file", zValidator("param", fileParam, onInvalid), (c) => {
41
+ const saved = deps.stylePreviews?.file(c.req.valid("param").file.slice(0, 64));
42
+ if (saved === undefined)
43
+ return problem(c, {
44
+ status: 404,
45
+ title: titleOf(404),
46
+ detail: "This style preview is no longer saved; Slopify keeps only the most recent ones. Press Render again to make it.",
47
+ });
48
+ const headers = {
49
+ "content-type": "video/mp4",
50
+ "accept-ranges": "bytes",
51
+ "cache-control": "no-cache",
52
+ "x-content-type-options": "nosniff",
53
+ };
54
+ const range = byteRange(c.req.header("range"), saved.bytes);
55
+ if (range === "unsatisfiable")
56
+ return new Response(null, {
57
+ status: 416,
58
+ headers: { ...headers, "content-range": `bytes */${String(saved.bytes)}` },
59
+ });
60
+ const { start, end } = range ?? { start: 0, end: saved.bytes - 1 };
61
+ const body = Readable.toWeb(createReadStream(saved.path, { start, end }));
62
+ return new Response(body, {
63
+ status: range === undefined ? 200 : 206,
64
+ headers: {
65
+ ...headers,
66
+ "content-length": String(end - start + 1),
67
+ ...(range === undefined
68
+ ? {}
69
+ : { "content-range": `bytes ${String(start)}-${String(end)}/${String(saved.bytes)}` }),
70
+ },
71
+ });
72
+ });
73
+ }
74
+ // One `bytes=` range, as browsers ask for media; anything else is answered with the whole file.
75
+ export function byteRange(header, size) {
76
+ const matched = /^bytes=(\d*)-(\d*)$/.exec(header?.trim() ?? "");
77
+ if (matched === null)
78
+ return undefined;
79
+ const [, from = "", to = ""] = matched;
80
+ if (from === "" && to === "")
81
+ return undefined;
82
+ if (from === "") {
83
+ const suffix = Number(to);
84
+ if (suffix === 0)
85
+ return "unsatisfiable";
86
+ return { start: Math.max(0, size - suffix), end: size - 1 };
87
+ }
88
+ const start = Number(from);
89
+ const end = to === "" ? size - 1 : Math.min(Number(to), size - 1);
90
+ if (start >= size || end < start)
91
+ return "unsatisfiable";
92
+ return { start, end };
93
+ }
94
+ function unavailable(c) {
95
+ return problem(c, {
96
+ status: 503,
97
+ title: titleOf(503),
98
+ detail: "The style preview isn't available because this Slopify started without its video renderer. Restart Slopify, then press Render again.",
99
+ });
100
+ }
@@ -16,7 +16,7 @@ function revisionRequired(c) {
16
16
  return problem(c, {
17
17
  status: 409,
18
18
  title: titleOf(409),
19
- detail: "This change is now made from the Edit tab. Choose Edit project, save your change, then use Rebuild affected outputs.",
19
+ detail: "This change is now made from the Edit tab. Choose Edit project, save your change, then use Choose what to remake (in the project's More menu).",
20
20
  extensions: { reason: "revision-required" },
21
21
  });
22
22
  }
@@ -0,0 +1,69 @@
1
+ import { zValidator } from "@hono/zod-validator";
2
+ import { Hono } from "hono";
3
+ import { z } from "zod";
4
+ import { withProjectControl } from "../../slices/control/lock.js";
5
+ import { trashKinds } from "../../slices/trash/model.js";
6
+ import { deleteNow, listTrash, restoreItem } from "../../slices/trash/service.js";
7
+ import { onInvalid, problem, titleOf } from "./problem.js";
8
+ const itemParam = z.object({
9
+ kind: z.enum(trashKinds),
10
+ id: z
11
+ .string()
12
+ .min(1)
13
+ .max(64)
14
+ .regex(/^[0-9A-Za-z_-]+$/),
15
+ });
16
+ const status = {
17
+ "not-found": 404,
18
+ running: 409,
19
+ files: 500,
20
+ "template-in-trash": 409,
21
+ "template-gone": 409,
22
+ };
23
+ const details = {
24
+ "not-found": "This item is no longer in the trash: it was restored or removed for good. Reload Settings → Trash to see what is left.",
25
+ running: "This project is still running. Use Cancel run on the project page first, then try again.",
26
+ files: "Some of this project's files could not be removed. Close any program using files in the project folder, then press Delete now again.",
27
+ "template-in-trash": "This schedule's template is in the trash too. Restore the template first (it is listed here), then restore the schedule.",
28
+ "template-gone": "This schedule's template was removed for good, so the schedule has nothing to run. It cannot be restored; Delete now removes it from the trash.",
29
+ };
30
+ function refused(c, reason, detail) {
31
+ return problem(c, {
32
+ status: status[reason],
33
+ title: titleOf(status[reason]),
34
+ detail: detail === undefined ? details[reason] : `${details[reason]} (${detail})`,
35
+ extensions: { reason },
36
+ });
37
+ }
38
+ // Settings → Trash: the list, Restore and Delete now. The daily purge runs from main.ts.
39
+ export function trashRoutes(deps) {
40
+ const trash = {
41
+ db: deps.db,
42
+ paths: deps.paths,
43
+ clock: deps.clock,
44
+ log: deps.log,
45
+ hasInflight: deps.runner.hasInflight,
46
+ };
47
+ return new Hono()
48
+ .get("/", (c) => c.json({ items: listTrash(trash) }))
49
+ .post("/:kind/:id/restore", zValidator("param", itemParam, onInvalid), (c) => {
50
+ const { kind, id } = c.req.valid("param");
51
+ const result = restoreItem(trash, kind, id);
52
+ if (!result.ok)
53
+ return refused(c, result.reason);
54
+ // A restored project picks up where it was: unfinished work goes on, a queued batch
55
+ // item is back in its place.
56
+ if (kind === "project")
57
+ deps.runner.tick(id);
58
+ return c.json({ restored: result.value });
59
+ })
60
+ .delete("/:kind/:id", zValidator("param", itemParam, onInvalid), async (c) => {
61
+ const { kind, id } = c.req.valid("param");
62
+ const result = kind === "project"
63
+ ? await withProjectControl(deps.db, id, () => deleteNow(trash, kind, id))
64
+ : deleteNow(trash, kind, id);
65
+ if (!result.ok)
66
+ return refused(c, result.reason, result.detail);
67
+ return c.body(null, 204);
68
+ });
69
+ }
@@ -10,7 +10,7 @@ export function updateRoutes(deps) {
10
10
  status: "idle",
11
11
  blockedReason: "This copy of Slopify cannot update itself from the app. Update it the way you installed it, for example by running the launcher again.",
12
12
  };
13
- return new Hono()
13
+ return (new Hono()
14
14
  .get("/ready", (c) => {
15
15
  c.header("Cache-Control", "no-store");
16
16
  if (!deps.updater?.ready(c.req.header("X-Slopify-Update-Token") ?? ""))
@@ -31,6 +31,23 @@ export function updateRoutes(deps) {
31
31
  return c.json(deps.updater === undefined
32
32
  ? unavailable
33
33
  : await deps.updater.check(c.req.query("refresh") === "1"));
34
+ })
35
+ // Drops an update that is waiting for running work to finish.
36
+ .delete("/", async (c) => {
37
+ const origin = c.req.header("Origin");
38
+ if (origin !== undefined && origin !== new URL(c.req.url).origin)
39
+ return problem(c, {
40
+ status: 403,
41
+ title: titleOf(403),
42
+ detail: "For your safety, updates can only be changed from the Slopify page itself. Open Slopify and try again there.",
43
+ });
44
+ if (deps.updater?.cancelWaiting() !== true)
45
+ return problem(c, {
46
+ status: 409,
47
+ title: titleOf(409),
48
+ detail: "No update is waiting to install. Reload the page to see the current update status.",
49
+ });
50
+ return c.json(await deps.updater.check());
34
51
  })
35
52
  .post("/", async (c) => {
36
53
  const origin = c.req.header("Origin");
@@ -41,7 +58,11 @@ export function updateRoutes(deps) {
41
58
  detail: "For your safety, updates can only be started from the Slopify page itself. Open Slopify and try again there.",
42
59
  });
43
60
  if (deps.updater === undefined)
44
- return problem(c, { status: 409, title: titleOf(409), detail: unavailable.blockedReason });
61
+ return problem(c, {
62
+ status: 409,
63
+ title: titleOf(409),
64
+ detail: unavailable.blockedReason,
65
+ });
45
66
  const result = await deps.updater.start();
46
67
  if (result.ok)
47
68
  return c.json(result.info, 202);
@@ -53,5 +74,5 @@ export function updateRoutes(deps) {
53
74
  result.info.error ??
54
75
  "You already have the latest version of Slopify.",
55
76
  });
56
- });
77
+ }));
57
78
  }
@@ -0,0 +1,15 @@
1
+ import { Hono } from "hono";
2
+ import { markPatchNotesSeen } from "../../slices/patch-notes/seen.js";
3
+ import { dismissWhatsNew, readWhatsNew } from "../../slices/settings/whats-new.js";
4
+ // Preserve Hono's inferred route types for the web client.
5
+ export function whatsNewRoutes(deps) {
6
+ return (new Hono()
7
+ .get("/", (c) => c.json(readWhatsNew(deps.db, deps.version)))
8
+ // The tour stands in for the patch notes on a major update (its last step links to
9
+ // them), so closing it also closes this version's notes: never two popups in a row.
10
+ .post("/seen", (c) => {
11
+ const view = dismissWhatsNew(deps.db, deps.version);
12
+ markPatchNotesSeen(deps.db, deps.version);
13
+ return c.json(view);
14
+ }));
15
+ }
@@ -0,0 +1,83 @@
1
+ import { zValidator } from "@hono/zod-validator";
2
+ import { Hono } from "hono";
3
+ import { z } from "zod";
4
+ import { transact } from "../../kernel/db/tx.js";
5
+ import { projectExists } from "../../slices/admission/repo.js";
6
+ import { descriptionFields, withEdit, withoutEdit } from "../../slices/youtube/edits.js";
7
+ import { readDescriptionEdits, writeDescriptionEdits } from "../../slices/youtube/edits-repo.js";
8
+ import { descriptionMaxCharacters } from "../../slices/youtube/model.js";
9
+ import { channelLinksProblem } from "../../slices/youtube/placeholders.js";
10
+ import { onInvalid, problem, titleOf } from "./problem.js";
11
+ import { channelLinksBody } from "./settings.js";
12
+ const idParam = z.object({ id: z.string().min(1).max(64) });
13
+ const fieldParam = idParam.extend({ field: z.enum(descriptionFields) });
14
+ // A field is at most a description long; the bound keeps a pasted novel off the table, and
15
+ // YouTube's own limit is shown beside the field rather than refused here.
16
+ const fieldBody = z.object({
17
+ text: z.string().max(descriptionMaxCharacters * 4),
18
+ base: z.string().max(descriptionMaxCharacters * 4),
19
+ });
20
+ const gone = "This project no longer exists. Go back to Projects to pick another.";
21
+ // The project page's hand edits to the YouTube description (`slices/youtube/edits.ts`). The
22
+ // server keeps them; the page applies the merge rules to the text it shows.
23
+ export function youtubeEditRoutes(deps) {
24
+ const exists = (id) => projectExists(deps.db, id);
25
+ const missing = (c) => problem(c, { status: 404, title: titleOf(404), detail: gone });
26
+ const now = () => deps.clock.now().toISOString();
27
+ return (new Hono()
28
+ .get("/:id/youtube-edits", zValidator("param", idParam, onInvalid), (c) => {
29
+ const { id } = c.req.valid("param");
30
+ return exists(id) ? c.json(readDescriptionEdits(deps.db, id)) : missing(c);
31
+ })
32
+ // Saves the user's text for one field with the generated text it was edited from; text
33
+ // equal to that generated text clears the edit. "Keep mine" is this call with the new
34
+ // generated text as the base.
35
+ .put("/:id/youtube-edits/fields/:field", zValidator("param", fieldParam, onInvalid), zValidator("json", fieldBody, onInvalid), (c) => {
36
+ const { id, field } = c.req.valid("param");
37
+ if (!exists(id))
38
+ return missing(c);
39
+ const { text, base } = c.req.valid("json");
40
+ const saved = transact(deps.db, () => {
41
+ const current = readDescriptionEdits(deps.db, id);
42
+ const next = { ...current, fields: withEdit(current.fields, field, text, base) };
43
+ writeDescriptionEdits(deps.db, id, next, now());
44
+ return next;
45
+ });
46
+ return c.json(saved);
47
+ })
48
+ // "Use it": the field follows the generated text again.
49
+ .delete("/:id/youtube-edits/fields/:field", zValidator("param", fieldParam, onInvalid), (c) => {
50
+ const { id, field } = c.req.valid("param");
51
+ if (!exists(id))
52
+ return missing(c);
53
+ const saved = transact(deps.db, () => {
54
+ const current = readDescriptionEdits(deps.db, id);
55
+ const next = { ...current, fields: withoutEdit(current.fields, field) };
56
+ writeDescriptionEdits(deps.db, id, next, now());
57
+ return next;
58
+ });
59
+ return c.json(saved);
60
+ })
61
+ // The project's own links, such as its "Previous video"; they win over Settings' list.
62
+ .put("/:id/youtube-edits/links", zValidator("param", idParam, onInvalid), zValidator("json", channelLinksBody, onInvalid), (c) => {
63
+ const { id } = c.req.valid("param");
64
+ if (!exists(id))
65
+ return missing(c);
66
+ const links = c.req
67
+ .valid("json")
68
+ .links.map((link) => ({ name: link.name.trim(), url: link.url.trim() }));
69
+ const refused = channelLinksProblem(links);
70
+ if (refused !== undefined)
71
+ return problem(c, {
72
+ status: 400,
73
+ title: titleOf(400),
74
+ detail: `This project's links weren't saved: ${refused} Then press Save links under YouTube on the Video section.`,
75
+ });
76
+ const saved = transact(deps.db, () => {
77
+ const next = { ...readDescriptionEdits(deps.db, id), links };
78
+ writeDescriptionEdits(deps.db, id, next, now());
79
+ return next;
80
+ });
81
+ return c.json(saved);
82
+ }));
83
+ }
@@ -0,0 +1,62 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ /**
3
+ * `slopify update` for a native install: asks the running app to update itself, exactly as the
4
+ * Update button in Settings does (wait for running work, install, back up the database, start the
5
+ * new version, roll back if it doesn't come up), and follows it until it has finished.
6
+ */
7
+ export async function runNativeUpdate(deps) {
8
+ const poll = deps.pollMs ?? 2000;
9
+ const read = async (refresh = false) => {
10
+ const response = await deps.fetch(`${deps.origin}/api/update${refresh ? "?refresh=1" : ""}`);
11
+ if (!response.ok)
12
+ throw new Error(`Slopify answered ${response.status}.`);
13
+ return (await response.json());
14
+ };
15
+ let info;
16
+ try {
17
+ info = await read(true);
18
+ }
19
+ catch {
20
+ throw new Error(`Slopify isn't running at ${deps.origin}, so there is nothing to update. Start it (npx @gentbajko/slopify) and run slopify update again; if it runs on another port, add --port <number>. To just start the newest version, run npx @gentbajko/slopify@latest.`);
21
+ }
22
+ if (info.status === "error" && info.error !== undefined && info.latestVersion === null)
23
+ throw new Error(info.error);
24
+ if (!info.available) {
25
+ deps.report(`Slopify ${info.currentVersion} is already the newest version.`);
26
+ return;
27
+ }
28
+ const target = info.latestVersion;
29
+ let told = false;
30
+ while (info.busy) {
31
+ if (!told)
32
+ deps.report("Slopify is generating right now. Waiting for running work to finish before updating (press Ctrl+C to cancel; nothing has been changed yet)...");
33
+ told = true;
34
+ await delay(poll);
35
+ info = await read();
36
+ }
37
+ if (!info.canUpdate)
38
+ throw new Error(info.blockedReason ??
39
+ info.error ??
40
+ "Slopify can't update right now. Open Settings in Slopify to see why.");
41
+ const started = await deps.fetch(`${deps.origin}/api/update`, { method: "POST" });
42
+ if (!started.ok) {
43
+ const body = (await started.json().catch(() => ({})));
44
+ throw new Error(body.detail ?? `Slopify refused to start the update (${started.status}).`);
45
+ }
46
+ deps.report(`Updating Slopify ${info.currentVersion} to ${target}...`);
47
+ const deadline = Date.now() + (deps.restartMs ?? 10 * 60_000);
48
+ while (Date.now() < deadline) {
49
+ await delay(poll);
50
+ const now = await read().catch(() => undefined);
51
+ if (now === undefined || now.status === "installing" || now.status === "restarting")
52
+ continue;
53
+ if (now.currentVersion === target && now.status !== "error") {
54
+ deps.report(`Slopify ${target} is running at ${deps.origin}`);
55
+ return;
56
+ }
57
+ if (now.status === "error" || now.error !== undefined)
58
+ throw new Error(now.error ??
59
+ "The update did not finish, so Slopify kept or put back the previous version. The reason is in logs/updates.log inside your Slopify data folder.");
60
+ }
61
+ throw new Error(`Slopify didn't answer at ${deps.origin} within 10 minutes after the update started. Check it in the browser; the updater puts the previous version back on its own if the new one doesn't start, and the reason is in logs/updates.log inside your Slopify data folder.`);
62
+ }