diff --git a/backend/src/users/media-sources/media-source.dto.ts b/backend/src/users/media-sources/media-source.dto.ts index 182977d..677a409 100644 --- a/backend/src/users/media-sources/media-source.dto.ts +++ b/backend/src/users/media-sources/media-source.dto.ts @@ -6,7 +6,10 @@ import { PartialType, } from '@nestjs/swagger'; import { PickAndPartial } from 'src/common/common.dto'; -import { ValidationResponseDto } from 'src/source-providers/source-provider.dto'; +import { + StreamCandidateDto, + ValidationResponseDto, +} from 'src/source-providers/source-provider.dto'; import { MediaSource } from './media-source.entity'; import { CatalogueCapabilities, @@ -26,6 +29,7 @@ import { ListWithDetailsItem, SortableProperty, StreamActionElement, + MediaSourceProvider, } from '@aleksilassila/reiverr-shared'; import { ViewBaseDto } from 'src/source-providers/ui.dto'; @@ -441,3 +445,11 @@ export class MediaSourceViewResponseDto { // @ApiProperty({ required: false, type: ListWithDetailsViewDto }) // listWithDetailsView?: ListWithDetailsViewDto; } + +export class ProviderWithStreamsDto { + @ApiProperty() + provider: MediaSourceDto; + + @ApiProperty({ type: [StreamCandidateDto] }) + streams: StreamCandidateDto[]; +} diff --git a/backend/src/users/media-sources/media-sources.controller.ts b/backend/src/users/media-sources/media-sources.controller.ts index 5dde486..17f47eb 100644 --- a/backend/src/users/media-sources/media-sources.controller.ts +++ b/backend/src/users/media-sources/media-sources.controller.ts @@ -37,10 +37,13 @@ import { User } from 'src/users/user.entity'; import { AutoplayResponseDto } from './media-source-responses.dto'; import { MediaSourceViewResponseDto, + ProviderWithStreamsDto, ViewProvidersResponseDto as ViewGroupsResponseDto, ViewProviderDto, } from './media-source.dto'; import { MediaSourcesService } from './media-sources.service'; +import { PaginatedResponseDto } from 'src/common/common.dto'; +import { PaginatedApiOkResponse } from 'src/common/common.decorator'; @Injectable() export class ServiceOwnershipValidator implements CanActivate { @@ -79,6 +82,66 @@ export class MediaSourcesController { private metadataService: MetadataService, ) {} + @Get('candidates') + // @ApiOkResponse({ + // description: 'TMDB episode media candidates', + // type: , + // }) + @PaginatedApiOkResponse(ProviderWithStreamsDto) + @ApiQuery({ name: 'tmdbId', type: 'string' }) + @ApiQuery({ name: 'season', type: 'number', required: false }) + @ApiQuery({ name: 'episode', type: 'number', required: false }) + async getTmdbEpisodeMedia( + @Query('tmdbId') tmdbId: string, + @Query('season') season: number, + @Query('episode') episode: number, + @GetAuthUser() user: User, + @GetAuthToken() token: string, + ): Promise> { + const context = this.getPlayablePluginContext(tmdbId, season, episode); + + const tmdbSeriesP = this.metadataService.getSeriesByTmdbId(tmdbId); + const tmdbEpisodeP = this.metadataService.getEpisodeByTmdbId({ + tmdbId, + season, + episode, + }); + + const providers = await Promise.all( + user.mediaSources.map(async (ms) => { + const mediaSourceDto = + await this.mediaSourcesService.getMediaSourceDto(ms); + + const connection = await this.getConnection({ + sourceId: ms.id, + userId: user.id, + token, + }); + const tmdbSeries = await tmdbSeriesP; + const tmdbEpisode = await tmdbEpisodeP; + + const { candidates: streams } = + await connection.provider.getTmdbEpisodeCandidates?.({ + tmdbSeries: tmdbSeries.tmdbSeries, + tmdbEpisode: tmdbEpisode.tmdbEpisode, + }); + + return { + provider: mediaSourceDto, + context, + streams, + }; + }), + ); + + return { + items: providers, + itemsPerPage: 0, + page: 0, + total: 0, + }; + } + @Get('views') @ApiOkResponse({ description: 'Movie views', diff --git a/frontend/package.json b/frontend/package.json index 4c55a91..dc4d97b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,7 +54,7 @@ "svelte": "^3.59.2", "svelte-check": "^3.6.2", "svelte-i18n": "^4.0.0", - "swagger-typescript-api": "^13.0.23", + "swagger-typescript-api": "13.2.8", "tailwind-scrollbar-hide": "^1.1.7", "tailwindcss": "^3.4.17", "terser": "^5.26.0", diff --git a/frontend/src/lib/apis/reiverr/reiverr.openapi.ts b/frontend/src/lib/apis/reiverr/reiverr.openapi.ts index 02fae0b..570eff2 100644 --- a/frontend/src/lib/apis/reiverr/reiverr.openapi.ts +++ b/frontend/src/lib/apis/reiverr/reiverr.openapi.ts @@ -1,5 +1,6 @@ /* eslint-disable */ /* tslint:disable */ +// @ts-nocheck /* * --------------------------------------------------------------- * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## @@ -10,916 +11,956 @@ */ export interface SonarrSettings { - apiKey: string; - baseUrl: string; - qualityProfileId: number; - rootFolderPath: string; - languageProfileId: number; + apiKey: string; + baseUrl: string; + qualityProfileId: number; + rootFolderPath: string; + languageProfileId: number; } export interface RadarrSettings { - apiKey: string; - baseUrl: string; - qualityProfileId: number; - rootFolderPath: string; + apiKey: string; + baseUrl: string; + qualityProfileId: number; + rootFolderPath: string; } export interface JellyfinSettings { - apiKey: string; - baseUrl: string; - userId: string; + apiKey: string; + baseUrl: string; + userId: string; } export interface TmdbSettings { - sessionId: string; - userId: string; + sessionId: string; + userId: string; } export interface Settings { - autoplayTrailers: boolean; - language: string; - animationDuration: number; - sonarr: SonarrSettings; - radarr: RadarrSettings; - jellyfin: JellyfinSettings; - tmdb: TmdbSettings; + autoplayTrailers: boolean; + language: string; + animationDuration: number; + sonarr: SonarrSettings; + radarr: RadarrSettings; + jellyfin: JellyfinSettings; + tmdb: TmdbSettings; } export interface PlayState { - id: string; - tmdbId: number; - mediaType: 'movie' | 'series' | 'episode'; - userId: string; - season?: number; - episode?: number; - /** - * Whether the user has watched this media - * @default false - */ - watched: boolean; - /** - * A number between 0 and 1 - * @default false - * @example 0.5 - */ - progress: number; - /** Last time the user played this media */ - lastPlayedAt: string; + id: string; + tmdbId: number; + mediaType: "movie" | "series" | "episode"; + userId: string; + season?: number; + episode?: number; + /** + * Whether the user has watched this media + * @default false + */ + watched: boolean; + /** + * A number between 0 and 1 + * @default false + * @example 0.5 + */ + progress: number; + /** Last time the user played this media */ + lastPlayedAt: string; } export interface MovieMetadata { - id?: string; - tmdbId: string; - tmdbMovie?: object; - name?: string; - releaseDate?: string; - libraryItems?: any[][]; - updatedAt: string; + id?: string; + tmdbId: string; + tmdbMovie?: object; + name?: string; + releaseDate?: string; + libraryItems?: any[][]; + updatedAt: string; } export interface SeriesMetadata { - id?: string; - tmdbId: string; - tmdbSeries?: object; - name?: string; - firstReleaseDate?: string; - lastReleaseDate?: string; - nextReleaseDate?: string; - lastSeasonNumber?: number; - lastEpisodeNumber?: number; - libraryItems?: any[][]; - updatedAt: string; + id?: string; + tmdbId: string; + tmdbSeries?: object; + name?: string; + firstReleaseDate?: string; + lastReleaseDate?: string; + nextReleaseDate?: string; + lastSeasonNumber?: number; + lastEpisodeNumber?: number; + libraryItems?: any[][]; + updatedAt: string; } export interface PlayStateDto { - id: string; - tmdbId: number; - mediaType: 'movie' | 'series' | 'episode'; - userId: string; - season?: number; - episode?: number; - /** - * Whether the user has watched this media - * @default false - */ - watched: boolean; - /** - * A number between 0 and 1 - * @default false - * @example 0.5 - */ - progress: number; - /** Last time the user played this media */ - lastPlayedAt: string; + id: string; + tmdbId: number; + mediaType: "movie" | "series" | "episode"; + userId: string; + season?: number; + episode?: number; + /** + * Whether the user has watched this media + * @default false + */ + watched: boolean; + /** + * A number between 0 and 1 + * @default false + * @example 0.5 + */ + progress: number; + /** Last time the user played this media */ + lastPlayedAt: string; } export interface LibraryItem { - id?: string; - tmdbId: string; - mediaType: 'movie' | 'series'; - lastPlayedAt?: string; - movieMetadata?: MovieMetadata; - seriesMetadata?: SeriesMetadata; - userId: string; - user?: string; - playStates?: PlayStateDto[]; - updatedAt: string; - createdAt: string; + id?: string; + tmdbId: string; + mediaType: "movie" | "series"; + lastPlayedAt?: string; + movieMetadata?: MovieMetadata; + seriesMetadata?: SeriesMetadata; + userId: string; + user?: string; + playStates?: PlayStateDto[]; + updatedAt: string; + createdAt: string; } export interface CatalogueOrderDirectionOption { - label: string; - value: string; + label: string; + value: string; } export interface OrderOptionDto { - label: string; - value: string; - directions: CatalogueOrderDirectionOption[]; + label: string; + value: string; + directions: CatalogueOrderDirectionOption[]; } export interface CatalogueCapability { - isSupported: boolean; - orderOptions: OrderOptionDto[]; + isSupported: boolean; + orderOptions: OrderOptionDto[]; } export interface CatalogueCapabilitiesDto { - combinedCatalogue: CatalogueCapability; - missingCatalogue: CatalogueCapability; - moviesCatalogue: CatalogueCapability; - seriesCatalogue: CatalogueCapability; + combinedCatalogue: CatalogueCapability; + missingCatalogue: CatalogueCapability; + moviesCatalogue: CatalogueCapability; + seriesCatalogue: CatalogueCapability; } export interface MediaSourceDto { - id: string; - pluginId: string; - name: string; - userId: string; - /** @default false */ - enabled?: boolean; - /** @default false */ - adminControlled?: boolean; - priority: number; - pluginSettings?: Record; - catalogueCapabilities: CatalogueCapabilitiesDto; + id: string; + pluginId: string; + name: string; + userId: string; + /** @default false */ + enabled?: boolean; + /** @default false */ + adminControlled?: boolean; + priority: number; + pluginSettings?: Record; + catalogueCapabilities: CatalogueCapabilitiesDto; } export interface UserDto { - id: string; - name: string; - isAdmin: boolean; - onboardingDone?: boolean; - settings: Settings; - playStates?: PlayState[]; - libraryItems?: LibraryItem[]; - profilePicture: string; - mediaSources: MediaSourceDto[]; + id: string; + name: string; + isAdmin: boolean; + onboardingDone?: boolean; + settings: Settings; + playStates?: PlayState[]; + libraryItems?: LibraryItem[]; + profilePicture: string; + mediaSources: MediaSourceDto[]; } export interface CreateUserDto { - name: string; - password: string; - isAdmin: boolean; - profilePicture?: string; + name: string; + password: string; + isAdmin: boolean; + profilePicture?: string; } export interface UpdateUserDto { - name?: string; - password?: string; - isAdmin?: boolean; - onboardingDone?: boolean; - settings?: Settings; - profilePicture?: string; - oldPassword?: string; -} - -export interface ViewBaseDto { - id: string; - type: 'general' | 'list-with-details'; - label: string; - priority?: number; -} - -export interface ViewProviderDto { - view: ViewBaseDto; - sourceId: string; -} - -export interface ViewGroupDto { - label: string; - viewProviders: ViewProviderDto[]; -} - -export interface ViewProvidersResponseDto { - viewGroups: ViewGroupDto[]; -} - -export interface HeadingElementDto { - type: 'heading'; - label: string; - description?: string; -} - -export interface ToggleElementDto { - type: 'toggle'; - label: string; - description?: string; - value: boolean; - style: 'checkbox' | 'switch'; -} - -export interface SelectOptionDto { - label: string; - value: string; -} - -export interface SelectElementDto { - type: 'select'; - label: string; - description?: string; - value: string; - options: SelectOptionDto[]; - style: 'dropdown' | 'radio'; -} - -export interface StreamActionElementDto { - type: 'action'; - label: 'Stream'; - action: 'stream'; - disabled?: boolean; -} - -export interface IconDto { - type: 'play' | 'download' | 'delete' | 'info' | 'external-link'; - size?: 'lg' | 'md' | 'sm'; -} - -export interface ActionElementDto { - type: 'action'; - label: string; - action: string; - disabled?: boolean; - icon?: IconDto; -} - -export interface InputElementDto { - type: 'input'; - label: string; - description?: string; - value?: string; - placeholder?: string; - style: 'number' | 'text' | 'email' | 'password'; - min?: number; - max?: number; - maxLength?: number; - minLength?: number; - disabled?: boolean; -} - -export interface ExternalLinkElementDto { - type: 'external-link'; - label: string; - url: string; - icon?: IconDto; -} - -export interface OpenViewElementDto { - type: 'open-view'; - label: string; - viewId: string; - icon?: IconDto; -} - -export interface GeneralViewDto { - type: 'general'; - elements: ( - | HeadingElementDto - | ToggleElementDto - | SelectElementDto - | StreamActionElementDto - | ActionElementDto - | InputElementDto - | ExternalLinkElementDto - | OpenViewElementDto - )[]; -} - -export interface SortablePropertyDto { - label: string; - value: string | number; - formatted: string; - secondary?: boolean; -} - -export interface ListWithDetailsItemDto { - id: string; - label: string; - description: string; - properties: SortablePropertyDto[]; - actions: (StreamActionElementDto | ActionElementDto | OpenViewElementDto)[]; -} - -export interface ListWithDetailsViewDto { - id: string; - type: 'list-with-details'; - label: string; - priority?: number; - items: ListWithDetailsItemDto[]; - order?: OrderOptionDto; - orderOptions: OrderOptionDto[]; -} - -export interface MediaSourceViewResponseDto { - view: GeneralViewDto | ListWithDetailsViewDto; -} - -export interface VideoStreamPropertyDto { - label: string; - value: string | number; - formatted?: string; -} - -export interface StreamActionDto { - label: string; - type: string; -} - -export interface StreamCandidateDto { - streamId: string; - title: string; - properties: VideoStreamPropertyDto[]; - actions: StreamActionDto[]; -} - -export interface StreamCandidatesDto { - candidates: StreamCandidateDto[]; -} - -export interface StreamBaseDto { - streamId: string; - title: string; - properties: VideoStreamPropertyDto[]; -} - -export interface AutoplayResponseDto { - candidate?: StreamBaseDto; -} - -export interface DirectPlayProfileDto { - /** Gets or sets the container. */ - Container?: string | null; - /** Gets or sets the audio codec. */ - AudioCodec?: string | null; - /** Gets or sets the video codec. */ - VideoCodec?: string | null; - /** Gets or sets the Dlna profile type. */ - Type?: 'Audio' | 'Video' | 'Photo' | 'Subtitle' | 'Lyric'; -} - -export interface ProfileConditionDto { - /** Gets or sets the condition. */ - Condition?: 'Equals' | 'NotEquals' | 'LessThanEqual' | 'GreaterThanEqual' | 'EqualsAny' | null; - /** Gets or sets the property. */ - Property?: - | 'AudioChannels' - | 'AudioBitrate' - | 'AudioProfile' - | 'Width' - | 'Height' - | 'Has64BitOffsets' - | 'PacketLength' - | 'VideoBitDepth' - | 'VideoBitrate' - | 'VideoFramerate' - | 'VideoLevel' - | 'VideoProfile' - | 'VideoTimestamp' - | 'IsAnamorphic' - | 'RefFrames' - | 'NumAudioStreams' - | 'NumVideoStreams' - | 'IsSecondaryAudio' - | 'VideoCodecTag' - | 'IsAvc' - | 'IsInterlaced' - | 'AudioSampleRate' - | 'AudioBitDepth' - | 'VideoRangeType' - | null; - /** Gets or sets the value. */ - Value?: string | null; - /** Indicates if the condition is required. */ - IsRequired?: boolean | null; -} - -export interface TranscodingProfileDto { - /** Gets or sets the container. */ - Container?: string | null; - /** Gets or sets the DLNA profile type. */ - Type?: 'Audio' | 'Video' | 'Photo' | 'Subtitle' | 'Lyric'; - /** Gets or sets the video codec. */ - VideoCodec?: string | null; - /** Gets or sets the audio codec. */ - AudioCodec?: string | null; - /** Media streaming protocol. */ - Protocol?: 'http' | 'hls'; - /** - * Indicates if the content length should be estimated. - * @default false - */ - EstimateContentLength?: boolean; - /** - * Indicates if M2TS mode is enabled. - * @default false - */ - EnableMpegtsM2TsMode?: boolean; - /** - * Gets or sets the transcoding seek info mode. - * @default "Auto" - */ - TranscodeSeekInfo?: 'Auto' | 'Bytes'; - /** - * Indicates if timestamps should be copied. - * @default false - */ - CopyTimestamps?: boolean; - /** - * Gets or sets the encoding context. - * @default "Streaming" - */ - Context?: 'Streaming' | 'Static'; - /** - * Indicates if subtitles are allowed in the manifest. - * @default false - */ - EnableSubtitlesInManifest?: boolean; - /** Gets or sets the maximum audio channels. */ - MaxAudioChannels?: string | null; - /** - * Gets or sets the minimum amount of segments. - * @format int32 - * @default 0 - */ - MinSegments?: number; - /** - * Gets or sets the segment length. - * @format int32 - * @default 0 - */ - SegmentLength?: number; - /** - * Indicates if breaking the video stream on non-keyframes is supported. - * @default false - */ - BreakOnNonKeyFrames?: boolean; - /** Gets or sets the profile conditions. */ - Conditions?: ProfileConditionDto[] | null; - /** - * Indicates if variable bitrate encoding is supported. - * @default true - */ - EnableAudioVbrEncoding?: boolean; -} - -export interface ContainerProfileDto { - /** Gets or sets the MediaBrowser.Model.Dlna.DlnaProfileType which this container must meet. */ - Type?: 'Audio' | 'Video' | 'Photo' | 'Subtitle' | 'Lyric' | null; - /** Gets or sets the profile conditions. */ - Conditions?: ProfileConditionDto[] | null; - /** Gets or sets the container(s) which this container must meet. */ - Container?: string | null; - /** Gets or sets the sub container(s) which this container must meet. */ - SubContainer?: string | null; -} - -export interface CodecProfileDto { - /** Gets or sets the MediaBrowser.Model.Dlna.CodecType which this container must meet. */ - Type?: 'Video' | 'VideoAudio' | 'Audio' | null; - /** Gets or sets the profile conditions. */ - Conditions?: ProfileConditionDto[] | null; - /** Gets or sets the apply conditions if this profile is met. */ - ApplyConditions?: ProfileConditionDto[] | null; - /** Gets or sets the codec(s) that this profile applies to. */ - Codec?: string | null; - /** Gets or sets the container(s) which this profile will be applied to. */ - Container?: string | null; - /** Gets or sets the sub-container(s) which this profile will be applied to. */ - SubContainer?: string | null; -} - -export interface SubtitleProfileDto { - /** Gets or sets the format. */ - Format?: string | null; - /** Gets or sets the delivery method. */ - Method?: 'Encode' | 'Embed' | 'External' | 'Hls' | 'Drop' | null; - /** Gets or sets the DIDL mode. */ - DidlMode?: string | null; - /** Gets or sets the language. */ - Language?: string | null; - /** Gets or sets the container. */ - Container?: string | null; -} - -export interface DeviceProfileDto { - /** Gets or sets the name of this device profile. User profiles must have a unique name. */ - Name?: string | null; - /** - * Gets or sets the unique internal identifier. - * @format uuid - */ - Id?: string | null; - /** - * Gets or sets the maximum allowed bitrate for all streamed content. - * @format int32 - */ - MaxStreamingBitrate?: number | null; - /** - * Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files). - * @format int32 - */ - MaxStaticBitrate?: number | null; - /** - * Gets or sets the maximum allowed bitrate for transcoded music streams. - * @format int32 - */ - MusicStreamingTranscodingBitrate?: number | null; - /** - * Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files. - * @format int32 - */ - MaxStaticMusicBitrate?: number | null; - /** Gets or sets the direct play profiles. */ - DirectPlayProfiles?: DirectPlayProfileDto[] | null; - /** Gets or sets the transcoding profiles. */ - TranscodingProfiles?: TranscodingProfileDto[] | null; - /** Gets or sets the container profiles. */ - ContainerProfiles?: ContainerProfileDto[] | null; - /** Gets or sets the codec profiles. */ - CodecProfiles?: CodecProfileDto[] | null; - /** Gets or sets the subtitle profiles. */ - SubtitleProfiles?: SubtitleProfileDto[] | null; -} - -export interface PlaybackConfigDto { - /** @example 0 */ - bitrate?: number; - /** @example 0 */ - audioStreamIndex?: number; - /** @example 0 */ - progress?: number; - /** @example "en" */ - deviceProfile?: DeviceProfileDto; - /** @example "en" */ - defaultLanguage?: string; -} - -export interface MediaSourceActionBodyDto { - playbackConfig?: PlaybackConfigDto; -} - -export interface ActionResponseErrorDto { - /** @example "Stream not found" */ - message: string; -} - -export interface ToastDto { - title: string; - message: string; - type: 'info' | 'success' | 'error'; -} - -export interface AudioStreamDto { - index: number; - label: string; - /** @example "aac" */ - codec?: string; - /** @example 96000 */ - bitrate?: number; -} - -export interface QualityDto { - index: number; - bitrate: number; - label: string; - codec?: string; - original: boolean; -} - -export interface SubtitlesDto { - src: string; - lang: string; - kind: 'subtitles' | 'captions' | 'descriptions'; - label: string; -} - -export interface StreamDto { - streamId: string; - title: string; - properties: VideoStreamPropertyDto[]; - src: string; - directPlay: boolean; - /** Duration in seconds */ - duration: number; - /** Play progress as a number between 0 and 1 */ - progress: number; - audioStreams: AudioStreamDto[]; - audioStreamIndex: number; - qualities: QualityDto[]; - qualityIndex: number; - subtitles: SubtitlesDto[]; -} - -export interface StreamActionResponseDto { - error?: ActionResponseErrorDto; - toast?: ToastDto; - stream?: StreamDto; -} - -export interface ActionResponseResultDto { - success: boolean; - message?: string; -} - -export interface ActionResponseDto { - error?: ActionResponseErrorDto; - toast?: ToastDto; - result?: ActionResponseResultDto; -} - -export interface UpdateOrCreateMediaSourceDto { - pluginId: string; - pluginSettings?: Record; - id?: string; - name?: string; - /** @default false */ - adminControlled?: boolean; - priority?: number; -} - -export interface ValidationResponseDto { - /** @example true */ - isValid: boolean; - /** @example {"setting1":"error message","setting2":"another error message"} */ - errors: Record; - /** @example {"setting1":"new value","setting2":"another new value"} */ - settings: Record; -} - -export interface UpdateMediaSourceResponseDto { - mediaSource: MediaSourceDto; - validationResponse?: ValidationResponseDto; -} - -export interface SignInDto { - name: string; - password: string; -} - -export interface SignInResponse { - accessToken: string; - user: UserDto; -} - -export interface PluginSettingsTemplateDto { - /** @example {"setting1":"string","setting2":{"type":"link","url":"https://example.com"}} */ - settings: Record; -} - -export interface PluginSettingsDto { - /** @example {"setting1":"some value","setting2":12345,"setting3":true,"setting4":{"nestedKey":"nestedValue"}} */ - settings: Record; -} - -export interface MovieUserDataDto { - tmdbId: string; - inLibrary: boolean; - playState?: PlayStateDto; -} - -export interface SeriesUserDataDto { - tmdbId: string; - inLibrary: boolean; - playStates: PlayStateDto[]; -} - -export interface UpdatePlayStateDto { - season?: number; - episode?: number; - /** - * Whether the user has watched this media - * @default false - */ - watched?: boolean; - /** - * A number between 0 and 1 - * @default false - * @example 0.5 - */ - progress?: number; -} - -export interface BulkUpdatePlayStateDto { - playStates: UpdatePlayStateDto[]; + name?: string; + password?: string; + isAdmin?: boolean; + onboardingDone?: boolean; + settings?: Settings; + profilePicture?: string; + oldPassword?: string; } export interface PaginatedResponseDto { - total: number; - page: number; - itemsPerPage: number; + total: number; + page: number; + itemsPerPage: number; +} + +export interface VideoStreamPropertyDto { + label: string; + value: string | number; + formatted?: string; +} + +export interface StreamActionDto { + label: string; + type: string; +} + +export interface StreamCandidateDto { + streamId: string; + title: string; + properties: VideoStreamPropertyDto[]; + actions: StreamActionDto[]; +} + +export interface ProviderWithStreamsDto { + provider: MediaSourceDto; + streams: StreamCandidateDto[]; +} + +export interface ViewBaseDto { + id: string; + type: "general" | "list-with-details"; + label: string; + priority?: number; +} + +export interface ViewProviderDto { + view: ViewBaseDto; + sourceId: string; +} + +export interface ViewGroupDto { + label: string; + viewProviders: ViewProviderDto[]; +} + +export interface ViewProvidersResponseDto { + viewGroups: ViewGroupDto[]; +} + +export interface HeadingElementDto { + type: "heading"; + label: string; + description?: string; +} + +export interface ToggleElementDto { + type: "toggle"; + label: string; + description?: string; + value: boolean; + style: "checkbox" | "switch"; +} + +export interface SelectOptionDto { + label: string; + value: string; +} + +export interface SelectElementDto { + type: "select"; + label: string; + description?: string; + value: string; + options: SelectOptionDto[]; + style: "dropdown" | "radio"; +} + +export interface StreamActionIconDto { + type: "play"; + size?: "lg" | "md" | "sm"; +} + +export interface StreamActionElementDto { + type: "action"; + label: "Stream"; + action: "stream"; + icon: StreamActionIconDto; + disabled?: boolean; +} + +export interface IconDto { + type: "play" | "download" | "delete" | "info" | "external-link"; + size?: "lg" | "md" | "sm"; +} + +export interface ActionElementDto { + type: "action"; + label: string; + action: string; + disabled?: boolean; + icon?: IconDto; +} + +export interface InputElementDto { + type: "input"; + label: string; + description?: string; + value?: string; + placeholder?: string; + style: "number" | "text" | "email" | "password"; + min?: number; + max?: number; + maxLength?: number; + minLength?: number; + disabled?: boolean; +} + +export interface ExternalLinkElementDto { + type: "external-link"; + label: string; + url: string; + icon?: IconDto; +} + +export interface OpenViewElementDto { + type: "open-view"; + label: string; + viewId: string; + icon?: IconDto; +} + +export interface GeneralViewDto { + type: "general"; + elements: ( + | HeadingElementDto + | ToggleElementDto + | SelectElementDto + | StreamActionElementDto + | ActionElementDto + | InputElementDto + | ExternalLinkElementDto + | OpenViewElementDto + )[]; +} + +export interface SortablePropertyDto { + label: string; + value: string | number; + formatted: string; + secondary?: boolean; +} + +export interface ListWithDetailsItemDto { + id: string; + label: string; + description: string; + properties: SortablePropertyDto[]; + actions: (StreamActionElementDto | ActionElementDto | OpenViewElementDto)[]; +} + +export interface ListWithDetailsViewDto { + id: string; + type: "list-with-details"; + label: string; + priority?: number; + items: ListWithDetailsItemDto[]; + order?: OrderOptionDto; + orderOptions: OrderOptionDto[]; +} + +export interface MediaSourceViewResponseDto { + view: GeneralViewDto | ListWithDetailsViewDto; +} + +export interface StreamCandidatesDto { + candidates: StreamCandidateDto[]; +} + +export interface StreamBaseDto { + streamId: string; + title: string; + properties: VideoStreamPropertyDto[]; +} + +export interface AutoplayResponseDto { + candidate?: StreamBaseDto; +} + +export interface DirectPlayProfileDto { + /** Gets or sets the container. */ + Container?: string | null; + /** Gets or sets the audio codec. */ + AudioCodec?: string | null; + /** Gets or sets the video codec. */ + VideoCodec?: string | null; + /** Gets or sets the Dlna profile type. */ + Type?: "Audio" | "Video" | "Photo" | "Subtitle" | "Lyric"; +} + +export interface ProfileConditionDto { + /** Gets or sets the condition. */ + Condition?: + | "Equals" + | "NotEquals" + | "LessThanEqual" + | "GreaterThanEqual" + | "EqualsAny" + | null; + /** Gets or sets the property. */ + Property?: + | "AudioChannels" + | "AudioBitrate" + | "AudioProfile" + | "Width" + | "Height" + | "Has64BitOffsets" + | "PacketLength" + | "VideoBitDepth" + | "VideoBitrate" + | "VideoFramerate" + | "VideoLevel" + | "VideoProfile" + | "VideoTimestamp" + | "IsAnamorphic" + | "RefFrames" + | "NumAudioStreams" + | "NumVideoStreams" + | "IsSecondaryAudio" + | "VideoCodecTag" + | "IsAvc" + | "IsInterlaced" + | "AudioSampleRate" + | "AudioBitDepth" + | "VideoRangeType" + | null; + /** Gets or sets the value. */ + Value?: string | null; + /** Indicates if the condition is required. */ + IsRequired?: boolean | null; +} + +export interface TranscodingProfileDto { + /** Gets or sets the container. */ + Container?: string | null; + /** Gets or sets the DLNA profile type. */ + Type?: "Audio" | "Video" | "Photo" | "Subtitle" | "Lyric"; + /** Gets or sets the video codec. */ + VideoCodec?: string | null; + /** Gets or sets the audio codec. */ + AudioCodec?: string | null; + /** Media streaming protocol. */ + Protocol?: "http" | "hls"; + /** + * Indicates if the content length should be estimated. + * @default false + */ + EstimateContentLength?: boolean; + /** + * Indicates if M2TS mode is enabled. + * @default false + */ + EnableMpegtsM2TsMode?: boolean; + /** + * Gets or sets the transcoding seek info mode. + * @default "Auto" + */ + TranscodeSeekInfo?: "Auto" | "Bytes"; + /** + * Indicates if timestamps should be copied. + * @default false + */ + CopyTimestamps?: boolean; + /** + * Gets or sets the encoding context. + * @default "Streaming" + */ + Context?: "Streaming" | "Static"; + /** + * Indicates if subtitles are allowed in the manifest. + * @default false + */ + EnableSubtitlesInManifest?: boolean; + /** Gets or sets the maximum audio channels. */ + MaxAudioChannels?: string | null; + /** + * Gets or sets the minimum amount of segments. + * @format int32 + * @default 0 + */ + MinSegments?: number; + /** + * Gets or sets the segment length. + * @format int32 + * @default 0 + */ + SegmentLength?: number; + /** + * Indicates if breaking the video stream on non-keyframes is supported. + * @default false + */ + BreakOnNonKeyFrames?: boolean; + /** Gets or sets the profile conditions. */ + Conditions?: ProfileConditionDto[] | null; + /** + * Indicates if variable bitrate encoding is supported. + * @default true + */ + EnableAudioVbrEncoding?: boolean; +} + +export interface ContainerProfileDto { + /** Gets or sets the MediaBrowser.Model.Dlna.DlnaProfileType which this container must meet. */ + Type?: "Audio" | "Video" | "Photo" | "Subtitle" | "Lyric" | null; + /** Gets or sets the profile conditions. */ + Conditions?: ProfileConditionDto[] | null; + /** Gets or sets the container(s) which this container must meet. */ + Container?: string | null; + /** Gets or sets the sub container(s) which this container must meet. */ + SubContainer?: string | null; +} + +export interface CodecProfileDto { + /** Gets or sets the MediaBrowser.Model.Dlna.CodecType which this container must meet. */ + Type?: "Video" | "VideoAudio" | "Audio" | null; + /** Gets or sets the profile conditions. */ + Conditions?: ProfileConditionDto[] | null; + /** Gets or sets the apply conditions if this profile is met. */ + ApplyConditions?: ProfileConditionDto[] | null; + /** Gets or sets the codec(s) that this profile applies to. */ + Codec?: string | null; + /** Gets or sets the container(s) which this profile will be applied to. */ + Container?: string | null; + /** Gets or sets the sub-container(s) which this profile will be applied to. */ + SubContainer?: string | null; +} + +export interface SubtitleProfileDto { + /** Gets or sets the format. */ + Format?: string | null; + /** Gets or sets the delivery method. */ + Method?: "Encode" | "Embed" | "External" | "Hls" | "Drop" | null; + /** Gets or sets the DIDL mode. */ + DidlMode?: string | null; + /** Gets or sets the language. */ + Language?: string | null; + /** Gets or sets the container. */ + Container?: string | null; +} + +export interface DeviceProfileDto { + /** Gets or sets the name of this device profile. User profiles must have a unique name. */ + Name?: string | null; + /** + * Gets or sets the unique internal identifier. + * @format uuid + */ + Id?: string | null; + /** + * Gets or sets the maximum allowed bitrate for all streamed content. + * @format int32 + */ + MaxStreamingBitrate?: number | null; + /** + * Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files). + * @format int32 + */ + MaxStaticBitrate?: number | null; + /** + * Gets or sets the maximum allowed bitrate for transcoded music streams. + * @format int32 + */ + MusicStreamingTranscodingBitrate?: number | null; + /** + * Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files. + * @format int32 + */ + MaxStaticMusicBitrate?: number | null; + /** Gets or sets the direct play profiles. */ + DirectPlayProfiles?: DirectPlayProfileDto[] | null; + /** Gets or sets the transcoding profiles. */ + TranscodingProfiles?: TranscodingProfileDto[] | null; + /** Gets or sets the container profiles. */ + ContainerProfiles?: ContainerProfileDto[] | null; + /** Gets or sets the codec profiles. */ + CodecProfiles?: CodecProfileDto[] | null; + /** Gets or sets the subtitle profiles. */ + SubtitleProfiles?: SubtitleProfileDto[] | null; +} + +export interface PlaybackConfigDto { + /** @example 0 */ + bitrate?: number; + /** @example 0 */ + audioStreamIndex?: number; + /** @example 0 */ + progress?: number; + /** @example "en" */ + deviceProfile?: DeviceProfileDto; + /** @example "en" */ + defaultLanguage?: string; +} + +export interface MediaSourceActionBodyDto { + playbackConfig?: PlaybackConfigDto; +} + +export interface ActionResponseErrorDto { + /** @example "Stream not found" */ + message: string; +} + +export interface ToastDto { + title: string; + message: string; + type: "info" | "success" | "error"; +} + +export interface AudioStreamDto { + index: number; + label: string; + /** @example "aac" */ + codec?: string; + /** @example 96000 */ + bitrate?: number; +} + +export interface QualityDto { + index: number; + bitrate: number; + label: string; + codec?: string; + original: boolean; +} + +export interface SubtitlesDto { + src: string; + lang: string; + kind: "subtitles" | "captions" | "descriptions"; + label: string; +} + +export interface StreamDto { + streamId: string; + title: string; + properties: VideoStreamPropertyDto[]; + src: string; + directPlay: boolean; + /** Duration in seconds */ + duration: number; + /** Play progress as a number between 0 and 1 */ + progress: number; + audioStreams: AudioStreamDto[]; + audioStreamIndex: number; + qualities: QualityDto[]; + qualityIndex: number; + subtitles: SubtitlesDto[]; +} + +export interface StreamActionResponseDto { + error?: ActionResponseErrorDto; + toast?: ToastDto; + stream?: StreamDto; +} + +export interface ActionResponseResultDto { + success: boolean; + message?: string; +} + +export interface ActionResponseDto { + error?: ActionResponseErrorDto; + toast?: ToastDto; + result?: ActionResponseResultDto; +} + +export interface UpdateOrCreateMediaSourceDto { + pluginId: string; + pluginSettings?: Record; + id?: string; + name?: string; + /** @default false */ + adminControlled?: boolean; + priority?: number; +} + +export interface ValidationResponseDto { + /** @example true */ + isValid: boolean; + /** @example {"setting1":"error message","setting2":"another error message"} */ + errors: Record; + /** @example {"setting1":"new value","setting2":"another new value"} */ + settings: Record; +} + +export interface UpdateMediaSourceResponseDto { + mediaSource: MediaSourceDto; + validationResponse?: ValidationResponseDto; +} + +export interface SignInDto { + name: string; + password: string; +} + +export interface SignInResponse { + accessToken: string; + user: UserDto; +} + +export interface PluginSettingsTemplateDto { + /** @example {"setting1":"string","setting2":{"type":"link","url":"https://example.com"}} */ + settings: Record; +} + +export interface PluginSettingsDto { + /** @example {"setting1":"some value","setting2":12345,"setting3":true,"setting4":{"nestedKey":"nestedValue"}} */ + settings: Record; +} + +export interface MovieUserDataDto { + tmdbId: string; + inLibrary: boolean; + playState?: PlayStateDto; +} + +export interface SeriesUserDataDto { + tmdbId: string; + inLibrary: boolean; + playStates: PlayStateDto[]; +} + +export interface UpdatePlayStateDto { + season?: number; + episode?: number; + /** + * Whether the user has watched this media + * @default false + */ + watched?: boolean; + /** + * A number between 0 and 1 + * @default false + * @example 0.5 + */ + progress?: number; +} + +export interface BulkUpdatePlayStateDto { + playStates: UpdatePlayStateDto[]; } export interface NextEpisodeToAir { - air_date?: string; + air_date?: string; } export interface Season { - air_date?: string; - episode_count?: number; - id?: number; - name?: string; - overview?: string; - poster_path?: string; - season_number?: number; - vote_average?: number; + air_date?: string; + episode_count?: number; + id?: number; + name?: string; + overview?: string; + poster_path?: string; + season_number?: number; + vote_average?: number; } export interface TmdbItemDto { - id?: number; - poster_path?: string; - vote_average?: number; - title?: string; - release_date?: string; - runtime?: number; - name?: string; - first_air_date?: string; - last_air_date?: string; - next_episode_to_air?: NextEpisodeToAir; - seasons?: Season[]; + id?: number; + poster_path?: string; + vote_average?: number; + title?: string; + release_date?: string; + runtime?: number; + name?: string; + first_air_date?: string; + last_air_date?: string; + next_episode_to_air?: NextEpisodeToAir; + seasons?: Season[]; } export interface LibraryItemDto { - tmdbId: string; - mediaType: 'movie' | 'series'; - playStates?: PlayStateDto[]; - tmdbItem: TmdbItemDto; - lastPlayState?: PlayStateDto; - watched?: boolean; + tmdbId: string; + mediaType: "movie" | "series"; + playStates?: PlayStateDto[]; + tmdbItem: TmdbItemDto; + lastPlayState?: PlayStateDto; + watched?: boolean; } export interface SuccessResponseDto { - success: boolean; + success: boolean; } import type { - AxiosInstance, - AxiosRequestConfig, - AxiosResponse, - HeadersDefaults, - ResponseType -} from 'axios'; -import axios from 'axios'; + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, + HeadersDefaults, + ResponseType, +} from "axios"; +import axios from "axios"; export type QueryParamsType = Record; export interface FullRequestParams - extends Omit { - /** set parameter to `true` for call `securityWorker` for this request */ - secure?: boolean; - /** request path */ - path: string; - /** content type of request body */ - type?: ContentType; - /** query params */ - query?: QueryParamsType; - /** format of response (i.e. response.json() -> format: "json") */ - format?: ResponseType; - /** request body */ - body?: unknown; + extends Omit { + /** set parameter to `true` for call `securityWorker` for this request */ + secure?: boolean; + /** request path */ + path: string; + /** content type of request body */ + type?: ContentType; + /** query params */ + query?: QueryParamsType; + /** format of response (i.e. response.json() -> format: "json") */ + format?: ResponseType; + /** request body */ + body?: unknown; } -export type RequestParams = Omit; +export type RequestParams = Omit< + FullRequestParams, + "body" | "method" | "query" | "path" +>; export interface ApiConfig - extends Omit { - securityWorker?: ( - securityData: SecurityDataType | null - ) => Promise | AxiosRequestConfig | void; - secure?: boolean; - format?: ResponseType; + extends Omit { + securityWorker?: ( + securityData: SecurityDataType | null, + ) => Promise | AxiosRequestConfig | void; + secure?: boolean; + format?: ResponseType; } export enum ContentType { - Json = 'application/json', - FormData = 'multipart/form-data', - UrlEncoded = 'application/x-www-form-urlencoded', - Text = 'text/plain' + Json = "application/json", + JsonApi = "application/vnd.api+json", + FormData = "multipart/form-data", + UrlEncoded = "application/x-www-form-urlencoded", + Text = "text/plain", } export class HttpClient { - public instance: AxiosInstance; - private securityData: SecurityDataType | null = null; - private securityWorker?: ApiConfig['securityWorker']; - private secure?: boolean; - private format?: ResponseType; + public instance: AxiosInstance; + private securityData: SecurityDataType | null = null; + private securityWorker?: ApiConfig["securityWorker"]; + private secure?: boolean; + private format?: ResponseType; - constructor({ - securityWorker, - secure, - format, - ...axiosConfig - }: ApiConfig = {}) { - this.instance = axios.create({ ...axiosConfig, baseURL: axiosConfig.baseURL || '' }); - this.secure = secure; - this.format = format; - this.securityWorker = securityWorker; - } + constructor({ + securityWorker, + secure, + format, + ...axiosConfig + }: ApiConfig = {}) { + this.instance = axios.create({ + ...axiosConfig, + baseURL: axiosConfig.baseURL || "", + }); + this.secure = secure; + this.format = format; + this.securityWorker = securityWorker; + } - public setSecurityData = (data: SecurityDataType | null) => { - this.securityData = data; - }; + public setSecurityData = (data: SecurityDataType | null) => { + this.securityData = data; + }; - protected mergeRequestParams( - params1: AxiosRequestConfig, - params2?: AxiosRequestConfig - ): AxiosRequestConfig { - const method = params1.method || (params2 && params2.method); + protected mergeRequestParams( + params1: AxiosRequestConfig, + params2?: AxiosRequestConfig, + ): AxiosRequestConfig { + const method = params1.method || (params2 && params2.method); - return { - ...this.instance.defaults, - ...params1, - ...(params2 || {}), - headers: { - ...((method && - this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || - {}), - ...(params1.headers || {}), - ...((params2 && params2.headers) || {}) - } - }; - } + return { + ...this.instance.defaults, + ...params1, + ...(params2 || {}), + headers: { + ...((method && + this.instance.defaults.headers[ + method.toLowerCase() as keyof HeadersDefaults + ]) || + {}), + ...(params1.headers || {}), + ...((params2 && params2.headers) || {}), + }, + }; + } - protected stringifyFormItem(formItem: unknown) { - if (typeof formItem === 'object' && formItem !== null) { - return JSON.stringify(formItem); - } else { - return `${formItem}`; - } - } + protected stringifyFormItem(formItem: unknown) { + if (typeof formItem === "object" && formItem !== null) { + return JSON.stringify(formItem); + } else { + return `${formItem}`; + } + } - protected createFormData(input: Record): FormData { - if (input instanceof FormData) { - return input; - } - return Object.keys(input || {}).reduce((formData, key) => { - const property = input[key]; - const propertyContent: any[] = property instanceof Array ? property : [property]; + protected createFormData(input: Record): FormData { + if (input instanceof FormData) { + return input; + } + return Object.keys(input || {}).reduce((formData, key) => { + const property = input[key]; + const propertyContent: any[] = + property instanceof Array ? property : [property]; - for (const formItem of propertyContent) { - const isFileType = formItem instanceof Blob || formItem instanceof File; - formData.append(key, isFileType ? formItem : this.stringifyFormItem(formItem)); - } + for (const formItem of propertyContent) { + const isFileType = formItem instanceof Blob || formItem instanceof File; + formData.append( + key, + isFileType ? formItem : this.stringifyFormItem(formItem), + ); + } - return formData; - }, new FormData()); - } + return formData; + }, new FormData()); + } - public request = async ({ - secure, - path, - type, - query, - format, - body, - ...params - }: FullRequestParams): Promise> => { - const secureParams = - ((typeof secure === 'boolean' ? secure : this.secure) && - this.securityWorker && - (await this.securityWorker(this.securityData))) || - {}; - const requestParams = this.mergeRequestParams(params, secureParams); - const responseFormat = format || this.format || undefined; + public request = async ({ + secure, + path, + type, + query, + format, + body, + ...params + }: FullRequestParams): Promise> => { + const secureParams = + ((typeof secure === "boolean" ? secure : this.secure) && + this.securityWorker && + (await this.securityWorker(this.securityData))) || + {}; + const requestParams = this.mergeRequestParams(params, secureParams); + const responseFormat = format || this.format || undefined; - if (type === ContentType.FormData && body && body !== null && typeof body === 'object') { - body = this.createFormData(body as Record); - } + if ( + type === ContentType.FormData && + body && + body !== null && + typeof body === "object" + ) { + body = this.createFormData(body as Record); + } - if (type === ContentType.Text && body && body !== null && typeof body !== 'string') { - body = JSON.stringify(body); - } + if ( + type === ContentType.Text && + body && + body !== null && + typeof body !== "string" + ) { + body = JSON.stringify(body); + } - return this.instance.request({ - ...requestParams, - headers: { - ...(requestParams.headers || {}), - ...(type ? { 'Content-Type': type } : {}) - }, - params: query, - responseType: responseFormat, - data: body, - url: path - }); - }; + return this.instance.request({ + ...requestParams, + headers: { + ...(requestParams.headers || {}), + ...(type ? { "Content-Type": type } : {}), + }, + params: query, + responseType: responseFormat, + data: body, + url: path, + }); + }; } /** @@ -927,1023 +968,1090 @@ export class HttpClient { * @version 1.0.0 * @contact */ -export class Api extends HttpClient { - users = { - /** - * No description - * - * @tags users - * @name FindAllUsers - * @request GET:/api/users - */ - findAllUsers: (params: RequestParams = {}) => - this.request({ - path: `/api/users`, - method: 'GET', - format: 'json', - ...params - }), +export class Api< + SecurityDataType extends unknown, +> extends HttpClient { + users = { + /** + * No description + * + * @tags users + * @name FindAllUsers + * @request GET:/api/users + */ + findAllUsers: (params: RequestParams = {}) => + this.request({ + path: `/api/users`, + method: "GET", + format: "json", + ...params, + }), - /** - * No description - * - * @tags users - * @name CreateUser - * @request POST:/api/users - */ - createUser: (data: CreateUserDto, params: RequestParams = {}) => - this.request< - UserDto, - | { - /** @example 400 */ - statusCode: number; - /** @example "Bad Request" */ - message: string; - /** @example "Bad Request" */ - error?: string; - } - | { - /** @example 401 */ - statusCode: number; - /** @example "Unauthorized" */ - message: string; - /** @example "Unauthorized" */ - error?: string; - } - >({ - path: `/api/users`, - method: 'POST', - body: data, - type: ContentType.Json, - format: 'json', - ...params - }), + /** + * No description + * + * @tags users + * @name CreateUser + * @request POST:/api/users + */ + createUser: (data: CreateUserDto, params: RequestParams = {}) => + this.request< + UserDto, + | { + /** @example 400 */ + statusCode: number; + /** @example "Bad Request" */ + message: string; + /** @example "Bad Request" */ + error?: string; + } + | { + /** @example 401 */ + statusCode: number; + /** @example "Unauthorized" */ + message: string; + /** @example "Unauthorized" */ + error?: string; + } + >({ + path: `/api/users`, + method: "POST", + body: data, + type: ContentType.Json, + format: "json", + ...params, + }), - /** - * No description - * - * @tags users - * @name FindUserById - * @request GET:/api/users/{id} - */ - findUserById: (id: string, params: RequestParams = {}) => - this.request< - UserDto, - { - /** @example 404 */ - statusCode: number; - /** @example "Not Found" */ - message: string; - /** @example "Not Found" */ - error?: string; - } - >({ - path: `/api/users/${id}`, - method: 'GET', - format: 'json', - ...params - }), + /** + * No description + * + * @tags users + * @name FindUserById + * @request GET:/api/users/{id} + */ + findUserById: (id: string, params: RequestParams = {}) => + this.request< + UserDto, + { + /** @example 404 */ + statusCode: number; + /** @example "Not Found" */ + message: string; + /** @example "Not Found" */ + error?: string; + } + >({ + path: `/api/users/${id}`, + method: "GET", + format: "json", + ...params, + }), - /** - * No description - * - * @tags users - * @name UpdateUser - * @request PUT:/api/users/{id} - */ - updateUser: (id: string, data: UpdateUserDto, params: RequestParams = {}) => - this.request< - UserDto, - { - /** @example 404 */ - statusCode: number; - /** @example "Not Found" */ - message: string; - /** @example "Not Found" */ - error?: string; - } - >({ - path: `/api/users/${id}`, - method: 'PUT', - body: data, - type: ContentType.Json, - format: 'json', - ...params - }), + /** + * No description + * + * @tags users + * @name UpdateUser + * @request PUT:/api/users/{id} + */ + updateUser: (id: string, data: UpdateUserDto, params: RequestParams = {}) => + this.request< + UserDto, + { + /** @example 404 */ + statusCode: number; + /** @example "Not Found" */ + message: string; + /** @example "Not Found" */ + error?: string; + } + >({ + path: `/api/users/${id}`, + method: "PUT", + body: data, + type: ContentType.Json, + format: "json", + ...params, + }), - /** - * No description - * - * @tags users - * @name DeleteUser - * @request DELETE:/api/users/{id} - */ - deleteUser: (id: string, params: RequestParams = {}) => - this.request< - void, - { - /** @example 404 */ - statusCode: number; - /** @example "Not Found" */ - message: string; - /** @example "Not Found" */ - error?: string; - } - >({ - path: `/api/users/${id}`, - method: 'DELETE', - ...params - }), + /** + * No description + * + * @tags users + * @name DeleteUser + * @request DELETE:/api/users/{id} + */ + deleteUser: (id: string, params: RequestParams = {}) => + this.request< + void, + { + /** @example 404 */ + statusCode: number; + /** @example "Not Found" */ + message: string; + /** @example "Not Found" */ + error?: string; + } + >({ + path: `/api/users/${id}`, + method: "DELETE", + ...params, + }), - /** - * No description - * - * @tags users - * @name UpdateSource - * @request PUT:/api/users/{userId}/sources - */ - updateSource: ( - userId: string, - data: UpdateOrCreateMediaSourceDto, - params: RequestParams = {} - ) => - this.request({ - path: `/api/users/${userId}/sources`, - method: 'PUT', - body: data, - type: ContentType.Json, - format: 'json', - ...params - }), + /** + * No description + * + * @tags users + * @name UpdateSource + * @request PUT:/api/users/{userId}/sources + */ + updateSource: ( + userId: string, + data: UpdateOrCreateMediaSourceDto, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/sources`, + method: "PUT", + body: data, + type: ContentType.Json, + format: "json", + ...params, + }), - /** - * No description - * - * @tags users - * @name DeleteSource - * @request DELETE:/api/users/{userId}/sources/{sourceId} - */ - deleteSource: (sourceId: string, userId: string, params: RequestParams = {}) => - this.request({ - path: `/api/users/${userId}/sources/${sourceId}`, - method: 'DELETE', - format: 'json', - ...params - }), + /** + * No description + * + * @tags users + * @name DeleteSource + * @request DELETE:/api/users/{userId}/sources/{sourceId} + */ + deleteSource: ( + sourceId: string, + userId: string, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/sources/${sourceId}`, + method: "DELETE", + format: "json", + ...params, + }), - /** - * No description - * - * @tags users - * @name GetMovieUserData - * @request GET:/api/users/{userId}/user-data/movie/tmdb/{tmdbId} - */ - getMovieUserData: (userId: string, tmdbId: string, params: RequestParams = {}) => - this.request({ - path: `/api/users/${userId}/user-data/movie/tmdb/${tmdbId}`, - method: 'GET', - format: 'json', - ...params - }), + /** + * No description + * + * @tags users + * @name GetMovieUserData + * @request GET:/api/users/{userId}/user-data/movie/tmdb/{tmdbId} + */ + getMovieUserData: ( + userId: string, + tmdbId: string, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/user-data/movie/tmdb/${tmdbId}`, + method: "GET", + format: "json", + ...params, + }), - /** - * No description - * - * @tags users - * @name GetSeriesUserData - * @request GET:/api/users/{userId}/user-data/series/tmdb/{tmdbId} - */ - getSeriesUserData: (userId: string, tmdbId: string, params: RequestParams = {}) => - this.request({ - path: `/api/users/${userId}/user-data/series/tmdb/${tmdbId}`, - method: 'GET', - format: 'json', - ...params - }), + /** + * No description + * + * @tags users + * @name GetSeriesUserData + * @request GET:/api/users/{userId}/user-data/series/tmdb/{tmdbId} + */ + getSeriesUserData: ( + userId: string, + tmdbId: string, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/user-data/series/tmdb/${tmdbId}`, + method: "GET", + format: "json", + ...params, + }), - /** - * No description - * - * @tags users - * @name GetEpisodeUserData - * @request GET:/api/users/{userId}/user-data/series/tmdb/{tmdbId}/season/{season}/episode/{episode} - */ - getEpisodeUserData: ( - userId: string, - tmdbId: string, - season: number, - episode: number, - params: RequestParams = {} - ) => - this.request({ - path: `/api/users/${userId}/user-data/series/tmdb/${tmdbId}/season/${season}/episode/${episode}`, - method: 'GET', - format: 'json', - ...params - }), + /** + * No description + * + * @tags users + * @name GetEpisodeUserData + * @request GET:/api/users/{userId}/user-data/series/tmdb/{tmdbId}/season/{season}/episode/{episode} + */ + getEpisodeUserData: ( + userId: string, + tmdbId: string, + season: number, + episode: number, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/user-data/series/tmdb/${tmdbId}/season/${season}/episode/${episode}`, + method: "GET", + format: "json", + ...params, + }), - /** - * No description - * - * @tags users - * @name UpdateMoviePlayStateByTmdbId - * @request PUT:/api/users/{userId}/play-state/movie/tmdb/{tmdbId} - */ - updateMoviePlayStateByTmdbId: ( - userId: string, - tmdbId: string, - data: UpdatePlayStateDto, - params: RequestParams = {} - ) => - this.request({ - path: `/api/users/${userId}/play-state/movie/tmdb/${tmdbId}`, - method: 'PUT', - body: data, - type: ContentType.Json, - ...params - }), + /** + * No description + * + * @tags users + * @name UpdateMoviePlayStateByTmdbId + * @request PUT:/api/users/{userId}/play-state/movie/tmdb/{tmdbId} + */ + updateMoviePlayStateByTmdbId: ( + userId: string, + tmdbId: string, + data: UpdatePlayStateDto, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/play-state/movie/tmdb/${tmdbId}`, + method: "PUT", + body: data, + type: ContentType.Json, + ...params, + }), - /** - * No description - * - * @tags users - * @name DeleteMoviePlayStateByTmdbId - * @request DELETE:/api/users/{userId}/play-state/movie/tmdb/{tmdbId} - */ - deleteMoviePlayStateByTmdbId: (userId: string, tmdbId: string, params: RequestParams = {}) => - this.request({ - path: `/api/users/${userId}/play-state/movie/tmdb/${tmdbId}`, - method: 'DELETE', - ...params - }), + /** + * No description + * + * @tags users + * @name DeleteMoviePlayStateByTmdbId + * @request DELETE:/api/users/{userId}/play-state/movie/tmdb/{tmdbId} + */ + deleteMoviePlayStateByTmdbId: ( + userId: string, + tmdbId: string, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/play-state/movie/tmdb/${tmdbId}`, + method: "DELETE", + ...params, + }), - /** - * No description - * - * @tags users - * @name UpdateEpisodePlayStateByTmdbId - * @request PUT:/api/users/{userId}/play-state/series/tmdb/{tmdbId}/season/{season}/episode/{episode} - */ - updateEpisodePlayStateByTmdbId: ( - userId: string, - tmdbId: string, - season: number, - episode: number, - data: UpdatePlayStateDto, - params: RequestParams = {} - ) => - this.request({ - path: `/api/users/${userId}/play-state/series/tmdb/${tmdbId}/season/${season}/episode/${episode}`, - method: 'PUT', - body: data, - type: ContentType.Json, - ...params - }), + /** + * No description + * + * @tags users + * @name UpdateEpisodePlayStateByTmdbId + * @request PUT:/api/users/{userId}/play-state/series/tmdb/{tmdbId}/season/{season}/episode/{episode} + */ + updateEpisodePlayStateByTmdbId: ( + userId: string, + tmdbId: string, + season: number, + episode: number, + data: UpdatePlayStateDto, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/play-state/series/tmdb/${tmdbId}/season/${season}/episode/${episode}`, + method: "PUT", + body: data, + type: ContentType.Json, + ...params, + }), - /** - * No description - * - * @tags users - * @name DeleteEpisodePlayStateByTmdbId - * @request DELETE:/api/users/{userId}/play-state/series/tmdb/{tmdbId}/season/{season}/episode/{episode} - */ - deleteEpisodePlayStateByTmdbId: ( - userId: string, - tmdbId: string, - season: number, - episode: number, - params: RequestParams = {} - ) => - this.request({ - path: `/api/users/${userId}/play-state/series/tmdb/${tmdbId}/season/${season}/episode/${episode}`, - method: 'DELETE', - ...params - }), + /** + * No description + * + * @tags users + * @name DeleteEpisodePlayStateByTmdbId + * @request DELETE:/api/users/{userId}/play-state/series/tmdb/{tmdbId}/season/{season}/episode/{episode} + */ + deleteEpisodePlayStateByTmdbId: ( + userId: string, + tmdbId: string, + season: number, + episode: number, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/play-state/series/tmdb/${tmdbId}/season/${season}/episode/${episode}`, + method: "DELETE", + ...params, + }), - /** - * No description - * - * @tags users - * @name UpdateSeriesPlayStatesByTmdbId - * @request PUT:/api/users/{userId}/play-state/series/tmdb/{tmdbId} - */ - updateSeriesPlayStatesByTmdbId: ( - userId: string, - tmdbId: string, - data: BulkUpdatePlayStateDto, - params: RequestParams = {} - ) => - this.request({ - path: `/api/users/${userId}/play-state/series/tmdb/${tmdbId}`, - method: 'PUT', - body: data, - type: ContentType.Json, - ...params - }) - }; - sources = { - /** - * No description - * - * @tags sources - * @name GetMediaSourceViewGroups - * @request GET:/api/sources/views - */ - getMediaSourceViewGroups: ( - query: { - tmdbId: string; - season?: number; - episode?: number; - }, - params: RequestParams = {} - ) => - this.request({ - path: `/api/sources/views`, - method: 'GET', - query: query, - format: 'json', - ...params - }), + /** + * No description + * + * @tags users + * @name UpdateSeriesPlayStatesByTmdbId + * @request PUT:/api/users/{userId}/play-state/series/tmdb/{tmdbId} + */ + updateSeriesPlayStatesByTmdbId: ( + userId: string, + tmdbId: string, + data: BulkUpdatePlayStateDto, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/play-state/series/tmdb/${tmdbId}`, + method: "PUT", + body: data, + type: ContentType.Json, + ...params, + }), + }; + sources = { + /** + * No description + * + * @tags sources + * @name GetTmdbEpisodeMedia + * @request GET:/api/sources/candidates + */ + getTmdbEpisodeMedia: ( + query: { + tmdbId: string; + season?: number; + episode?: number; + }, + params: RequestParams = {}, + ) => + this.request< + PaginatedResponseDto & { + items: ProviderWithStreamsDto[]; + }, + any + >({ + path: `/api/sources/candidates`, + method: "GET", + query: query, + format: "json", + ...params, + }), - /** - * No description - * - * @tags sources - * @name GetView - * @request GET:/api/sources/{sourceId}/views/{viewId} - */ - getView: ( - sourceId: string, - viewId: string, - query: { - tmdbId: string; - season?: number; - episode?: number; - }, - params: RequestParams = {} - ) => - this.request({ - path: `/api/sources/${sourceId}/views/${viewId}`, - method: 'GET', - query: query, - format: 'json', - ...params - }), + /** + * No description + * + * @tags sources + * @name GetMediaSourceViewGroups + * @request GET:/api/sources/views + */ + getMediaSourceViewGroups: ( + query: { + tmdbId: string; + season?: number; + episode?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/sources/views`, + method: "GET", + query: query, + format: "json", + ...params, + }), - /** - * No description - * - * @tags sources - * @name GetTmdbMovieCandidates - * @request GET:/api/sources/{sourceId}/candidates/tmdb/{tmdbId} - */ - getTmdbMovieCandidates: (sourceId: string, tmdbId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/candidates/tmdb/${tmdbId}`, - method: 'GET', - format: 'json', - ...params - }), + /** + * No description + * + * @tags sources + * @name GetView + * @request GET:/api/sources/{sourceId}/views/{viewId} + */ + getView: ( + sourceId: string, + viewId: string, + query: { + tmdbId: string; + season?: number; + episode?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/sources/${sourceId}/views/${viewId}`, + method: "GET", + query: query, + format: "json", + ...params, + }), - /** - * No description - * - * @tags sources - * @name GetTmdbEpisodeCandidates - * @request GET:/api/sources/{sourceId}/candidates/tmdb/{tmdbId}/season/{season}/episode/{episode} - */ - getTmdbEpisodeCandidates: ( - sourceId: string, - tmdbId: string, - season: number, - episode: number, - params: RequestParams = {} - ) => - this.request({ - path: `/api/sources/${sourceId}/candidates/tmdb/${tmdbId}/season/${season}/episode/${episode}`, - method: 'GET', - format: 'json', - ...params - }), + /** + * No description + * + * @tags sources + * @name GetTmdbMovieCandidates + * @request GET:/api/sources/{sourceId}/candidates/tmdb/{tmdbId} + */ + getTmdbMovieCandidates: ( + sourceId: string, + tmdbId: string, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/sources/${sourceId}/candidates/tmdb/${tmdbId}`, + method: "GET", + format: "json", + ...params, + }), - /** - * No description - * - * @tags sources - * @name GetAutoplayStream - * @request POST:/api/sources/{sourceId}/autoplay-stream - */ - getAutoplayStream: ( - sourceId: string, - query: { - tmdbId: string; - season?: number; - episode?: number; - }, - params: RequestParams = {} - ) => - this.request({ - path: `/api/sources/${sourceId}/autoplay-stream`, - method: 'POST', - query: query, - format: 'json', - ...params - }), + /** + * No description + * + * @tags sources + * @name GetTmdbEpisodeCandidates + * @request GET:/api/sources/{sourceId}/candidates/tmdb/{tmdbId}/season/{season}/episode/{episode} + */ + getTmdbEpisodeCandidates: ( + sourceId: string, + tmdbId: string, + season: number, + episode: number, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/sources/${sourceId}/candidates/tmdb/${tmdbId}/season/${season}/episode/${episode}`, + method: "GET", + format: "json", + ...params, + }), - /** - * No description - * - * @tags sources - * @name GetStream - * @request POST:/api/sources/{sourceId}/stream/{streamId} - */ - getStream: ( - sourceId: string, - streamId: string, - data?: MediaSourceActionBodyDto, - params: RequestParams = {} - ) => - this.request({ - path: `/api/sources/${sourceId}/stream/${streamId}`, - method: 'POST', - body: data, - type: ContentType.Json, - format: 'json', - ...params - }), + /** + * No description + * + * @tags sources + * @name GetAutoplayStream + * @request POST:/api/sources/{sourceId}/autoplay-stream + */ + getAutoplayStream: ( + sourceId: string, + query: { + tmdbId: string; + season?: number; + episode?: number; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/sources/${sourceId}/autoplay-stream`, + method: "POST", + query: query, + format: "json", + ...params, + }), - /** - * No description - * - * @tags sources - * @name HandleViewAction - * @request POST:/api/sources/{sourceId}/action/{action}/{targetId} - */ - handleViewAction: ( - sourceId: string, - targetId: string, - action: string, - params: RequestParams = {} - ) => - this.request({ - path: `/api/sources/${sourceId}/action/${action}/${targetId}`, - method: 'POST', - format: 'json', - ...params - }), + /** + * No description + * + * @tags sources + * @name GetStream + * @request POST:/api/sources/{sourceId}/stream/{streamId} + */ + getStream: ( + sourceId: string, + streamId: string, + data?: MediaSourceActionBodyDto, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/sources/${sourceId}/stream/${streamId}`, + method: "POST", + body: data, + type: ContentType.Json, + format: "json", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerGet - * @request GET:/api/sources/{sourceId}/proxy - */ - proxyHandlerGet: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy`, - method: 'GET', - ...params - }), + /** + * No description + * + * @tags sources + * @name HandleViewAction + * @request POST:/api/sources/{sourceId}/action/{action}/{targetId} + */ + handleViewAction: ( + sourceId: string, + targetId: string, + action: string, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/sources/${sourceId}/action/${action}/${targetId}`, + method: "POST", + format: "json", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerPost - * @request POST:/api/sources/{sourceId}/proxy - */ - proxyHandlerPost: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy`, - method: 'POST', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerGet + * @request GET:/api/sources/{sourceId}/proxy + */ + proxyHandlerGet: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy`, + method: "GET", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerPut - * @request PUT:/api/sources/{sourceId}/proxy - */ - proxyHandlerPut: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy`, - method: 'PUT', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerPost + * @request POST:/api/sources/{sourceId}/proxy + */ + proxyHandlerPost: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy`, + method: "POST", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerDelete - * @request DELETE:/api/sources/{sourceId}/proxy - */ - proxyHandlerDelete: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy`, - method: 'DELETE', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerPut + * @request PUT:/api/sources/{sourceId}/proxy + */ + proxyHandlerPut: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy`, + method: "PUT", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerPatch - * @request PATCH:/api/sources/{sourceId}/proxy - */ - proxyHandlerPatch: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy`, - method: 'PATCH', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerDelete + * @request DELETE:/api/sources/{sourceId}/proxy + */ + proxyHandlerDelete: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy`, + method: "DELETE", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerOptions - * @request OPTIONS:/api/sources/{sourceId}/proxy - */ - proxyHandlerOptions: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy`, - method: 'OPTIONS', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerPatch + * @request PATCH:/api/sources/{sourceId}/proxy + */ + proxyHandlerPatch: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy`, + method: "PATCH", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerHead - * @request HEAD:/api/sources/{sourceId}/proxy - */ - proxyHandlerHead: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy`, - method: 'HEAD', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerOptions + * @request OPTIONS:/api/sources/{sourceId}/proxy + */ + proxyHandlerOptions: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy`, + method: "OPTIONS", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerSearch - * @request SEARCH:/api/sources/{sourceId}/proxy - */ - proxyHandlerSearch: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy`, - method: 'SEARCH', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerHead + * @request HEAD:/api/sources/{sourceId}/proxy + */ + proxyHandlerHead: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy`, + method: "HEAD", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerGet2 - * @request GET:/api/sources/{sourceId}/proxy/* - * @originalName proxyHandlerGet - * @duplicate - */ - proxyHandlerGet2: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy/*`, - method: 'GET', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerSearch + * @request SEARCH:/api/sources/{sourceId}/proxy + */ + proxyHandlerSearch: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy`, + method: "SEARCH", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerPost2 - * @request POST:/api/sources/{sourceId}/proxy/* - * @originalName proxyHandlerPost - * @duplicate - */ - proxyHandlerPost2: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy/*`, - method: 'POST', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerGet2 + * @request GET:/api/sources/{sourceId}/proxy/* + * @originalName proxyHandlerGet + * @duplicate + */ + proxyHandlerGet2: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy/*`, + method: "GET", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerPut2 - * @request PUT:/api/sources/{sourceId}/proxy/* - * @originalName proxyHandlerPut - * @duplicate - */ - proxyHandlerPut2: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy/*`, - method: 'PUT', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerPost2 + * @request POST:/api/sources/{sourceId}/proxy/* + * @originalName proxyHandlerPost + * @duplicate + */ + proxyHandlerPost2: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy/*`, + method: "POST", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerDelete2 - * @request DELETE:/api/sources/{sourceId}/proxy/* - * @originalName proxyHandlerDelete - * @duplicate - */ - proxyHandlerDelete2: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy/*`, - method: 'DELETE', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerPut2 + * @request PUT:/api/sources/{sourceId}/proxy/* + * @originalName proxyHandlerPut + * @duplicate + */ + proxyHandlerPut2: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy/*`, + method: "PUT", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerPatch2 - * @request PATCH:/api/sources/{sourceId}/proxy/* - * @originalName proxyHandlerPatch - * @duplicate - */ - proxyHandlerPatch2: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy/*`, - method: 'PATCH', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerDelete2 + * @request DELETE:/api/sources/{sourceId}/proxy/* + * @originalName proxyHandlerDelete + * @duplicate + */ + proxyHandlerDelete2: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy/*`, + method: "DELETE", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerOptions2 - * @request OPTIONS:/api/sources/{sourceId}/proxy/* - * @originalName proxyHandlerOptions - * @duplicate - */ - proxyHandlerOptions2: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy/*`, - method: 'OPTIONS', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerPatch2 + * @request PATCH:/api/sources/{sourceId}/proxy/* + * @originalName proxyHandlerPatch + * @duplicate + */ + proxyHandlerPatch2: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy/*`, + method: "PATCH", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerHead2 - * @request HEAD:/api/sources/{sourceId}/proxy/* - * @originalName proxyHandlerHead - * @duplicate - */ - proxyHandlerHead2: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy/*`, - method: 'HEAD', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerOptions2 + * @request OPTIONS:/api/sources/{sourceId}/proxy/* + * @originalName proxyHandlerOptions + * @duplicate + */ + proxyHandlerOptions2: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy/*`, + method: "OPTIONS", + ...params, + }), - /** - * No description - * - * @tags sources - * @name ProxyHandlerSearch2 - * @request SEARCH:/api/sources/{sourceId}/proxy/* - * @originalName proxyHandlerSearch - * @duplicate - */ - proxyHandlerSearch2: (sourceId: string, params: RequestParams = {}) => - this.request({ - path: `/api/sources/${sourceId}/proxy/*`, - method: 'SEARCH', - ...params - }) - }; - api = { - /** - * No description - * - * @name SignIn - * @request POST:/api/auth - */ - signIn: (data: SignInDto, params: RequestParams = {}) => - this.request< - SignInResponse, - { - /** @example 401 */ - statusCode: number; - /** @example "Unauthorized" */ - message: string; - /** @example "Unauthorized" */ - error?: string; - } - >({ - path: `/api/auth`, - method: 'POST', - body: data, - type: ContentType.Json, - format: 'json', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerHead2 + * @request HEAD:/api/sources/{sourceId}/proxy/* + * @originalName proxyHandlerHead + * @duplicate + */ + proxyHandlerHead2: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy/*`, + method: "HEAD", + ...params, + }), - /** - * No description - * - * @name TmdbProxyGet - * @request GET:/api/tmdb/v3/proxy/* - */ - tmdbProxyGet: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'GET', - ...params - }), + /** + * No description + * + * @tags sources + * @name ProxyHandlerSearch2 + * @request SEARCH:/api/sources/{sourceId}/proxy/* + * @originalName proxyHandlerSearch + * @duplicate + */ + proxyHandlerSearch2: (sourceId: string, params: RequestParams = {}) => + this.request({ + path: `/api/sources/${sourceId}/proxy/*`, + method: "SEARCH", + ...params, + }), + }; + api = { + /** + * No description + * + * @name SignIn + * @request POST:/api/auth + */ + signIn: (data: SignInDto, params: RequestParams = {}) => + this.request< + SignInResponse, + { + /** @example 401 */ + statusCode: number; + /** @example "Unauthorized" */ + message: string; + /** @example "Unauthorized" */ + error?: string; + } + >({ + path: `/api/auth`, + method: "POST", + body: data, + type: ContentType.Json, + format: "json", + ...params, + }), - /** - * No description - * - * @name TmdbProxyPost - * @request POST:/api/tmdb/v3/proxy/* - */ - tmdbProxyPost: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'POST', - ...params - }), + /** + * No description + * + * @name TmdbProxyGet + * @request GET:/api/tmdb/v3/proxy/* + */ + tmdbProxyGet: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: "GET", + ...params, + }), - /** - * No description - * - * @name TmdbProxyPut - * @request PUT:/api/tmdb/v3/proxy/* - */ - tmdbProxyPut: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'PUT', - ...params - }), + /** + * No description + * + * @name TmdbProxyPost + * @request POST:/api/tmdb/v3/proxy/* + */ + tmdbProxyPost: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: "POST", + ...params, + }), - /** - * No description - * - * @name TmdbProxyDelete - * @request DELETE:/api/tmdb/v3/proxy/* - */ - tmdbProxyDelete: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'DELETE', - ...params - }), + /** + * No description + * + * @name TmdbProxyPut + * @request PUT:/api/tmdb/v3/proxy/* + */ + tmdbProxyPut: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: "PUT", + ...params, + }), - /** - * No description - * - * @name TmdbProxyPatch - * @request PATCH:/api/tmdb/v3/proxy/* - */ - tmdbProxyPatch: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'PATCH', - ...params - }), + /** + * No description + * + * @name TmdbProxyDelete + * @request DELETE:/api/tmdb/v3/proxy/* + */ + tmdbProxyDelete: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: "DELETE", + ...params, + }), - /** - * No description - * - * @name TmdbProxyOptions - * @request OPTIONS:/api/tmdb/v3/proxy/* - */ - tmdbProxyOptions: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'OPTIONS', - ...params - }), + /** + * No description + * + * @name TmdbProxyPatch + * @request PATCH:/api/tmdb/v3/proxy/* + */ + tmdbProxyPatch: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: "PATCH", + ...params, + }), - /** - * No description - * - * @name TmdbProxyHead - * @request HEAD:/api/tmdb/v3/proxy/* - */ - tmdbProxyHead: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'HEAD', - ...params - }), + /** + * No description + * + * @name TmdbProxyOptions + * @request OPTIONS:/api/tmdb/v3/proxy/* + */ + tmdbProxyOptions: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: "OPTIONS", + ...params, + }), - /** - * No description - * - * @name TmdbProxySearch - * @request SEARCH:/api/tmdb/v3/proxy/* - */ - tmdbProxySearch: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'SEARCH', - ...params - }), + /** + * No description + * + * @name TmdbProxyHead + * @request HEAD:/api/tmdb/v3/proxy/* + */ + tmdbProxyHead: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: "HEAD", + ...params, + }), - /** - * No description - * - * @name GetHello - * @request GET:/api - */ - getHello: (params: RequestParams = {}) => - this.request({ - path: `/api`, - method: 'GET', - ...params - }) - }; - metadata = { - /** - * No description - * - * @tags metadata - * @name ClearCache - * @request POST:/api/metadata/clear-cache - */ - clearCache: (params: RequestParams = {}) => - this.request({ - path: `/api/metadata/clear-cache`, - method: 'POST', - ...params - }) - }; - providers = { - /** - * No description - * - * @tags providers - * @name GetSourceProviders - * @request GET:/api/providers - */ - getSourceProviders: (params: RequestParams = {}) => - this.request({ - path: `/api/providers`, - method: 'GET', - format: 'json', - ...params - }), + /** + * No description + * + * @name TmdbProxySearch + * @request SEARCH:/api/tmdb/v3/proxy/* + */ + tmdbProxySearch: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: "SEARCH", + ...params, + }), - /** - * No description - * - * @tags providers - * @name GetSourceSettingsTemplate - * @request GET:/api/providers/{providerId}/settings/template - */ - getSourceSettingsTemplate: (providerId: string, params: RequestParams = {}) => - this.request({ - path: `/api/providers/${providerId}/settings/template`, - method: 'GET', - format: 'json', - ...params - }), + /** + * No description + * + * @name GetHello + * @request GET:/api + */ + getHello: (params: RequestParams = {}) => + this.request({ + path: `/api`, + method: "GET", + ...params, + }), + }; + metadata = { + /** + * No description + * + * @tags metadata + * @name ClearCache + * @request POST:/api/metadata/clear-cache + */ + clearCache: (params: RequestParams = {}) => + this.request({ + path: `/api/metadata/clear-cache`, + method: "POST", + ...params, + }), + }; + providers = { + /** + * No description + * + * @tags providers + * @name GetSourceProviders + * @request GET:/api/providers + */ + getSourceProviders: (params: RequestParams = {}) => + this.request({ + path: `/api/providers`, + method: "GET", + format: "json", + ...params, + }), - /** - * No description - * - * @tags providers - * @name ValidateSourceSettings - * @request POST:/api/providers/{providerId}/settings/validate - */ - validateSourceSettings: ( - providerId: string, - data: PluginSettingsDto, - params: RequestParams = {} - ) => - this.request({ - path: `/api/providers/${providerId}/settings/validate`, - method: 'POST', - body: data, - type: ContentType.Json, - format: 'json', - ...params - }) - }; - library = { - /** - * No description - * - * @tags library - * @name GetMyList - * @request GET:/api/users/{userId}/library/my-list - */ - getMyList: ( - userId: string, - query?: { - status?: 'all' | 'upcoming' | 'unwatched' | 'watched' | 'continue-watching'; - type?: 'movies' | 'series' | 'all'; - order?: 'date-added' | 'name' | 'first-release-date' | 'last-release-date' | 'last-played'; - direction?: 'asc' | 'desc'; - page?: number; - itemsPerPage?: number; - }, - params: RequestParams = {} - ) => - this.request< - PaginatedResponseDto & { - items: LibraryItemDto[]; - }, - any - >({ - path: `/api/users/${userId}/library/my-list`, - method: 'GET', - query: query, - format: 'json', - ...params - }), + /** + * No description + * + * @tags providers + * @name GetSourceSettingsTemplate + * @request GET:/api/providers/{providerId}/settings/template + */ + getSourceSettingsTemplate: ( + providerId: string, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/providers/${providerId}/settings/template`, + method: "GET", + format: "json", + ...params, + }), - /** - * No description - * - * @tags library - * @name GetCatalogue - * @request GET:/api/users/{userId}/library/catalogue/{sourceId} - */ - getCatalogue: ( - userId: string, - sourceId: string, - query?: { - type?: 'all' | 'movies' | 'series' | 'missing'; - order?: string; - direction?: string; - page?: number; - itemsPerPage?: number; - }, - params: RequestParams = {} - ) => - this.request< - PaginatedResponseDto & { - items: LibraryItemDto[]; - }, - any - >({ - path: `/api/users/${userId}/library/catalogue/${sourceId}`, - method: 'GET', - query: query, - format: 'json', - ...params - }), + /** + * No description + * + * @tags providers + * @name ValidateSourceSettings + * @request POST:/api/providers/{providerId}/settings/validate + */ + validateSourceSettings: ( + providerId: string, + data: PluginSettingsDto, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/providers/${providerId}/settings/validate`, + method: "POST", + body: data, + type: ContentType.Json, + format: "json", + ...params, + }), + }; + library = { + /** + * No description + * + * @tags library + * @name GetMyList + * @request GET:/api/users/{userId}/library/my-list + */ + getMyList: ( + userId: string, + query?: { + status?: + | "all" + | "upcoming" + | "unwatched" + | "watched" + | "continue-watching"; + type?: "movies" | "series" | "all"; + order?: + | "date-added" + | "name" + | "first-release-date" + | "last-release-date" + | "last-played"; + direction?: "asc" | "desc"; + page?: number; + itemsPerPage?: number; + }, + params: RequestParams = {}, + ) => + this.request< + PaginatedResponseDto & { + items: LibraryItemDto[]; + }, + any + >({ + path: `/api/users/${userId}/library/my-list`, + method: "GET", + query: query, + format: "json", + ...params, + }), - /** - * No description - * - * @tags library - * @name AddLibraryItem - * @request PUT:/api/users/{userId}/library/tmdb/{tmdbId} - */ - addLibraryItem: ( - userId: string, - tmdbId: string, - query: { - mediaType: 'movie' | 'series'; - }, - params: RequestParams = {} - ) => - this.request({ - path: `/api/users/${userId}/library/tmdb/${tmdbId}`, - method: 'PUT', - query: query, - format: 'json', - ...params - }), + /** + * No description + * + * @tags library + * @name GetCatalogue + * @request GET:/api/users/{userId}/library/catalogue/{sourceId} + */ + getCatalogue: ( + userId: string, + sourceId: string, + query?: { + type?: "all" | "movies" | "series" | "missing"; + order?: string; + direction?: string; + page?: number; + itemsPerPage?: number; + }, + params: RequestParams = {}, + ) => + this.request< + PaginatedResponseDto & { + items: LibraryItemDto[]; + }, + any + >({ + path: `/api/users/${userId}/library/catalogue/${sourceId}`, + method: "GET", + query: query, + format: "json", + ...params, + }), - /** - * No description - * - * @tags library - * @name RemoveLibraryItem - * @request DELETE:/api/users/{userId}/library/tmdb/{tmdbId} - */ - removeLibraryItem: (userId: string, tmdbId: string, params: RequestParams = {}) => - this.request({ - path: `/api/users/${userId}/library/tmdb/${tmdbId}`, - method: 'DELETE', - format: 'json', - ...params - }) - }; + /** + * No description + * + * @tags library + * @name AddLibraryItem + * @request PUT:/api/users/{userId}/library/tmdb/{tmdbId} + */ + addLibraryItem: ( + userId: string, + tmdbId: string, + query: { + mediaType: "movie" | "series"; + }, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/library/tmdb/${tmdbId}`, + method: "PUT", + query: query, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags library + * @name RemoveLibraryItem + * @request DELETE:/api/users/{userId}/library/tmdb/{tmdbId} + */ + removeLibraryItem: ( + userId: string, + tmdbId: string, + params: RequestParams = {}, + ) => + this.request({ + path: `/api/users/${userId}/library/tmdb/${tmdbId}`, + method: "DELETE", + format: "json", + ...params, + }), + }; } diff --git a/frontend/src/lib/components/Button.svelte b/frontend/src/lib/components/Button/Button.svelte similarity index 95% rename from frontend/src/lib/components/Button.svelte rename to frontend/src/lib/components/Button/Button.svelte index b325f65..88e2145 100644 --- a/frontend/src/lib/components/Button.svelte +++ b/frontend/src/lib/components/Button/Button.svelte @@ -1,12 +1,12 @@ + +
+ {#if loading} + + {:else} + + {/if} +
diff --git a/frontend/src/lib/components/Dialog/ConfirmDialog.svelte b/frontend/src/lib/components/Dialog/ConfirmDialog.svelte index 9a7523d..41d5b31 100644 --- a/frontend/src/lib/components/Dialog/ConfirmDialog.svelte +++ b/frontend/src/lib/components/Dialog/ConfirmDialog.svelte @@ -1,6 +1,6 @@ + + + +
{ + /* For a11y */ + }} + > + +
+ + {#if showCloseButton} +
+ + + +
+ {/if} + + +
+ +
+
+
+
\ No newline at end of file diff --git a/frontend/src/lib/components/Sheet/index.ts b/frontend/src/lib/components/Sheet/index.ts new file mode 100644 index 0000000..40ae4d9 --- /dev/null +++ b/frontend/src/lib/components/Sheet/index.ts @@ -0,0 +1 @@ +export { default as Sheet } from './Sheet.svelte'; diff --git a/frontend/src/lib/components/VideoPlayer/SelectAudioModal.svelte b/frontend/src/lib/components/VideoPlayer/SelectAudioModal.svelte index 1fdb43a..bec0f90 100644 --- a/frontend/src/lib/components/VideoPlayer/SelectAudioModal.svelte +++ b/frontend/src/lib/components/VideoPlayer/SelectAudioModal.svelte @@ -1,6 +1,6 @@ + + + + +
+ {#each items as { streams, source }} + {#each streams as row, index} + { + selectedRow = row; + selectedActionIndex = 0; + + const el = + detail.selectable.getSibling(-1)?.getHtmlElement() ?? + detail.selectable?.getHtmlElement(); + + if (el) scrollElementIntoView(el, { top: 32 }); + }} + on:select={() => handleClickItem({ item: row, source })} + on:click={() => { + selectedRow = row; + selectedActionIndex = 0; + }} + on:navigate={({ detail }) => { + if (detail.direction === 'left') { + selectedActionIndex = Math.max(0, selectedActionIndex - 1); + } else if (detail.direction === 'right') { + selectedActionIndex = Math.min(row.actions.length - 1, selectedActionIndex + 1); + } + }} + focusOnClick + let:hasFocus + > +
+ + + {capitalize(row.label)} + + + + + {row.properties?.map((p) => `${p.label}: ${p.formatted || p.value}`).join(', ')} + +
+
+ + + {:else} +
No streams available
+ {/each} + + {/each} +
+
diff --git a/frontend/src/lib/pages/TitlePages/ActionsPage/StreamListMenu.svelte b/frontend/src/lib/pages/TitlePages/ActionsPage/StreamListMenu.svelte new file mode 100644 index 0000000..9ce3e9b --- /dev/null +++ b/frontend/src/lib/pages/TitlePages/ActionsPage/StreamListMenu.svelte @@ -0,0 +1,155 @@ + + + + +
+
+
+ + + { + componentStack.pop(); + detail.stopPropagation(); + }} + > + +
+

+ {series.name} + {`S${episode.season_number}`} E{episode.episode_number} +

+

+ {episode.name} +

+
+ {#each items as provider} + (selectedProvider = provider)} + class="mx-4 cursor-pointer flex-shrink-0 h3" + let:hasFocus + > + + {provider.provider.name} ({provider.streams.length}) + + + {/each} +
+ + + {#each selectedProvider?.streams ?? [] as row, index} + + playStream({ + source: toNonNullable(selectedProvider).provider, + streamId: row.streamId + })} + > +
+ + + {capitalize(row.title)} + + + + + {row.properties?.map((p) => `${p.label}: ${p.formatted || p.value}`).join(', ')} + +
+
+ + + {:else} +
No streams available
+ {/each} +
+
+
diff --git a/frontend/src/lib/pages/TitlePages/ActionsPage/TheListContainer.svelte b/frontend/src/lib/pages/TitlePages/ActionsPage/TheListContainer.svelte new file mode 100644 index 0000000..693f303 --- /dev/null +++ b/frontend/src/lib/pages/TitlePages/ActionsPage/TheListContainer.svelte @@ -0,0 +1,103 @@ + + + +
+ {#each items as { streams, source }} +
+

{source.name}

+
+ + {#each streams as row, index} + { + selectedRow = row; + selectedActionIndex = 0; + + const el = + detail.selectable.getSibling(-1)?.getHtmlElement() ?? + detail.selectable?.getHtmlElement(); + + if (el) scrollElementIntoView(el, { top: 32 }); + }} + on:select={() => handleClickItem({ item: row, source })} + on:click={() => { + selectedRow = row; + selectedActionIndex = 0; + }} + on:navigate={({ detail }) => { + if (detail.direction === 'left') { + selectedActionIndex = Math.max(0, selectedActionIndex - 1); + } else if (detail.direction === 'right') { + selectedActionIndex = Math.min(row.actions.length - 1, selectedActionIndex + 1); + } + }} + focusOnClick + let:hasFocus + > +
+ + + {capitalize(row.label)} + + + + + {row.properties?.map((p) => `${p.label}: ${p.formatted || p.value}`).join(', ')} + +
+
+ + + {:else} +
No streams available
+ {/each} + + {/each} +
+
diff --git a/frontend/src/lib/pages/TitlePages/ActionsPage/actions-page.ts b/frontend/src/lib/pages/TitlePages/ActionsPage/actions-page.ts index 3938275..c3608df 100644 --- a/frontend/src/lib/pages/TitlePages/ActionsPage/actions-page.ts +++ b/frontend/src/lib/pages/TitlePages/ActionsPage/actions-page.ts @@ -3,7 +3,7 @@ import { createErrorNotification, createInfoNotification } from '$lib/components/Notifications/notification.store'; -import { useComponentStack } from '$lib/stores/component-stack.store'; +import { componentStackContext, useComponentStack } from '$lib/stores/component-stack.store'; import { TITLE_USER_DATA_CONTEXT, type TitleUserData @@ -49,8 +49,9 @@ function usePlayableDataStore(options: { tmdbId: string; season?: number; episod }; } +/** @deprecated */ function useTitlePage() { - const componentStack = useComponentStack(); + const componentStack = componentStackContext.createContext(); const openEpisodeMenu = (tmdbId: string, season: number, episode: number) => componentStack.create(ActionsMenu, { @@ -98,6 +99,7 @@ export const mediaSourceContext = createStoreContext('media-source', () => writable(undefined) ); +/** @deprecated */ export const titlePageContext = createStoreContext('title-page', useTitlePage, { required: true }); diff --git a/frontend/src/lib/pages/TitlePages/EpisodePage.svelte b/frontend/src/lib/pages/TitlePages/EpisodePage.svelte index 3e18b6a..362ba99 100644 --- a/frontend/src/lib/pages/TitlePages/EpisodePage.svelte +++ b/frontend/src/lib/pages/TitlePages/EpisodePage.svelte @@ -6,7 +6,7 @@ import { useEpisodeUserData } from '$lib/stores/user-data/title-user-data.store'; import { Check, ExternalLink, Play } from 'radix-icons-svelte'; import { onDestroy } from 'svelte'; - import Button from '../../components/Button.svelte'; + import Button from '../../components/Button/Button.svelte'; import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../../constants'; import { formatThousands } from '../../utils'; import TitleProperties from './HeroTitleInfo.svelte'; diff --git a/frontend/src/lib/pages/TitlePages/MoviePage/MoviePageDetails.svelte b/frontend/src/lib/pages/TitlePages/MoviePage/MoviePageDetails.svelte index 96a5667..0d7a321 100644 --- a/frontend/src/lib/pages/TitlePages/MoviePage/MoviePageDetails.svelte +++ b/frontend/src/lib/pages/TitlePages/MoviePage/MoviePageDetails.svelte @@ -1,6 +1,6 @@ + + +
+
+ +
+ +
+

{episode.name ?? series.name ?? ''}

+

{`Season ${episode.season_number} Episode ${episode.episode_number}`}

+
+ +
+ + + + + +
+
+
diff --git a/frontend/src/lib/pages/UIComponents.svelte b/frontend/src/lib/pages/UIComponents.svelte index e4a135f..52603d1 100644 --- a/frontend/src/lib/pages/UIComponents.svelte +++ b/frontend/src/lib/pages/UIComponents.svelte @@ -1,6 +1,6 @@
diff --git a/frontend/src/lib/pages/UsersPage.svelte b/frontend/src/lib/pages/UsersPage.svelte index 587861c..6e9d362 100644 --- a/frontend/src/lib/pages/UsersPage.svelte +++ b/frontend/src/lib/pages/UsersPage.svelte @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { Plus, Trash } from 'radix-icons-svelte'; import { getReiverrApi } from '../apis/reiverr/reiverr-api'; - import Button from '../components/Button.svelte'; + import Button from '../components/Button/Button.svelte'; import AddUserDialog from '../components/Dialog/AddUserDialog.svelte'; import Login from '../components/LoginForm.svelte'; import { createModal } from '../components/Modal/modal.store'; diff --git a/frontend/src/lib/stores/component-stack.store.ts b/frontend/src/lib/stores/component-stack.store.ts index 1519fc6..16ecc4d 100644 --- a/frontend/src/lib/stores/component-stack.store.ts +++ b/frontend/src/lib/stores/component-stack.store.ts @@ -1,3 +1,4 @@ +import { createStoreContext } from '$lib/utils'; import { type ComponentProps, type ComponentType, type SvelteComponentTyped } from 'svelte'; import { derived, get, writable } from 'svelte/store'; @@ -79,3 +80,7 @@ export function useComponentStack

>(initial?: { reset }; } + +export const componentStackContext = createStoreContext('component-stack', useComponentStack, { + required: true +}); diff --git a/frontend/src/lib/stores/user-data/is-watched.store.ts b/frontend/src/lib/stores/user-data/is-watched.store.ts index 1ef65b4..10ea9ff 100644 --- a/frontend/src/lib/stores/user-data/is-watched.store.ts +++ b/frontend/src/lib/stores/user-data/is-watched.store.ts @@ -1,17 +1,38 @@ -import type { MovieUserDataDto } from '$lib/apis/reiverr/reiverr.openapi'; +import type { MovieUserDataDto, SeriesUserDataDto } from '$lib/apis/reiverr/reiverr.openapi'; import { get_store_value as get } from 'svelte/internal'; import { type Readable, writable } from 'svelte/store'; import { libraryRefresher } from '../data.store'; import { user } from '../user.store'; +import type { EpisodeUserData } from './title-user-data.store'; -export function useIsWatched( - userData: Readable, - toggleFn: (userId: string, watched: boolean) => Promise -) { +export type WatchStore = { + isWatched: Readable; + toggleIsWatched: () => Promise; +}; + +export function useIsWatched(opts: { + userData: Readable; + season?: number; + episode?: number; + toggleFn: (userId: string, watched: boolean) => Promise; +}): WatchStore { const isWatched = writable(undefined); - userData.subscribe((d) => { - isWatched.set(d?.playState?.watched ?? false); + opts.userData.subscribe((d) => { + if (d && 'playState' in d) { + isWatched.set(d.playState?.watched ?? false); + } else if (d && 'playStates' in d) { + isWatched.set( + opts.season !== undefined && opts.episode !== undefined + ? d.playStates.find((p) => p.episode === opts.episode && p.season === opts.season) + ?.watched ?? false + : d.playStates.every((e) => e.watched) // || e.upcoming + ); + } else if (d && 'upcoming' in d) { + isWatched.set(d.watched || d.upcoming); + } else { + isWatched.set(false); + } }); async function toggleIsWatched() { @@ -22,10 +43,15 @@ export function useIsWatched( return; } - return toggleFn(userId, !watched).finally(() => { - isWatched.set(!watched); - libraryRefresher.refreshIn(500); - }); + return opts + .toggleFn(userId, !watched) + .then(() => { + isWatched.set(!watched); + }) + .catch(() => {}) + .finally(() => { + libraryRefresher.refreshIn(500); + }); } return { diff --git a/frontend/src/lib/stores/user-data/title-user-data.store.ts b/frontend/src/lib/stores/user-data/title-user-data.store.ts index 2df20e6..398fdb5 100644 --- a/frontend/src/lib/stores/user-data/title-user-data.store.ts +++ b/frontend/src/lib/stores/user-data/title-user-data.store.ts @@ -1,3 +1,4 @@ +import type { TmdbSeriesFull } from '$lib/apis/tmdb/tmdb-api'; import { getBackgroundPage } from '$lib/components/GlobalBackground/BackgroundStack'; import { createModal } from '$lib/components/Modal/modal.store'; import { createErrorNotification } from '$lib/components/Notifications/notification.store'; @@ -7,6 +8,7 @@ import { createStoreContext } from '$lib/utils'; import { derived, get, writable } from 'svelte/store'; import type { MediaSourceDto, + SeriesUserDataDto, StreamBaseDto, StreamCandidateDto } from '../../apis/reiverr/reiverr.openapi'; @@ -21,7 +23,7 @@ import { reiverrApi, tmdbApi, user } from '../user.store'; import { useIsWatched } from './is-watched.store'; import { useUserLibrary } from './library.store'; -export type EpisodeData = { +export type EpisodeUserData = { season: number; episode: number; watched: boolean; @@ -180,6 +182,51 @@ function useCanStream() { }; } +function createEpisodesUserData(opts: { + seriesUserData?: SeriesUserDataDto; + tmdbSeries: TmdbSeriesFull; +}) { + const { seriesUserData, tmdbSeries } = opts; + + let nextEpisodeData: EpisodeUserData | undefined; + const episodesData: EpisodeUserData[] = []; + let foundNext = false; + const lastWatchedPlayState = seriesUserData?.playStates?.filter((p) => p.watched).pop(); + for (let season = 1; season <= (tmdbSeries.number_of_seasons ?? 0); season++) { + const s = tmdbSeries.seasons?.find((s) => s.season_number === season); + for (let episode = 1; episode <= (s?.episode_count ?? 0); episode++) { + const ep = seriesUserData?.playStates?.find( + (p) => p.season === season && p.episode === episode + ); + const upcoming = !s?.air_date || new Date(s.air_date) > new Date(); + + const episodeData = { + season, + episode, + watched: ep?.watched ?? false, + progress: ep?.progress ?? 0, + upcoming + }; + + if ( + !foundNext && + ((lastWatchedPlayState?.season ?? 0) < season || + ((lastWatchedPlayState?.season ?? 0) === season && + (lastWatchedPlayState?.episode ?? 0) < episode)) + ) { + nextEpisodeData = episodeData; + foundNext = true; + } + episodesData.push(episodeData); + } + } + + return { + nextEpisodeData, + episodesData + }; +} + export type TitleUserData = ReturnType & ReturnType; @@ -193,8 +240,8 @@ export function useSeriesUserData(tmdbId: string) { ); const tmdbSeriesRequest = useRequest(() => tmdbApi.getSeriesFull(Number(tmdbId))); const libraryStore = useUserLibrary('series', tmdbId, userDataRequest); - const episodesUserData = writable([]); - const nextEpisode = writable({ + const episodesUserData = writable([]); + const nextEpisode = writable({ season: 1, episode: 1, progress: 0, @@ -206,100 +253,22 @@ export function useSeriesUserData(tmdbId: string) { ); const background = getBackgroundPage(); - // const episodeData = derviedRequest( - // [userDataRequest, tmdbSeriesRequest], - // async ([userData, tmdbSeries]) => { - // if (!tmdbSeries) return; + const unsub = derived([userDataRequest, tmdbSeriesRequest], (_) => _).subscribe( + ([userData, tmdbSeries]) => { + if (!tmdbSeries) return; - // let nextEpisode: EpisodeData | undefined; - // const episodesData: EpisodeData[] = []; + const { episodesData, nextEpisodeData } = createEpisodesUserData({ + seriesUserData: userData, + tmdbSeries + }); - // let foundNext = false; - // const lastWatchedPlayState = userData?.playStates?.filter((p) => p.watched).pop(); - // for (let season = 1; season <= (tmdbSeries.number_of_seasons ?? 0); season++) { - // const s = tmdbSeries.seasons?.find((s) => s.season_number === season); - // for (let episode = 1; episode <= (s?.episode_count ?? 0); episode++) { - // const ep = userData?.playStates?.find( - // (p) => p.season === season && p.episode === episode - // ); - // const upcoming = !s?.air_date || new Date(s.air_date) > new Date(); - // if ( - // !foundNext && - // ((lastWatchedPlayState?.season ?? 0) < season || - // ((lastWatchedPlayState?.season ?? 0) === season && - // (lastWatchedPlayState?.episode ?? 0) < episode)) - // ) { - // nextEpisode = { - // season, - // episode, - // progress: ep?.progress ?? 0, - // watched: ep?.watched ?? false, - // upcoming - // }; - // foundNext = true; - // } - // episodesData.push({ - // season, - // episode, - // watched: ep?.watched ?? false, - // progress: ep?.progress ?? 0, - // upcoming - // }); - // } - // } - - // return { - // nextEpisode, - // episodesData - // }; - // } - // ); - - derived([userDataRequest, tmdbSeriesRequest], (_) => _).subscribe(([userData, tmdbSeries]) => { - if (!tmdbSeries) return; - - const episodesData: EpisodeData[] = []; - let foundNext = false; - const lastWatchedPlayState = userData?.playStates?.filter((p) => p.watched).pop(); - for (let season = 1; season <= (tmdbSeries.number_of_seasons ?? 0); season++) { - const s = tmdbSeries.seasons?.find((s) => s.season_number === season); - for (let episode = 1; episode <= (s?.episode_count ?? 0); episode++) { - const ep = userData?.playStates?.find((p) => p.season === season && p.episode === episode); - const upcoming = !s?.air_date || new Date(s.air_date) > new Date(); - if ( - !foundNext && - ((lastWatchedPlayState?.season ?? 0) < season || - ((lastWatchedPlayState?.season ?? 0) === season && - (lastWatchedPlayState?.episode ?? 0) < episode)) - ) { - nextEpisode.set({ - season, - episode, - progress: ep?.progress ?? 0, - watched: ep?.watched ?? false, - upcoming - }); - foundNext = true; - } - episodesData.push({ - season, - episode, - watched: ep?.watched ?? false, - progress: ep?.progress ?? 0, - upcoming - }); + if (nextEpisodeData) { + nextEpisode.set(nextEpisodeData); } + + episodesUserData.set(episodesData); } - - episodesUserData.set(episodesData); - }); - - // const mediaPlayback = usePlayback({ - // tmdbId, - // season: get(nextEpisode)?.season, - // episode: get(nextEpisode)?.episode, - // getVideoProps - // }); + ); const autoplay = useAutoplay({ tmdbId, @@ -379,7 +348,7 @@ export function useSeriesUserData(tmdbId: string) { const tmdbSeriesData = get(tmdbSeriesRequest); - let episodeData: EpisodeData | undefined; + let episodeData: EpisodeUserData | undefined; if (options?.season && options?.episode) { episodeData = get(episodesUserData).find( (e) => e.season === options.season && e.episode === options.episode @@ -498,6 +467,7 @@ export function useSeriesUserData(tmdbId: string) { unsubscribe: () => { userDataRequest.unsubscribe(); tmdbSeriesRequest.unsubscribe(); + unsub(); } }; } @@ -516,11 +486,13 @@ export function useMovieUserData(tmdbId: string) { const tmdbMovie = useRequest(() => tmdbApi.getMovieFull(Number(tmdbId))); const libraryStore = useUserLibrary('movie', tmdbId, userData); - const isWatchedStore = useIsWatched(userData, (userId, watched) => - reiverrApi.users.updateMoviePlayStateByTmdbId(userId, tmdbId, { - watched - }) - ); + const isWatchedStore = useIsWatched({ + userData, + toggleFn: (userId, watched) => + reiverrApi.users.updateMoviePlayStateByTmdbId(userId, tmdbId, { + watched + }) + }); const progress = derived(userData, ($userData) => $userData?.playState?.progress ?? 0); const getVideoProps = async () => { @@ -638,13 +610,15 @@ export function useEpisodeUserData(tmdbId: string, season: number, episode: numb ); const canStreamStore = useCanStream(); - const isWatchedStore = useIsWatched(userData, (userId, watched) => - reiverrApi.users - .updateEpisodePlayStateByTmdbId(userId, tmdbId, season, episode, { - watched - }) - .finally(() => seriesUserDataRefresher.refresh(tmdbId)) - ); + const isWatchedStore = useIsWatched({ + userData, + toggleFn: (userId, watched) => + reiverrApi.users + .updateEpisodePlayStateByTmdbId(userId, tmdbId, season, episode, { + watched + }) + .finally(() => seriesUserDataRefresher.refresh(tmdbId)) + }); const progress = derived(userData, ($userData) => $userData?.playState?.progress ?? 0); const getVideoProps = async () => { diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index ba2c14b..34556f4 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -278,3 +278,10 @@ export function createStoreContext< useStore: storeCreator }; } + +export function toNonNullable(value: T | null | undefined): NonNullable { + if (value == null) { + throw new Error('Value is null or undefined'); + } + return value; +} diff --git a/package.json b/package.json index ee80037..3d9a56e 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "format": "npm run format --workspace=frontend && npm run format --workspace=backend", "clean": "npm run clean --workspaces --if-present", "install-all": "npm install", - "preview": "npm run preview --workspace=frontend" + "preview": "npm run preview --workspace=frontend", + "openapi:generate:reiverr": "swagger-typescript-api generate -p \"backend/swagger-spec.json\" -o frontend/src/lib/apis/reiverr -n reiverr.openapi.ts --axios --module-name-first-tag" }, "devDependencies": { "typescript": "^5.2.2"