E frontend reconciliation (1/2): types.ts sources from bindings; backend field fixes
- types.ts now re-exports wire types from generated bindings (single source of truth); keeps frontend-only unions (ItemType/LibraryType/PersonType/ SessionCommand) and aliases Session = SessionInfo. - Backend: MediaItem.runtime_ticks serializes as runTimeTicks (matches frontend, fixes a latent undefined-read bug); ArtistItem serializes camelCase id/name (PascalCase aliases retained for Jellyfin deserialize). - player.ts: normalize remote NowPlayingItem -> MediaItem in mergedMedia so display components treat local/remote items uniformly. - Reduces svelte-check errors 141 -> 70 (remaining: nullable guards + played->isPlayed).
This commit is contained in:
parent
6146d70bc5
commit
76c78e2edc
@ -60,9 +60,11 @@ pub struct UserData {
|
||||
|
||||
/// Artist item with ID and name (for clickable artist links)
|
||||
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ArtistItem {
|
||||
#[serde(alias = "Id")]
|
||||
pub id: String,
|
||||
#[serde(alias = "Name")]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
@ -118,6 +120,7 @@ pub struct MediaItem {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub official_rating: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "runTimeTicks")]
|
||||
pub runtime_ticks: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub primary_image_tag: Option<String>,
|
||||
|
||||
@ -1271,7 +1271,7 @@ export type AlbumStorageInfo = { album_id: string; album_name: string; artist_na
|
||||
/**
|
||||
* Artist item with ID and name (for clickable artist links)
|
||||
*/
|
||||
export type ArtistItem = { Id: string; Name: string }
|
||||
export type ArtistItem = { id: string; name: string }
|
||||
/**
|
||||
* Audio playback settings
|
||||
*/
|
||||
@ -1452,7 +1452,7 @@ export type Library = { id: string; name: string; collectionType: string; imageT
|
||||
/**
|
||||
* Media item
|
||||
*/
|
||||
export type MediaItem = { id: string; name: string; type: string; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null; communityRating?: number | null; officialRating?: string | null; runtimeTicks?: number | null; primaryImageTag?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }
|
||||
export type MediaItem = { id: string; name: string; type: string; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null; communityRating?: number | null; officialRating?: string | null; runTimeTicks?: number | null; primaryImageTag?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }
|
||||
/**
|
||||
* Media session type tracking the high-level playback context
|
||||
*/
|
||||
@ -1764,7 +1764,7 @@ export type PlaylistEntry =
|
||||
/**
|
||||
* The underlying media item
|
||||
*/
|
||||
({ id: string; name: string; type: string; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null; communityRating?: number | null; officialRating?: string | null; runtimeTicks?: number | null; primaryImageTag?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }) & {
|
||||
({ id: string; name: string; type: string; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null; communityRating?: number | null; officialRating?: string | null; runTimeTicks?: number | null; primaryImageTag?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }) & {
|
||||
/**
|
||||
* The playlist-scoped entry ID (Jellyfin's PlaylistItemId)
|
||||
*/
|
||||
|
||||
@ -1,76 +1,41 @@
|
||||
// Jellyfin API Types
|
||||
// Frontend type layer.
|
||||
//
|
||||
// Wire types are the single source of truth, generated from Rust by tauri-specta
|
||||
// in ./bindings.ts. This module re-exports them under the names the frontend uses
|
||||
// and adds frontend-only unions/helpers that have no backend equivalent.
|
||||
|
||||
export interface ServerInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
export type {
|
||||
AuthResult,
|
||||
Genre,
|
||||
GetItemsOptions,
|
||||
ImageOptions,
|
||||
ImageType,
|
||||
Library,
|
||||
MediaItem,
|
||||
MediaSource,
|
||||
MediaStream,
|
||||
Person,
|
||||
PlaybackInfo,
|
||||
PlaylistCreatedResult,
|
||||
PlaylistEntry,
|
||||
PlayState,
|
||||
SearchOptions,
|
||||
SearchResult,
|
||||
ServerInfo,
|
||||
User,
|
||||
UserData,
|
||||
} from "./bindings";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
serverId: string;
|
||||
primaryImageTag?: string;
|
||||
}
|
||||
import type { SessionInfo } from "./bindings";
|
||||
|
||||
export interface AuthResult {
|
||||
user: User;
|
||||
accessToken: string;
|
||||
serverId: string;
|
||||
}
|
||||
// The frontend's "Session" is the backend remote-control session (SessionInfo).
|
||||
// (Note: bindings also has a `Session` type — that's the auth/login session, a
|
||||
// different concept — so we alias explicitly here rather than re-export it.)
|
||||
export type Session = SessionInfo;
|
||||
|
||||
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[];
|
||||
|
||||
// Genres
|
||||
genres?: string[];
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Frontend-only types (not part of the Rust command surface)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type ItemType =
|
||||
| "Movie"
|
||||
@ -87,7 +52,18 @@ export type ItemType =
|
||||
| "ChannelFolderItem"
|
||||
| "Person";
|
||||
|
||||
// Person/Cast types
|
||||
export type LibraryType =
|
||||
| "movies"
|
||||
| "tvshows"
|
||||
| "music"
|
||||
| "books"
|
||||
| "photos"
|
||||
| "homevideos"
|
||||
| "boxsets"
|
||||
| "playlists"
|
||||
| "channels"
|
||||
| "unknown";
|
||||
|
||||
export type PersonType =
|
||||
| "Actor"
|
||||
| "Director"
|
||||
@ -99,81 +75,6 @@ export type PersonType =
|
||||
| "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"
|
||||
@ -183,94 +84,3 @@ export type SessionCommand =
|
||||
| "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;
|
||||
}
|
||||
|
||||
// Playlist types
|
||||
export interface PlaylistEntry extends MediaItem {
|
||||
playlistItemId: string;
|
||||
}
|
||||
|
||||
export interface PlaylistCreatedResult {
|
||||
id: 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;
|
||||
}
|
||||
|
||||
@ -9,7 +9,8 @@
|
||||
*/
|
||||
|
||||
import { writable, derived } from "svelte/store";
|
||||
import type { MediaItem } from "$lib/api/types";
|
||||
import type { MediaItem, ItemType } from "$lib/api/types";
|
||||
import type { NowPlayingItem } from "$lib/api/bindings";
|
||||
import { isRemoteMode } from "./playbackMode";
|
||||
import { selectedSession } from "./sessions";
|
||||
import { ticksToSeconds } from "$lib/utils/playbackUnits";
|
||||
@ -161,13 +162,36 @@ export const isMuted = derived(player, ($p) => $p.muted);
|
||||
/**
|
||||
* Merged media item - prefers remote session when in remote mode
|
||||
*/
|
||||
export const mergedMedia = derived(
|
||||
/**
|
||||
* Normalize a remote session's NowPlayingItem into the MediaItem shape the UI
|
||||
* renders, so display components can treat local and remote items uniformly.
|
||||
* (NowPlayingItem has `album`/`artists` but no `albumName`/`artistItems`; the UI
|
||||
* falls back to the `artists` string list when `artistItems` is absent.)
|
||||
*/
|
||||
function nowPlayingToMediaItem(npi: NowPlayingItem): MediaItem {
|
||||
return {
|
||||
id: npi.id ?? "",
|
||||
name: npi.name ?? "",
|
||||
type: (npi.Type ?? "Audio") as ItemType,
|
||||
serverId: "",
|
||||
albumName: npi.album,
|
||||
albumId: npi.albumId,
|
||||
artists: npi.artists,
|
||||
primaryImageTag: npi.primaryImageTag ?? npi.albumPrimaryImageTag,
|
||||
runTimeTicks: npi.runTimeTicks,
|
||||
} as MediaItem;
|
||||
}
|
||||
|
||||
export const mergedMedia = derived<
|
||||
[typeof isRemoteMode, typeof selectedSession, typeof currentMedia],
|
||||
MediaItem | null
|
||||
>(
|
||||
[isRemoteMode, selectedSession, currentMedia],
|
||||
([$isRemote, $session, $local]) => {
|
||||
if ($isRemote && $session?.nowPlayingItem) {
|
||||
return $session.nowPlayingItem;
|
||||
return nowPlayingToMediaItem($session.nowPlayingItem);
|
||||
}
|
||||
return $local;
|
||||
return $local ?? null;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user