dsh-vidi 0.9.3 → 0.9.4

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 {
@@ -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();
@@ -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
  })
@@ -37758,8 +37797,10 @@ const en = {
37758
37797
  "dev.and": " and ",
37759
37798
  "how.title": "How subtitles are made",
37760
37799
  "how.more": "More services and keys are in Settings.",
37761
- "lines.merge": "Join with the next line",
37762
- "lines.split": "Split this line",
37800
+ "lines.merge": "Join this line and the next into one",
37801
+ "lines.split": "Break this line in two: then click where it should break",
37802
+ "lines.mergeShort": "Join",
37803
+ "lines.splitShort": "Split",
37763
37804
  "lines.splitHint": "Click where the line should break.",
37764
37805
  "hist.goLive": "Watch live again",
37765
37806
  "hist.replay": "Replay",
@@ -37767,6 +37808,14 @@ const en = {
37767
37808
  "hist.untitled": "Untitled video",
37768
37809
  "hist.live": "live",
37769
37810
  "hist.running": "Live now",
37811
+ "hist.recording": "Recording",
37812
+ "hist.loading": "Loading",
37813
+ "hist.subs": "Making subtitles",
37814
+ "hist.subsPct": "Subtitles {p}%",
37815
+ "hist.pushing": "Pushing out",
37816
+ "hist.stop": "Stop",
37817
+ "hist.stopping": "Stopping…",
37818
+ "hist.stop.tip": "Stop: {w}",
37770
37819
  "hist.more": "More ({n})",
37771
37820
  "set.spoken.tip": "The language people speak in this video; Auto detects it.",
37772
37821
  "rs.mode": "Subtitle mode of the push-out",
@@ -38219,8 +38268,10 @@ const zh = {
38219
38268
  "dev.and": "和",
38220
38269
  "how.title": "字幕怎么做",
38221
38270
  "how.more": "更多服务和 key 在设置里。",
38222
- "lines.merge": "和下一句合并",
38223
- "lines.split": "拆成两句",
38271
+ "lines.merge": "把这一句和下一句合成一句",
38272
+ "lines.split": "把这一句拆成两句:点了以后,再点文字里要断开的位置",
38273
+ "lines.mergeShort": "合并",
38274
+ "lines.splitShort": "拆开",
38224
38275
  "lines.splitHint": "点一下要断开的位置。",
38225
38276
  "hist.goLive": "回到直播",
38226
38277
  "hist.replay": "回放",
@@ -38228,6 +38279,14 @@ const zh = {
38228
38279
  "hist.untitled": "未命名视频",
38229
38280
  "hist.live": "直播",
38230
38281
  "hist.running": "直播中",
38282
+ "hist.recording": "录制中",
38283
+ "hist.loading": "加载中",
38284
+ "hist.subs": "字幕制作中",
38285
+ "hist.subsPct": "字幕 {p}%",
38286
+ "hist.pushing": "推流中",
38287
+ "hist.stop": "停止",
38288
+ "hist.stopping": "停止中…",
38289
+ "hist.stop.tip": "停止:{w}",
38231
38290
  "hist.more": "更多(还有 {n} 个)",
38232
38291
  "set.spoken.tip": "视频里说的是哪种语言;自动 = 自己识别。",
38233
38292
  "rs.mode": "推流用的字幕模式",
@@ -38912,6 +38971,9 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
38912
38971
  .dshl-history .tx { display: grid; gap: 3px; min-width: 0; flex: 1; }
38913
38972
  .dshl-history .tt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 550; }
38914
38973
  .dshl-history .mm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--faint); font-size: 12px; }
38974
+ .dshl-history .stop { flex: none; padding: 4px 6px; font-size: 12.5px; color: var(--faint); }
38975
+ .dshl-history .stop:hover { color: var(--live); }
38976
+ .dshl-history .stop:disabled { opacity: .5; }
38915
38977
  .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
38978
  .dshl-history .more { margin: 8px 0 0 6px; font-size: 12.5px; }
38917
38979
  .dshl-modemenu .mh { margin: 6px 4px 2px; color: rgba(255,255,255,.55); font-size: 11.5px; }
@@ -38921,8 +38983,11 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
38921
38983
  .dshl-line { position: relative; }
38922
38984
  .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
38985
  .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; }
38986
+ .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; }
38987
+ .dshl-line .tools svg { width: 14px; height: 14px; flex: none; }
38988
+ /* Says what a button does the moment the pointer is on it (the system tooltip takes a second). */
38989
+ .dshl-history .stop { position: relative; }
38990
+ .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
38991
  .dshl-line .tools .ghost:hover { background: var(--hover); color: var(--ink); }
38927
38992
  .dshl-line.splitting { cursor: default; background: color-mix(in srgb, var(--accent) 8%, transparent); }
