mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-26 18:55:12 +02:00
Experimental video player
This commit is contained in:
76
src/routes/components/Card/Card.svelte
Normal file
76
src/routes/components/Card/Card.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import type { TmdbMovie, TmdbMovieFull } from '$lib/tmdb-api';
|
||||
import { formatGenres, formatMinutes } from '$lib/utils';
|
||||
import classNames from 'classnames';
|
||||
import { TMDB_IMAGES } from '$lib/constants';
|
||||
import { onMount } from 'svelte';
|
||||
import { fetchTmdbMovie, fetchTmdbMovieImages, TmdbApi } from '$lib/tmdb-api';
|
||||
import CardPlaceholder from './CardPlaceholder.svelte';
|
||||
|
||||
export let tmdbId: string;
|
||||
|
||||
export let available = true;
|
||||
export let progress = 0;
|
||||
export let progressType: 'watched' | 'downloading' = 'watched';
|
||||
export let randomProgress = false;
|
||||
if (randomProgress) {
|
||||
progress = Math.random() > 0.3 ? Math.random() * 100 : 0;
|
||||
}
|
||||
|
||||
let tmdbMovie: TmdbMovie;
|
||||
let backdropUrl;
|
||||
|
||||
onMount(async () => {
|
||||
if (!tmdbId) return;
|
||||
|
||||
fetchTmdbMovieImages(String(tmdbId))
|
||||
.then(
|
||||
(r) =>
|
||||
(backdropUrl = TMDB_IMAGES + r.backdrops.filter((b) => b.iso_639_1 === 'en')[0].file_path)
|
||||
)
|
||||
.catch((err) => (backdropUrl = null));
|
||||
fetchTmdbMovie(tmdbId).then((movie) => (tmdbMovie = movie));
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !tmdbMovie || !backdropUrl}
|
||||
<CardPlaceholder />
|
||||
{:else}
|
||||
<div
|
||||
style={"background-image: url('" + backdropUrl + "')"}
|
||||
class="bg-center bg-cover h-40 w-72 rounded overflow-hidden relative drop-shadow-2xl"
|
||||
>
|
||||
<div style={'width: ' + progress + '%'} class="h-[2px] bg-zinc-200 bottom-0 absolute z-[1]" />
|
||||
<div
|
||||
on:click={() => window.open('/movie/' + tmdbMovie.id, '_self')}
|
||||
class="h-full w-full opacity-0 hover:opacity-100 transition-opacity flex flex-col justify-between cursor-pointer p-2 px-3 relative z-[1] peer"
|
||||
style={progress > 0 ? 'padding-bottom: 0.6rem;' : ''}
|
||||
>
|
||||
<div>
|
||||
<h1 class="font-bold tracking-wider">{tmdbMovie.original_title}</h1>
|
||||
<div class="text-xs text-zinc-300 tracking-wider font-medium">
|
||||
{formatGenres(tmdbMovie.genres)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-end">
|
||||
{#if progressType === 'watched'}
|
||||
<div class="text-xs font-medium text-zinc-200">
|
||||
{progress
|
||||
? formatMinutes(tmdbMovie.runtime - tmdbMovie.runtime * (progress / 100)) + ' left'
|
||||
: formatMinutes(tmdbMovie.runtime)}
|
||||
</div>
|
||||
{:else if progressType === 'downloading'}
|
||||
<div class="text-xs font-medium text-zinc-200">
|
||||
{Math.floor(progress) + '% Downloaded'}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class={classNames('absolute inset-0', {
|
||||
'bg-darken opacity-0 peer-hover:opacity-100': available,
|
||||
'bg-[#00000055] peer-hover:bg-darken': !available
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
8
src/routes/components/Card/CardPlaceholder.svelte
Normal file
8
src/routes/components/Card/CardPlaceholder.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
export let index = 0;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="h-40 w-72 rounded overflow-hidden drop-shadow-2xl bg-darken animate-pulse"
|
||||
style={'animation-delay: ' + ((index * 100) % 2000) + 'ms;'}
|
||||
/>
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
|
||||
$: {
|
||||
transparent = y === 0;
|
||||
transparent = y <= 0;
|
||||
baseStyle = classNames(
|
||||
'fixed px-8 inset-x-0 grid grid-cols-[min-content_1fr_min-content] items-center z-10',
|
||||
'transition-all',
|
||||
|
||||
@@ -6,18 +6,25 @@
|
||||
import RequestModal from '../RequestModal/RequestModal.svelte';
|
||||
import { addRadarrMovie, getQueuedById, getRadarrMovie } from '$lib/radarr/radarr';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { formatMinutes } from '$lib/utils.js';
|
||||
import classNames from 'classnames';
|
||||
import VideoPlayer from '../VideoPlayer/VideoPlayer.svelte';
|
||||
import { getJellyfinItemByTmdbId } from '$lib/jellyfin/jellyfin';
|
||||
|
||||
let isRequestModalVisible = false;
|
||||
export let tmdbId: string;
|
||||
|
||||
let videoPlayerVisible;
|
||||
|
||||
const { data: localResource, load, didLoad } = getRadarrMovie();
|
||||
const { data: queueResponse, load: loadQueued } = getQueuedById();
|
||||
const { data: addMovieResponse, loading: addMovieLoading, load: addToRadarr } = addRadarrMovie();
|
||||
const { data: jellyfinData, load: loadJellyfinData } = getJellyfinItemByTmdbId();
|
||||
|
||||
function refreshRadarrMovie() {
|
||||
if (tmdbId) load(tmdbId);
|
||||
if (tmdbId) {
|
||||
load(tmdbId);
|
||||
loadJellyfinData(tmdbId);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -101,7 +108,7 @@
|
||||
{movieFile.mediaInfo.videoCodec}
|
||||
</h2>
|
||||
</div>
|
||||
<Button size="sm">Stream</Button>
|
||||
<Button size="sm" on:click={() => (videoPlayerVisible = true)}>Stream</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -122,3 +129,7 @@
|
||||
on:download={() => refreshRadarrMovie()}
|
||||
/>
|
||||
{/if}
|
||||
{#if $jellyfinData?.Id}
|
||||
player
|
||||
<VideoPlayer bind:visible={videoPlayerVisible} jellyfinVideoId={$jellyfinData.Id} />
|
||||
{/if}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { TmdbMovieFull } from '$lib/tmdb-api';
|
||||
import { formatGenres, formatMinutes } from '$lib/utils';
|
||||
import classNames from 'classnames';
|
||||
import { TMDB_IMAGES } from '$lib/constants';
|
||||
|
||||
export let tmdbMovie: TmdbMovieFull;
|
||||
|
||||
export let available = true;
|
||||
export let progress = 0;
|
||||
export let progressType: 'watched' | 'downloading' = 'watched';
|
||||
export let randomProgress = false;
|
||||
if (randomProgress) {
|
||||
progress = Math.random() > 0.3 ? Math.random() * 100 : 0;
|
||||
}
|
||||
|
||||
const backdropUrl =
|
||||
TMDB_IMAGES + tmdbMovie.images.backdrops.filter((b) => b.iso_639_1 === 'en')[0].file_path;
|
||||
</script>
|
||||
|
||||
<div
|
||||
style={"background-image: url('" + backdropUrl + "')"}
|
||||
class="bg-center bg-cover h-40 w-72 rounded overflow-hidden relative drop-shadow-2xl"
|
||||
>
|
||||
<div style={'width: ' + progress + '%'} class="h-[2px] bg-zinc-200 bottom-0 absolute z-[1]" />
|
||||
<div
|
||||
on:click={() => window.open('/movie/' + tmdbMovie.id, '_self')}
|
||||
class="h-full w-full opacity-0 hover:opacity-100 transition-opacity flex flex-col justify-between cursor-pointer p-2 px-3 relative z-[1] peer"
|
||||
style={progress > 0 ? 'padding-bottom: 0.6rem;' : ''}
|
||||
>
|
||||
<div>
|
||||
<h1 class="font-bold tracking-wider">{tmdbMovie.original_title}</h1>
|
||||
<div class="text-xs text-zinc-300 tracking-wider font-medium">
|
||||
{formatGenres(tmdbMovie.genres)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-end">
|
||||
{#if progressType === 'watched'}
|
||||
<div class="text-xs font-medium text-zinc-200">
|
||||
{progress
|
||||
? formatMinutes(tmdbMovie.runtime - tmdbMovie.runtime * (progress / 100)) + ' left'
|
||||
: formatMinutes(tmdbMovie.runtime)}
|
||||
</div>
|
||||
{:else if progressType === 'downloading'}
|
||||
<div class="text-xs font-medium text-zinc-200">
|
||||
{Math.floor(progress) + '% Downloaded'}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class={classNames('absolute inset-0', {
|
||||
'bg-darken opacity-0 peer-hover:opacity-100': available,
|
||||
'bg-[#00000055] peer-hover:bg-darken': !available
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
48
src/routes/components/VideoPlayer/VideoPlayer.svelte
Normal file
48
src/routes/components/VideoPlayer/VideoPlayer.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getJellyfinPlaybackInfo } from '$lib/jellyfin/jellyfin';
|
||||
import Hls from 'hls.js';
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import { JELLYFIN_BASE_URL } from '$lib/jellyfin/jellyfin.js';
|
||||
|
||||
export let visible = false;
|
||||
|
||||
export let jellyfinVideoId: string;
|
||||
|
||||
let video: HTMLVideoElement;
|
||||
|
||||
const { data: playbackInfo, load: loadPlaybackInfo } = getJellyfinPlaybackInfo();
|
||||
|
||||
onMount(() => {
|
||||
if (!Hls.isSupported()) {
|
||||
throw new Error('HLS is not supported');
|
||||
}
|
||||
|
||||
if (!jellyfinVideoId) {
|
||||
throw new Error('No video id provided');
|
||||
}
|
||||
|
||||
loadPlaybackInfo(jellyfinVideoId);
|
||||
});
|
||||
|
||||
playbackInfo.subscribe((info) => {
|
||||
console.log('Subscribe info', info);
|
||||
if (!info) return;
|
||||
|
||||
console.log(video.src);
|
||||
|
||||
const hls = new Hls();
|
||||
|
||||
hls.loadSource(JELLYFIN_BASE_URL + info);
|
||||
hls.attachMedia(video);
|
||||
});
|
||||
|
||||
function handleClose() {
|
||||
visible = false;
|
||||
video?.pause();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {visible} close={handleClose}>
|
||||
<video controls bind:this={video} />
|
||||
</Modal>
|
||||
Reference in New Issue
Block a user