Files
siftlode/frontend/src/lib/clipRange.ts
T
peter b4bf71ea64 fix(clip): a clip is held to its owner's storage cap, and its limits and failures say the server's truth
- The cut file is measured against what its owner has left, for a YouTube range (whose fetch never
  reads yt-dlp's size ceiling) and for a Plex clip (which fetches nothing), before the fetch and
  again before it is placed; a refusal leaves the asset pending for a Retry.
- A silent Plex file is cut silent; a file the probe cannot read says so.
- A second job on a failed or renamed clip gets the same YouTube diagnosis and the fetched title.
- The "too long" refusal names the server's cap; a contract test holds the client's copies of the
  clip limits to clip.py; the shipped player clients are pinned against the installed yt-dlp.
- One HDR transfer set and one rating_key lookup; job status words from JobStatus; the display-name
  guard sees subscript writes and the clip route; stale comments and types follow the clip job.
2026-10-10 15:35:12 +02:00

67 lines
3.1 KiB
TypeScript

// The range an ad-hoc clip is cut from, marked in a player: the rules the clip panel's handles, its
// In / Out fields and the players' `[` / `]` keys all go by, so every way of marking keeps a range the
// server takes (backend downloads/clip.py: at least MIN_CLIP_S, at most MAX_CLIP_S — the owner's cap;
// clipLimits.contract.test.ts holds the two copies equal).
const MIN_CLIP_S = 1;
export const MAX_CLIP_S = 600;
/** What a fresh mark spans before the second end is set: a moment, not the cap. */
export const DEFAULT_CLIP_S = 30;
export interface ClipRange {
start: number;
end: number;
}
const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));
/** A range opened at `t`: from there, DEFAULT_CLIP_S long (shorter at the item's end). */
export function openAt(t: number, duration: number): ClipRange {
const start = clamp(t, 0, Math.max(0, duration - MIN_CLIP_S));
return { start, end: Math.min(duration, start + DEFAULT_CLIP_S) };
}
/** In moved to `t`. Out stays where it is while the range stays one the server takes; otherwise it
* follows (a fresh DEFAULT_CLIP_S after an In past it, the cap after an In too far before it). */
export function markIn(r: ClipRange, t: number, duration: number): ClipRange {
const start = clamp(t, 0, Math.max(0, duration - MIN_CLIP_S));
let end = r.end;
if (end - start < MIN_CLIP_S) end = Math.min(duration, start + DEFAULT_CLIP_S);
else if (end - start > MAX_CLIP_S) end = start + MAX_CLIP_S;
return { start, end };
}
/** Out moved to `t`; In follows the same way when the range would stop being one. */
export function markOut(r: ClipRange, t: number, duration: number): ClipRange {
const end = clamp(t, Math.min(MIN_CLIP_S, duration), duration);
let start = r.start;
if (end - start < MIN_CLIP_S) start = Math.max(0, end - DEFAULT_CLIP_S);
else if (end - start > MAX_CLIP_S) start = end - MAX_CLIP_S;
return { start, end };
}
/** Where each end may be dragged to without the other moving. */
export function handleBounds(r: ClipRange, duration: number) {
return {
in: { min: Math.max(0, r.end - MAX_CLIP_S), max: r.end - MIN_CLIP_S },
out: { min: r.start + MIN_CLIP_S, max: Math.min(duration, r.start + MAX_CLIP_S) },
};
}
/** The zoomed strip's window: the range with a margin either side (a quarter of it, at least 5 s),
* so both ends sit inside with room to move them out. */
export function zoomWindow(r: ClipRange, duration: number): { from: number; to: number } {
const pad = Math.max(5, (r.end - r.start) * 0.25);
return { from: Math.max(0, r.start - pad), to: Math.min(duration, r.end + pad) };
}
/** The `[` key: In at the playhead `t`, opening a range there when none is marked yet. */
export function markInAt(r: ClipRange | null, t: number, duration: number): ClipRange {
return markIn(r ?? openAt(t, duration), t, duration);
}
/** The `]` key: Out at the playhead `t`; with no range yet, one that ENDS there. */
export function markOutAt(r: ClipRange | null, t: number, duration: number): ClipRange {
return markOut(r ?? openAt(Math.max(0, t - DEFAULT_CLIP_S), duration), t, duration);
}