Files
streamyfin/providers/Downloads/types.ts
Gauvain e85fc77643 feat(player): add media segment skip with all 5 Jellyfin segment types
Closes #1312
Fixes #883

Adds a unified segment skip feature using the Jellyfin 10.11+
MediaSegments API. Replaces the legacy intro-only and credits-only
hooks with a single useSegmentSkipper hook covering Intro, Outro,
Recap, Commercial, and Preview.

Three modes per segment type: none, ask (show button), auto (skip
automatically). A dedicated submenu under Playback Controls keeps
the main settings page uncluttered.

Highlights:
- utils/segments.ts uses getMediaSegmentsApi from @jellyfin/sdk so
  includeSegmentTypes is serialized as repeated keys instead of the
  bracket-encoded form axios produces by default (the Jellyfin
  server silently ignored the filter otherwise). Falls back to the
  pre-10.11 intro-skipper / chapter-credits plugin endpoints when
  the new API is unavailable.
- hooks/useSegmentSkipper.ts stores seek and haptic in refs so the
  auto-skip effect does not re-run when their identities change
  (useHaptic returns a fresh no-op every render when disabled).
  currentSegment is memoized; the per-segment-type setting lookup
  uses a small map instead of a switch IIFE.
- components/video-player/controls/Controls.tsx prioritizes
  Commercial > Recap > Intro > Preview > Outro when multiple
  segments overlap and exposes the active type to BottomControls
  via skipButtonText.
- components/video-player/controls/BottomControls.tsx accepts the
  dynamic skipButtonText/skipCreditButtonText props.
- providers/Downloads/types.ts extends DownloadedItem with the
  three new segment buckets for offline playback.
- utils/atoms/settings.ts adds SegmentSkipMode and the five skip
  settings, defaulting to "ask".
- app/(auth)/(tabs)/(home)/settings/segment-skip/page.tsx renders
  the five dropdowns from a data table.
- translations/en.json and translations/fr.json add the new keys.
2026-05-27 23:20:41 +02:00

158 lines
5.7 KiB
TypeScript

import type {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models";
import { Bitrate } from "@/components/BitrateSelector";
/**
* Represents the data for downloaded trickplay files.
*/
export interface TrickPlayData {
/** The local directory path where trickplay image sheets are stored. */
path: string;
/** The total size of all trickplay images in bytes. */
size: number;
}
/**
* Represents the user data for a downloaded item.
*/
interface UserData {
subtitleStreamIndex: number;
/** The last known audio stream index. */
audioStreamIndex: number;
/** Whether the downloaded file was transcoded (has only one audio track). */
isTranscoded: boolean;
}
/** Represents a segment of time in a media item, used for intro/credit skipping. */
export interface MediaTimeSegment {
startTime: number;
endTime: number;
text: string;
}
/** Represents a single downloaded media item with all necessary metadata for offline playback. */
export interface DownloadedItem {
/** The Jellyfin item DTO. */
item: BaseItemDto;
/** The media source information. */
mediaSource: MediaSourceInfo;
/** The local file path of the downloaded video. */
videoFilePath: string;
/** The size of the video file in bytes. */
videoFileSize: number;
/** The video filename (for easy File object reconstruction). Optional for backwards compatibility. */
videoFileName?: string;
/** The local file path of the downloaded trickplay images. */
trickPlayData?: TrickPlayData;
/** The intro segments for the item. */
introSegments?: MediaTimeSegment[];
/** The credit segments for the item. */
creditSegments?: MediaTimeSegment[];
/** The recap segments for the item. */
recapSegments?: MediaTimeSegment[];
/** The commercial segments for the item. */
commercialSegments?: MediaTimeSegment[];
/** The preview segments for the item. */
previewSegments?: MediaTimeSegment[];
/** The user data for the item. */
userData: UserData;
}
/**
* Represents a downloaded Season, containing a map of its episodes.
*/
export interface DownloadedSeason {
/** A map of episode numbers to their downloaded item data. */
episodes: Record<number, DownloadedItem>;
}
/**
* Represents a downloaded series, containing seasons and their episodes.
*/
export interface DownloadedSeries {
/** The Jellyfin item DTO for the series. */
seriesInfo: BaseItemDto;
/** A map of season numbers to their downloaded season data. */
seasons: Record<
number,
{
/** A map of episode numbers to their downloaded episode data. */
episodes: Record<number, DownloadedItem>;
}
>;
}
/**
* The main structure for all downloaded content stored locally.
* This object is what will be saved to your local storage.
*/
export interface DownloadsDatabase {
/** A map of movie IDs to their downloaded movie data. */
movies: Record<string, DownloadedItem>;
/** A map of series IDs to their downloaded series data. */
series: Record<string, DownloadedSeries>;
/** A map of IDs to downloaded items that are neither movies nor episodes */
other: Record<string, DownloadedItem>;
}
/**
* Represents the status of a download job.
*/
export type JobStatus = {
/** Unique identifier for the download job (also the {@link itemId}) */
id: string;
/** The input URL for the media to be downloaded (passed in when first downloading) */
inputUrl: string;
/** The Jellyfin {@link BaseItemDto} associated with this job */
item: BaseItemDto;
/** The ID of the item being downloaded */
itemId: string;
/** The device ID where the download is occurring */
deviceId: string;
/** Download progress as a percentage (0-100) */
progress: number;
/** Current status of the download job */
status:
| "downloading" // The job is actively downloading
| "error" // The job encountered an error
| "pending" // The job is waiting to start
| "completed" // The job has finished downloading
| "queued"; // The job is queued to start
/** Timestamp of when the job was created or last updated */
timestamp: Date;
/** The {@link MediaSourceInfo} for the download */
mediaSource: MediaSourceInfo;
/** The bit rate we are downloading the media file atq */
maxBitrate: Bitrate;
/** The number of bytes downloaded so far (optional) */
bytesDownloaded?: number;
/** The last time the download progress was updated (optional) */
lastProgressUpdateTime?: Date;
/** Current download speed in bytes per second (optional) */
speed?: number;
/** Estimated total size of the download in bytes (optional) this is used when we
* download transcoded content because we don't know the size of the file until it's downloaded */
estimatedTotalSizeBytes?: number;
/** Timestamp of when the download actually started (optional) */
startTime?: Date;
/** Whether the download is being transcoded (optional) */
isTranscoding?: boolean;
/** Pre-downloaded trickplay data (optional) - downloaded before video starts */
trickPlayData?: TrickPlayData;
/** Pre-downloaded intro segments (optional) - downloaded before video starts */
introSegments?: MediaTimeSegment[];
/** Pre-downloaded credit segments (optional) - downloaded before video starts */
creditSegments?: MediaTimeSegment[];
/** Pre-downloaded recap segments (optional) - downloaded before video starts */
recapSegments?: MediaTimeSegment[];
/** Pre-downloaded commercial segments (optional) - downloaded before video starts */
commercialSegments?: MediaTimeSegment[];
/** Pre-downloaded preview segments (optional) - downloaded before video starts */
previewSegments?: MediaTimeSegment[];
/** The audio stream index selected for this download */
audioStreamIndex?: number;
/** The subtitle stream index selected for this download */
subtitleStreamIndex?: number;
};