mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-20 15:55:14 +02:00
feat: tmdb cache, plugin support changes, series page, episode page, movie page streaming updated
This commit is contained in:
@@ -22,8 +22,6 @@
|
||||
export let rating: number | undefined = undefined;
|
||||
export let progress = 0;
|
||||
|
||||
$: console.log('progress', progress);
|
||||
|
||||
export let disabled = false;
|
||||
export let shadow = false;
|
||||
export let size: 'dynamic' | 'md' | 'lg' | 'sm' = 'md';
|
||||
|
||||
@@ -6,7 +6,16 @@
|
||||
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
|
||||
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../../constants';
|
||||
import classNames from 'classnames';
|
||||
import { Cross1, DotFilled, ExternalLink, Play, Plus, Trash } from 'radix-icons-svelte';
|
||||
import {
|
||||
Bookmark,
|
||||
Cross1,
|
||||
DotFilled,
|
||||
ExternalLink,
|
||||
Minus,
|
||||
Play,
|
||||
Plus,
|
||||
Trash
|
||||
} from 'radix-icons-svelte';
|
||||
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api';
|
||||
import {
|
||||
type EpisodeDownload,
|
||||
@@ -16,7 +25,7 @@
|
||||
import Button from '../Button.svelte';
|
||||
import { playerState } from '../VideoPlayer/VideoPlayer';
|
||||
import { createModal, modalStack } from '../Modal/modal.store';
|
||||
import { get } from 'svelte/store';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { scrollIntoView, useRegistrar } from '../../selectable';
|
||||
import ScrollHelper from '../ScrollHelper.svelte';
|
||||
import Carousel from '../Carousel/Carousel.svelte';
|
||||
@@ -29,15 +38,34 @@
|
||||
import MMAddToSonarrDialog from '../MediaManagerModal/MMAddToSonarrDialog.svelte';
|
||||
import ConfirmDialog from '../Dialog/ConfirmDialog.svelte';
|
||||
import DownloadDetailsDialog from './DownloadDetailsDialog.svelte';
|
||||
import { reiverrApiNew, sources, user } from '../../stores/user.store';
|
||||
import type { VideoStreamCandidateDto } from '../../apis/reiverr/reiverr.openapi';
|
||||
import type { MediaSource } from '../../apis/reiverr/reiverr.openapi';
|
||||
import SelectDialog from '../Dialog/SelectDialog.svelte';
|
||||
import { useUserData } from '../../stores/library.store';
|
||||
|
||||
export let id: string;
|
||||
const tmdbId = Number(id);
|
||||
|
||||
const { promise: tmdbSeries, data: tmdbSeriesData } = useRequest(
|
||||
tmdbApi.getTmdbSeries,
|
||||
Number(id)
|
||||
const showUserData = reiverrApiNew.users
|
||||
.getShowUserData($user?.id as string, id)
|
||||
.then((r) => r.data);
|
||||
const streams = getStreams();
|
||||
|
||||
const availableForStreaming = writable(false);
|
||||
const { inLibrary, progress, handleAddToLibrary, handleRemoveFromLibrary } = useUserData(
|
||||
'Series',
|
||||
id,
|
||||
showUserData
|
||||
);
|
||||
let sonarrItem = sonarrApi.getSeriesByTmdbId(Number(id));
|
||||
const { promise: recommendations } = useRequest(tmdbApi.getSeriesRecommendations, Number(id));
|
||||
|
||||
streams.forEach((p) =>
|
||||
p.streams.then((s) => availableForStreaming.update((p) => p || s.length > 0))
|
||||
);
|
||||
|
||||
const { promise: tmdbSeries, data: tmdbSeriesData } = useRequest(tmdbApi.getTmdbSeries, tmdbId);
|
||||
let sonarrItem = sonarrApi.getSeriesByTmdbId(tmdbId);
|
||||
const { promise: recommendations } = useRequest(tmdbApi.getSeriesRecommendations, tmdbId);
|
||||
|
||||
$: sonarrDownloads = getDownloads(sonarrItem);
|
||||
$: sonarrFiles = getFiles(sonarrItem);
|
||||
@@ -71,6 +99,25 @@
|
||||
// hideInterface = !!modal;
|
||||
// });
|
||||
|
||||
function getStreams() {
|
||||
const out: { source: MediaSource; streams: Promise<VideoStreamCandidateDto[]> }[] = [];
|
||||
|
||||
for (const source of get(sources)) {
|
||||
out.push({
|
||||
source: source.source,
|
||||
streams: showUserData.then((userData) => {
|
||||
const { season, episode } = userData.playState ?? {};
|
||||
|
||||
return reiverrApiNew.sources
|
||||
.getEpisodeStreams(source.source.id, id, season ?? 1, episode ?? 1)
|
||||
.then((r) => r.data?.streams ?? []);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function getJellyfinSeries(id: string) {
|
||||
return jellyfinApi.getLibraryItemFromTmdbId(id);
|
||||
}
|
||||
@@ -78,7 +125,7 @@
|
||||
const onGrabRelease = () => setTimeout(() => (sonarrDownloads = getDownloads(sonarrItem)), 8000);
|
||||
|
||||
function handleAddedToSonarr() {
|
||||
sonarrItem = sonarrApi.getSeriesByTmdbId(Number(id));
|
||||
sonarrItem = sonarrApi.getSeriesByTmdbId(tmdbId);
|
||||
sonarrItem.then(
|
||||
(sonarrItem) =>
|
||||
sonarrItem &&
|
||||
@@ -141,6 +188,49 @@
|
||||
.then(() => (sonarrDownloads = getDownloads(sonarrItem)))
|
||||
});
|
||||
}
|
||||
|
||||
async function handlePlay() {
|
||||
const awaitedStreams = await Promise.all(
|
||||
streams.map(async (p) => ({ ...p, streams: await p.streams }))
|
||||
).then((d) => d.filter((p) => p.streams.length > 0));
|
||||
|
||||
if (awaitedStreams.length > 1) {
|
||||
modalStack.create(SelectDialog, {
|
||||
title: 'Select Media Source',
|
||||
subtitle: 'Select the media source you want to use',
|
||||
options: awaitedStreams.map((p) => p.source.id),
|
||||
handleSelectOption: (sourceId) => {
|
||||
const s = awaitedStreams.find((p) => p.source.id === sourceId);
|
||||
const key = s?.streams[0]?.key;
|
||||
showUserData.then((userData) =>
|
||||
playerState.streamEpisode(
|
||||
id,
|
||||
userData.playState?.season ?? 1,
|
||||
userData.playState?.episode ?? 1,
|
||||
userData,
|
||||
sourceId,
|
||||
key
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
} else if (awaitedStreams.length === 1) {
|
||||
const asd = awaitedStreams.find((p) => p.streams.length > 0);
|
||||
const sourceId = asd?.source.id;
|
||||
const key = asd?.streams[0]?.key;
|
||||
|
||||
showUserData.then((userData) =>
|
||||
playerState.streamEpisode(
|
||||
id,
|
||||
userData.playState?.season ?? 1,
|
||||
userData.playState?.episode ?? 1,
|
||||
userData,
|
||||
sourceId,
|
||||
key
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DetachedPage let:handleGoBack let:registrar>
|
||||
@@ -214,7 +304,20 @@
|
||||
on:back={handleGoBack}
|
||||
on:mount={registrar}
|
||||
>
|
||||
{#if nextJellyfinEpisode}
|
||||
<Button class="mr-4" action={handlePlay} disabled={!$availableForStreaming}>
|
||||
Play
|
||||
<Play size={19} slot="icon" />
|
||||
</Button>
|
||||
{#if !$inLibrary}
|
||||
<Button class="mr-4" action={handleAddToLibrary} icon={Bookmark}>
|
||||
Add to Library
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="mr-4" action={handleRemoveFromLibrary} icon={Minus}>
|
||||
Remove from Library
|
||||
</Button>
|
||||
{/if}
|
||||
<!-- {#if nextJellyfinEpisode}
|
||||
<Button
|
||||
class="mr-4"
|
||||
on:clickOrSelect={() =>
|
||||
@@ -223,13 +326,13 @@
|
||||
Play Season {nextJellyfinEpisode?.ParentIndexNumber} Episode
|
||||
{nextJellyfinEpisode?.IndexNumber}
|
||||
<Play size={19} slot="icon" />
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="mr-4" action={() => handleRequestSeason(1)}>
|
||||
Request
|
||||
<Plus size={19} slot="icon" />
|
||||
</Button>
|
||||
{/if}
|
||||
</Button> -->
|
||||
<!-- {:else} -->
|
||||
<Button class="mr-4" action={() => handleRequestSeason(1)}>
|
||||
Request
|
||||
<Plus size={19} slot="icon" />
|
||||
</Button>
|
||||
<!-- {/if} -->
|
||||
|
||||
{#if PLATFORM_WEB}
|
||||
<Button class="mr-4">
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
import { modalStackTop } from '../Modal/modal.store';
|
||||
|
||||
export let tmdbId: string;
|
||||
export let season: number | undefined = undefined;
|
||||
export let episode: number | undefined = undefined;
|
||||
export let sourceId: string;
|
||||
export let key: string = '';
|
||||
export let progress: number = 0;
|
||||
@@ -46,27 +48,50 @@
|
||||
|
||||
let videoStreamP: Promise<VideoStreamDto>;
|
||||
|
||||
const movieP = tmdbApi.getTmdbMovie(Number(tmdbId)).then((r) => {
|
||||
title = r?.title || '';
|
||||
subtitle = '';
|
||||
});
|
||||
// const movieP = tmdbApi.getTmdbMovie(Number(tmdbId)).then((r) => {
|
||||
// title = r?.title || '';
|
||||
// subtitle = '';
|
||||
// });
|
||||
|
||||
function reportProgress() {
|
||||
const userId = get(user)?.id;
|
||||
|
||||
if (!userId) {
|
||||
console.error('Update progress failed: User not logged in');
|
||||
return;
|
||||
}
|
||||
|
||||
if (video?.readyState === 4 && video?.currentTime > 0 && video?.duration > 0)
|
||||
reiverrApiNew.users.updateMoviePlayStateByTmdbId($user?.id as string, tmdbId, {
|
||||
progress: video.currentTime / video?.duration,
|
||||
watched: progressTime > 0.9
|
||||
});
|
||||
if (season !== undefined && episode !== undefined) {
|
||||
reiverrApiNew.users.updateEpisodePlayStateByTmdbId(userId, tmdbId, season, episode, {
|
||||
progress: video.currentTime / video?.duration,
|
||||
watched: progressTime > 0.9
|
||||
});
|
||||
} else {
|
||||
reiverrApiNew.users.updateMoviePlayStateByTmdbId(userId, tmdbId, {
|
||||
progress: video.currentTime / video?.duration,
|
||||
watched: progressTime > 0.9
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const refreshVideoStream = async (audioStreamIndex = 0) => {
|
||||
videoStreamP = reiverrApiNew.sources
|
||||
.getMovieStream(tmdbId, sourceId, key, {
|
||||
// bitrate: getQualities(1080)?.[0]?.maxBitrate || 10000000,
|
||||
progress,
|
||||
audioStreamIndex,
|
||||
deviceProfile: getDeviceProfile() as any
|
||||
})
|
||||
console.log('refreshVideoStream', season, episode);
|
||||
videoStreamP = (
|
||||
season !== undefined && episode !== undefined
|
||||
? reiverrApiNew.sources.getEpisodeStream(sourceId, tmdbId, season, episode, key, {
|
||||
// bitrate: getQualities(1080)?.[0]?.maxBitrate || 10000000,
|
||||
progress,
|
||||
audioStreamIndex,
|
||||
deviceProfile: getDeviceProfile() as any
|
||||
})
|
||||
: reiverrApiNew.sources.getMovieStream(tmdbId, sourceId, key, {
|
||||
// bitrate: getQualities(1080)?.[0]?.maxBitrate || 10000000,
|
||||
progress,
|
||||
audioStreamIndex,
|
||||
deviceProfile: getDeviceProfile() as any
|
||||
})
|
||||
)
|
||||
.then((r) => r.data)
|
||||
.then((d) => ({
|
||||
...d,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { reiverrApiNew, sources } from '../../stores/user.store';
|
||||
import { createErrorNotification } from '../Notifications/notification.store';
|
||||
import VideoPlayerModal from './VideoPlayerModal.svelte';
|
||||
import MovieVideoPlayerModal from './MovieVideoPlayerModal.svelte';
|
||||
import type { MovieUserDataDto } from '../../apis/reiverr/reiverr.openapi';
|
||||
import type { MediaUserDataDto } from '../../apis/reiverr/reiverr.openapi';
|
||||
|
||||
export type SubtitleInfo = {
|
||||
subtitles?: Subtitles;
|
||||
@@ -51,7 +51,7 @@ function usePlayerState() {
|
||||
|
||||
async function streamTmdbMovie(
|
||||
tmdbId: string,
|
||||
userData: MovieUserDataDto,
|
||||
userData: MediaUserDataDto,
|
||||
sourceId: string = '',
|
||||
key: string = ''
|
||||
) {
|
||||
@@ -81,9 +81,47 @@ function usePlayerState() {
|
||||
});
|
||||
}
|
||||
|
||||
async function streamTmdbEpisode(
|
||||
tmdbId: string,
|
||||
season: number,
|
||||
episode: number,
|
||||
userData: MediaUserDataDto,
|
||||
sourceId: string = '',
|
||||
key: string = ''
|
||||
) {
|
||||
if (!sourceId) {
|
||||
const streams = await Promise.all(
|
||||
get(sources).map((s) =>
|
||||
reiverrApiNew.sources
|
||||
.getEpisodeStreams(s.source.id, tmdbId, season, episode)
|
||||
.then((r) => ({ source: s.source, streams: r.data.streams }))
|
||||
)
|
||||
);
|
||||
sourceId = streams?.[0]?.source.id || '';
|
||||
key = streams?.[0]?.streams?.[0]?.key || '';
|
||||
}
|
||||
|
||||
if (!sourceId) {
|
||||
createErrorNotification('Could not find a suitable source');
|
||||
return;
|
||||
}
|
||||
|
||||
store.set({ visible: true, jellyfinId: tmdbId, sourceId });
|
||||
console.log('sourceId', season, episode);
|
||||
modalStack.create(MovieVideoPlayerModal, {
|
||||
tmdbId,
|
||||
episode,
|
||||
season,
|
||||
sourceId,
|
||||
key,
|
||||
progress: userData.playState?.progress || 0
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...store,
|
||||
streamMovie: streamTmdbMovie,
|
||||
streamEpisode: streamTmdbEpisode,
|
||||
streamJellyfinId: (id: string) => {
|
||||
store.set({ visible: true, jellyfinId: id, sourceId: '' });
|
||||
modalStack.create(JellyfinVideoPlayerModal, { id });
|
||||
|
||||
Reference in New Issue
Block a user