dsh-vidi 0.9.3 → 0.9.5

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.
package/lib/client.js CHANGED
@@ -35416,6 +35416,31 @@ function History({ t, ui, onOpen }) {
35416
35416
  (0, react.useEffect)(() => {
35417
35417
  load(0, 5);
35418
35418
  }, []);
35419
+ const busy = items.some((v) => v.work);
35420
+ const shown = (0, react.useRef)(5);
35421
+ shown.current = Math.max(5, items.length);
35422
+ (0, react.useEffect)(() => {
35423
+ if (!busy) return;
35424
+ const timer = window.setInterval(() => void load(0, shown.current), 3e3);
35425
+ return () => window.clearInterval(timer);
35426
+ }, [busy]);
35427
+ const [halting, setHalting] = (0, react.useState)(null);
35428
+ const halt = async (v) => {
35429
+ setHalting(v.id);
35430
+ await fetch("/live/api/halt", {
35431
+ method: "POST",
35432
+ headers: { "content-type": "application/json" },
35433
+ body: JSON.stringify({ jobId: v.id })
35434
+ }).catch(() => {});
35435
+ await load(0, shown.current);
35436
+ setHalting(null);
35437
+ };
35438
+ const doing = (w) => [
35439
+ w.recording ? t("hist.recording") : "",
35440
+ w.loading ? t("hist.loading") : "",
35441
+ w.subs !== void 0 ? w.subs >= 0 ? t("hist.subsPct").replace("{p}", String(w.subs)) : t("hist.subs") : "",
35442
+ w.pushing ? t("hist.pushing") : ""
35443
+ ].filter(Boolean);
35419
35444
  if (!items.length) return null;
35420
35445
  const from = (v) => v.source === "file" ? t("src.file") : v.source === "device" ? t("src.camera") : v.source === "push" ? t("src.push") : siteName(v.url ?? "") || (() => {
35421
35446
  try {
@@ -35441,7 +35466,7 @@ function History({ t, ui, onOpen }) {
35441
35466
  onKeyDown: (e) => {
35442
35467
  if (e.key === "Enter") onOpen(v);
35443
35468
  },
35444
- title: v.live && v.source === "url" && !v.running ? t("hist.goLive") : void 0,
35469
+ title: v.live && v.source === "url" && !v.running && !v.work ? t("hist.goLive") : void 0,
35445
35470
  children: [
35446
35471
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
35447
35472
  className: "th",
@@ -35470,11 +35495,23 @@ function History({ t, ui, onOpen }) {
35470
35495
  ].filter(Boolean).join(" · ")
35471
35496
  })]
35472
35497
  }),
35473
- v.running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35498
+ v.work ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35499
+ className: "chip",
35500
+ children: doing(v.work).join(" · ")
35501
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35502
+ className: "dshl-link stop",
35503
+ disabled: halting === v.id,
35504
+ "data-tip": t("hist.stop.tip").replace("{w}", doing(v.work).join("、")),
35505
+ onClick: (e) => {
35506
+ e.stopPropagation();
35507
+ halt(v);
35508
+ },
35509
+ children: t(halting === v.id ? "hist.stopping" : "hist.stop")
35510
+ })] }) : v.running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
35474
35511
  className: "chip",
35475
35512
  children: t("hist.running")
35476
- }),
35477
- v.live && v.source === "url" && !v.running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35513
+ }) : null,
35514
+ v.live && v.source === "url" && !v.running && !v.work && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
35478
35515
  className: "dshl-link replay",
