First working POC
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Autoplay API
|
||||
*
|
||||
* Functions to control autoplay settings in the backend.
|
||||
*/
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export interface AutoplaySettings {
|
||||
enabled: boolean;
|
||||
countdownSeconds: number;
|
||||
}
|
||||
|
||||
export async function getAutoplaySettings(): Promise<AutoplaySettings> {
|
||||
return invoke("player_get_autoplay_settings");
|
||||
}
|
||||
|
||||
export async function setAutoplaySettings(settings: AutoplaySettings): Promise<AutoplaySettings> {
|
||||
return invoke("player_set_autoplay_settings", { settings });
|
||||
}
|
||||
|
||||
export async function cancelAutoplayCountdown(): Promise<void> {
|
||||
return invoke("player_cancel_autoplay_countdown");
|
||||
}
|
||||
|
||||
export async function playNextEpisode(item: any): Promise<void> {
|
||||
return invoke("player_play_next_episode", { item });
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Rust-based conversion utilities via Tauri commands
|
||||
*
|
||||
* These functions provide centralized conversion logic in Rust.
|
||||
* All conversions are handled by the Rust backend for consistency.
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
/**
|
||||
* Format time in seconds to MM:SS display string
|
||||
* @param seconds - Time in seconds
|
||||
* @returns Formatted string like "3:45" or "12:09"
|
||||
*/
|
||||
export async function formatTime(seconds: number): Promise<string> {
|
||||
return invoke('format_time_seconds', { seconds });
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in seconds to HH:MM:SS or MM:SS display string
|
||||
* Automatically chooses format based on duration
|
||||
* @param seconds - Time in seconds
|
||||
* @returns Formatted string like "1:23:45" or "3:45"
|
||||
*/
|
||||
export async function formatTimeLong(seconds: number): Promise<string> {
|
||||
return invoke('format_time_seconds_long', { seconds });
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Jellyfin ticks to seconds
|
||||
* @param ticks - Time in Jellyfin ticks (10,000,000 ticks = 1 second)
|
||||
* @returns Time in seconds
|
||||
*/
|
||||
export async function ticksToSeconds(ticks: number): Promise<number> {
|
||||
return invoke('convert_ticks_to_seconds', { ticks });
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate progress percentage from position and duration
|
||||
* @param position - Current position in seconds
|
||||
* @param duration - Total duration in seconds
|
||||
* @returns Progress as percentage (0.0 to 100.0)
|
||||
*/
|
||||
export async function calculateProgress(position: number, duration: number): Promise<number> {
|
||||
return invoke('calc_progress', { position, duration });
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert percentage volume (0-100) to normalized (0.0-1.0)
|
||||
* @param percent - Volume as percentage (0 to 100)
|
||||
* @returns Normalized volume (0.0 to 1.0)
|
||||
*/
|
||||
export async function percentToVolume(percent: number): Promise<number> {
|
||||
return invoke('convert_percent_to_volume', { percent });
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous time formatting utilities for high-frequency UI updates
|
||||
*
|
||||
* These are kept in TypeScript for performance reasons when formatting
|
||||
* needs to happen frequently (e.g., position updates every 100ms).
|
||||
*
|
||||
* For less frequent conversions, prefer the async Rust-based functions above.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format time in seconds to MM:SS display string (synchronous)
|
||||
* @param seconds - Time in seconds
|
||||
* @returns Formatted string like "3:45" or "12:09"
|
||||
*/
|
||||
export function formatTimeSync(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in seconds to HH:MM:SS or MM:SS display string (synchronous)
|
||||
* @param seconds - Time in seconds
|
||||
* @returns Formatted string like "1:23:45" or "3:45"
|
||||
*/
|
||||
export function formatTimeLongSync(seconds: number): string {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate progress percentage (synchronous)
|
||||
* @param position - Current position in seconds
|
||||
* @param duration - Total duration in seconds
|
||||
* @returns Progress as percentage (0 to 100)
|
||||
*/
|
||||
export function calculateProgressSync(position: number, duration: number): number {
|
||||
if (duration <= 0) return 0;
|
||||
return Math.min(100, Math.max(0, (position / duration) * 100));
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Quality preset for video downloads
|
||||
*/
|
||||
export type QualityPreset = "original" | "high" | "medium" | "low";
|
||||
|
||||
/**
|
||||
* Quality preset configuration
|
||||
*/
|
||||
export interface QualityPresetConfig {
|
||||
videoBitrate: number | null;
|
||||
audioBitrate: number;
|
||||
maxHeight: number | null;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality presets with their configuration
|
||||
*/
|
||||
export const QUALITY_PRESETS: Record<QualityPreset, QualityPresetConfig> = {
|
||||
original: {
|
||||
videoBitrate: null,
|
||||
audioBitrate: 384000,
|
||||
maxHeight: null,
|
||||
label: "Original",
|
||||
},
|
||||
high: {
|
||||
videoBitrate: 8_000_000,
|
||||
audioBitrate: 384000,
|
||||
maxHeight: 1080,
|
||||
label: "1080p",
|
||||
},
|
||||
medium: {
|
||||
videoBitrate: 4_000_000,
|
||||
audioBitrate: 256000,
|
||||
maxHeight: 720,
|
||||
label: "720p",
|
||||
},
|
||||
low: {
|
||||
videoBitrate: 1_500_000,
|
||||
audioBitrate: 128000,
|
||||
maxHeight: 480,
|
||||
label: "480p",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,346 @@
|
||||
// Thin TypeScript wrapper for Rust repository implementation
|
||||
// All API calls go through Tauri commands in src-tauri/src/commands/repository.rs
|
||||
// NO direct HTTP calls - everything routes through Rust backend
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { QualityPreset } from "./quality-presets";
|
||||
import { QUALITY_PRESETS } from "./quality-presets";
|
||||
import type {
|
||||
Library,
|
||||
MediaItem,
|
||||
SearchResult,
|
||||
GetItemsOptions,
|
||||
SearchOptions,
|
||||
PlaybackInfo,
|
||||
ImageType,
|
||||
ImageOptions,
|
||||
Genre,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Repository client - thin wrapper over Rust HybridRepository
|
||||
* Uses handle-based system: create() returns a UUID handle for all operations
|
||||
*/
|
||||
export class RepositoryClient {
|
||||
private handle: string | null = null;
|
||||
private _serverUrl: string | null = null;
|
||||
private _accessToken: string | null = null;
|
||||
|
||||
/**
|
||||
* Create a new repository instance in Rust
|
||||
* Returns the repository handle for subsequent operations
|
||||
*/
|
||||
async create(
|
||||
serverUrl: string,
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
serverId: string
|
||||
): Promise<string> {
|
||||
console.log("[RepositoryClient] Creating Rust repository...");
|
||||
this.handle = await invoke<string>("repository_create", {
|
||||
serverUrl,
|
||||
userId,
|
||||
accessToken,
|
||||
serverId,
|
||||
});
|
||||
|
||||
// Store for URL construction
|
||||
this._serverUrl = serverUrl;
|
||||
this._accessToken = accessToken;
|
||||
|
||||
console.log("[RepositoryClient] Repository created with handle:", this.handle);
|
||||
return this.handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the repository instance in Rust
|
||||
* Call this on logout or when switching servers
|
||||
*/
|
||||
async destroy(): Promise<void> {
|
||||
if (this.handle) {
|
||||
await invoke("repository_destroy", { handle: this.handle });
|
||||
this.handle = null;
|
||||
this._serverUrl = null;
|
||||
this._accessToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
private ensureHandle(): string {
|
||||
if (!this.handle) {
|
||||
throw new Error("Repository not initialized - call create() first");
|
||||
}
|
||||
return this.handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the repository handle for passing to backend commands
|
||||
*/
|
||||
getHandle(): string {
|
||||
return this.ensureHandle();
|
||||
}
|
||||
|
||||
// ===== Library Methods (all via Rust) =====
|
||||
|
||||
async getLibraries(): Promise<Library[]> {
|
||||
return invoke<Library[]>("repository_get_libraries", {
|
||||
handle: this.ensureHandle(),
|
||||
});
|
||||
}
|
||||
|
||||
async getItems(parentId: string, options?: GetItemsOptions): Promise<SearchResult> {
|
||||
return invoke<SearchResult>("repository_get_items", {
|
||||
handle: this.ensureHandle(),
|
||||
parentId,
|
||||
options: options ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getItem(itemId: string): Promise<MediaItem> {
|
||||
return invoke<MediaItem>("repository_get_item", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
});
|
||||
}
|
||||
|
||||
async getLatestItems(parentId: string, limit?: number): Promise<MediaItem[]> {
|
||||
return invoke<MediaItem[]>("repository_get_latest_items", {
|
||||
handle: this.ensureHandle(),
|
||||
parentId,
|
||||
limit: limit ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getResumeItems(parentId?: string, limit?: number): Promise<MediaItem[]> {
|
||||
return invoke<MediaItem[]>("repository_get_resume_items", {
|
||||
handle: this.ensureHandle(),
|
||||
parentId: parentId ?? null,
|
||||
limit: limit ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getNextUpEpisodes(seriesId?: string, limit?: number): Promise<MediaItem[]> {
|
||||
return invoke<MediaItem[]>("repository_get_next_up_episodes", {
|
||||
handle: this.ensureHandle(),
|
||||
seriesId: seriesId ?? null,
|
||||
limit: limit ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getRecentlyPlayedAudio(limit?: number): Promise<MediaItem[]> {
|
||||
return invoke<MediaItem[]>("repository_get_recently_played_audio", {
|
||||
handle: this.ensureHandle(),
|
||||
limit: limit ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getResumeMovies(limit?: number): Promise<MediaItem[]> {
|
||||
return invoke<MediaItem[]>("repository_get_resume_movies", {
|
||||
handle: this.ensureHandle(),
|
||||
limit: limit ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getGenres(parentId?: string): Promise<Genre[]> {
|
||||
return invoke<Genre[]>("repository_get_genres", {
|
||||
handle: this.ensureHandle(),
|
||||
parentId: parentId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async search(query: string, options?: SearchOptions): Promise<SearchResult> {
|
||||
return invoke<SearchResult>("repository_search", {
|
||||
handle: this.ensureHandle(),
|
||||
query,
|
||||
options: options ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Playback Methods (all via Rust) =====
|
||||
|
||||
async getPlaybackInfo(itemId: string): Promise<PlaybackInfo> {
|
||||
return invoke<PlaybackInfo>("repository_get_playback_info", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
});
|
||||
}
|
||||
|
||||
async reportPlaybackStart(itemId: string, positionTicks: number): Promise<void> {
|
||||
return invoke("repository_report_playback_start", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
positionTicks,
|
||||
});
|
||||
}
|
||||
|
||||
async reportPlaybackProgress(itemId: string, positionTicks: number): Promise<void> {
|
||||
return invoke("repository_report_playback_progress", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
positionTicks,
|
||||
});
|
||||
}
|
||||
|
||||
async reportPlaybackStopped(itemId: string, positionTicks: number): Promise<void> {
|
||||
return invoke("repository_report_playback_stopped", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
positionTicks,
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Stream URL Methods (via Rust) =====
|
||||
|
||||
async getAudioStreamUrl(itemId: string): Promise<string> {
|
||||
return invoke<string>("repository_get_audio_stream_url", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
});
|
||||
}
|
||||
|
||||
async getVideoStreamUrl(
|
||||
itemId: string,
|
||||
mediaSourceId?: string,
|
||||
startTimeSeconds?: number,
|
||||
audioStreamIndex?: number
|
||||
): Promise<string> {
|
||||
return invoke<string>("repository_get_video_stream_url", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
mediaSourceId: mediaSourceId ?? null,
|
||||
startTimeSeconds: startTimeSeconds ?? null,
|
||||
audioStreamIndex: audioStreamIndex ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
// ===== URL Construction Methods (sync, no server call) =====
|
||||
|
||||
/**
|
||||
* Get image URL - constructs URL synchronously (no server call)
|
||||
*/
|
||||
getImageUrl(itemId: string, imageType: ImageType = "Primary", options?: ImageOptions): string {
|
||||
if (!this._serverUrl || !this._accessToken) {
|
||||
throw new Error("Repository not initialized - call create() first");
|
||||
}
|
||||
|
||||
let url = `${this._serverUrl}/Items/${itemId}/Images/${imageType}`;
|
||||
const params: string[] = [`api_key=${this._accessToken}`];
|
||||
|
||||
if (options) {
|
||||
if (options.maxWidth) params.push(`maxWidth=${options.maxWidth}`);
|
||||
if (options.maxHeight) params.push(`maxHeight=${options.maxHeight}`);
|
||||
if (options.quality) params.push(`quality=${options.quality}`);
|
||||
if (options.tag) params.push(`tag=${options.tag}`);
|
||||
}
|
||||
|
||||
return `${url}?${params.join('&')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subtitle URL - constructs URL synchronously (no server call)
|
||||
*/
|
||||
getSubtitleUrl(itemId: string, mediaSourceId: string, streamIndex: number, format: string = "vtt"): string {
|
||||
if (!this._serverUrl || !this._accessToken) {
|
||||
throw new Error("Repository not initialized - call create() first");
|
||||
}
|
||||
return `${this._serverUrl}/Videos/${itemId}/${mediaSourceId}/Subtitles/${streamIndex}/Stream.${format}?api_key=${this._accessToken}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get video download URL with quality preset - constructs URL synchronously
|
||||
* Used for offline downloads
|
||||
*/
|
||||
getVideoDownloadUrl(
|
||||
itemId: string,
|
||||
quality: QualityPreset = "original",
|
||||
mediaSourceId?: string
|
||||
): string {
|
||||
if (!this._serverUrl || !this._accessToken) {
|
||||
throw new Error("Repository not initialized - call create() first");
|
||||
}
|
||||
|
||||
const preset = QUALITY_PRESETS[quality];
|
||||
|
||||
if (quality === "original" || !preset.videoBitrate) {
|
||||
// Direct stream for original quality
|
||||
const params = new URLSearchParams({
|
||||
api_key: this._accessToken,
|
||||
Static: "true",
|
||||
audioStreamIndex: "0",
|
||||
});
|
||||
if (mediaSourceId) {
|
||||
params.append("MediaSourceId", mediaSourceId);
|
||||
}
|
||||
return `${this._serverUrl}/Videos/${itemId}/stream?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Transcoded download with quality preset
|
||||
const params = new URLSearchParams({
|
||||
api_key: this._accessToken,
|
||||
DeviceId: localStorage.getItem("jellytau_device_id") || "jellytau",
|
||||
Container: "mp4",
|
||||
VideoCodec: "h264",
|
||||
AudioCodec: "aac",
|
||||
AudioStreamIndex: "0",
|
||||
VideoBitrate: preset.videoBitrate.toString(),
|
||||
AudioBitrate: preset.audioBitrate.toString(),
|
||||
MaxHeight: preset.maxHeight?.toString() ?? "",
|
||||
TranscodingMaxAudioChannels: "2",
|
||||
});
|
||||
|
||||
if (mediaSourceId) {
|
||||
params.append("MediaSourceId", mediaSourceId);
|
||||
}
|
||||
|
||||
return `${this._serverUrl}/Videos/${itemId}/stream.mp4?${params.toString()}`;
|
||||
}
|
||||
|
||||
// ===== Favorite Methods (via Rust) =====
|
||||
|
||||
async markFavorite(itemId: string): Promise<void> {
|
||||
return invoke("repository_mark_favorite", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
});
|
||||
}
|
||||
|
||||
async unmarkFavorite(itemId: string): Promise<void> {
|
||||
return invoke("repository_unmark_favorite", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Person Methods (via Rust) =====
|
||||
|
||||
async getPerson(personId: string): Promise<MediaItem> {
|
||||
return invoke<MediaItem>("repository_get_person", {
|
||||
handle: this.ensureHandle(),
|
||||
personId,
|
||||
});
|
||||
}
|
||||
|
||||
async getItemsByPerson(personId: string, options?: GetItemsOptions): Promise<SearchResult> {
|
||||
return invoke<SearchResult>("repository_get_items_by_person", {
|
||||
handle: this.ensureHandle(),
|
||||
personId,
|
||||
options: options ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async getSimilarItems(itemId: string, limit?: number): Promise<SearchResult> {
|
||||
return invoke<SearchResult>("repository_get_similar_items", {
|
||||
handle: this.ensureHandle(),
|
||||
itemId,
|
||||
limit: limit ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Getters =====
|
||||
|
||||
get serverUrl(): string {
|
||||
if (!this._serverUrl) {
|
||||
throw new Error("Repository not initialized - call create() first");
|
||||
}
|
||||
return this._serverUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Sleep Timer API
|
||||
*
|
||||
* Functions to control the sleep timer in the backend.
|
||||
*/
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { SleepTimerMode } from "$lib/services/playerEvents";
|
||||
|
||||
export interface SleepTimerState {
|
||||
mode: SleepTimerMode;
|
||||
remainingSeconds: number;
|
||||
}
|
||||
|
||||
export async function setSleepTimer(mode: SleepTimerMode): Promise<SleepTimerState> {
|
||||
return invoke("player_set_sleep_timer", { mode });
|
||||
}
|
||||
|
||||
export async function cancelSleepTimer(): Promise<SleepTimerState> {
|
||||
return invoke("player_cancel_sleep_timer");
|
||||
}
|
||||
|
||||
export async function getSleepTimer(): Promise<SleepTimerState> {
|
||||
return invoke("player_get_sleep_timer");
|
||||
}
|
||||
|
||||
// Helper functions for common timer modes
|
||||
|
||||
export async function setTimeBasedTimer(minutes: number): Promise<SleepTimerState> {
|
||||
const endTime = Date.now() + minutes * 60 * 1000;
|
||||
return setSleepTimer({ kind: "time", endTime });
|
||||
}
|
||||
|
||||
export async function setEndOfTrackTimer(): Promise<SleepTimerState> {
|
||||
return setSleepTimer({ kind: "endOfTrack" });
|
||||
}
|
||||
|
||||
export async function setEpisodesTimer(count: number): Promise<SleepTimerState> {
|
||||
return setSleepTimer({ kind: "episodes", remaining: count });
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// Jellyfin API Types
|
||||
|
||||
export interface ServerInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
serverId: string;
|
||||
primaryImageTag?: string;
|
||||
}
|
||||
|
||||
export interface AuthResult {
|
||||
user: User;
|
||||
accessToken: string;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export interface Library {
|
||||
id: string;
|
||||
name: string;
|
||||
collectionType: LibraryType;
|
||||
imageTag?: string;
|
||||
}
|
||||
|
||||
export type LibraryType = "movies" | "tvshows" | "music" | "books" | "photos" | "homevideos" | "boxsets" | "playlists" | "channels" | "unknown";
|
||||
|
||||
export interface MediaItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ItemType;
|
||||
serverId: string;
|
||||
|
||||
// Common fields
|
||||
overview?: string;
|
||||
productionYear?: number;
|
||||
communityRating?: number;
|
||||
officialRating?: string;
|
||||
runTimeTicks?: number;
|
||||
|
||||
// Images
|
||||
primaryImageTag?: string;
|
||||
backdropImageTags?: string[];
|
||||
parentBackdropImageTags?: string[];
|
||||
|
||||
// For audio
|
||||
albumId?: string;
|
||||
albumName?: string;
|
||||
artists?: string[];
|
||||
artistItems?: { id: string; name: string }[];
|
||||
indexNumber?: number; // Track number
|
||||
parentIndexNumber?: number; // Disc number
|
||||
|
||||
// For video
|
||||
seriesId?: string;
|
||||
seriesName?: string;
|
||||
seasonId?: string;
|
||||
seasonName?: string;
|
||||
|
||||
// Playback
|
||||
userData?: UserData;
|
||||
mediaStreams?: MediaStream[];
|
||||
mediaSources?: MediaSource[];
|
||||
|
||||
// Cast & Crew
|
||||
people?: Person[];
|
||||
}
|
||||
|
||||
export type ItemType =
|
||||
| "Movie"
|
||||
| "Series"
|
||||
| "Season"
|
||||
| "Episode"
|
||||
| "MusicAlbum"
|
||||
| "MusicArtist"
|
||||
| "Audio"
|
||||
| "Playlist"
|
||||
| "Folder"
|
||||
| "CollectionFolder"
|
||||
| "Channel"
|
||||
| "ChannelFolderItem"
|
||||
| "Person";
|
||||
|
||||
// Person/Cast types
|
||||
export type PersonType =
|
||||
| "Actor"
|
||||
| "Director"
|
||||
| "Writer"
|
||||
| "Producer"
|
||||
| "Composer"
|
||||
| "GuestStar"
|
||||
| "Creator"
|
||||
| "Conductor"
|
||||
| "Lyricist";
|
||||
|
||||
export interface Person {
|
||||
id: string;
|
||||
name: string;
|
||||
role?: string; // Character name for actors
|
||||
type: PersonType;
|
||||
primaryImageTag?: string;
|
||||
}
|
||||
|
||||
export interface UserData {
|
||||
playbackPositionTicks: number;
|
||||
playCount: number;
|
||||
isFavorite: boolean;
|
||||
played: boolean;
|
||||
lastPlayedDate?: string;
|
||||
}
|
||||
|
||||
export interface MediaStream {
|
||||
type: "Video" | "Audio" | "Subtitle";
|
||||
codec?: string;
|
||||
language?: string;
|
||||
displayTitle?: string;
|
||||
index: number;
|
||||
isDefault: boolean;
|
||||
isForced: boolean;
|
||||
}
|
||||
|
||||
export interface MediaSource {
|
||||
id: string;
|
||||
name: string;
|
||||
container?: string;
|
||||
size?: number;
|
||||
bitrate?: number;
|
||||
supportsDirectPlay: boolean;
|
||||
supportsDirectStream: boolean;
|
||||
supportsTranscoding: boolean;
|
||||
directStreamUrl?: string;
|
||||
}
|
||||
|
||||
export interface PlaybackInfo {
|
||||
mediaSourceId: string;
|
||||
playSessionId: string;
|
||||
streamUrl: string;
|
||||
directPlay: boolean;
|
||||
/** True if content requires transcoding (HEVC, 10-bit) - seeking requires server-side StartTimeTicks */
|
||||
needsTranscoding: boolean;
|
||||
}
|
||||
|
||||
// Remote session control types
|
||||
export interface Session {
|
||||
id: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
client: string;
|
||||
deviceName: string;
|
||||
deviceId: string;
|
||||
applicationVersion: string;
|
||||
isActive: boolean;
|
||||
supportsMediaControl: boolean;
|
||||
supportsRemoteControl: boolean;
|
||||
playState: PlayState | null;
|
||||
nowPlayingItem: MediaItem | null;
|
||||
playableMediaTypes: string[];
|
||||
supportedCommands: string[];
|
||||
}
|
||||
|
||||
export interface PlayState {
|
||||
positionTicks: number;
|
||||
canSeek: boolean;
|
||||
isPaused: boolean;
|
||||
isMuted: boolean;
|
||||
volumeLevel: number;
|
||||
repeatMode: string;
|
||||
shuffleMode: string;
|
||||
}
|
||||
|
||||
export type SessionCommand =
|
||||
| "PlayPause"
|
||||
| "Stop"
|
||||
| "Pause"
|
||||
| "Unpause"
|
||||
| "NextTrack"
|
||||
| "PreviousTrack"
|
||||
| "Mute"
|
||||
| "Unmute";
|
||||
|
||||
export interface SearchResult {
|
||||
items: MediaItem[];
|
||||
totalRecordCount: number;
|
||||
}
|
||||
|
||||
// Repository pattern for offline support
|
||||
export interface MediaRepository {
|
||||
// Libraries
|
||||
getLibraries(): Promise<Library[]>;
|
||||
|
||||
// Items
|
||||
getItems(parentId: string, options?: GetItemsOptions): Promise<SearchResult>;
|
||||
getItem(itemId: string): Promise<MediaItem>;
|
||||
getLatestItems(parentId: string, limit?: number): Promise<MediaItem[]>;
|
||||
|
||||
// Home screen
|
||||
getResumeItems(parentId?: string, limit?: number): Promise<MediaItem[]>;
|
||||
getNextUpEpisodes(seriesId?: string, limit?: number): Promise<MediaItem[]>;
|
||||
getRecentlyPlayedAudio(limit?: number): Promise<MediaItem[]>;
|
||||
getResumeMovies(limit?: number): Promise<MediaItem[]>;
|
||||
|
||||
// Genres
|
||||
getGenres(parentId?: string): Promise<Genre[]>;
|
||||
|
||||
// Search
|
||||
search(query: string, options?: SearchOptions): Promise<SearchResult>;
|
||||
|
||||
// Playback
|
||||
getPlaybackInfo(itemId: string): Promise<PlaybackInfo>;
|
||||
reportPlaybackStart(itemId: string, positionTicks: number): Promise<void>;
|
||||
reportPlaybackProgress(itemId: string, positionTicks: number): Promise<void>;
|
||||
reportPlaybackStopped(itemId: string, positionTicks: number): Promise<void>;
|
||||
|
||||
// Images
|
||||
getImageUrl(itemId: string, imageType: ImageType, options?: ImageOptions): string;
|
||||
|
||||
// Subtitles
|
||||
getSubtitleUrl(itemId: string, mediaSourceId: string, streamIndex: number, format?: string): string;
|
||||
|
||||
// Favorites
|
||||
markFavorite(itemId: string): Promise<void>;
|
||||
unmarkFavorite(itemId: string): Promise<void>;
|
||||
|
||||
// People/Cast
|
||||
getPerson(personId: string): Promise<MediaItem>;
|
||||
getItemsByPerson(personId: string, options?: GetItemsOptions): Promise<SearchResult>;
|
||||
|
||||
// Related Content
|
||||
getSimilarItems(itemId: string, limit?: number): Promise<SearchResult>;
|
||||
}
|
||||
|
||||
export interface Genre {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface GetItemsOptions {
|
||||
startIndex?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: "Ascending" | "Descending";
|
||||
includeItemTypes?: ItemType[];
|
||||
recursive?: boolean;
|
||||
fields?: string[];
|
||||
genres?: string[];
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
limit?: number;
|
||||
includeItemTypes?: ItemType[];
|
||||
searchTerm?: string;
|
||||
}
|
||||
|
||||
export type ImageType = "Primary" | "Backdrop" | "Banner" | "Thumb" | "Logo";
|
||||
|
||||
export interface ImageOptions {
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
quality?: number;
|
||||
tag?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user