38928
38993
  .dshl-line .cutline .ch { cursor: col-resize; border-left: 1px solid transparent; }
package/lib/index.mjs CHANGED
@@ -2021,6 +2021,12 @@ function cutNear(cs, ratio) {
2021
2021
  }
2022
2022
  return best > 0 ? best : Math.max(1, Math.min(n - 1, target));
2023
2023
  }
2024
+ /** When a video was first opened: its folder is named after that moment (UTC); older saves could carry a later time. */
2025
+ function openedAt(id, saved) {
2026
+ const named = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})/.exec(id);
2027
+ const fromName = named ? Date.parse(`${named[1]}T${named[2]}:${named[3]}:${named[4]}Z`) : NaN;
2028
+ return Number.isFinite(fromName) ? fromName : typeof saved === "number" ? saved : void 0;
2029
+ }
2024
2030
  var LiveJob = class LiveJob extends EventEmitter {
2025
2031
  opts;
2026
2032
  deps;
@@ -2033,6 +2039,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2033
2039
  status = "resolving";
2034
2040
  message;
2035
2041
  title;
2042
+ /** When it was first opened (kept across restores, so the history keeps its place). */
2036
2043
  startedAt = Date.now();
2037
2044
  cues = [];
2038
2045
  ffmpeg;
@@ -2175,6 +2182,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2175
2182
  const j = JSON.parse(readFileSync(join(dir, "job.json"), "utf8"));
2176
2183
  const job = new LiveJob(j.id, j.opts, deps);
2177
2184
  job.title = j.title;
2185
+ job.startedAt = openedAt(j.id, j.startedAt) ?? job.startedAt;
2178
2186
  job.vod = !!j.vod;
2179
2187
  job.duration = j.duration;
2180
2188
  job.captureDone = true;
@@ -6611,7 +6619,7 @@ function apply(ctx, config) {
6611
6619
  title: j.title || device || (j.opts.source === "url" ? j.opts.url : ""),
6612
6620
  source: j.opts.source,
6613
6621
  url: j.opts.source === "url" ? j.opts.url : void 0,
6614
- startedAt: j.startedAt,
6622
+ startedAt: openedAt(j.id, j.startedAt),
6615
6623
  duration,
6616
6624
  live: !j.vod,
6617
6625
  running: false,
@@ -6627,7 +6635,26 @@ function apply(ctx, config) {
6627
6635
  }
6628
6636
  if (hit.item) out.push(hit.item);
6629
6637
  }
6630
- return out;
6638
+ for (const item of out) {
6639
+ const j = jobs.get(String(item.id));
6640
+ if (!j) continue;
6641
+ const w = workOf(j);
6642
+ if (w) item.work = w;
6643
+ }
6644
+ return [...out.filter((i) => i.work), ...out.filter((i) => !i.work)];
6645
+ }
6646
+ /** Still working: recording a live stream, loading a video, making subtitles, pushing out. */
6647
+ function workOf(j) {
6648
+ const s = j.summary();
6649
+ const ok = !s.message;
6650
+ const total = s.vod ? s.duration : s.status === "running" ? void 0 : s.recorded || s.duration;
6651
+ const w = {
6652
+ recording: s.status === "running" && !s.vod && ok,
6653
+ loading: (s.status === "running" || s.status === "resolving") && !!s.vod && ok,
6654
+ subs: s.subs?.state === "running" ? total ? Math.min(99, Math.round(s.subs.at / total * 100)) : -1 : void 0,
6655
+ pushing: !!s.restream?.running
6656
+ };
6657
+ return w.recording || w.loading || w.subs !== void 0 || w.pushing ? w : void 0;
6631
6658
  }
6632
6659
  /** A small picture of a video for its history row (made once, kept in its folder). */
6633
6660
  async function thumbnail(id) {
@@ -6810,6 +6837,16 @@ function apply(ctx, config) {
6810
6837
  } catch {}
6811
6838
  return send(res, 200, { ok: true });
6812
6839
  }
6840
+ if (route === "halt" && req.method === "POST") {
6841
+ const b = await readJson(req);
6842
+ const job = jobs.get(String(b.jobId));
6843
+ if (!job) return send(res, 404, { error: "no such job" });
6844
+ const s = job.summary();
6845
+ if (s.subs?.state === "running") await job.pauseSubs();
6846
+ if (s.restream?.running) await job.stopRestream();
6847
+ if ((s.status === "running" || s.status === "resolving") && !s.vod) await job.stop();
6848
+ return send(res, 200, { ok: true });
6849
+ }
6813
6850
  if (route === "stop" && req.method === "POST") {
6814
6851
  const b = await readJson(req);
6815
6852
  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.4",
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",