35479
35516
  onClick: (e) => {
35480
35517
  e.stopPropagation();
@@ -36256,7 +36293,7 @@ function LiveView(props) {
36256
36293
  };
36257
36294
  const openPast = async (v, replay = false) => {
36258
36295
  setError("");
36259
- if (!replay && v.live && v.source === "url" && v.url && !v.running) {
36296
+ if (!replay && v.live && v.source === "url" && v.url && !v.running && !v.work) {
36260
36297
  if (v.conversationId !== sessionId && props.onOpenVideo && await props.onOpenVideo(v.conversationId, v.id, v.url)) return;
36261
36298
  start(v.url, "url");
36262
36299
  return;
@@ -36518,21 +36555,23 @@ function LiveView(props) {
36518
36555
  c.final && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
36519
36556
  className: "tools",
36520
36557
  onClick: (e) => e.stopPropagation(),
36521
- children: [idx < shown.length - 1 && shown[idx + 1]?.final && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36558
+ children: [idx < shown.length - 1 && shown[idx + 1]?.final && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
36522
36559
  className: "ghost",
36523
- title: t("lines.merge"),
36560
+ "data-tip": t("lines.merge"),
36561
+ "aria-label": t("lines.merge"),
36524
36562
  onClick: () => void mergeLine(c.id),
36525
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.merge, {})
36563
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.merge, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("lines.mergeShort") })]
36526
36564
  }), (() => {
36527
36565
  const k = shownLines.find((x) => (lineText(c, x) ?? "").length > 1) ?? "src";
36528
- return Array.from(lineText(c, k) ?? c.src).length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
36566
+ return Array.from(lineText(c, k) ?? c.src).length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
36529
36567
  className: "ghost",
36530
- title: t("lines.split"),
36568
+ "data-tip": t("lines.split"),
36569
+ "aria-label": t("lines.split"),
36531
36570
  onClick: () => setSplitting({
36532
36571
  id: c.id,
36533
36572
  key: lineKey(k)
36534
36573
  }),
36535
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.scissors, {})
36574
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.scissors, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("lines.splitShort") })]
36536
36575
  });
36537
36576
  })()]
36538
36577
  })
@@ -37060,6 +37099,10 @@ function SubsButton(props) {
37060
37099
  style: { ["--p"]: `${pct ?? 0}%` },
37061
37100
  children: [
37062
37101
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: "dot" }),
37102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
37103
+ className: "pz",
37104
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.pause, {})
37105
+ }),
37063
37106
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
37064
37107
  className: "now",
37065
37108
  children: pct !== void 0 ? t("subs.progress").replace("{p}", String(pct)) : t("subs.progressLive").replace("{t}", CLOCK(at))
@@ -37078,7 +37121,14 @@ function SubsButton(props) {
37078
37121
  className: "dshl-subsbtn",
37079
37122
  onClick: onResume,
37080
37123
  title: t("subs.resume.tip").replace("{t}", CLOCK(at)),
37081
- children: [t("subs.resume"), pct !== void 0 ? ` · ${pct}%` : ""]
37124
+ children: [
37125
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
37126
+ className: "pz on",
37127
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.play, {})
37128
+ }),
37129
+ t("subs.resume"),
37130
+ pct !== void 0 ? ` · ${pct}%` : ""
37131
+ ]
37082
37132
  });
37083
37133
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
37084
37134
  className: "dshl-subsbtn",
