diff --git a/package-lock.json b/package-lock.json index e04e314..90845c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "prettier-plugin-svelte": "^2.10.1", "radix-icons-svelte": "^1.2.1", "reflect-metadata": "^0.1.13", - "svelte": "^3.59.1", + "svelte": "^3.59.2", "svelte-check": "^3.6.2", "svelte-i18n": "^4.0.0", "svelte-navigator": "^3.2.2", diff --git a/package.json b/package.json index 2c5cd2d..d6ad371 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "prettier-plugin-svelte": "^2.10.1", "radix-icons-svelte": "^1.2.1", "reflect-metadata": "^0.1.13", - "svelte": "^3.59.1", + "svelte": "^3.59.2", "svelte-check": "^3.6.2", "svelte-i18n": "^4.0.0", "svelte-navigator": "^3.2.2", diff --git a/src/lib/apis/jellyfin/jellyfin-api.ts b/src/lib/apis/jellyfin/jellyfin-api.ts index e5d7866..2e5d75b 100644 --- a/src/lib/apis/jellyfin/jellyfin-api.ts +++ b/src/lib/apis/jellyfin/jellyfin-api.ts @@ -5,6 +5,7 @@ import type { Api } from '../api.interface'; import { appState } from '../../stores/app-state.store'; import type { DeviceProfile } from './playback-profiles'; import axios from 'axios'; +import { log } from '../../utils'; export type JellyfinItem = components['schemas']['BaseItemDto']; @@ -82,7 +83,13 @@ export class JellyfinApi implements Api { hasTmdbId: true, recursive: true, includeItemTypes: ['Movie', 'Series'], - fields: ['ProviderIds', 'Genres', 'DateLastMediaAdded', 'DateCreated'] + fields: [ + 'ProviderIds', + 'Genres', + 'DateLastMediaAdded', + 'DateCreated', + 'MediaSources' + ] } } }) @@ -265,6 +272,39 @@ export class JellyfinApi implements Api { // } // }).then((r) => r.data?.Items || []); + episodesCache: JellyfinItem[] = []; + getEpisode = async ( + seriesId: string, + season: number, + episode: number, + refreshCache = false + ): Promise => + this.getClient() + .GET('/Users/{userId}/Items', { + params: { + path: { + userId: this.getUserId() + }, + query: { + // @ts-ignore + seriesId, + parentIndexNumber: season, + indexNumber: episode, + recursive: true, + includeItemTypes: ['Episode'], + fields: ['ProviderIds', 'Genres', 'DateLastMediaAdded', 'DateCreated', 'MediaSources'] + } + } + }) + .then((r) => + r.data?.Items?.find( + (i) => + i?.ParentIndexNumber === season && + i?.IndexNumber === episode && + i?.SeriesId === seriesId + ) + ); + getJellyfinEpisodes = async (parentId = '') => this.getClient() ?.GET('/Users/{userId}/Items', { @@ -415,28 +455,33 @@ export class JellyfinApi implements Api { } }); - setJellyfinItemWatched = async (jellyfinId: string) => - this.getClient()?.POST('/Users/{userId}/PlayedItems/{itemId}', { - params: { - path: { - userId: this.getUserId(), - itemId: jellyfinId - }, - query: { - datePlayed: new Date().toISOString() + markAsWatched = async (jellyfinId: string) => + this.getClient() + ?.POST('/Users/{userId}/PlayedItems/{itemId}', { + params: { + path: { + userId: this.getUserId(), + itemId: jellyfinId + }, + query: { + datePlayed: new Date().toISOString() + } } - } - }); + }) - setJellyfinItemUnwatched = async (jellyfinId: string) => - this.getClient()?.DELETE('/Users/{userId}/PlayedItems/{itemId}', { - params: { - path: { - userId: this.getUserId(), - itemId: jellyfinId + .then((res) => res.response.status === 200); + + markAsUnwatched = async (jellyfinId: string) => + this.getClient() + ?.DELETE('/Users/{userId}/PlayedItems/{itemId}', { + params: { + path: { + userId: this.getUserId(), + itemId: jellyfinId + } } - } - }); + }) + .then((res) => res.response.status === 200); getJellyfinHealth = async ( baseUrl: string | undefined = undefined, diff --git a/src/lib/apis/tmdb/tmdb-api.ts b/src/lib/apis/tmdb/tmdb-api.ts index d3215a9..541836d 100644 --- a/src/lib/apis/tmdb/tmdb-api.ts +++ b/src/lib/apis/tmdb/tmdb-api.ts @@ -15,12 +15,14 @@ export type TmdbSeries2 = operations['tv-series-details']['responses']['200']['content']['application/json']; export type TmdbSeason = operations['tv-season-details']['responses']['200']['content']['application/json']; -export type TmdbEpisode = NonNullable[0]; +export type TmdbSeasonEpisode = NonNullable[0]; export type TmdbPerson = operations['person-details']['responses']['200']['content']['application/json']; export type TmdbCredit = | NonNullable[0] | NonNullable[0]; +export type TmdbEpisode = + operations['tv-episode-details']['responses']['200']['content']['application/json']; export interface TmdbPersonFull extends TmdbPerson { images: operations['person-images']['responses']['200']['content']['application/json']; @@ -176,6 +178,26 @@ export class TmdbApi implements Api { } }).then((res) => res.data?.results || []); + getEpisode = ( + seriesId: number, + season: number, + episode: number + ): Promise => + this.getClient() + .GET('/3/tv/{series_id}/season/{season_number}/episode/{episode_number}', { + params: { + path: { + series_id: seriesId, + season_number: season, + episode_number: episode + }, + query: { + append_to_response: 'credits,external_ids,images' + } + } + }) + .then((res) => res.data); + // OTHER } diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte index 83587c2..87bc9d9 100644 --- a/src/lib/components/Button.svelte +++ b/src/lib/components/Button.svelte @@ -6,6 +6,7 @@ export let inactive: boolean = false; export let focusOnMount: boolean = false; + export let style: 'primary' | 'secondary' = 'primary'; let hasFocus: Readable; @@ -14,12 +15,9 @@ import Container from '../../../Container.svelte'; import classNames from 'classnames'; - import { Check, CheckCircled, TriangleRight } from 'radix-icons-svelte'; + import { ArrowDown, Check, TriangleRight } from 'radix-icons-svelte'; import type { Readable } from 'svelte/store'; import AnimateScale from '../AnimateScale.svelte'; @@ -22,10 +22,7 @@ class={classNames( 'w-full h-64', 'flex flex-col shrink-0', - 'overflow-hidden rounded-2xl cursor-pointer group relative px-4 py-3 selectable transition-opacity', - { - 'opacity-75': !isOnDeck && !$hasFocus - } + 'overflow-hidden rounded-2xl cursor-pointer group relative px-4 py-3 selectable transition-opacity' )} on:clickOrSelect on:enter @@ -55,24 +52,28 @@
- {#if handlePlay} -
+
+ {#if handlePlay}
-
- {/if} + {:else if !isOnDeck} +
+ +
+ {/if} +
diff --git a/src/lib/components/EpisodeCard/TmdbEpisodeCard.svelte b/src/lib/components/EpisodeCard/TmdbEpisodeCard.svelte index 60b9b01..e117c1a 100644 --- a/src/lib/components/EpisodeCard/TmdbEpisodeCard.svelte +++ b/src/lib/components/EpisodeCard/TmdbEpisodeCard.svelte @@ -1,9 +1,9 @@ + +
= 15 + } + )} +> + {title} +
diff --git a/src/lib/components/SeriesPage/EpisodeCarousel.svelte b/src/lib/components/SeriesPage/EpisodeCarousel.svelte index 28307b3..1d87c16 100644 --- a/src/lib/components/SeriesPage/EpisodeCarousel.svelte +++ b/src/lib/components/SeriesPage/EpisodeCarousel.svelte @@ -5,7 +5,7 @@ import { derived, get, type Readable } from 'svelte/store'; import { tmdbApi, - type TmdbEpisode, + type TmdbSeasonEpisode, type TmdbSeason, type TmdbSeriesFull2 } from '../../apis/tmdb/tmdb-api'; @@ -24,9 +24,9 @@ export let nextJellyfinEpisode: Readable; // Exports - export let selectedTmdbEpisode: TmdbEpisode | undefined; + export let selectedTmdbEpisode: TmdbSeasonEpisode | undefined; - const containers = new Map(); + const containers = new Map(); let scrollTop: number; const { data: tmdbSeasons, isLoading: isTmdbSeasonsLoading } = useDependantRequest( @@ -58,7 +58,7 @@ if (seasonSelectable) seasonSelectable.focus({ setFocusedElement: false }); } - function handleEpisodeMount(event: CustomEvent, tmdbEpisode: TmdbEpisode) { + function handleEpisodeMount(event: CustomEvent, tmdbEpisode: TmdbSeasonEpisode) { containers.set(tmdbEpisode, event.detail); const selectable = event.detail; diff --git a/src/lib/components/SeriesPage/EpisodeGrid.svelte b/src/lib/components/SeriesPage/EpisodeGrid.svelte index 2d9bdf6..175e7a5 100644 --- a/src/lib/components/SeriesPage/EpisodeGrid.svelte +++ b/src/lib/components/SeriesPage/EpisodeGrid.svelte @@ -1,6 +1,6 @@ diff --git a/src/lib/components/SeriesPage/SeriesPage.svelte b/src/lib/components/SeriesPage/SeriesPage.svelte index f85992c..d075df0 100644 --- a/src/lib/components/SeriesPage/SeriesPage.svelte +++ b/src/lib/components/SeriesPage/SeriesPage.svelte @@ -3,7 +3,7 @@ import HeroCarousel from '../HeroCarousel/HeroCarousel.svelte'; import DetachedPage from '../DetachedPage/DetachedPage.svelte'; import { useActionRequest, useDependantRequest, useRequest } from '../../stores/data.store'; - import { tmdbApi, type TmdbEpisode } from '../../apis/tmdb/tmdb-api'; + import { tmdbApi, type TmdbSeasonEpisode } from '../../apis/tmdb/tmdb-api'; import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../../constants'; import classNames from 'classnames'; import { DotFilled, Download, ExternalLink, File, Play, Plus } from 'radix-icons-svelte'; @@ -48,7 +48,7 @@ sonarrApi.addSeriesToSonarr ); - let selectedTmdbEpisode: TmdbEpisode | undefined; + let selectedTmdbEpisode: TmdbSeasonEpisode | undefined; const episodeCards = useRegistrar(); let scrollTop: number; @@ -267,4 +267,4 @@
- + diff --git a/src/lib/pages/EpisodePage.svelte b/src/lib/pages/EpisodePage.svelte index a46c42a..3eb7d0a 100644 --- a/src/lib/pages/EpisodePage.svelte +++ b/src/lib/pages/EpisodePage.svelte @@ -1,10 +1,127 @@ - - Episode Page for {season}x{episode} + + {#await tmdbEpisode then episode} +
+
+
+
+
+ + + + +
+ Season {episode?.season_number} Episode {episode?.episode_number} +
+ +
+ + + + + + + + +

+ + {episode?.vote_average} TMDB + +

+ +

{episode?.runtime} Minutes

+ + {#await jellyfinEpisode then episode} + {#if episode?.MediaSources?.[0]?.Size} + +

{formatSize(episode?.MediaSources?.[0]?.Size)}

+ {/if} + {#if episode?.MediaSources?.[0]?.MediaStreams?.[0]?.DisplayTitle} + +

+ {episode?.MediaSources?.[0]?.MediaStreams?.[0]?.DisplayTitle} +

+ {/if} + {/await} +
+
+ {episode?.overview} +
+ + {#await jellyfinEpisode then episode} + + + {/await} + + + +
+ {/await} diff --git a/src/lib/selectable.ts b/src/lib/selectable.ts index e11872c..98262dc 100644 --- a/src/lib/selectable.ts +++ b/src/lib/selectable.ts @@ -702,7 +702,7 @@ export function handleKeyboardNavigation(event: KeyboardEvent) { event.preventDefault(); } -Selectable.focusedObject.subscribe(console.debug); +Selectable.focusedObject.subscribe((e) => console.debug('Focused object', e)); type Offsets = Partial< Record< diff --git a/src/lib/stores/data.store.ts b/src/lib/stores/data.store.ts index d350db5..5992c0d 100644 --- a/src/lib/stores/data.store.ts +++ b/src/lib/stores/data.store.ts @@ -328,3 +328,39 @@ export const useActionRequest =

Promise, A exten send }; }; + +export const useActionRequest2 =

Promise, A extends any[]>( + fn: P +) => { + const request = writable>(undefined); + const data = writable> | undefined>(undefined); + const isFetching = writable(false); + + function send(...args: Parameters

): ReturnType

{ + isFetching.set(true); + // @ts-ignore + const p: ReturnType

= fn(...args) + .then((res) => { + data.set(res); + return res; + }) + .finally(() => { + isFetching.set(false); + }); + + request.set(p); + return p; + } + + return { + promise: request, + data: { + subscribe: data.subscribe + }, + + isFetching: { + subscribe: isFetching.subscribe + }, + send + }; +};