mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-22 23:06:39 +01:00
79 lines
2.5 KiB
TypeScript
79 lines
2.5 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;
|
|
};
|
|
|
|
/** `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)}`;
|
|
};
|