@@ -37758,8 +37808,10 @@ const en = {
37758
37808
  "dev.and": " and ",
37759
37809
  "how.title": "How subtitles are made",
37760
37810
  "how.more": "More services and keys are in Settings.",
37761
- "lines.merge": "Join with the next line",
37762
- "lines.split": "Split this line",
37811
+ "lines.merge": "Join this line and the next into one",
37812
+ "lines.split": "Break this line in two: then click where it should break",
37813
+ "lines.mergeShort": "Join",
37814
+ "lines.splitShort": "Split",
37763
37815
  "lines.splitHint": "Click where the line should break.",
37764
37816
  "hist.goLive": "Watch live again",
37765
37817
  "hist.replay": "Replay",
@@ -37767,6 +37819,14 @@ const en = {
37767
37819
  "hist.untitled": "Untitled video",
37768
37820
  "hist.live": "live",
37769
37821
  "hist.running": "Live now",
37822
+ "hist.recording": "Recording",
37823
+ "hist.loading": "Loading",
37824
+ "hist.subs": "Making subtitles",
37825
+ "hist.subsPct": "Subtitles {p}%",
37826
+ "hist.pushing": "Pushing out",
37827
+ "hist.stop": "Stop",
37828
+ "hist.stopping": "Stopping…",
37829
+ "hist.stop.tip": "Stop: {w}",
37770
37830
  "hist.more": "More ({n})",
37771
37831
  "set.spoken.tip": "The language people speak in this video; Auto detects it.",
37772
37832
  "rs.mode": "Subtitle mode of the push-out",
@@ -38219,8 +38279,10 @@ const zh = {
38219
38279
  "dev.and": "和",
38220
38280
  "how.title": "字幕怎么做",
38221
38281
  "how.more": "更多服务和 key 在设置里。",
38222
- "lines.merge": "和下一句合并",
38223
- "lines.split": "拆成两句",
38282
+ "lines.merge": "把这一句和下一句合成一句",
38283
+ "lines.split": "把这一句拆成两句:点了以后,再点文字里要断开的位置",
38284
+ "lines.mergeShort": "合并",
38285
+ "lines.splitShort": "拆开",
38224
38286
  "lines.splitHint": "点一下要断开的位置。",
38225
38287
  "hist.goLive": "回到直播",
38226
38288
  "hist.replay": "回放",
@@ -38228,6 +38290,14 @@ const zh = {
38228
38290
  "hist.untitled": "未命名视频",
38229
38291
  "hist.live": "直播",
38230
38292
  "hist.running": "直播中",
38293
+ "hist.recording": "录制中",
38294
+ "hist.loading": "加载中",
38295
+ "hist.subs": "字幕制作中",
38296
+ "hist.subsPct": "字幕 {p}%",
38297
+ "hist.pushing": "推流中",
38298
+ "hist.stop": "停止",
38299
+ "hist.stopping": "停止中…",
38300
+ "hist.stop.tip": "停止:{w}",
38231
38301
  "hist.more": "更多(还有 {n} 个)",
38232
38302
  "set.spoken.tip": "视频里说的是哪种语言;自动 = 自己识别。",
38233
38303
  "rs.mode": "推流用的字幕模式",
@@ -38859,7 +38929,12 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
38859
38929
  @container (max-width: 640px) { .dshl-subsbtn .now { display: none; } .dshl-subsbtn.running:not(:hover) .short { display: inline; } }
38860
38930
  .dshl-subsbtn.running:hover .now { display: none; }
38861
38931
  .dshl-subsbtn.running:hover .act { display: inline; }
38862
- .dshl-subsbtn.running:hover .dot { width: 8px; height: 8px; border-radius: 1px; background: #fff; animation: none; }
38932
+ .dshl-subsbtn .pz { display: none; margin: 0 -2px 0 -4px; }
38933
+ .dshl-subsbtn .pz svg { display: block; width: 15px; height: 15px; }
38934
+ .dshl-subsbtn .pz.on { display: inline-flex; }
38935
+ /* Hover on a running one: the pause sign stands where the pulsing dot was. */
38936
+ .dshl-subsbtn.running:hover .dot { display: none; }
38937
+ .dshl-subsbtn.running:hover .pz { display: inline-flex; }
38863
38938
  @container (max-width: 520px) { .dshl-subsbtn { margin: 0 2px; padding: 0 9px; } }
38864
38939
  .dshl-starter { position: relative; display: inline-flex; }
38865
38940
  .dshl-startpop { position: absolute; left: 50%; bottom: calc(100% + 26px); z-index: 6; display: grid; gap: 10px; width: 290px; padding: 12px 12px 14px; transform: translateX(-50%);
@@ -38912,6 +38987,9 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
38912
38987
  .dshl-history .tx { display: grid; gap: 3px; min-width: 0; flex: 1; }
38913
38988
  .dshl-history .tt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 550; }
38914
38989
  .dshl-history .mm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--faint); font-size: 12px; }
38990
+ .dshl-history .stop { flex: none; padding: 4px 6px; font-size: 12.5px; color: var(--faint); }
38991
+ .dshl-history .stop:hover { color: var(--live); }
38992
+ .dshl-history .stop:disabled { opacity: .5; }
38915
38993
  .dshl-history .chip { flex: none; padding: 2px 8px; border-radius: 999px; background: color-mix(in srgb, var(--live) 14%, transparent); color: var(--live); font-size: 11.5px; font-weight: 600; }
38916
38994
  .dshl-history .more { margin: 8px 0 0 6px; font-size: 12.5px; }
38917
38995
  .dshl-modemenu .mh { margin: 6px 4px 2px; color: rgba(255,255,255,.55); font-size: 11.5px; }
@@ -38921,8 +38999,11 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
38921
38999
  .dshl-line { position: relative; }
38922
39000
  .dshl-line .tools { position: absolute; top: 4px; right: 4px; display: none; gap: 2px; padding: 2px; border-radius: 7px; background: var(--bg); box-shadow: 0 1px 4px rgba(0,0,0,.12); }
38923
39001
  .dshl-line:hover .tools, .dshl-line:focus-within .tools { display: flex; }
38924
- .dshl-line .tools .ghost { display: grid; place-items: center; width: 26px; height: 24px; border: 0; border-radius: 5px; background: none; color: var(--muted); cursor: pointer; }
38925
- .dshl-line .tools svg { width: 15px; height: 15px; }
39002
+ .dshl-line .tools .ghost { position: relative; display: flex; align-items: center; gap: 4px; height: 24px; padding: 0 7px 0 5px; border: 0; border-radius: 5px; background: none; color: var(--muted); font-size: 12px; white-space: nowrap; cursor: pointer; }
39003
+ .dshl-line .tools svg { width: 14px; height: 14px; flex: none; }
39004
+ /* Says what a button does the moment the pointer is on it (the system tooltip takes a second). */
39005
+ .dshl-history .stop { position: relative; }
39006
+ .dshl-history .stop[data-tip]:hover::after, .dshl-line .tools .ghost[data-tip]:hover::after { content: attr(data-tip); position: absolute; top: calc(100% + 6px); right: 0; z-index: 5; width: max-content; max-width: 220px; padding: 5px 8px; border-radius: 6px; background: var(--ink); color: var(--bg); font-size: 12px; line-height: 1.45; white-space: normal; text-align: left; pointer-events: none; }
38926
39007
  .dshl-line .tools .ghost:hover { background: var(--hover); color: var(--ink); }
38927
39008
  .dshl-line.splitting { cursor: default; background: color-mix(in srgb, var(--accent) 8%, transparent); }
38928
39009
  .dshl-line .cutline .ch { cursor: col-resize; border-left: 1px solid transparent; }
package/lib/index.mjs CHANGED
@@ -2021,6 +2021,28 @@ function cutNear(cs, ratio) {
2021
2021
  }
2022
2022
  return best > 0 ? best : Math.max(1, Math.min(n - 1, target));
2023
2023
  }
2024
+ /**
2025
+ * A line meant to be in `lang` that is written in another script: live interpretation now and then
2026
+ * passes the speaker's words through untranslated (Chinese in a Japanese line). Latin words (names,
2027
+ * brands) never count; a Japanese line has kana, a Chinese one none, a Korean one hangul.
2028
+ */
2029
+ function wrongScript(text, lang) {
2030
+ const n = (re) => (text.match(re) ?? []).length;
2031
+ const han = n(/[\u3400-\u9fff]/g), kana = n(/[\u3040-\u30ff]/g), hangul = n(/[\uac00-\ud7af\u1100-\u11ff]/g);
2032
+ const letters = n(/\p{L}/gu);
2033
+ const base = lang.split("-")[0];
2034
+ if (base === "ja") return kana === 0 && (han >= 4 || /[是吧嗯么们这那的呢吗啊呃对没还就说几]/.test(text)) || hangul >= 2;
2035
+ if (base === "zh") return kana >= 2 || hangul >= 2;
2036
+ if (base === "ko") return hangul === 0 && han + kana >= 3;
2037
+ const cjk = han + kana + hangul;
2038
+ return cjk >= 3 && cjk > letters * .3;
2039
+ }
2040
+ /** When a video was first opened: its folder is named after that moment (UTC); older saves could carry a later time. */
2041
+ function openedAt(id, saved) {
2042
+ const named = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})/.exec(id);
2043
+ const fromName = named ? Date.parse(`${named[1]}T${named[2]}:${named[3]}:${named[4]}Z`) : NaN;
2044
+ return Number.isFinite(fromName) ? fromName : typeof saved === "number" ? saved : void 0;
2045
+ }
2024
2046
  var LiveJob = class LiveJob extends EventEmitter {
2025
2047
  opts;
2026
2048
  deps;
@@ -2033,6 +2055,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2033
2055
  status = "resolving";
2034
2056
  message;
2035
2057
  title;
2058
+ /** When it was first opened (kept across restores, so the history keeps its place). */
2036
2059
  startedAt = Date.now();
2037
2060
  cues = [];
2038
2061
  ffmpeg;
@@ -2175,6 +2198,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2175
2198
  const j = JSON.parse(readFileSync(join(dir, "job.json"), "utf8"));
2176
2199
  const job = new LiveJob(j.id, j.opts, deps);
2177
2200
  job.title = j.title;
2201
+ job.startedAt = openedAt(j.id, j.startedAt) ?? job.startedAt;
2178
2202
  job.vod = !!j.vod;
2179
2203
  job.duration = j.duration;
2180
2204
  job.captureDone = true;
@@ -2213,6 +2237,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2213
2237
  state: j.subs?.state === "done" ? "done" : at > 0 ? "paused" : "off",
2214
2238
  at
2215
2239
  };
