Improved library store & Partly working episode playback

This commit is contained in:
Aleksi Lassila
2023-08-03 22:00:03 +03:00
parent a50ea33f1b
commit 3092e1cc9d
8 changed files with 216 additions and 62 deletions

View File

@@ -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));

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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];
}