mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-27 19:15:12 +02:00
Improved library store & Partly working episode playback
This commit is contained in:
@@ -59,7 +59,7 @@ export const getJellyfinItems = () =>
|
||||
// }
|
||||
// }).then((r) => r.data?.Items || []);
|
||||
|
||||
export const getJellyfinEpisodes = (seriesId: string) =>
|
||||
export const getJellyfinEpisodes = () =>
|
||||
JellyfinApi.get('/Users/{userId}/Items', {
|
||||
params: {
|
||||
path: {
|
||||
@@ -73,7 +73,10 @@ export const getJellyfinEpisodes = (seriesId: string) =>
|
||||
headers: {
|
||||
'cache-control': 'max-age=10'
|
||||
}
|
||||
}).then((r) => r.data?.Items?.filter((i) => i.SeriesId === seriesId) || []);
|
||||
}).then((r) => r.data?.Items || []);
|
||||
|
||||
export const getJellyfinEpisodesBySeries = (seriesId: string) =>
|
||||
getJellyfinEpisodes().then((items) => items.filter((i) => i.SeriesId === seriesId) || []);
|
||||
|
||||
export const getJellyfinItemByTmdbId = (tmdbId: string) =>
|
||||
getJellyfinItems().then((items) => items.find((i) => i.ProviderIds?.Tmdb == tmdbId));
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface TmdbMovieFull2 extends TmdbMovie2 {
|
||||
videos: operations['movie-videos']['responses']['200']['content']['application/json'];
|
||||
credits: operations['movie-credits']['responses']['200']['content']['application/json'];
|
||||
external_ids: operations['movie-external-ids']['responses']['200']['content']['application/json'];
|
||||
images: operations['movie-images']['responses']['200']['content']['application/json'];
|
||||
}
|
||||
|
||||
export interface TmdbSeriesFull2 extends TmdbSeries2 {
|
||||
@@ -43,7 +44,8 @@ export const getTmdbMovie = async (tmdbId: number) =>
|
||||
movie_id: tmdbId
|
||||
},
|
||||
query: {
|
||||
append_to_response: 'videos,credits,external_ids'
|
||||
append_to_response: 'videos,credits,external_ids,images',
|
||||
...({ include_image_language: get(settings).language + ',en,null' } as any)
|
||||
}
|
||||
}
|
||||
}).then((res) => res.data as TmdbMovieFull2 | undefined);
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let backdropPath: string;
|
||||
export let title: string;
|
||||
export let subtitle: string;
|
||||
|
||||
export let title = '';
|
||||
export let subtitle = '';
|
||||
export let episodeNumber: string | undefined = undefined;
|
||||
export let runtime: number;
|
||||
export let runtime = 0;
|
||||
export let progress = 0;
|
||||
|
||||
export let handlePlay: (() => void) | undefined = undefined;
|
||||
|
||||
export let size: 'md' | 'dynamic' = 'md';
|
||||
@@ -31,36 +33,49 @@
|
||||
transition:fade|global
|
||||
>
|
||||
<div
|
||||
class="opacity-100 group-hover:opacity-0 flex flex-col justify-between p-2 lg:p-3 lg:px-3 bg-darken h-full transition-opacity"
|
||||
class={classNames(
|
||||
'flex flex-col justify-between h-full group-hover:opacity-0 transition-opacity',
|
||||
{
|
||||
'px-2 lg:px-3 pt-2': true,
|
||||
' pb-4 lg:pb-6': progress,
|
||||
'pb-2': !progress,
|
||||
'bg-gradient-to-t from-darken': !!handlePlay,
|
||||
'bg-darken': !handlePlay
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'flex justify-between items-center text-xs lg:text-sm font-medium text-zinc-300',
|
||||
{
|
||||
'opacity-60': !handlePlay
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<slot name="left-info">
|
||||
{episodeNumber}
|
||||
<slot name="left-top">
|
||||
{#if episodeNumber}
|
||||
<p class="text-xs lg:text-sm font-medium text-zinc-300">{episodeNumber}</p>
|
||||
{/if}
|
||||
</slot>
|
||||
</div>
|
||||
<div>
|
||||
<slot name="right-info">
|
||||
{runtime} min
|
||||
<slot name="right-top">
|
||||
{#if runtime}
|
||||
<p class="text-xs lg:text-sm font-medium text-zinc-300">
|
||||
{runtime} min
|
||||
</p>
|
||||
{/if}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class={classNames({
|
||||
'opacity-60': !handlePlay
|
||||
})}
|
||||
>
|
||||
<div class="text-xs lg:text-sm text-zinc-300 font-medium tracking-wide">{subtitle}</div>
|
||||
<div class="font-semibold lg:text-lg">
|
||||
{title}
|
||||
</div>
|
||||
<div class="flex items-bottom justify-between">
|
||||
<slot name="left-bottom">
|
||||
<div class="flex flex-col">
|
||||
{#if subtitle}
|
||||
<div class="text-zinc-300 text-sm font-medium">{subtitle}</div>
|
||||
{/if}
|
||||
{#if title}
|
||||
<div class="font-medium">
|
||||
{title}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="right-bottom" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
@@ -72,5 +87,11 @@
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div style={'width: ' + progress + '%'} class="h-[2px] bg-zinc-200 bottom-0 absolute z-[1]" />
|
||||
{#if progress}
|
||||
<div
|
||||
class="absolute h-1 bg-zinc-300 bg-opacity-50 bottom-2 lg:bottom-3 inset-x-2 lg:inset-x-3 rounded-full overflow-hidden group-hover:opacity-0 transition-opacity"
|
||||
>
|
||||
<div style={'width: ' + progress + '%'} class="h-full bg-zinc-200" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<div
|
||||
<button
|
||||
class={classNames('text-zinc-300 hover:text-zinc-50 p-1 flex items-center justify-center', {
|
||||
'opacity-30 cursor-not-allowed pointer-events-none': disabled,
|
||||
'cursor-pointer': !disabled
|
||||
@@ -12,4 +12,4 @@
|
||||
on:click|stopPropagation
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getJellyfinEpisodes } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { getJellyfinEpisodesBySeries } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import {
|
||||
addMovieToRadarr,
|
||||
cancelDownloadRadarrMovie,
|
||||
@@ -50,7 +50,7 @@
|
||||
: undefined;
|
||||
const jellyfinEpisodesPromise =
|
||||
item.jellyfinItem?.Id && item.sonarrSeries?.id
|
||||
? getJellyfinEpisodes(item.jellyfinItem?.Id)
|
||||
? getJellyfinEpisodesBySeries(item.jellyfinItem?.Id)
|
||||
: undefined;
|
||||
|
||||
const sonarrEpisodes = await sonarrEpisodesPromise;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getJellyfinEpisodes } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { getJellyfinEpisodesBySeries } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { getTmdbSeriesSeasons } from '$lib/apis/tmdb/tmdbApi';
|
||||
import classNames from 'classnames';
|
||||
import { Check, StarFilled } from 'radix-icons-svelte';
|
||||
@@ -20,7 +20,9 @@
|
||||
|
||||
async function fetchSeriesData() {
|
||||
const tmdbSeasonsPromise = getTmdbSeriesSeasons(tmdbId, totalSeasons);
|
||||
const jellyfinEpisodesPromise = jellyfinId ? getJellyfinEpisodes(jellyfinId) : undefined;
|
||||
const jellyfinEpisodesPromise = jellyfinId
|
||||
? getJellyfinEpisodesBySeries(jellyfinId)
|
||||
: undefined;
|
||||
|
||||
const tmdbSeasons = await tmdbSeasonsPromise;
|
||||
const jellyfinEpisodes = await jellyfinEpisodesPromise;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
getJellyfinContinueWatching,
|
||||
getJellyfinEpisodes,
|
||||
getJellyfinEpisodesBySeries,
|
||||
getJellyfinItems,
|
||||
type JellyfinItem
|
||||
} from '$lib/apis/jellyfin/jellyfinApi';
|
||||
@@ -16,15 +18,16 @@ import {
|
||||
type SonarrSeries
|
||||
} from '$lib/apis/sonarr/sonarrApi';
|
||||
import {
|
||||
getTmdbMovie,
|
||||
getTmdbMovieBackdrop,
|
||||
getTmdbSeries,
|
||||
getTmdbSeriesBackdrop,
|
||||
getTmdbSeriesFromTvdbId
|
||||
} from '$lib/apis/tmdb/tmdbApi';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { settings } from './settings.store';
|
||||
|
||||
export interface PlayableItem {
|
||||
type: 'movie' | 'series';
|
||||
tmdbId: number;
|
||||
tmdbRating: number;
|
||||
cardBackdropUrl: string;
|
||||
download?: {
|
||||
@@ -37,7 +40,11 @@ export interface PlayableItem {
|
||||
};
|
||||
isPlayed: boolean;
|
||||
jellyfinId?: string;
|
||||
|
||||
type: 'movie' | 'series';
|
||||
tmdbId: number;
|
||||
jellyfinItem?: JellyfinItem;
|
||||
jellyfinEpisodes?: JellyfinItem[];
|
||||
radarrMovie?: RadarrMovie;
|
||||
radarrDownloads?: RadarrDownload[];
|
||||
sonarrSeries?: SonarrSeries;
|
||||
@@ -59,6 +66,7 @@ async function getLibrary(): Promise<Library> {
|
||||
|
||||
const continueWatchingPromise = getJellyfinContinueWatching();
|
||||
const jellyfinLibraryItemsPromise = getJellyfinItems();
|
||||
const jellyfinEpisodesPromise = getJellyfinEpisodes();
|
||||
|
||||
const radarrMovies = await radarrMoviesPromise;
|
||||
const radarrDownloads = await radarrDownloadsPromise;
|
||||
@@ -68,6 +76,9 @@ async function getLibrary(): Promise<Library> {
|
||||
|
||||
const jellyfinContinueWatching = await continueWatchingPromise;
|
||||
const jellyfinLibraryItems = await jellyfinLibraryItemsPromise;
|
||||
const jellyfinEpisodes = await jellyfinEpisodesPromise.then((episodes) =>
|
||||
episodes?.sort((a, b) => (a.IndexNumber || 99) - (b.IndexNumber || 99))
|
||||
);
|
||||
|
||||
const items: Record<string, PlayableItem> = {};
|
||||
|
||||
@@ -101,7 +112,15 @@ async function getLibrary(): Promise<Library> {
|
||||
? { length, progress: watchingProgress }
|
||||
: undefined;
|
||||
|
||||
const backdropUrl = await getTmdbMovieBackdrop(radarrMovie.tmdbId || 0);
|
||||
const backdropUrl = await getTmdbMovie(radarrMovie.tmdbId || 0).then(
|
||||
(r) =>
|
||||
(
|
||||
r?.images?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
|
||||
r?.images?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
|
||||
r?.images?.backdrops?.find((b) => b.iso_639_1) ||
|
||||
r?.images?.backdrops?.[0]
|
||||
)?.file_path
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'movie' as const,
|
||||
@@ -153,7 +172,15 @@ async function getLibrary(): Promise<Library> {
|
||||
: undefined;
|
||||
const tmdbId = tmdbItem?.id || undefined;
|
||||
|
||||
const backdropUrl = tmdbId ? await getTmdbSeriesBackdrop(tmdbId) : undefined;
|
||||
const backdropUrl = await getTmdbSeries(tmdbId || 0).then(
|
||||
(r) =>
|
||||
(
|
||||
r?.images?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
|
||||
r?.images?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
|
||||
r?.images?.backdrops?.find((b) => b.iso_639_1) ||
|
||||
r?.images?.backdrops?.[0]
|
||||
)?.file_path
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'series' as const,
|
||||
@@ -166,7 +193,8 @@ async function getLibrary(): Promise<Library> {
|
||||
jellyfinId: jellyfinItem?.Id,
|
||||
jellyfinItem,
|
||||
sonarrSeries,
|
||||
sonarrDownloads: itemSonarrDownloads
|
||||
sonarrDownloads: itemSonarrDownloads,
|
||||
jellyfinEpisodes: jellyfinEpisodes.filter((i) => i.SeriesId === jellyfinItem?.Id)
|
||||
};
|
||||
});
|
||||
|
||||
@@ -200,3 +228,29 @@ function createLibraryStore() {
|
||||
}
|
||||
|
||||
export const library = createLibraryStore();
|
||||
|
||||
function _createLibraryItemStore(tmdbId: number) {
|
||||
const store = writable<{ loading: boolean; item?: PlayableItem }>({
|
||||
loading: true,
|
||||
item: undefined
|
||||
});
|
||||
|
||||
library.subscribe(async (library) => {
|
||||
const item = (await library).items[tmdbId];
|
||||
store.set({ loading: false, item });
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe
|
||||
};
|
||||
}
|
||||
|
||||
const itemStores: Record<string, ReturnType<typeof _createLibraryItemStore>> = {};
|
||||
|
||||
export function createLibraryItemStore(tmdbId: number) {
|
||||
if (!itemStores[tmdbId]) {
|
||||
itemStores[tmdbId] = _createLibraryItemStore(tmdbId);
|
||||
}
|
||||
|
||||
return itemStores[tmdbId];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user