2240
+ setTimeout(() => job.repairEchoes(), 3e3);
2216
2241
  if (!existsSync(join(dir, "recording.mp4"))) job.remux();
2217
2242
  return job;
2218
2243
  } catch {
@@ -2849,20 +2874,49 @@ var LiveJob = class LiveJob extends EventEmitter {
2849
2874
  this.emit("cue", cue);
2850
2875
  if (cue.final) {
2851
2876
  this.deps.glossary.notice(cue.src);
2852
- if (this.opts.mode !== "polished" && !this.provider?.translates && cue.tr === void 0 && this.targets().length) this.translate(cue);
2877
+ const echoed = this.echoOf(cue);
2878
+ if (echoed) this.translate(cue, echoed);
2879
+ else if (this.opts.mode !== "polished" && !this.provider?.translates && cue.tr === void 0 && this.targets().length) this.translate(cue);
2853
2880
  else {
2854
2881
  this.record(cue);
2855
2882
  this.polisher.add(cue);
2856
2883
  }
2857
2884
  }
2858
2885
  }
2859
- translate(cue) {
2886
+ /**
2887
+ * Translations of a line that are in the wrong language: taken out (the line shows without them
2888
+ * until the proper one comes), and the text to translate from returned (the line's source, or the
2889
+ * echoed text itself when the line has none of its own: its source sits with the next line).
2890
+ */
2891
+ echoOf(cue) {
2892
+ let from;
2893
+ for (const t of this.targets()) {
2894
+ const x = cue.tr?.[t];
2895
+ if (!x || !wrongScript(x, t)) continue;
2896
+ from ??= cue.src.trim() || x;
2897
+ delete cue.tr[t];
2898
+ }
2899
+ if (from && cue.tr && !Object.keys(cue.tr).length) cue.tr = void 0;
2900
+ return from;
2901
+ }
2902
+ /** Lines saved with a translation in the wrong language (before this check existed): translate them again. */
2903
+ repairEchoes() {
2904
+ for (const cue of this.cues) {
2905
+ if (!cue.final || cue.edited) continue;
2906
+ const from = this.echoOf(cue);
2907
+ if (from) {
2908
+ this.emit("cue", cue);
2909
+ this.translate(cue, from);
2910
+ }
2911
+ }
2912
+ }
2913
+ translate(cue, text = cue.src) {
2860
2914
  const context = this.cues.filter((c) => c.final && c.id < cue.id).slice(-6);
2861
2915
  this.pendingTranslations++;
2862
2916
  this.translating = this.translating.then(async () => {
2863
2917
  try {
2864
2918
  const ref = this.siteText(cue.start, cue.end).map((x) => `[${x.lang}${x.auto ? ", auto" : ""}] ${x.text}`).join("\n");
2865
- const tr = await this.deps.translator.translate(this.targets(), cue.src, context, this.deps.glossary.forPrompt(), void 0, ref);
2919
+ const tr = await this.deps.translator.translate(this.targets(), text, context, this.deps.glossary.forPrompt(), void 0, ref);
2866
2920
  if (tr) {
2867
2921
  cue.tr = {
2868
2922
  ...cue.tr,
@@ -6611,7 +6665,7 @@ function apply(ctx, config) {
6611
6665
  title: j.title || device || (j.opts.source === "url" ? j.opts.url : ""),
6612
6666
  source: j.opts.source,
6613
6667
  url: j.opts.source === "url" ? j.opts.url : void 0,
6614
- startedAt: j.startedAt,
6668
+ startedAt: openedAt(j.id, j.startedAt),
6615
6669
  duration,
6616
6670
  live: !j.vod,
6617
6671
  running: false,
@@ -6627,7 +6681,26 @@ function apply(ctx, config) {
6627
6681
  }
6628
6682
  if (hit.item) out.push(hit.item);
6629
6683
  }
6630
- return out;
6684
+ for (const item of out) {
6685
+ const j = jobs.get(String(item.id));
6686
+ if (!j) continue;
6687
+ const w = workOf(j);
6688
+ if (w) item.work = w;
6689
+ }
6690
+ return [...out.filter((i) => i.work), ...out.filter((i) => !i.work)];
6691
+ }
6692
+ /** Still working: recording a live stream, loading a video, making subtitles, pushing out. */
6693
+ function workOf(j) {
6694
+ const s = j.summary();
6695
+ const ok = !s.message;
6696
+ const total = s.vod ? s.duration : s.status === "running" ? void 0 : s.recorded || s.duration;
6697
+ const w = {
6698
+ recording: s.status === "running" && !s.vod && ok,
6699
+ loading: (s.status === "running" || s.status === "resolving") && !!s.vod && ok,
6700
+ subs: s.subs?.state === "running" ? total ? Math.min(99, Math.round(s.subs.at / total * 100)) : -1 : void 0,
6701
+ pushing: !!s.restream?.running
6702
+ };
6703
+ return w.recording || w.loading || w.subs !== void 0 || w.pushing ? w : void 0;
6631
6704
  }
6632
6705
  /** A small picture of a video for its history row (made once, kept in its folder). */
6633
6706
  async function thumbnail(id) {
@@ -6810,6 +6883,16 @@ function apply(ctx, config) {
6810
6883
  } catch {}
6811
6884
  return send(res, 200, { ok: true });
6812
6885
  }
6886
+ if (route === "halt" && req.method === "POST") {
6887
+ const b = await readJson(req);
6888
+ const job = jobs.get(String(b.jobId));
6889
+ if (!job) return send(res, 404, { error: "no such job" });
6890
+ const s = job.summary();
6891
+ if (s.subs?.state === "running") await job.pauseSubs();
6892
+ if (s.restream?.running) await job.stopRestream();
6893
+ if ((s.status === "running" || s.status === "resolving") && !s.vod) await job.stop();
6894
+ return send(res, 200, { ok: true });
6895
+ }
6813
6896
  if (route === "stop" && req.method === "POST") {
6814
6897
  const b = await readJson(req);
6815
6898
  const job = jobs.get(String(b.jobId));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vidi",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "A video tool for DeepSeek Harness: watch live streams, videos and podcasts with real-time subtitles in any language, then clip and download",
5
5
  "type": "module",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",