mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-28 01:28:27 +01:00
Adds dependency-free helpers in utils/chapters.ts for working with Jellyfin chapter markers: - chapterMarkers(chapters, durationMs): markers within range, with precomputed percent for slider overlays. - chapterStartsMs(chapters): sorted start positions in ms (skips entries without StartPositionTicks). - currentChapterIndex(positionMs, chapters): active chapter index for the live playback position (-1 if before the first chapter). - chapterNameAt(positionMs, chapters): the active chapter name, or null if missing/unnamed. - sortedChapters(chapters): chapter entries paired with ms start. - formatChapterTime(positionMs): m:ss or h:mm:ss label. All helpers tolerate null/undefined/empty inputs. 17 unit tests under bun test cover sort order, boundary positions, missing fields, and out-of-range inputs.
91 lines
2.9 KiB
TypeScript
91 lines
2.9 KiB
TypeScript
/**
|
|
* Pure helpers for Jellyfin chapter markers. Dependency-free so they are
|
|
* unit-testable under `bun test`.
|
|
*/
|
|
|
|
import type { ChapterInfo } from "@jellyfin/sdk/lib/generated-client/models";
|
|
import { ticksToMs } from "@/utils/time";
|
|
|
|
export interface ChapterMarker {
|
|
/** Chapter start, in milliseconds. */
|
|
positionMs: number;
|
|
/** Chapter start as a percentage (0-100) of the media duration. */
|
|
percent: number;
|
|
}
|
|
|
|
export interface ChapterEntry {
|
|
chapter: ChapterInfo;
|
|
/** Chapter start, in milliseconds. */
|
|
positionMs: number;
|
|
}
|
|
|
|
/** Chapters paired with their millisecond start, sorted ascending by start. */
|
|
export const sortedChapters = (
|
|
chapters: ChapterInfo[] | null | undefined,
|
|
): ChapterEntry[] =>
|
|
(chapters ?? [])
|
|
.filter((c) => c.StartPositionTicks != null)
|
|
.map((chapter) => ({
|
|
chapter,
|
|
positionMs: ticksToMs(chapter.StartPositionTicks),
|
|
}))
|
|
.sort((a, b) => a.positionMs - b.positionMs);
|
|
|
|
/** Chapter start positions in milliseconds, ascending. */
|
|
export const chapterStartsMs = (
|
|
chapters: ChapterInfo[] | null | undefined,
|
|
): number[] =>
|
|
(chapters ?? [])
|
|
.filter((c) => c.StartPositionTicks != null)
|
|
.map((c) => ticksToMs(c.StartPositionTicks))
|
|
.sort((a, b) => a - b);
|
|
|
|
/** Chapter markers within [0, durationMs]; empty when duration is unknown. */
|
|
export const chapterMarkers = (
|
|
chapters: ChapterInfo[] | null | undefined,
|
|
durationMs: number,
|
|
): ChapterMarker[] => {
|
|
if (durationMs <= 0) return [];
|
|
return chapterStartsMs(chapters)
|
|
.filter((ms) => ms >= 0 && ms < durationMs)
|
|
.map((ms) => ({ positionMs: ms, percent: (ms / durationMs) * 100 }));
|
|
};
|
|
|
|
/** Index of the chapter containing `positionMs`, or -1 if before the first. */
|
|
export const currentChapterIndex = (
|
|
positionMs: number,
|
|
chapters: ChapterInfo[] | null | undefined,
|
|
): number => {
|
|
const starts = chapterStartsMs(chapters);
|
|
let index = -1;
|
|
for (let i = 0; i < starts.length; i++) {
|
|
if (positionMs >= starts[i]) index = i;
|
|
else break;
|
|
}
|
|
return index;
|
|
};
|
|
|
|
/** Name of the chapter containing `positionMs`, or null if none / unnamed. */
|
|
export const chapterNameAt = (
|
|
positionMs: number,
|
|
chapters: ChapterInfo[] | null | undefined,
|
|
): string | null => {
|
|
const idx = currentChapterIndex(positionMs, chapters);
|
|
if (idx < 0) return null;
|
|
const sorted = sortedChapters(chapters);
|
|
const name = sorted[idx]?.chapter.Name;
|
|
return name && name.length > 0 ? name : null;
|
|
};
|
|
|
|
/** `m:ss` (or `h:mm:ss` past an hour) label for a millisecond position. */
|
|
export const formatChapterTime = (positionMs: number): string => {
|
|
const total = Math.max(0, Math.floor(positionMs / 1000));
|
|
const hours = Math.floor(total / 3600);
|
|
const minutes = Math.floor((total % 3600) / 60);
|
|
const seconds = total % 60;
|
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
return hours > 0
|
|
? `${hours}:${pad(minutes)}:${pad(seconds)}`
|
|
: `${minutes}:${pad(seconds)}`;
|
|
};
|