Merge branch 'dev'
This commit is contained in:
@@ -119,7 +119,7 @@ I'm not a designer, so if you have any ideas for improving the UI, I'd love to l
|
||||
To get started with development:
|
||||
|
||||
1. Clone the repository
|
||||
2. Check out the `dev` branch
|
||||
2. Checkout the `dev` branch
|
||||
3. Run `npm install`
|
||||
4. Run `npm run dev`
|
||||
|
||||
|
||||
56
package-lock.json
generated
56
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "reiverr",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "reiverr",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.1",
|
||||
"dependencies": {
|
||||
"@jellyfin/sdk": "^0.7.0",
|
||||
"axios": "^1.4.0",
|
||||
@@ -22,7 +22,7 @@
|
||||
"devDependencies": {
|
||||
"@fontsource/fira-mono": "^4.5.10",
|
||||
"@neoconfetti/svelte": "^1.0.0",
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
@@ -1197,22 +1197,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.0.tgz",
|
||||
"integrity": "sha512-6qXdd5edCBynOwsz1YcNfgX8tNWeuS9fxy5o59D0rvHXxRtjXRebB4gE4vFVfEMXl/z8zTnAzfOs7aQDEs8G4Q==",
|
||||
"version": "1.41.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
|
||||
"integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.35.0"
|
||||
"playwright": "1.41.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
@@ -4265,6 +4261,20 @@
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
@@ -5965,10 +5975,28 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.41.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz",
|
||||
"integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.41.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.35.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.0.tgz",
|
||||
"integrity": "sha512-muMXyPmIx/2DPrCHOD1H1ePT01o7OdKxKj2ebmCAYvqhUy+Y1bpal7B0rdoxros7YrXI294JT/DWw2LqyiqTPA==",
|
||||
"version": "1.41.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz",
|
||||
"integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "reiverr",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aleksilassila/reiverr"
|
||||
@@ -22,7 +22,7 @@
|
||||
"devDependencies": {
|
||||
"@fontsource/fira-mono": "^4.5.10",
|
||||
"@neoconfetti/svelte": "^1.0.0",
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
|
||||
@@ -135,10 +135,8 @@ export const getTmdbSeriesSeason = async (
|
||||
}
|
||||
}).then((res) => res.data);
|
||||
|
||||
export const getTmdbSeriesSeasons = async (tmdbId: number, seasons: number) =>
|
||||
Promise.all([...Array(seasons).keys()].map((i) => getTmdbSeriesSeason(tmdbId, i + 1))).then(
|
||||
(r) => r.filter((s) => s) as TmdbSeason[]
|
||||
);
|
||||
export const getTmdbSeriesSeasons = (tmdbId: number, seasons: number) =>
|
||||
[...Array(seasons).keys()].map((i) => getTmdbSeriesSeason(tmdbId, i + 1));
|
||||
|
||||
export const getTmdbSeriesImages = async (tmdbId: number) =>
|
||||
TmdbApiOpen.get('/3/tv/{series_id}/images', {
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class="grid grid-cols-4 sm:grid-cols-6 gap-4 sm:gap-x-8 rounded-xl max-w-screen-2xl 2xl:mx-auto py-4"
|
||||
class="grid grid-cols-4 sm:grid-cols-6 gap-4 sm:gap-x-8 rounded-xl py-4 max-w-screen-2xl 2xl:mx-auto"
|
||||
>
|
||||
<slot name="info-description">
|
||||
<div
|
||||
@@ -221,30 +221,9 @@
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="carousels" />
|
||||
<!-- <div class="max-w-screen-2xl 2xl:mx-auto w-full">
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<slot name="cast-crew-carousel-title" slot="title" />
|
||||
<slot name="cast-crew-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div>
|
||||
<div class="max-w-screen-2xl 2xl:mx-auto w-full">
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<slot name="recommendations-carousel-title" slot="title" />
|
||||
<slot name="recommendations-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div>
|
||||
<div class="max-w-screen-2xl 2xl:mx-auto w-full">
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<slot name="similar-carousel-title" slot="title" />
|
||||
<slot name="similar-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div> -->
|
||||
<div class="flex flex-col gap-6 max-w-screen-2xl 2xl:mx-auto">
|
||||
<!-- TODO: Remove mx-auto as it's bugged when in modal and on firefox -->
|
||||
<slot name="carousels" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
const ANIMATION_DURATION = $settings.animationDuration;
|
||||
|
||||
export let tmdbId: number;
|
||||
|
||||
export let trailerId: string | undefined = undefined;
|
||||
|
||||
export let lazyTrailerId: Promise<string | undefined>;
|
||||
export let backdropUri: string;
|
||||
|
||||
let scrollY: number;
|
||||
@@ -23,6 +21,9 @@
|
||||
export let UIVisible = true;
|
||||
$: UIVisible = !(hoverTrailer && trailerVisible);
|
||||
|
||||
let trailerId: string | undefined = undefined;
|
||||
lazyTrailerId.then((v) => (trailerId = v));
|
||||
|
||||
let trailerShowTimeout: NodeJS.Timeout | undefined = undefined;
|
||||
$: {
|
||||
tmdbId;
|
||||
@@ -64,7 +65,7 @@
|
||||
|
||||
<svelte:window bind:scrollY on:scroll={handleWindowScroll} />
|
||||
|
||||
{#if !trailerVisible}
|
||||
{#if !trailerVisible || !trailerId}
|
||||
{#key tmdbId}
|
||||
<div
|
||||
style={"background-image: url('" + TMDB_IMAGES_ORIGINAL + backdropUri + "');"}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import type { TitleType } from '$lib/types';
|
||||
import { openTitleModal } from '$lib/stores/modal.store';
|
||||
import { settings } from '$lib/stores/settings.store';
|
||||
import { TMDB_MOVIE_GENRES } from '$lib/apis/tmdb/tmdbApi';
|
||||
|
||||
const ANIMATION_DURATION = $settings.animationDuration;
|
||||
|
||||
@@ -15,8 +16,8 @@
|
||||
export let type: TitleType;
|
||||
|
||||
export let title: string;
|
||||
export let genres: string[];
|
||||
export let runtime: number;
|
||||
export let genreIds: number[];
|
||||
export let lazyRuntime: Promise<number>;
|
||||
export let releaseDate: Date;
|
||||
export let tmdbRating: number;
|
||||
|
||||
@@ -24,7 +25,14 @@
|
||||
|
||||
export let hideUI = false;
|
||||
|
||||
let runtime = 0;
|
||||
let loadingAdditionalDetails = true;
|
||||
lazyRuntime.then((rn) => (runtime = rn)).finally(() => (loadingAdditionalDetails = false));
|
||||
|
||||
$: tmdbUrl = `https://www.themoviedb.org/${type}/${tmdbId}`;
|
||||
$: genres = genreIds
|
||||
.map((gId) => TMDB_MOVIE_GENRES.find((g) => g.id === gId)?.name)
|
||||
.filter<string>((g): g is string => typeof g === 'string');
|
||||
|
||||
function handleOpenTitle() {
|
||||
openTitleModal({ type, id: tmdbId, provider: 'tmdb' });
|
||||
@@ -60,7 +68,7 @@
|
||||
>
|
||||
<p class="flex-shrink-0">{releaseDate.getFullYear()}</p>
|
||||
<DotFilled />
|
||||
<p class="flex-shrink-0">{formatMinutesToTime(runtime)}</p>
|
||||
<p class="flex-shrink-0">{loadingAdditionalDetails ? 'LOADING' : formatMinutesToTime(runtime)}</p>
|
||||
<DotFilled />
|
||||
<p class="flex-shrink-0"><a href={tmdbUrl}>{tmdbRating.toFixed(1)} TMDB</a></p>
|
||||
</div>
|
||||
|
||||
@@ -65,11 +65,49 @@
|
||||
}
|
||||
});
|
||||
|
||||
const tmdbPopularMoviesPromise = getTmdbPopularMovies()
|
||||
.then((movies) => Promise.all(movies.map((movie) => getTmdbMovie(movie.id || 0))))
|
||||
.then((movies) => movies.filter((m) => !!m).slice(0, 10));
|
||||
let popularMovies: (
|
||||
| {
|
||||
movie: Awaited<ReturnType<typeof getTmdbPopularMovies>>[0];
|
||||
lazyRuntime: Promise<number>;
|
||||
lazyTrailerId: Promise<string | undefined>;
|
||||
}
|
||||
)[] = [];
|
||||
|
||||
/**
|
||||
* Here we load a list of popular movies:
|
||||
* * runtime & video data is not available as part of the initial request
|
||||
* * If an additional detail request fails, we unload the movie from the showcase
|
||||
*/
|
||||
const tmdbPopularMoviesPromise = getTmdbPopularMovies().then(
|
||||
(movies) =>
|
||||
(popularMovies = movies.map((movie) => {
|
||||
const movieDetails = getTmdbMovie(movie.id || 0);
|
||||
const movieDetailsPromise = movieDetails.then((fullMovie) => ({
|
||||
runtime: fullMovie?.runtime || 0,
|
||||
trailerId: fullMovie?.videos?.results?.find(
|
||||
(v) => v.site === 'YouTube' && v.type === 'Trailer'
|
||||
)?.key
|
||||
}));
|
||||
|
||||
movieDetails.catch(() => unloadMovie());
|
||||
movieDetails.then((md) => !md && unloadMovie());
|
||||
const unloadMovie = () => {
|
||||
const idx = popularMovies.findIndex((m) => m.movie === movie);
|
||||
popularMovies.splice(idx, 1);
|
||||
popularMovies = popularMovies;
|
||||
};
|
||||
|
||||
return {
|
||||
movie,
|
||||
lazyRuntime: movieDetailsPromise.then((fm) => fm.runtime),
|
||||
lazyTrailerId: movieDetailsPromise.then((fm) => fm.trailerId)
|
||||
};
|
||||
}))
|
||||
);
|
||||
|
||||
let showcaseIndex = 0;
|
||||
$: clampedPopularMovies = popularMovies.slice(0, 10);
|
||||
$: visibleShowcaseMovie = clampedPopularMovies[showcaseIndex];
|
||||
|
||||
async function onNext() {
|
||||
showcaseIndex = (showcaseIndex + 1) % (await tmdbPopularMoviesPromise).length;
|
||||
@@ -95,7 +133,7 @@
|
||||
// return () => clearInterval(interval);
|
||||
// });
|
||||
|
||||
const PADDING = 'px-4 lg:px-8 xl:px-16';
|
||||
const PADDING = 'px-4 lg:px-8 2xl:px-16';
|
||||
</script>
|
||||
|
||||
<div class="h-screen flex flex-col relative pb-6 gap-6 xl:gap-8 overflow-hidden">
|
||||
@@ -105,16 +143,16 @@
|
||||
PADDING
|
||||
)}
|
||||
>
|
||||
{#await tmdbPopularMoviesPromise then movies}
|
||||
{@const movie = movies[showcaseIndex]}
|
||||
{#if visibleShowcaseMovie}
|
||||
{@const { movie, lazyRuntime, lazyTrailerId } = visibleShowcaseMovie}
|
||||
|
||||
{#key movie?.id}
|
||||
<TitleShowcaseVisuals
|
||||
tmdbId={movie?.id || 0}
|
||||
type="movie"
|
||||
title={movie?.title || ''}
|
||||
genres={movie?.genres?.map((g) => g.name || '') || []}
|
||||
runtime={movie?.runtime || 0}
|
||||
genreIds={movie.genre_ids || []}
|
||||
{lazyRuntime}
|
||||
releaseDate={new Date(movie?.release_date || Date.now())}
|
||||
tmdbRating={movie?.vote_average || 0}
|
||||
posterUri={movie?.poster_path || ''}
|
||||
@@ -124,7 +162,13 @@
|
||||
<div
|
||||
class="md:relative self-stretch flex justify-center items-end row-start-2 row-span-1 col-start-1 col-span-2 md:row-start-1 md:row-span-2 md:col-start-2 md:col-span-2"
|
||||
>
|
||||
<PageDots index={showcaseIndex} length={movies.length} {onJump} {onPrevious} {onNext} />
|
||||
<PageDots
|
||||
index={showcaseIndex}
|
||||
length={clampedPopularMovies.length}
|
||||
{onJump}
|
||||
{onPrevious}
|
||||
{onNext}
|
||||
/>
|
||||
{#if !hideUI}
|
||||
<div class="absolute top-1/2 right-0 z-10">
|
||||
<IconButton on:click={onNext}>
|
||||
@@ -133,13 +177,15 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<TitleShowcase
|
||||
tmdbId={movie?.id || 0}
|
||||
trailerId={movie?.videos?.results?.find((v) => v.site === 'YouTube' && v.type === 'Trailer')
|
||||
?.key}
|
||||
backdropUri={movie?.backdrop_path || ''}
|
||||
/>
|
||||
{/await}
|
||||
|
||||
{#key movie?.id}
|
||||
<TitleShowcase
|
||||
tmdbId={movie?.id || 0}
|
||||
{lazyTrailerId}
|
||||
backdropUri={movie?.backdrop_path || ''}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class={classNames('z-[1] transition-opacity', {
|
||||
|
||||
@@ -5,4 +5,4 @@ export const TMDB_BACKDROP_SMALL = 'https://www.themoviedb.org/t/p/w780';
|
||||
export const TMDB_POSTER_SMALL = 'https://www.themoviedb.org/t/p/w342';
|
||||
export const TMDB_PROFILE_SMALL = 'https://www.themoviedb.org/t/p/w185';
|
||||
|
||||
export const PLACEHOLDER_BACKDROP = '/plcaeholder.jpg';
|
||||
export const PLACEHOLDER_BACKDROP = '/placeholder.jpg';
|
||||
|
||||
@@ -1,102 +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 <span class='font-medium underline'>sind Sichtbar</span> 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"
|
||||
}
|
||||
}
|
||||
"appName": "Reiverr",
|
||||
"setupRequiredTitle": "Willkommen zu",
|
||||
"setupRequiredDescription": "Es scheint das einige Umgebungsvariables zur fehlerfreien Ausführung fehlen. Bitte gib 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 <span class='font-medium underline'>sind Sichtbar</span> 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +217,8 @@
|
||||
function parseIncludedLanguages(includedLanguages: string) {
|
||||
return includedLanguages.replace(' ', '').split(',').join('|');
|
||||
}
|
||||
|
||||
const PADDING = 'px-4 lg:px-8 2xl:px-16';
|
||||
</script>
|
||||
|
||||
<TitleShowcases />
|
||||
@@ -229,7 +231,7 @@
|
||||
}}
|
||||
out:fade|global={{ duration: $settings.animationDuration }}
|
||||
>
|
||||
<Carousel scrollClass="px-2 sm:px-8 2xl:px-16">
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.popularPeople')}
|
||||
</div>
|
||||
@@ -241,7 +243,7 @@
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel scrollClass="px-2 sm:px-8 2xl:px-16">
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.upcomingMovies')}
|
||||
</div>
|
||||
@@ -253,7 +255,7 @@
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel scrollClass="px-2 sm:px-8 2xl:px-16">
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.upcomingSeries')}
|
||||
</div>
|
||||
@@ -265,7 +267,7 @@
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel scrollClass="px-2 sm:px-8 2xl:px-16">
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.genres')}
|
||||
</div>
|
||||
@@ -273,7 +275,7 @@
|
||||
<GenreCard {genre} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
<Carousel scrollClass="px-2 sm:px-8 2xl:px-16">
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.newDigitalReleases')}
|
||||
</div>
|
||||
@@ -285,7 +287,7 @@
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel scrollClass="px-2 sm:px-8 2xl:px-16">
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.streamingNow')}
|
||||
</div>
|
||||
@@ -297,7 +299,7 @@
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
<Carousel scrollClass="px-2 sm:px-8 2xl:px-16">
|
||||
<Carousel scrollClass={PADDING}>
|
||||
<div slot="title" class="text-lg font-semibold text-zinc-300">
|
||||
{$_('discover.TVNetworks')}
|
||||
</div>
|
||||
|
||||
@@ -33,15 +33,14 @@
|
||||
export let handleCloseModal: () => void = () => {};
|
||||
|
||||
const tmdbUrl = 'https://www.themoviedb.org/movie/' + tmdbId;
|
||||
const data = loadInitialPageData();
|
||||
const data = getTmdbMovie(tmdbId);
|
||||
const recommendationData = preloadRecommendationData();
|
||||
|
||||
const jellyfinItemStore = createJellyfinItemStore(tmdbId);
|
||||
const radarrMovieStore = createRadarrMovieStore(tmdbId);
|
||||
const radarrDownloadStore = createRadarrDownloadStore(radarrMovieStore);
|
||||
|
||||
async function loadInitialPageData() {
|
||||
const tmdbMoviePromise = getTmdbMovie(tmdbId);
|
||||
|
||||
async function preloadRecommendationData() {
|
||||
const tmdbRecommendationProps = getTmdbMovieRecommendations(tmdbId)
|
||||
.then((r) => Promise.all(r.map(fetchCardTmdbProps)))
|
||||
.then((r) => r.filter((p) => p.backdropUrl));
|
||||
@@ -49,7 +48,7 @@
|
||||
.then((r) => Promise.all(r.map(fetchCardTmdbProps)))
|
||||
.then((r) => r.filter((p) => p.backdropUrl));
|
||||
|
||||
const castPropsPromise: Promise<ComponentProps<PersonCard>[]> = tmdbMoviePromise.then((m) =>
|
||||
const castPropsPromise: Promise<ComponentProps<PersonCard>[]> = data.then((m) =>
|
||||
Promise.all(
|
||||
m?.credits?.cast?.slice(0, 20).map((m) => ({
|
||||
tmdbId: m.id || 0,
|
||||
@@ -61,10 +60,9 @@
|
||||
);
|
||||
|
||||
return {
|
||||
tmdbMovie: await tmdbMoviePromise,
|
||||
tmdbRecommendationProps: await tmdbRecommendationProps,
|
||||
tmdbSimilarProps: await tmdbSimilarProps,
|
||||
castProps: await castPropsPromise
|
||||
castProps: await castPropsPromise,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,8 +93,7 @@
|
||||
|
||||
{#await data}
|
||||
<TitlePageLayout {isModal} {handleCloseModal} />
|
||||
{:then { tmdbMovie, tmdbRecommendationProps, tmdbSimilarProps, castProps }}
|
||||
{@const movie = tmdbMovie}
|
||||
{:then movie }
|
||||
<TitlePageLayout
|
||||
titleInformation={{
|
||||
tmdbId,
|
||||
@@ -268,7 +265,7 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="carousels">
|
||||
{#await data}
|
||||
{#await recommendationData}
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<div slot="title" class="font-medium text-lg">Cast & Crew</div>
|
||||
<CarouselPlaceholderItems />
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { getJellyfinEpisodes, type JellyfinItem } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { addSeriesToSonarr, type SonarrSeries } from '$lib/apis/sonarr/sonarrApi';
|
||||
import { addSeriesToSonarr } from '$lib/apis/sonarr/sonarrApi';
|
||||
import {
|
||||
getTmdbIdFromTvdbId,
|
||||
getTmdbSeries,
|
||||
getTmdbSeriesRecommendations,
|
||||
getTmdbSeriesSeasons,
|
||||
getTmdbSeriesSimilar
|
||||
getTmdbSeriesSimilar,
|
||||
type TmdbSeriesFull2
|
||||
} from '$lib/apis/tmdb/tmdbApi';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Card from '$lib/components/Card/Card.svelte';
|
||||
@@ -39,7 +40,8 @@
|
||||
export let isModal = false;
|
||||
export let handleCloseModal: () => void = () => {};
|
||||
|
||||
let data = loadInitialPageData();
|
||||
const data = loadInitialPageData();
|
||||
const recommendationData = preloadRecommendationData();
|
||||
|
||||
const jellyfinItemStore = createJellyfinItemStore(data.then((d) => d.tmdbId));
|
||||
const sonarrSeriesStore = createSonarrSeriesStore(data.then((d) => d.tmdbSeries?.name || ''));
|
||||
@@ -48,16 +50,16 @@
|
||||
let seasonSelectVisible = false;
|
||||
let visibleSeasonNumber: number = 1;
|
||||
let visibleEpisodeIndex: number | undefined = undefined;
|
||||
let nextJellyfinEpisode: JellyfinItem | undefined = undefined;
|
||||
|
||||
let jellyfinEpisodeData: {
|
||||
const jellyfinEpisodeData: {
|
||||
[key: string]: {
|
||||
jellyfinId: string | undefined;
|
||||
progress: number;
|
||||
watched: boolean;
|
||||
};
|
||||
} = {};
|
||||
let episodeComponents: HTMLDivElement[] = [];
|
||||
let nextJellyfinEpisode: JellyfinItem | undefined = undefined;
|
||||
const episodeComponents: HTMLDivElement[] = [];
|
||||
|
||||
// Refresh jellyfin episode data
|
||||
jellyfinItemStore.subscribe(async (value) => {
|
||||
@@ -87,65 +89,70 @@
|
||||
const tmdbId = await (titleId.provider === 'tvdb'
|
||||
? getTmdbIdFromTvdbId(titleId.id)
|
||||
: Promise.resolve(titleId.id));
|
||||
|
||||
const tmdbSeriesPromise = getTmdbSeries(tmdbId);
|
||||
const tmdbSeasonsPromise = tmdbSeriesPromise.then((s) =>
|
||||
getTmdbSeriesSeasons(s?.id || 0, s?.number_of_seasons || 0)
|
||||
);
|
||||
|
||||
const tmdbUrl = 'https://www.themoviedb.org/tv/' + tmdbId;
|
||||
|
||||
const tmdbRecommendationPropsPromise = getTmdbSeriesRecommendations(tmdbId).then((r) =>
|
||||
Promise.all(r.map(fetchCardTmdbProps))
|
||||
);
|
||||
const tmdbSimilarPropsPromise = getTmdbSeriesSimilar(tmdbId)
|
||||
.then((r) => Promise.all(r.map(fetchCardTmdbProps)))
|
||||
.then((r) => r.filter((p) => p.backdropUrl));
|
||||
|
||||
const castPropsPromise: Promise<ComponentProps<PersonCard>[]> = tmdbSeriesPromise.then((s) =>
|
||||
Promise.all(
|
||||
s?.aggregate_credits?.cast?.slice(0, 20)?.map((m) => ({
|
||||
tmdbId: m.id || 0,
|
||||
backdropUri: m.profile_path || '',
|
||||
name: m.name || '',
|
||||
subtitle: m.roles?.[0]?.character || m.known_for_department || ''
|
||||
})) || []
|
||||
)
|
||||
);
|
||||
|
||||
const tmdbEpisodePropsPromise: Promise<ComponentProps<EpisodeCard>[][]> =
|
||||
tmdbSeasonsPromise.then((seasons) =>
|
||||
seasons.map(
|
||||
(season) =>
|
||||
season?.episodes?.map((episode) => ({
|
||||
title: episode?.name || '',
|
||||
subtitle: `Episode ${episode?.episode_number}`,
|
||||
backdropUrl: TMDB_BACKDROP_SMALL + episode?.still_path || '',
|
||||
airDate:
|
||||
episode.air_date && new Date(episode.air_date) > new Date()
|
||||
? new Date(episode.air_date)
|
||||
: undefined
|
||||
})) || []
|
||||
)
|
||||
);
|
||||
const tmdbSeries = await getTmdbSeries(tmdbId);
|
||||
|
||||
return {
|
||||
tmdbId,
|
||||
tmdbSeries: await tmdbSeriesPromise,
|
||||
tmdbSeasons: await tmdbSeasonsPromise,
|
||||
tmdbUrl,
|
||||
tmdbRecommendationProps: await tmdbRecommendationPropsPromise,
|
||||
tmdbSimilarProps: await tmdbSimilarPropsPromise,
|
||||
castProps: await castPropsPromise,
|
||||
tmdbEpisodeProps: await tmdbEpisodePropsPromise
|
||||
tmdbUrl: 'https://www.themoviedb.org/tv/' + tmdbId,
|
||||
tmdbSeries,
|
||||
seasonsData: preloadAndMapSeasonsData(tmdbSeries)
|
||||
};
|
||||
}
|
||||
|
||||
async function preloadRecommendationData() {
|
||||
const { tmdbId, tmdbSeries } = await data;
|
||||
const tmdbRecommendationProps = getTmdbSeriesRecommendations(tmdbId).then((r) =>
|
||||
Promise.all(r.map(fetchCardTmdbProps))
|
||||
);
|
||||
|
||||
const tmdbSimilarProps = getTmdbSeriesSimilar(tmdbId)
|
||||
.then((r) => Promise.all(r.map(fetchCardTmdbProps)))
|
||||
.then((r) => r.filter((p) => p.backdropUrl));
|
||||
|
||||
const castProps: ComponentProps<PersonCard>[] =
|
||||
tmdbSeries?.aggregate_credits?.cast?.slice(0, 20)?.map((m) => ({
|
||||
tmdbId: m.id || 0,
|
||||
backdropUri: m.profile_path || '',
|
||||
name: m.name || '',
|
||||
subtitle: m.roles?.[0]?.character || m.known_for_department || ''
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
tmdbRecommendationProps: await tmdbRecommendationProps,
|
||||
tmdbSimilarProps: await tmdbSimilarProps,
|
||||
castProps
|
||||
};
|
||||
}
|
||||
|
||||
function preloadAndMapSeasonsData(
|
||||
tmdbSeries: TmdbSeriesFull2 | undefined
|
||||
): Promise<ComponentProps<EpisodeCard>[]>[] {
|
||||
const tmdbSeasons = getTmdbSeriesSeasons(
|
||||
tmdbSeries?.id || 0,
|
||||
tmdbSeries?.number_of_seasons || 0
|
||||
);
|
||||
|
||||
return tmdbSeasons.map((season) =>
|
||||
season.then(
|
||||
(s) =>
|
||||
s?.episodes?.map((episode) => ({
|
||||
title: episode?.name || '',
|
||||
subtitle: `Episode ${episode?.episode_number}`,
|
||||
backdropUrl: TMDB_BACKDROP_SMALL + episode?.still_path || '',
|
||||
airDate:
|
||||
episode.air_date && new Date(episode.air_date) > new Date()
|
||||
? new Date(episode.air_date)
|
||||
: undefined
|
||||
})) || []
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function playNextEpisode() {
|
||||
if (nextJellyfinEpisode?.Id) playerState.streamJellyfinId(nextJellyfinEpisode?.Id || '');
|
||||
}
|
||||
|
||||
async function refreshSonarr() {
|
||||
async function refreshSonarr() {
|
||||
await sonarrSeriesStore.refreshIn();
|
||||
}
|
||||
|
||||
@@ -211,7 +218,7 @@
|
||||
</Carousel>
|
||||
</div>
|
||||
</TitlePageLayout>
|
||||
{:then { tmdbSeries, tmdbId, ...data }}
|
||||
{:then { tmdbId, tmdbUrl, tmdbSeries, seasonsData }}
|
||||
<TitlePageLayout
|
||||
titleInformation={{
|
||||
tmdbId,
|
||||
@@ -230,7 +237,7 @@
|
||||
<DotFilled />
|
||||
{tmdbSeries?.status}
|
||||
<DotFilled />
|
||||
<a href={data.tmdbUrl} target="_blank">{tmdbSeries?.vote_average?.toFixed(1)} TMDB</a>
|
||||
<a href={tmdbUrl} target="_blank">{tmdbSeries?.vote_average?.toFixed(1)} TMDB</a>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="title-right">
|
||||
@@ -311,24 +318,28 @@
|
||||
{/each}
|
||||
</UiCarousel>
|
||||
{#key visibleSeasonNumber}
|
||||
{#each data.tmdbEpisodeProps[visibleSeasonNumber - 1] || [] as props, i}
|
||||
{@const jellyfinData = jellyfinEpisodeData[`S${visibleSeasonNumber}E${i + 1}`]}
|
||||
<div bind:this={episodeComponents[i]}>
|
||||
<EpisodeCard
|
||||
{...props}
|
||||
{...jellyfinData
|
||||
? {
|
||||
watched: jellyfinData.watched,
|
||||
progress: jellyfinData.progress,
|
||||
jellyfinId: jellyfinData.jellyfinId
|
||||
}
|
||||
: {}}
|
||||
on:click={() => (visibleEpisodeIndex = i)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
{#await seasonsData[visibleSeasonNumber - 1]}
|
||||
<CarouselPlaceholderItems />
|
||||
{/each}
|
||||
{:then seasonEpisodes}
|
||||
{#each seasonEpisodes || [] as props, i}
|
||||
{@const jellyfinData = jellyfinEpisodeData[`S${visibleSeasonNumber}E${i + 1}`]}
|
||||
<div bind:this={episodeComponents[i]}>
|
||||
<EpisodeCard
|
||||
{...props}
|
||||
{...jellyfinData
|
||||
? {
|
||||
watched: jellyfinData.watched,
|
||||
progress: jellyfinData.progress,
|
||||
jellyfinId: jellyfinData.jellyfinId
|
||||
}
|
||||
: {}}
|
||||
on:click={() => (visibleEpisodeIndex = i)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<CarouselPlaceholderItems />
|
||||
{/each}
|
||||
{/await}
|
||||
{/key}
|
||||
</Carousel>
|
||||
</div>
|
||||
@@ -439,7 +450,7 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="carousels">
|
||||
{#await data}
|
||||
{#await recommendationData}
|
||||
<Carousel gradientFromColor="from-stone-950">
|
||||
<div slot="title" class="font-medium text-lg">Cast & Crew</div>
|
||||
<CarouselPlaceholderItems />
|
||||
|
||||
122
tests/UI.spec.ts
Normal file
122
tests/UI.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
//@ts-check
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const file = fileURLToPath(new URL('../package.json', import.meta.url));
|
||||
const json = readFileSync(file, 'utf8');
|
||||
const pkg = JSON.parse(json);
|
||||
|
||||
test.describe('UI Tests', () => {
|
||||
test('Home page', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByText('Home')).toBeVisible();
|
||||
await page.getByText('Home').click();
|
||||
await test.step('Check top bar links exist', async () => {
|
||||
await expect(page.getByRole('link', {name: 'Reiverr', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Reiverr', exact: true})).toHaveAttribute('href', '/')
|
||||
await expect(page.getByRole('link', {name: 'Home', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Home', exact: true})).toHaveAttribute('href', '/')
|
||||
await expect(page.getByRole('link', {name: 'Library', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Library', exact: true})).toHaveAttribute('href', '/library')
|
||||
await expect(page.getByRole('link', {name: 'Sources', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Sources', exact: true})).toHaveAttribute('href', '/sources')
|
||||
await expect(page.getByRole('link', {name: 'Settings', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Settings', exact: true})).toHaveAttribute('href', '/settings')
|
||||
});
|
||||
|
||||
await test.step('Check Carousel sections exist', async () => {
|
||||
await expect(page.getByText('Popular People', {exact: true})).toBeVisible();
|
||||
await expect(page.getByText('Upcoming Movies', {exact: true})).toBeVisible();
|
||||
await expect(page.getByText('Upcoming Series', {exact: true})).toBeVisible();
|
||||
await expect(page.getByText('Genres', {exact: true})).toBeVisible();
|
||||
await expect(page.getByText('New Digital Releases', {exact: true})).toBeVisible();
|
||||
await expect(page.getByText('Streaming Now', {exact: true})).toBeVisible();
|
||||
await expect(page.getByText('TV Networks', {exact: true})).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('Library', async ({ page }) => {
|
||||
await page.goto('/library');
|
||||
await expect(page.getByText('Latest Addition')).toBeVisible();
|
||||
await expect(page.getByRole('button', {name: 'Available', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('button', {name: 'Watched', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('button', {name: 'Unavailable', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('button', {name: 'Play', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('button', {name: 'Details', exact: true})).toBeVisible();
|
||||
});
|
||||
|
||||
test('Sources', async ({ page }) => {
|
||||
await page.goto('/sources');
|
||||
await expect(page.getByText('Movies Provider')).toBeVisible();
|
||||
await expect(page.getByText('Radarr')).toBeVisible();
|
||||
await expect(page.getByText('Shows Provider')).toBeVisible();
|
||||
await expect(page.getByText('Sonarr')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Settings', async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
|
||||
await test.step('Check top bar links exist', async () => {
|
||||
await expect(page.getByRole('link', {name: 'Reiverr', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Reiverr', exact: true})).toHaveAttribute('href', '/')
|
||||
await expect(page.getByRole('link', {name: 'Home', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Home', exact: true})).toHaveAttribute('href', '/')
|
||||
await expect(page.getByRole('link', {name: 'Library', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Library', exact: true})).toHaveAttribute('href', '/library')
|
||||
await expect(page.getByRole('link', {name: 'Sources', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Sources', exact: true})).toHaveAttribute('href', '/sources')
|
||||
await expect(page.getByRole('link', {name: 'Settings', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Settings', exact: true})).toHaveAttribute('href', '/settings')
|
||||
});
|
||||
|
||||
await test.step('Check side buttons exist', async () => {
|
||||
await expect(page.getByRole('button', {name: 'General', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('button', {name: 'Integrations', exact: true})).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Check bottom bar links exist', async () => {
|
||||
await expect(page.getByText(pkg.version)).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Changelog', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Changelog', exact: true})).toHaveAttribute('href', 'https://github.com/aleksilassila/reiverr/releases');
|
||||
await expect(page.getByRole('link', {name: 'GitHub', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'GitHub', exact: true})).toHaveAttribute('href', 'https://github.com/aleksilassila/reiverr');
|
||||
});
|
||||
await test.step('Check User Interface section', async () => {
|
||||
await expect(page.getByRole('heading', {name: 'User Interface'})).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Language', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('combobox').first()).toBeVisible();
|
||||
await expect(page.getByRole('button', {name: 'Save Changes'})).toHaveClass(/cursor-not-allowed/);
|
||||
await page.getByRole('combobox').first().click()
|
||||
await page.getByRole('combobox').first().selectOption('en')
|
||||
await expect(page.getByRole('button', {name: 'Save Changes'})).toHaveClass(/cursor-not-allowed/);
|
||||
await page.getByRole('combobox').first().click()
|
||||
await page.getByRole('combobox').first().selectOption('de')
|
||||
await expect(page.getByRole('button', {name: 'Save Changes'})).not.toHaveClass(/cursor-not-allowed/);
|
||||
await page.getByRole('combobox').first().click()
|
||||
await page.getByRole('combobox').first().selectOption('es')
|
||||
await expect(page.getByRole('button', {name: 'Save Changes'})).not.toHaveClass(/cursor-not-allowed/);
|
||||
await page.getByRole('combobox').first().click()
|
||||
await page.getByRole('combobox').first().selectOption('fr')
|
||||
await expect(page.getByRole('button', {name: 'Save Changes'})).not.toHaveClass(/cursor-not-allowed/);
|
||||
await page.getByRole('combobox').first().click()
|
||||
await page.getByRole('combobox').first().selectOption('it')
|
||||
await expect(page.getByRole('button', {name: 'Save Changes'})).not.toHaveClass(/cursor-not-allowed/);
|
||||
await page.getByRole('combobox').first().click()
|
||||
await page.getByRole('combobox').first().selectOption('en')
|
||||
await expect(page.getByRole('button', {name: 'Save Changes'})).toHaveClass(/cursor-not-allowed/);
|
||||
await expect(page.locator('.w-11').first()).toBeEnabled();
|
||||
await expect(page.getByRole('spinbutton')).toHaveValue('150')
|
||||
});
|
||||
|
||||
await test.step('Check Discovery section', async () => {
|
||||
await expect(page.getByRole('heading', {name: 'Discovery', exact: true})).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Region', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('combobox').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Exclude library items from Discovery', exact: true })).toBeVisible();
|
||||
await expect(page.locator('.w-11').nth(1)).toBeEnabled();
|
||||
await expect(page.getByText('Filter results based on spoken language. Takes ISO 639-1 language codes separated with commas. Leave empty to disable.')).toBeVisible();
|
||||
await expect(page.getByPlaceholder('en,fr,de')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('about page has expected h1', async ({ page }) => {
|
||||
await page.goto('/about');
|
||||
await expect(page.getByRole('heading', { name: 'About this app' })).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user