+
{title}
{/if}
diff --git a/src/lib/components/Lang/I18n.svelte b/src/lib/components/Lang/I18n.svelte
index 80cd97c..3b08331 100644
--- a/src/lib/components/Lang/I18n.svelte
+++ b/src/lib/components/Lang/I18n.svelte
@@ -1,11 +1,14 @@
+
+
+ {#each Array.from({ length }) as _, i}
+
+
+
+
onJump(i)}>
+
+
+ {/each}
+
diff --git a/src/lib/components/Poster/Poster.svelte b/src/lib/components/Poster/Poster.svelte
index 22466ff..e3f9fb6 100644
--- a/src/lib/components/Poster/Poster.svelte
+++ b/src/lib/components/Poster/Poster.svelte
@@ -20,6 +20,7 @@
export let rating: number | undefined = undefined;
export let progress = 0;
+ export let shadow = false;
export let size: 'dynamic' | 'md' | 'lg' | 'sm' = 'md';
export let orientation: 'portrait' | 'landscape' = 'landscape';
@@ -37,7 +38,7 @@
}
}}
class={classNames(
- 'relative flex shadow-lg rounded-xl selectable group hover:text-inherit flex-shrink-0 overflow-hidden text-left',
+ 'relative flex rounded-xl selectable group hover:text-inherit flex-shrink-0 overflow-hidden text-left',
{
'aspect-video': orientation === 'landscape',
'aspect-[2/3]': orientation === 'portrait',
@@ -47,7 +48,8 @@
'h-44': size === 'md' && orientation === 'landscape',
'w-60': size === 'lg' && orientation === 'portrait',
'h-60': size === 'lg' && orientation === 'landscape',
- 'w-full': size === 'dynamic'
+ 'w-full': size === 'dynamic',
+ 'shadow-lg': shadow
}
)}
>
diff --git a/src/lib/components/TitleShowcase/TitleShowcase.svelte b/src/lib/components/TitleShowcase/TitleShowcase.svelte
deleted file mode 100644
index 7c99628..0000000
--- a/src/lib/components/TitleShowcase/TitleShowcase.svelte
+++ /dev/null
@@ -1,254 +0,0 @@
-
-
-
-
- {#if UIVisible}
-
-
-
-
{releaseDate.getFullYear()}
-
-
{formatMinutesToTime(runtime)}
-
-
{tmdbRating.toFixed(1)} TMDB
-
-
= 15
- })}
- in:fly|global={{ y: -10, delay: ANIMATION_DURATION, duration: ANIMATION_DURATION }}
- out:fly|global={{ y: 10, duration: ANIMATION_DURATION }}
- >
- {title}
-
-
-
- {#each genres.slice(0, 3) as genre}
-
- {genre}
-
- {/each}
-
-
- {/if}
-
-
-
-
- {#if trailerId}
-
- {/if}
-
-
-
-
-
-
- {$_('titleShowcase.releaseDate')}
-
-
-
- {releaseDate.toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- })}
-
-
- {#if director}
-
-
- {$_('titleShowcase.directedBy')}
-
-
{director}
-
- {/if}
-
-
-
- {#if UIVisible}
-
- {#each Array.from({ length: showcaseLength }, (_, i) => i) as i}
- {#if i === showcaseIndex}
-
- {:else}
-
- {/if}
- {/each}
-
- {/if}
-
- {#if !trailerVisible}
-
- {/if}
- {#if trailerId && $settings.autoplayTrailers && trailerMounted}
-
-
-
- {/if}
- {#if UIVisible}
-
- {:else if !UIVisible}
-
- {/if}
- {#if UIVisible}
-
-
- {/if}
-
diff --git a/src/lib/components/TitleShowcase/TitleShowcaseBackground.svelte b/src/lib/components/TitleShowcase/TitleShowcaseBackground.svelte
new file mode 100644
index 0000000..344b349
--- /dev/null
+++ b/src/lib/components/TitleShowcase/TitleShowcaseBackground.svelte
@@ -0,0 +1,94 @@
+
+
+
+
+{#if !trailerVisible}
+ {#key tmdbId}
+
+ {/key}
+{/if}
+{#if trailerId && $settings.autoplayTrailers && trailerMounted}
+
+
+
+{/if}
+{#if UIVisible}
+
+{:else if !UIVisible}
+
+{/if}
diff --git a/src/lib/components/TitleShowcase/TitleShowcaseVisuals.svelte b/src/lib/components/TitleShowcase/TitleShowcaseVisuals.svelte
new file mode 100644
index 0000000..71a2b89
--- /dev/null
+++ b/src/lib/components/TitleShowcase/TitleShowcaseVisuals.svelte
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
{releaseDate.getFullYear()}
+
+
{formatMinutesToTime(runtime)}
+
+
{tmdbRating.toFixed(1)} TMDB
+
+
+
+
+ {#each genres.slice(0, 3) as genre}
+
+ {genre}
+
+ {/each}
+
+
+
diff --git a/src/lib/components/TitleShowcase/TitleShowcasesContainer.svelte b/src/lib/components/TitleShowcase/TitleShowcasesContainer.svelte
new file mode 100644
index 0000000..1dee749
--- /dev/null
+++ b/src/lib/components/TitleShowcase/TitleShowcasesContainer.svelte
@@ -0,0 +1,166 @@
+
+
+
+
+ {#await tmdbPopularMoviesPromise then movies}
+ {@const movie = movies[showcaseIndex]}
+
+ {#key movie?.id}
+
g.name || '') || []}
+ runtime={movie?.runtime || 0}
+ releaseDate={new Date(movie?.release_date || Date.now())}
+ tmdbRating={movie?.vote_average || 0}
+ posterUri={movie?.poster_path || ''}
+ {hideUI}
+ />
+ {/key}
+
+
+ {#if !hideUI}
+
+
+
+
+
+ {/if}
+
+ v.site === 'YouTube' && v.type === 'Trailer')
+ ?.key}
+ backdropUri={movie?.backdrop_path || ''}
+ />
+ {/await}
+
+
+ {#if !continueWatchingEmpty}
+
+ Continue Watching
+ {#await nextUpProps}
+
+ {:then props}
+ {#each props as prop}
+ (window.location.href = `/${prop.type}/${prop.tmdbId}`)}
+ {...prop}
+ size="sm"
+ />
+ {/each}
+ {/await}
+
+ {/if}
+
+
diff --git a/src/lib/lang/de.json b/src/lib/lang/de.json
new file mode 100644
index 0000000..46b7a96
--- /dev/null
+++ b/src/lib/lang/de.json
@@ -0,0 +1,102 @@
+{
+ "appName": "Reiverr",
+ "setupRequiredTitle": "Willkommen zu",
+ "setupRequiredDescription": "Es scheint das einige Umgebungsvariables zur fehlerfreien Ausführung fehlen. Bitte gebe die folgenden Umgebungsvariablen an:",
+ "navbar": {
+ "home": "Start",
+ "discover": "Entdecken",
+ "library": "Bibliothek",
+ "sources": "Quellen",
+ "settings": "Einstellungen"
+ },
+ "search": {
+ "placeHolder": "Suche nach Filmen und Serien",
+ "noRecentSearches": "Kein Suchverlauf",
+ "noResults": "Keine Suchergebnisse"
+ },
+ "discover": {
+ "trending": "Aufsteigend",
+ "popularPeople": "Beliebte Personen",
+ "upcomingMovies": "Bald verfügbare Filme",
+ "upcomingSeries": "Bald verfügbare Serien",
+ "genres": "Genres",
+ "newDigitalReleases": "Neue Digitale Veröffentlichungen",
+ "streamingNow": "Aktuell im Stream",
+ "TVNetworks": "Anbieter"
+ },
+ "library": {
+ "missingConfiguration": "Konfiguriere Radarr, Sonarr und Jellyfin zum Verwalten und Abspielen der Bibliothek",
+ "available": "Verfügbar",
+ "watched": "Geschaut",
+ "unavailable": "Nicht verfügbar",
+ "sort": {
+ "byTitle": "Nach Titel"
+ },
+ "content": {
+ "movie": "Film",
+ "show": "Serie",
+ "requestContent": "Anfrage",
+ "directedBy": "Regie von",
+ "releaseDate": "Veröffentlichung",
+ "budget": "Budget",
+ "status": "Status",
+ "runtime": "Spielzeit",
+ "castAndCrew": "Cast & Crew",
+ "recommendations": "Empfehlungen",
+ "similarTitles": "Ähnliche Titel"
+ }
+ },
+ "sources": {},
+ "titleShowcase": {
+ "details": "Details",
+ "watchTrailer": "Trailer abspielen",
+ "releaseDate": "Veröffentlichung",
+ "directedBy": "Regie von"
+ },
+ "settings": {
+ "navbar": {
+ "settings": "Konfiguration",
+ "general": "Allgemein",
+ "integrations": "Integrationen"
+ },
+ "general": {
+ "userInterface": {
+ "userInterface": "Benutzeroberfläche",
+ "language": "Sprache",
+ "autoplayTrailers": "Trailer automatisch abspielen",
+ "animationDuration": "Animationsdauer"
+ },
+ "discovery": {
+ "discovery": "Entdecken",
+ "none": "Keine",
+ "region": "Region",
+ "excludeLibraryItemsFromDiscovery": "Schließe Bibliothekeinträge von 'Entdecken' aus",
+ "includedLanguages": "Enthaltene Sprachen",
+ "includedLanguagesDescription": "Filter Resultate nach gesprochener Sprache. Trage ISO 639-1 Sprachcodes kommasepariert ein. Freilassen zum deaktivieren."
+ }
+ },
+ "integrations": {
+ "integrations": "Integrationen",
+ "integrationsNote": "Anmerkungen: Basis URLs müssen vom Browser aus erreichbar sein. Interne Docker Adressen funktionieren nicht. API Schlüssel sind Sichtbar in den Browseranfragen.",
+ "baseUrl": "Basis URL",
+ "apiKey": "API Schlüssel",
+ "testConnection": "Verbindung testen",
+ "status": {
+ "connected": "Verbunden",
+ "disconnected": "Getrennt"
+ },
+ "options": {
+ "options": "Optionen",
+ "rootFolder": "Hauptordner",
+ "qualityProfile": "Qualitätsprofil",
+ "languageProfile": "Sprachprofil",
+ "jellyfinUser": "Jellyfin Benutzer"
+ }
+ },
+ "misc": {
+ "saveChanges": "Änderungen speichern",
+ "resetToDefaults": "Auf Standard zurücksetzen ",
+ "changelog": "Änderungen"
+ }
+ }
+}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 9e48f28..2e52ad1 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -2,14 +2,30 @@
import {
getJellyfinBackdrop,
getJellyfinContinueWatching,
- getJellyfinNextUp
+ getJellyfinNextUp,
+ type JellyfinItem
} from '$lib/apis/jellyfin/jellyfinApi';
- import { getTmdbMovie, getTmdbPopularMovies } from '$lib/apis/tmdb/tmdbApi';
+ import {
+ getPosterProps,
+ getTmdbMovie,
+ getTmdbPopularMovies,
+ TmdbApiOpen
+ } from '$lib/apis/tmdb/tmdbApi';
import Carousel from '$lib/components/Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '$lib/components/Carousel/CarouselPlaceholderItems.svelte';
- import EpisodeCard from '$lib/components/EpisodeCard/EpisodeCard.svelte';
- import TitleShowcase from '$lib/components/TitleShowcase/TitleShowcase.svelte';
+ import GenreCard from '$lib/components/GenreCard.svelte';
+ import NetworkCard from '$lib/components/NetworkCard.svelte';
+ import PersonCard from '$lib/components/PersonCard/PersonCard.svelte';
+ import Poster from '$lib/components/Poster/Poster.svelte';
+ import TitleShowcases from '$lib/components/TitleShowcase/TitleShowcasesContainer.svelte';
+ import { genres, networks } from '$lib/discover';
import { jellyfinItemsStore } from '$lib/stores/data.store';
+ import { settings } from '$lib/stores/settings.store';
+ import type { TitleType } from '$lib/types';
+ import { formatDateToYearMonthDay } from '$lib/utils';
+ import type { ComponentProps } from 'svelte';
+ import { _ } from 'svelte-i18n';
+ import { fade } from 'svelte/transition';
let continueWatchingVisible = true;
@@ -73,46 +89,220 @@
(showcaseIndex - 1 + (await tmdbPopularMoviesPromise).length) %
(await tmdbPopularMoviesPromise).length;
}
+
+ const jellyfinItemsPromise = new Promise((resolve) => {
+ jellyfinItemsStore.subscribe((data) => {
+ if (data.loading) return;
+ resolve(data.data || []);
+ });
+ });
+
+ const fetchCardProps = async (
+ items: {
+ name?: string;
+ title?: string;
+ id?: number;
+ vote_average?: number;
+ number_of_seasons?: number;
+ first_air_date?: string;
+ poster_path?: string;
+ }[],
+ type: TitleType | undefined = undefined
+ ): Promise[]> => {
+ const filtered = $settings.discover.excludeLibraryItems
+ ? items.filter(
+ async (item) =>
+ !(await jellyfinItemsPromise).find((i) => i.ProviderIds?.Tmdb === String(item.id))
+ )
+ : items;
+
+ return Promise.all(filtered.map(async (item) => getPosterProps(item, type))).then((props) =>
+ props.filter((p) => p.backdropUrl)
+ );
+ };
+
+ const trendingItemsPromise = TmdbApiOpen.get('/3/trending/all/{time_window}', {
+ params: {
+ path: {
+ time_window: 'day'
+ },
+ query: {
+ language: $settings.language
+ }
+ }
+ }).then((res) => res.data?.results || []);
+
+ const fetchTrendingProps = () => trendingItemsPromise.then(fetchCardProps);
+
+ const fetchTrendingActorProps = () =>
+ TmdbApiOpen.get('/3/trending/person/{time_window}', {
+ params: {
+ path: {
+ time_window: 'week'
+ }
+ }
+ })
+ .then((res) => res.data?.results || [])
+ .then((actors) =>
+ actors
+ .filter((a) => a.profile_path)
+ .map((actor) => ({
+ tmdbId: actor.id || 0,
+ backdropUri: actor.profile_path || '',
+ name: actor.name || '',
+ subtitle: actor.known_for_department || ''
+ }))
+ );
+
+ const fetchUpcomingMovies = () =>
+ TmdbApiOpen.get('/3/discover/movie', {
+ params: {
+ query: {
+ 'primary_release_date.gte': formatDateToYearMonthDay(new Date()),
+ sort_by: 'popularity.desc',
+ language: $settings.language,
+ region: $settings.discover.region,
+ with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
+ }
+ }
+ })
+ .then((res) => res.data?.results || [])
+ .then(fetchCardProps);
+
+ const fetchUpcomingSeries = () =>
+ TmdbApiOpen.get('/3/discover/tv', {
+ params: {
+ query: {
+ 'first_air_date.gte': formatDateToYearMonthDay(new Date()),
+ sort_by: 'popularity.desc',
+ language: $settings.language,
+ with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
+ }
+ }
+ })
+ .then((res) => res.data?.results || [])
+ .then((i) => fetchCardProps(i, 'series'));
+
+ const fetchDigitalReleases = () =>
+ TmdbApiOpen.get('/3/discover/movie', {
+ params: {
+ query: {
+ with_release_type: 4,
+ sort_by: 'popularity.desc',
+ 'release_date.lte': formatDateToYearMonthDay(new Date()),
+ language: $settings.language,
+ with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
+ // region: $settings.discover.region
+ }
+ }
+ })
+ .then((res) => res.data?.results || [])
+ .then(fetchCardProps);
+
+ const fetchNowStreaming = () =>
+ TmdbApiOpen.get('/3/discover/tv', {
+ params: {
+ query: {
+ 'air_date.gte': formatDateToYearMonthDay(new Date()),
+ 'first_air_date.lte': formatDateToYearMonthDay(new Date()),
+ sort_by: 'popularity.desc',
+ language: $settings.language,
+ with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
+ }
+ }
+ })
+ .then((res) => res.data?.results || [])
+ .then((i) => fetchCardProps(i, 'series'));
+
+ function parseIncludedLanguages(includedLanguages: string) {
+ return includedLanguages.replace(' ', '').split(',').join('|');
+ }
-
- {#await tmdbPopularMoviesPromise then movies}
- {@const movie = movies[showcaseIndex]}
- {#key movie?.id}
- g.name || '') || []}
- runtime={movie?.runtime || 0}
- releaseDate={new Date(movie?.release_date || Date.now())}
- tmdbRating={movie?.vote_average || 0}
- trailerId={movie?.videos?.results?.find((v) => v.site === 'YouTube' && v.type === 'Trailer')
- ?.key}
- director={movie?.credits?.crew?.find((c) => c.job === 'Director')?.name}
- backdropUri={movie?.backdrop_path || ''}
- posterUri={movie?.poster_path || ''}
- {onPrevious}
- {onNext}
- {showcaseIndex}
- showcaseLength={movies.length}
- />
- {/key}
- {/await}
-
+
-
-
- Continue Watching
- {#await nextUpProps}
+
+
+
+ {$_('discover.popularPeople')}
+
+ {#await fetchTrendingActorProps()}
{:then props}
- {#each props as prop}
- (window.location.href = `/${prop.type}/${prop.tmdbId}`)}
- {...prop}
- />
+ {#each props as prop (prop.tmdbId)}
+
{/each}
{/await}
+
+
+ {$_('discover.upcomingMovies')}
+
+ {#await fetchUpcomingMovies()}
+
+ {:then props}
+ {#each props as prop (prop.tmdbId)}
+
+ {/each}
+ {/await}
+
+
+
+ {$_('discover.upcomingSeries')}
+
+ {#await fetchUpcomingSeries()}
+
+ {:then props}
+ {#each props as prop (prop.tmdbId)}
+
+ {/each}
+ {/await}
+
+
+
+ {$_('discover.genres')}
+
+ {#each Object.values(genres) as genre (genre.tmdbGenreId)}
+
+ {/each}
+
+
+
+ {$_('discover.newDigitalReleases')}
+
+ {#await fetchDigitalReleases()}
+
+ {:then props}
+ {#each props as prop (prop.tmdbId)}
+
+ {/each}
+ {/await}
+
+
+
+ {$_('discover.streamingNow')}
+
+ {#await fetchNowStreaming()}
+
+ {:then props}
+ {#each props as prop (prop.tmdbId)}
+
+ {/each}
+ {/await}
+
+
+
+ {$_('discover.TVNetworks')}
+
+ {#each Object.values(networks) as network (network.tmdbNetworkId)}
+
+ {/each}
+
diff --git a/src/routes/discover/+page.svelte b/src/routes/discover/+page.svelte
index ee45fbf..de7f7e4 100644
--- a/src/routes/discover/+page.svelte
+++ b/src/routes/discover/+page.svelte
@@ -1,14 +1,12 @@
+
+
[] = [];
@@ -93,7 +93,14 @@
/>
{/await}
-