mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-19 08:53:23 +02:00
feat: Improved movie playback and listing stream options
This commit is contained in:
@@ -120,9 +120,20 @@ export interface ValidationResponsekDto {
|
||||
replace: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface SourceListDto {
|
||||
/** @example {"source1":{"uri":"/path/to/stream"},"source2":{"uri":"/path/to/other/stream"}} */
|
||||
sources: Record<string, any>;
|
||||
export interface VideoStreamPropertyDto {
|
||||
label: string;
|
||||
value: string | number;
|
||||
formatted?: string;
|
||||
}
|
||||
|
||||
export interface VideoStreamCandidateDto {
|
||||
key: string;
|
||||
title: string;
|
||||
properties: VideoStreamPropertyDto[];
|
||||
}
|
||||
|
||||
export interface VideoStreamListDto {
|
||||
streams: VideoStreamCandidateDto[];
|
||||
}
|
||||
|
||||
export interface DirectPlayProfileDto {
|
||||
@@ -347,6 +358,7 @@ export interface QualityDto {
|
||||
bitrate: number;
|
||||
label: string;
|
||||
codec?: string;
|
||||
original: boolean;
|
||||
}
|
||||
|
||||
export interface SubtitlesDto {
|
||||
@@ -357,13 +369,16 @@ export interface SubtitlesDto {
|
||||
}
|
||||
|
||||
export interface VideoStreamDto {
|
||||
key: string;
|
||||
title: string;
|
||||
properties: VideoStreamPropertyDto[];
|
||||
uri: string;
|
||||
directPlay: boolean;
|
||||
progress: number;
|
||||
audioStreams: AudioStreamDto[];
|
||||
audioStreamIndex: number;
|
||||
qualities: QualityDto[];
|
||||
quality: number;
|
||||
qualityIndex: number;
|
||||
subtitles: SubtitlesDto[];
|
||||
}
|
||||
|
||||
@@ -786,12 +801,12 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieSources
|
||||
* @request GET:/api/movies/{tmdbId}/sources
|
||||
* @name GetMovieStreams
|
||||
* @request GET:/api/movies/{tmdbId}/sources/{sourceId}/streams
|
||||
*/
|
||||
getMovieSources: (tmdbId: string, params: RequestParams = {}) =>
|
||||
this.request<SourceListDto, any>({
|
||||
path: `/api/movies/${tmdbId}/sources`,
|
||||
getMovieStreams: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<VideoStreamListDto, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/streams`,
|
||||
method: 'GET',
|
||||
format: 'json',
|
||||
...params
|
||||
@@ -805,14 +820,18 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
* @request POST:/api/movies/{tmdbId}/sources/{sourceId}/stream
|
||||
*/
|
||||
getMovieStream: (
|
||||
sourceId: string,
|
||||
tmdbId: string,
|
||||
sourceId: string,
|
||||
query: {
|
||||
key: string;
|
||||
},
|
||||
data: PlaybackConfigDto,
|
||||
params: RequestParams = {}
|
||||
) =>
|
||||
this.request<VideoStreamDto, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream`,
|
||||
method: 'POST',
|
||||
query: query,
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: 'json',
|
||||
@@ -824,11 +843,11 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyGet
|
||||
* @request GET:/api/movies/{tmdbId}/sources/{sourceId}/stream/*
|
||||
* @request GET:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyGet: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/*`,
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
method: 'GET',
|
||||
...params
|
||||
}),
|
||||
@@ -838,11 +857,11 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyPost
|
||||
* @request POST:/api/movies/{tmdbId}/sources/{sourceId}/stream/*
|
||||
* @request POST:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyPost: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/*`,
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
method: 'POST',
|
||||
...params
|
||||
}),
|
||||
@@ -852,11 +871,11 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyPut
|
||||
* @request PUT:/api/movies/{tmdbId}/sources/{sourceId}/stream/*
|
||||
* @request PUT:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyPut: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/*`,
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
method: 'PUT',
|
||||
...params
|
||||
}),
|
||||
@@ -866,11 +885,11 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyDelete
|
||||
* @request DELETE:/api/movies/{tmdbId}/sources/{sourceId}/stream/*
|
||||
* @request DELETE:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyDelete: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/*`,
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
method: 'DELETE',
|
||||
...params
|
||||
}),
|
||||
@@ -880,11 +899,11 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyPatch
|
||||
* @request PATCH:/api/movies/{tmdbId}/sources/{sourceId}/stream/*
|
||||
* @request PATCH:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyPatch: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/*`,
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
method: 'PATCH',
|
||||
...params
|
||||
}),
|
||||
@@ -894,11 +913,11 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyOptions
|
||||
* @request OPTIONS:/api/movies/{tmdbId}/sources/{sourceId}/stream/*
|
||||
* @request OPTIONS:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyOptions: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/*`,
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
method: 'OPTIONS',
|
||||
...params
|
||||
}),
|
||||
@@ -908,11 +927,11 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyHead
|
||||
* @request HEAD:/api/movies/{tmdbId}/sources/{sourceId}/stream/*
|
||||
* @request HEAD:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyHead: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/*`,
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
method: 'HEAD',
|
||||
...params
|
||||
}),
|
||||
@@ -922,11 +941,11 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxySearch
|
||||
* @request SEARCH:/api/movies/{tmdbId}/sources/{sourceId}/stream/*
|
||||
* @request SEARCH:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
*/
|
||||
getMovieStreamProxySearch: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/*`,
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
method: 'SEARCH',
|
||||
...params
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import SeriesPage from '../SeriesPage/SeriesPage.svelte';
|
||||
import EpisodePage from '../../pages/EpisodePage.svelte';
|
||||
import { modalStack } from '../Modal/modal.store';
|
||||
import MoviesHomePage from '../../pages/MoviesHomePage.svelte';
|
||||
import MoviePage from '../../pages/MoviePage.svelte';
|
||||
import MoviePage from '../../pages/MoviePage/MoviePage.svelte';
|
||||
import LibraryPage from '../../pages/LibraryPage.svelte';
|
||||
import SearchPage from '../../pages/SearchPage.svelte';
|
||||
import PageNotFound from '../../pages/PageNotFound.svelte';
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
export let tmdbId: string;
|
||||
export let sourceId: string;
|
||||
export let key: string = '';
|
||||
|
||||
export let modalId: symbol;
|
||||
export let hidden: boolean = false;
|
||||
@@ -32,12 +33,19 @@
|
||||
const refreshVideoStream = async (audioStreamIndex = 0) => {
|
||||
console.log('called2');
|
||||
videoStreamP = reiverrApiNew.movies
|
||||
.getMovieStream(sourceId, tmdbId, {
|
||||
// bitrate: getQualities(1080)?.[0]?.maxBitrate || 10000000,
|
||||
progress: 0,
|
||||
audioStreamIndex,
|
||||
deviceProfile: getDeviceProfile() as any
|
||||
})
|
||||
.getMovieStream(
|
||||
tmdbId,
|
||||
sourceId,
|
||||
{
|
||||
key
|
||||
},
|
||||
{
|
||||
// bitrate: getQualities(1080)?.[0]?.maxBitrate || 10000000,
|
||||
progress: 0,
|
||||
audioStreamIndex,
|
||||
deviceProfile: getDeviceProfile() as any
|
||||
}
|
||||
)
|
||||
.then((r) => r.data)
|
||||
.then((d) => ({
|
||||
...d,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { modalStack } from '../Modal/modal.store';
|
||||
import { jellyfinItemsStore } from '../../stores/data.store';
|
||||
import JellyfinVideoPlayerModal from './JellyfinVideoPlayerModal.svelte';
|
||||
@@ -49,10 +49,17 @@ export type PlayerStateValue = typeof initialValue;
|
||||
function usePlayerState() {
|
||||
const store = writable<PlayerStateValue>(initialValue);
|
||||
|
||||
async function streamMovie(tmdbId: string, sourceId: string = '') {
|
||||
async function streamMovie(tmdbId: string, sourceId: string = '', key: string = '') {
|
||||
if (!sourceId) {
|
||||
const sources = await reiverrApiNew.movies.getMovieSources(tmdbId).then((r) => r.data);
|
||||
sourceId = Object.keys(sources.sources)[0] || '';
|
||||
const streams = await Promise.all(
|
||||
get(sources).map((s) =>
|
||||
reiverrApiNew.movies
|
||||
.getMovieStreams(tmdbId, s.id)
|
||||
.then((r) => ({ source: s, streams: r.data.streams }))
|
||||
)
|
||||
);
|
||||
sourceId = streams?.[0]?.source.id || '';
|
||||
key = streams?.[0]?.streams?.[0]?.key || '';
|
||||
}
|
||||
|
||||
if (!sourceId) {
|
||||
@@ -63,7 +70,8 @@ function usePlayerState() {
|
||||
store.set({ visible: true, jellyfinId: tmdbId, sourceId });
|
||||
modalStack.create(MovieVideoPlayerModal, {
|
||||
tmdbId,
|
||||
sourceId
|
||||
sourceId,
|
||||
key
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../Container.svelte';
|
||||
import HeroCarousel from '../components/HeroCarousel/HeroCarousel.svelte';
|
||||
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
||||
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../constants';
|
||||
import Container from '../../../Container.svelte';
|
||||
import HeroCarousel from '../../components/HeroCarousel/HeroCarousel.svelte';
|
||||
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
|
||||
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../../constants';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Cross1,
|
||||
@@ -14,24 +14,34 @@
|
||||
Plus,
|
||||
Trash
|
||||
} from 'radix-icons-svelte';
|
||||
import Button from '../components/Button.svelte';
|
||||
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
|
||||
import { type MovieDownload, type MovieFileResource, radarrApi } from '../apis/radarr/radarr-api';
|
||||
import { useActionRequests, useRequest } from '../stores/data.store';
|
||||
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
||||
import { createModal, modalStack } from '../components/Modal/modal.store';
|
||||
import { playerState } from '../components/VideoPlayer/VideoPlayer';
|
||||
import { scrollIntoView } from '../selectable';
|
||||
import Carousel from '../components/Carousel/Carousel.svelte';
|
||||
import TmdbPersonCard from '../components/PersonCard/TmdbPersonCard.svelte';
|
||||
import TmdbCard from '../components/Card/TmdbCard.svelte';
|
||||
import MovieMediaManagerModal from '../components/MediaManagerModal/RadarrMediaManagerModal.svelte';
|
||||
import MMAddToRadarrDialog from '../components/MediaManagerModal/MMAddToRadarrDialog.svelte';
|
||||
import FileDetailsDialog from '../components/SeriesPage/FileDetailsDialog.svelte';
|
||||
import DownloadDetailsDialog from '../components/SeriesPage/DownloadDetailsDialog.svelte';
|
||||
import { capitalize, formatSize } from '../utils';
|
||||
import ConfirmDialog from '../components/Dialog/ConfirmDialog.svelte';
|
||||
import { TMDB_BACKDROP_SMALL } from '../constants.js';
|
||||
import Button from '../../components/Button.svelte';
|
||||
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api';
|
||||
import {
|
||||
type MovieDownload,
|
||||
type MovieFileResource,
|
||||
radarrApi
|
||||
} from '../../apis/radarr/radarr-api';
|
||||
import { useActionRequests, useRequest } from '../../stores/data.store';
|
||||
import DetachedPage from '../../components/DetachedPage/DetachedPage.svelte';
|
||||
import { createModal, modalStack } from '../../components/Modal/modal.store';
|
||||
import { playerState } from '../../components/VideoPlayer/VideoPlayer';
|
||||
import { scrollIntoView } from '../../selectable';
|
||||
import Carousel from '../../components/Carousel/Carousel.svelte';
|
||||
import TmdbPersonCard from '../../components/PersonCard/TmdbPersonCard.svelte';
|
||||
import TmdbCard from '../../components/Card/TmdbCard.svelte';
|
||||
import MovieMediaManagerModal from '../../components/MediaManagerModal/RadarrMediaManagerModal.svelte';
|
||||
import MMAddToRadarrDialog from '../../components/MediaManagerModal/MMAddToRadarrDialog.svelte';
|
||||
import FileDetailsDialog from '../../components/SeriesPage/FileDetailsDialog.svelte';
|
||||
import DownloadDetailsDialog from '../../components/SeriesPage/DownloadDetailsDialog.svelte';
|
||||
import { capitalize, formatSize } from '../../utils';
|
||||
import ConfirmDialog from '../../components/Dialog/ConfirmDialog.svelte';
|
||||
import { TMDB_BACKDROP_SMALL } from '../../constants.js';
|
||||
import { reiverrApiNew } from '../../stores/user.store';
|
||||
import { sources } from '../../stores/sources.store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { VideoStreamCandidateDto, MediaSource } from '../../apis/reiverr/reiverr.openapi';
|
||||
import MovieStreams from './MovieStreams.MoviePage.svelte';
|
||||
import StreamDetailsDialog from './StreamDetailsDialog.MoviePage.svelte';
|
||||
|
||||
export let id: string;
|
||||
const tmdbId = Number(id);
|
||||
@@ -42,6 +52,23 @@
|
||||
(id: string) => jellyfinApi.getLibraryItemFromTmdbId(id),
|
||||
id
|
||||
);
|
||||
|
||||
const streams = getStreams();
|
||||
|
||||
function getStreams(): Map<MediaSource, Promise<VideoStreamCandidateDto[]>> {
|
||||
const out = new Map();
|
||||
|
||||
for (const source of get(sources)) {
|
||||
out.set(
|
||||
source,
|
||||
reiverrApiNew.movies.getMovieStreams(id, source.id).then((r) => r.data?.streams ?? [])
|
||||
);
|
||||
}
|
||||
console.log(out);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
const { promise: radarrItemP, send: refreshRadarrItem } = useRequest(
|
||||
radarrApi.getMovieByTmdbId,
|
||||
tmdbId
|
||||
@@ -110,6 +137,18 @@
|
||||
.then(() => (radarrDownloads = getDownloads(radarrItem)))
|
||||
});
|
||||
}
|
||||
|
||||
async function createStreamDetailsDialog(source: MediaSource, stream: VideoStreamCandidateDto) {
|
||||
const movie = await tmdbMovie;
|
||||
modalStack.create(StreamDetailsDialog, {
|
||||
stream,
|
||||
// title: movie?.title || '',
|
||||
// subtitle: file.relativePath || '',
|
||||
backgroundUrl: TMDB_BACKDROP_SMALL + movie?.backdrop_path || '',
|
||||
streamMovie: () => playerState.streamMovie(id, source.id, stream.key),
|
||||
onDelete: () => (radarrFiles = getFiles(radarrItem))
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<DetachedPage let:handleGoBack let:registrar>
|
||||
@@ -173,8 +212,7 @@
|
||||
{#if jellyfinItem}
|
||||
<Button
|
||||
class="mr-4"
|
||||
on:clickOrSelect={() =>
|
||||
jellyfinItem.Id && playerState.streamMovie(id)}
|
||||
on:clickOrSelect={() => jellyfinItem.Id && playerState.streamMovie(id)}
|
||||
>
|
||||
Play
|
||||
<Play size={19} slot="icon" />
|
||||
@@ -281,7 +319,12 @@
|
||||
</div>
|
||||
</Container>
|
||||
{/await}
|
||||
{#await Promise.all([tmdbMovie, radarrFiles, radarrDownloads]) then [movie, files, downloads]}
|
||||
|
||||
{#if streams.size}
|
||||
<MovieStreams {streams} {createStreamDetailsDialog} />
|
||||
{/if}
|
||||
|
||||
<!-- {#await Promise.all([tmdbMovie, radarrFiles, radarrDownloads]) then [movie, files, downloads]}
|
||||
{#if files?.length || downloads?.length}
|
||||
<Container
|
||||
class="flex-1 bg-secondary-950 pt-8 pb-16 px-32 flex flex-col"
|
||||
@@ -390,6 +433,6 @@
|
||||
</div>
|
||||
</Container>
|
||||
{/if}
|
||||
{/await}
|
||||
{/await} -->
|
||||
</div>
|
||||
</DetachedPage>
|
||||
70
src/lib/pages/MoviePage/MovieStreams.MoviePage.svelte
Normal file
70
src/lib/pages/MoviePage/MovieStreams.MoviePage.svelte
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
import Container from '../../../Container.svelte';
|
||||
import type { VideoStreamCandidateDto, MediaSource } from '../../apis/reiverr/reiverr.openapi';
|
||||
import { scrollIntoView } from '../../selectable';
|
||||
import { capitalize } from '../../utils';
|
||||
import StreamDetailsDialog from './StreamDetailsDialog.MoviePage.svelte';
|
||||
import { modalStack } from '../../components/Modal/modal.store';
|
||||
|
||||
export let streams: Map<MediaSource, Promise<VideoStreamCandidateDto[]>>;
|
||||
export let createStreamDetailsDialog: (source: MediaSource, stream: VideoStreamCandidateDto) => void;
|
||||
</script>
|
||||
|
||||
<Container
|
||||
class="flex-1 bg-secondary-950 pt-8 pb-16 px-32 flex flex-col"
|
||||
on:enter={scrollIntoView({ top: 32 })}
|
||||
>
|
||||
{#each [...streams.keys()] as source}
|
||||
<h1 class="font-medium tracking-wide text-2xl text-zinc-300 mb-8">
|
||||
{capitalize(source.id)}
|
||||
</h1>
|
||||
{#await streams.get(source)}
|
||||
Loading...
|
||||
{:then streams}
|
||||
<Container
|
||||
direction="grid"
|
||||
gridCols={2}
|
||||
class={classNames('grid gap-8', {
|
||||
'grid-cols-1': (streams || []).length < 2,
|
||||
'grid-cols-2': (streams || []).length >= 2
|
||||
})}
|
||||
>
|
||||
{#each streams || [] as stream}
|
||||
<Container
|
||||
class={classNames(
|
||||
'flex space-x-8 items-center text-zinc-300 font-medium relative overflow-hidden',
|
||||
'px-8 py-4 border-2 border-transparent rounded-xl',
|
||||
{
|
||||
'bg-secondary-800 focus-within:bg-primary-700 focus-within:border-primary-500': true,
|
||||
'hover:bg-primary-700 hover:border-primary-500 cursor-pointer': true
|
||||
// 'bg-primary-700 focus-within:border-primary-500': selected,
|
||||
// 'bg-secondary-800 focus-within:border-zinc-300': !selected
|
||||
}
|
||||
)}
|
||||
on:clickOrSelect={() => createStreamDetailsDialog(source, stream)}
|
||||
on:enter={scrollIntoView({ vertical: 128 })}
|
||||
focusOnClick
|
||||
>
|
||||
<div class="flex-1">
|
||||
<h1 class="text-lg">
|
||||
{stream.title}
|
||||
</h1>
|
||||
</div>
|
||||
{#each stream.properties.slice(0, 2) as property}
|
||||
<div>
|
||||
{property.formatted ?? property.value}
|
||||
</div>
|
||||
{/each}
|
||||
<!-- <div>
|
||||
{file?.mediaInfo?.runTime}
|
||||
</div>
|
||||
<div>
|
||||
{formatSize(file?.size || 0)}
|
||||
</div> -->
|
||||
</Container>
|
||||
{/each}
|
||||
</Container>
|
||||
{/await}
|
||||
{/each}
|
||||
</Container>
|
||||
67
src/lib/pages/MoviePage/StreamDetailsDialog.MoviePage.svelte
Normal file
67
src/lib/pages/MoviePage/StreamDetailsDialog.MoviePage.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
|
||||
import Container from '../../../Container.svelte';
|
||||
import { formatSize } from '../../utils';
|
||||
import { Play, Trash } from 'radix-icons-svelte';
|
||||
import type { FileResource } from '../../apis/combined-types';
|
||||
import type { VideoStreamCandidateDto } from '../../apis/reiverr/reiverr.openapi';
|
||||
import Dialog from '../../components/Dialog/Dialog.svelte';
|
||||
import Button from '../../components/Button.svelte';
|
||||
import { playerState } from '../../components/VideoPlayer/VideoPlayer';
|
||||
|
||||
export let stream: VideoStreamCandidateDto;
|
||||
|
||||
// export let file: FileResource;
|
||||
// export let title = '';
|
||||
// export let subtitle = '';
|
||||
export let backgroundUrl: string;
|
||||
export let streamMovie: () => Promise<any>;
|
||||
export let onDelete: () => void;
|
||||
|
||||
async function handleDeleteFile() {
|
||||
// return sonarrApi.deleteSonarrEpisode(file.id || -1).then(() => onDelete());
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog class="flex flex-col relative">
|
||||
{#if backgroundUrl}
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center h-52"
|
||||
style="background-image: url({backgroundUrl}); -webkit-mask-image: radial-gradient(at 90% 10%, hsla(0,0%,0%,1) 0px, transparent 70%);"
|
||||
/>
|
||||
{/if}
|
||||
<div class="z-10">
|
||||
{#if backgroundUrl}
|
||||
<div class="h-24" />
|
||||
{/if}
|
||||
<h1 class="header2">{stream.title}</h1>
|
||||
<h2 class="header1 mb-4">{stream.key}</h2>
|
||||
<div
|
||||
class="grid grid-cols-[1fr_auto] font-medium mb-16
|
||||
[&>*:nth-child(odd)]:text-secondary-300 [&>*:nth-child(even)]:text-right [&>*:nth-child(even)]:text-secondary-100 *:py-2
|
||||
[&>*:nth-child(odd):not(:nth-last-child(-n+2))]:border-b [&>*:nth-child(odd):not(:nth-last-child(-n+2))]:border-secondary-600
|
||||
[&>*:nth-child(even):not(:nth-last-child(-n+2))]:border-b [&>*:nth-child(even):not(:nth-last-child(-n+2))]:border-secondary-600"
|
||||
>
|
||||
{#each stream.properties as property}
|
||||
<span class="pr-8">{property.label}</span>
|
||||
<span class="truncate" title={property.formatted ?? property.value.toString()}>
|
||||
{property.formatted ?? property.value}
|
||||
</span>
|
||||
{/each}
|
||||
<!-- <span class="border-b border-secondary-600">Runtime</span>
|
||||
<span class="border-b border-secondary-600">{file.mediaInfo?.runTime}</span>
|
||||
<span class="border-b border-secondary-600">Size on Disk</span>
|
||||
<span class="border-b border-secondary-600">{formatSize(file.size || 0)}</span>
|
||||
<span>Quality</span>
|
||||
<span>{file.quality?.quality?.name}</span> -->
|
||||
</div>
|
||||
|
||||
<Container class="flex flex-col space-y-4">
|
||||
<Button type="secondary" icon={Play} action={streamMovie}>Play</Button>
|
||||
<Button type="secondary" confirmDanger action={handleDeleteFile} disabled={true}>
|
||||
<Trash size={19} slot="icon" />
|
||||
Delete File
|
||||
</Button>
|
||||
</Container>
|
||||
</div>
|
||||
</Dialog>
|
||||
@@ -6,13 +6,13 @@ import { type Session, sessions } from './session.store';
|
||||
import { user } from './user.store';
|
||||
|
||||
function useSources() {
|
||||
const availableSources = derived(user, (user) =>
|
||||
user?.mediaSources?.filter((s) => s.enabled)?.map((s) => s.id)
|
||||
const availableSources = derived(
|
||||
user,
|
||||
(user) => user?.mediaSources?.filter((s) => s.enabled)?.map((s) => ({ ...s })) ?? []
|
||||
);
|
||||
|
||||
return {
|
||||
subscribe: availableSources.subscribe,
|
||||
|
||||
subscribe: availableSources.subscribe
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user