diff --git a/package-lock.json b/package-lock.json index c6d7f61..4bcb853 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "postcss": "^8.4.24", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.8.1", - "svelte": "^3.54.0", + "svelte": "^4.1.0", "svelte-check": "^3.0.1", "tailwindcss": "^3.3.2", "tslib": "^2.4.1", @@ -68,7 +68,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -2625,7 +2624,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -2639,7 +2637,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -2648,7 +2645,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -2656,14 +2652,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.18", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" @@ -2672,8 +2666,7 @@ "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "node_modules/@neoconfetti/svelte": { "version": "1.0.0", @@ -2920,6 +2913,11 @@ "integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==", "dev": true }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" + }, "node_modules/@types/js-yaml": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", @@ -3219,10 +3217,9 @@ } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true, + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "bin": { "acorn": "bin/acorn" }, @@ -3380,6 +3377,14 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -3487,6 +3492,14 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/babel-plugin-syntax-trailing-function-commas": { "version": "7.0.0-beta.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz", @@ -3966,6 +3979,18 @@ "node": ">=0.8" } }, + "node_modules/code-red": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.3.tgz", + "integrity": "sha512-kVwJELqiILQyG5aeuyKFbdsI1fmQy1Cmf7dQ8eGmVuJoaRVdwey7WaMknr2ZFeVSYSKT0rExsa8EGw0aoI/1QQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.14", + "@types/estree": "^1.0.0", + "acorn": "^8.8.2", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4097,6 +4122,18 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4203,6 +4240,14 @@ "node": ">= 0.6.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -4594,6 +4639,14 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5607,6 +5660,14 @@ "node": ">=8" } }, + "node_modules/is-reference": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.1.tgz", + "integrity": "sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -5869,6 +5930,11 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6021,7 +6087,6 @@ "version": "0.30.0", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", - "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.13" }, @@ -6038,6 +6103,11 @@ "node": ">=0.10.0" } }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6657,6 +6727,16 @@ "node": "*" } }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -7419,7 +7499,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7586,11 +7665,26 @@ } }, "node_modules/svelte": { - "version": "3.59.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.1.tgz", - "integrity": "sha512-pKj8fEBmqf6mq3/NfrB9SLtcJcUvjYSWyePlfCqN9gujLB25RitWK8PvFzlwim6hD/We35KbPlRteuA6rnPGcQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.1.0.tgz", + "integrity": "sha512-qob6IX0ui4Z++Lhwzvqb6aig79WhwsF3z6y1YMicjvw0rv71hxD+RmMFG3BM8lB7prNLXeOLnP64Zrynqa3Gtw==", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^3.2.1", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.0", + "periscopic": "^3.1.0" + }, "engines": { - "node": ">= 8" + "node": ">=16" } }, "node_modules/svelte-apollo": { diff --git a/package.json b/package.json index 175bb77..2e52984 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "postcss": "^8.4.24", "prettier": "^2.8.0", "prettier-plugin-svelte": "^2.8.1", - "svelte": "^3.54.0", + "svelte": "^4.1.0", "svelte-check": "^3.0.1", "tailwindcss": "^3.3.2", "tslib": "^2.4.1", diff --git a/src/lib/apis/jellyfin/jellyfinApi.ts b/src/lib/apis/jellyfin/jellyfinApi.ts index 1e11d08..31ec068 100644 --- a/src/lib/apis/jellyfin/jellyfinApi.ts +++ b/src/lib/apis/jellyfin/jellyfinApi.ts @@ -69,6 +69,9 @@ export const getJellyfinEpisodes = (seriesId: string) => recursive: true, includeItemTypes: ['Episode'] } + }, + headers: { + 'cache-control': 'max-age=10' } }).then((r) => r.data?.Items?.filter((i) => i.SeriesId === seriesId) || []); diff --git a/src/lib/apis/radarr/radarrApi.ts b/src/lib/apis/radarr/radarrApi.ts index 738e603..29e4fd7 100644 --- a/src/lib/apis/radarr/radarrApi.ts +++ b/src/lib/apis/radarr/radarrApi.ts @@ -92,9 +92,9 @@ export const cancelDownloadRadarrMovie = async (downloadId: number) => { return deleteResponse.response.ok; }; -export const fetchRadarrReleases = (movieId: string) => - RadarrApi.get('/api/v3/release', { params: { query: { movieId: Number(movieId) } } }).then( - (r) => r.data +export const fetchRadarrReleases = (movieId: number) => + RadarrApi.get('/api/v3/release', { params: { query: { movieId: movieId } } }).then( + (r) => r.data || [] ); export const downloadRadarrMovie = (guid: string) => @@ -127,6 +127,9 @@ export const getRadarrDownloads = (): Promise => export const getRadarrDownloadsById = (radarrId: number) => getRadarrDownloads().then((downloads) => downloads.filter((d) => d.movie.id === radarrId)); +export const getRadarrDownloadsByTmdbId = (tmdbId: number) => + getRadarrDownloads().then((downloads) => downloads.filter((d) => d.movie.tmdbId === tmdbId)); + const lookupRadarrMovieByTmdbId = (tmdbId: number) => RadarrApi.get('/api/v3/movie/lookup/tmdb', { params: { diff --git a/src/lib/apis/sonarr/sonarrApi.ts b/src/lib/apis/sonarr/sonarrApi.ts index a3b567b..59b02cb 100644 --- a/src/lib/apis/sonarr/sonarrApi.ts +++ b/src/lib/apis/sonarr/sonarrApi.ts @@ -10,6 +10,7 @@ export type SonarrSeries = components['schemas']['SeriesResource']; export type SonarrReleaseResource = components['schemas']['ReleaseResource']; export type SonarrDownload = components['schemas']['QueueResource'] & { series: SonarrSeries }; export type DiskSpaceInfo = components['schemas']['DiskSpaceResource']; +export type SonarrEpisode = components['schemas']['EpisodeResource']; export interface SonarrSeriesOptions { title: string; @@ -176,3 +177,23 @@ export const getSonarrEpisodes = async (seriesId: number) => { episodeFile: episodeFiles.find((file) => file.id === episode.episodeFileId) })); }; + +export const fetchSonarrReleases = async (episodeId: number) => { + return SonarrApi.get('/api/v3/release', { + params: { + query: { + episodeId + } + } + }).then((r) => r.data || []); +}; + +export const fetchSonarrEpisodes = async (seriesId: number): Promise => { + return SonarrApi.get('/api/v3/episode', { + params: { + query: { + seriesId + } + } + }).then((r) => r.data || []); +}; diff --git a/src/lib/apis/tmdb/tmdbApi.ts b/src/lib/apis/tmdb/tmdbApi.ts index 63ba9a3..7e66290 100644 --- a/src/lib/apis/tmdb/tmdbApi.ts +++ b/src/lib/apis/tmdb/tmdbApi.ts @@ -6,6 +6,8 @@ import createClient from 'openapi-fetch'; export type SeriesDetails = operations['tv-series-details']['responses']['200']['content']['application/json']; +export type SeasonDetails = + operations['tv-season-details']['responses']['200']['content']['application/json']; export interface SeriesDetailsFull extends SeriesDetails { videos: { results: Video[]; @@ -52,6 +54,9 @@ export const getTmdbSeriesFromTvdbId = async (tvdbId: number): Promise => query: { external_source: 'tvdb_id' } + }, + headers: { + 'Cache-Control': 'max-age=86400' } }).then((res) => res.data?.tv_results?.[0]); @@ -70,7 +75,10 @@ export const getTmdbSeries = async (tmdbId: number): Promise res.data as SeriesDetailsFull | undefined); -export const getTmdbSeriesSeason = async (tmdbId: number, season: number) => +export const getTmdbSeriesSeason = async ( + tmdbId: number, + season: number +): Promise => TmdbApiOpen.get('/3/tv/{series_id}/season/{season_number}', { params: { path: { @@ -81,7 +89,9 @@ export const getTmdbSeriesSeason = async (tmdbId: number, season: number) => }).then((res) => res.data); export const getTmdbSeriesSeasons = async (tmdbId: number, seasons: number) => - Promise.all([...Array(seasons).keys()].map((i) => getTmdbSeriesSeason(tmdbId, i + 1))); + Promise.all([...Array(seasons).keys()].map((i) => getTmdbSeriesSeason(tmdbId, i + 1))).then( + (r) => r.filter((s) => s) as SeasonDetails[] + ); export const getTmdbSeriesImages = async (tmdbId: number) => TmdbApiOpen.get('/3/tv/{series_id}/images', { @@ -89,6 +99,9 @@ export const getTmdbSeriesImages = async (tmdbId: number) => path: { series_id: tmdbId } + }, + headers: { + 'Cache-Control': 'max-age=86400' } }).then((res) => res.data); @@ -108,7 +121,11 @@ export const fetchTmdbMovieVideos = async (tmdbId: string): Promise => await TmdbApi.get('/movie/' + tmdbId + '/videos').then((res) => res.data.results); export const fetchTmdbMovieImages = async (tmdbId: string): Promise => - await TmdbApi.get('/movie/' + tmdbId + '/images').then((res) => res.data); + await TmdbApi.get('/movie/' + tmdbId + '/images', { + headers: { + 'Cache-Control': 'max-age=86400' + } + }).then((res) => res.data); export const fetchTmdbMovieCredits = async (tmdbId: string): Promise => await TmdbApi.get('/movie/' + tmdbId + '/credits').then((res) => res.data.cast); diff --git a/src/lib/components/Card/Card.svelte b/src/lib/components/Card/Card.svelte index f3c3390..b5de86a 100644 --- a/src/lib/components/Card/Card.svelte +++ b/src/lib/components/Card/Card.svelte @@ -16,7 +16,7 @@ export let available = true; export let progress = 0; - export let size: 'dynamic' | 'md' | 'large' = 'md'; + export let size: 'dynamic' | 'md' | 'lg' = 'md'; export let randomProgress = false; if (randomProgress) { progress = Math.random() > 0.3 ? Math.random() * 100 : 0; @@ -30,7 +30,7 @@ 'rounded overflow-hidden relative shadow-2xl shrink-0 aspect-video selectable', { 'h-40': size === 'md', - 'h-60': size === 'large', + 'h-60': size === 'lg', 'w-full': size === 'dynamic' } )} diff --git a/src/lib/components/Card/CardPlaceholder.svelte b/src/lib/components/Card/CardPlaceholder.svelte index 3552ac8..8ce49bc 100644 --- a/src/lib/components/Card/CardPlaceholder.svelte +++ b/src/lib/components/Card/CardPlaceholder.svelte @@ -2,13 +2,13 @@ import classNames from 'classnames'; export let index = 0; - export let size: 'dynamic' | 'md' | 'large' = 'md'; + export let size: 'dynamic' | 'md' | 'lg' = 'md';
import CardPlaceholder from '../Card/CardPlaceholder.svelte'; - export let size: 'dynamic' | 'md' | 'large' = 'md'; + export let size: 'dynamic' | 'md' | 'lg' = 'md'; {#each Array(10) as _, i (i)} diff --git a/src/lib/components/Modal/Modal.svelte b/src/lib/components/Modal/Modal.svelte index dec85ae..2d967f0 100644 --- a/src/lib/components/Modal/Modal.svelte +++ b/src/lib/components/Modal/Modal.svelte @@ -1,14 +1,34 @@ -{#if visible} + + +{#if visible && $modalStack.top === modalId} + +
({ + stack: [], + top: undefined + }); + + return { + ...store, + push: (symbol: Symbol) => { + store.update((s) => { + s.stack.push(symbol); + s.top = symbol; + return s; + }); + }, + remove: (symbol: Symbol) => { + store.update((s) => { + s.stack = s.stack.filter((x) => x !== symbol); + s.top = s.stack[s.stack.length - 1]; + return s; + }); + } + }; +} + +export const modalStack = createModalStack(); diff --git a/src/lib/components/Navbar/Navbar.svelte b/src/lib/components/Navbar/Navbar.svelte index 3db38a0..b7c2f09 100644 --- a/src/lib/components/Navbar/Navbar.svelte +++ b/src/lib/components/Navbar/Navbar.svelte @@ -22,7 +22,7 @@ 'fixed px-8 inset-x-0 grid grid-cols-[min-content_1fr_min-content] items-center z-10', 'transition-all', { - 'bg-zinc-900 bg-opacity-50 backdrop-blur-2xl h-16': !transparent, + 'bg-stone-900 bg-opacity-50 backdrop-blur-2xl h-16': !transparent, 'h-24': transparent } ); diff --git a/src/lib/components/RequestModal/RequestModal.svelte b/src/lib/components/RequestModal/RequestModal.svelte index fe0bb51..bba0fd3 100644 --- a/src/lib/components/RequestModal/RequestModal.svelte +++ b/src/lib/components/RequestModal/RequestModal.svelte @@ -1,59 +1,96 @@ @@ -61,32 +98,34 @@ {#await releasesResponse}
Loading...
- {:then data} - {#if showAllReleases ? data?.allReleases?.length : data?.filtered?.length} + {:then { releases, filtered, releasesSkipped }} + {#if showAllReleases ? releases?.length : filtered?.length}
- {#each showAllReleases ? data.allReleases : data.filtered as release} + {#each showAllReleases ? releases : filtered as release}
+ +
toggleShowDetails(release.guid)} + on:click={() => toggleShowDetails(release.guid || null)} >
{release.indexer}
-
{release.quality.quality.name}
+
{release?.quality?.quality?.name}
{release.seeders} seeders
-
{formatSize(release.size)}
+
{formatSize(release?.size || 0)}
{#if release.guid !== downloadingGuid} handleDownload(release.guid)} - disabled={downloadFetching === release.guid} + on:click={() => release.guid && handleDownload(release.guid)} + disabled={downloadFetchingGuid === release.guid} > - + {:else}
- +
{/if}
@@ -96,14 +135,14 @@
{release.title}
- -
{formatMinutesToTime(release.ageMinutes)} old
- + +
{formatMinutesToTime(release.ageMinutes || 0)} old
+
{release.seeders} seeders / {release.leechers} leechers
- + {#if release.seeders}
- {formatSize(release.size / release.seeders)} per seeder + {formatSize((release.size || 0) / release.seeders)} per seeder
{/if}
@@ -111,12 +150,12 @@
{/each}
- {#if data?.releasesSkipped > 0} + {#if releasesSkipped > 0}
- {showAllReleases ? 'Show less' : `Show all ${data.releasesSkipped} releases`} + {showAllReleases ? 'Show less' : `Show all ${releasesSkipped} releases`}
{/if} {:else} diff --git a/src/lib/components/RequestModal/SeriesRequestModal.svelte b/src/lib/components/RequestModal/SeriesRequestModal.svelte new file mode 100644 index 0000000..df21019 --- /dev/null +++ b/src/lib/components/RequestModal/SeriesRequestModal.svelte @@ -0,0 +1,79 @@ + + + + + +
+ {#await episodesPromise then seasons} + {console.log('saesons', seasons)} + {#each seasons as episodes, i} + {#if i > 0} +
+ {/if} + + {#each episodes as episode} +
+
+
+
+ {episode.episodeNumber}. {episode.title} +
+
+ {episode.airDate} +
+
+
+
+
+
+ {episode.episodeNumber}. {episode.title} +
+
+ {episode.airDate} +
+
+
+
+ {/each} + {/each} + {/await} +
+ + diff --git a/src/lib/components/ResourceDetails/LibraryDetails.svelte b/src/lib/components/ResourceDetails/LibraryDetails.svelte index a1ce9f6..00f89a7 100644 --- a/src/lib/components/ResourceDetails/LibraryDetails.svelte +++ b/src/lib/components/ResourceDetails/LibraryDetails.svelte @@ -1,205 +1,154 @@ -{#await dataPromise then data} -
- {#if !downloadProps.length && !seasonFileProps.length && !movieFileProps.length} -
-

No sources found

-

- No local or remote sources found for this title. You can configure your sources on the sources page. -

-
- {/if} +
+ {#if !downloadProps.length && !seasonFileProps.length && !movieFileProps.length} +
+

No sources found

+

+ No local or remote sources found for this title. You can configure your sources on the sources page. +

+
+ {/if} - {#if data.isAdded} -
-
-
Local Library
- - + {#if isAdded} +
+
+
Local Library
+ + + + servarrId && removeFromServarr(servarrId)} + > + + +
+ + - servarrId && removeFromServarr(servarrId)} - > - - -
- - - -
- - {#if downloadProps.length} -
-
- Downloading -
- +
+ {#if downloadProps.length} +
+
+ Downloading
- {/if} - {#each downloadProps as props} - - {/each} - {#if movieFileProps.length} -
-
- Available -
- + +
+ {/if} + {#each downloadProps as props} + + {/each} + {#if movieFileProps.length} +
+
+ Available
- {/if} - {#each movieFileProps as props} - - {/each} - {#each seasonFileProps as seasonProps, i} -
+ +
+ {/if} + {#each movieFileProps as props} + + {/each} + {#each seasonFileProps as seasonProps, i} + {#if seasonProps?.length} +
Season {i + 1}
@@ -393,66 +304,37 @@ {#each seasonProps as props} {/each} - {/each} - - - {#if !downloadProps.length && !seasonFileProps.length && !movieFileProps.length} -
Click + to add files
{/if} -
- {/if} -
- -
- {#if !data.isAdded || false} - {#if !data.isAdded} - + {/each} + {#if !downloadProps.length && !seasonFileProps.length && !movieFileProps.length} +
Click + to add files
{/if} - - - - {/if} -
+
+ {/if} +
- -{:catch err} - Could not load local movie data. - {console.error(err)} -{/await} +
+ {#if !isAdded || false} + {#if !isAdded} + + {/if} + + + + {/if} +
+ +{#if isAdded && servarrId && type === 'movie'} + setTimeout(refetch, 5000)} + /> +{:else if isAdded && servarrId && type === 'tv'} + +{:else} +
NO CONTENT
+ {console.log('NO CONTENT')} +{/if} diff --git a/src/lib/components/ResourceDetails/ResourceDetails.svelte b/src/lib/components/ResourceDetails/ResourceDetails.svelte index b0203c1..83fc90f 100644 --- a/src/lib/components/ResourceDetails/ResourceDetails.svelte +++ b/src/lib/components/ResourceDetails/ResourceDetails.svelte @@ -1,6 +1,6 @@ @@ -292,15 +514,11 @@ {/if}
- {#await nextEpisodeCardPropsPromise} - - {:then nextEpisodeCardProps} - {#if nextEpisodeCardProps} -
- -
- {/if} - {/await} + {#if nextEpisodeCardProps} +
+ +
+ {/if}
@@ -310,11 +528,43 @@
- {#key tmdbId} + {#if jellyfinId !== null && type === 'tv'} + + {/if} + + + {#key tmdbId}
diff --git a/src/lib/components/ResourceDetails/SeasonsDetails.svelte b/src/lib/components/ResourceDetails/SeasonsDetails.svelte index 6b9af4f..dff67c8 100644 --- a/src/lib/components/ResourceDetails/SeasonsDetails.svelte +++ b/src/lib/components/ResourceDetails/SeasonsDetails.svelte @@ -1,39 +1,32 @@
- {#await seasonsPromise} + {#await seriesPromise}
{#each [...Array(3).keys()] as season} diff --git a/src/lib/requestCache.ts b/src/lib/requestCache.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/stores/library.store.ts b/src/lib/stores/library.store.ts index 83f8891..153400a 100644 --- a/src/lib/stores/library.store.ts +++ b/src/lib/stores/library.store.ts @@ -1,6 +1,5 @@ import { getJellyfinContinueWatching, - getJellyfinItem, getJellyfinItems, type JellyfinItem } from '$lib/apis/jellyfin/jellyfinApi'; @@ -18,13 +17,15 @@ import { } from '$lib/apis/sonarr/sonarrApi'; import { fetchTmdbMovieImages, - getTmdbIdFromTvdbId, getTmdbSeriesFromTvdbId, getTmdbSeriesImages } from '$lib/apis/tmdb/tmdbApi'; import { writable } from 'svelte/store'; -interface PlayableItem { +export interface PlayableItem { + type: 'movie' | 'series'; + tmdbId: number; + tmdbRating: number; cardBackdropUrl: string; download?: { progress: number; @@ -36,21 +37,17 @@ interface PlayableItem { }; isPlayed: boolean; jellyfinId?: string; -} - -export interface PlayableRadarrMovie extends RadarrMovie, PlayableItem {} -export interface PlayableSonarrSeries extends SonarrSeries, PlayableItem { - tmdbId?: number; - tmdbRating: number; + jellyfinItem?: JellyfinItem; + radarrMovie?: RadarrMovie; + radarrDownloads?: RadarrDownload[]; + sonarrSeries?: SonarrSeries; + sonarrDownloads?: SonarrDownload[]; } export interface Library { - movies: PlayableRadarrMovie[]; - totalMovies: number; - series: PlayableSonarrSeries[]; - totalSeries: number; - getMovie: (tmdbId: number) => PlayableRadarrMovie | undefined; - getSeries: (tmdbId: number) => PlayableSonarrSeries | undefined; + items: Record; + itemsArray: PlayableItem[]; + continueWatching: PlayableItem[]; } async function getLibrary(): Promise { @@ -63,43 +60,25 @@ async function getLibrary(): Promise { const continueWatchingPromise = getJellyfinContinueWatching(); const jellyfinLibraryItemsPromise = getJellyfinItems(); - const movies: PlayableRadarrMovie[] = await radarrMoviesPromise.then(async (radarrMovies) => { - const radarrDownloads = await radarrDownloadsPromise; - const continueWatching = await continueWatchingPromise; - const jellyfinItems = await jellyfinLibraryItemsPromise; + const radarrMovies = await radarrMoviesPromise; + const radarrDownloads = await radarrDownloadsPromise; - return getLibraryMovies(radarrMovies, radarrDownloads, continueWatching, jellyfinItems); - }); + const sonarrSeries = await sonarrSeriesPromise; + const sonarrDownloads = await sonarrDownloadsPromise; - const series: PlayableSonarrSeries[] = await sonarrSeriesPromise.then(async (sonarrSeries) => { - const sonarrDownloads = await sonarrDownloadsPromise; - const continueWatching = await continueWatchingPromise; - const jellyfinItems = await jellyfinLibraryItemsPromise; + const jellyfinContinueWatching = await continueWatchingPromise; + const jellyfinLibraryItems = await jellyfinLibraryItemsPromise; - return getLibrarySeries(sonarrSeries, sonarrDownloads, continueWatching, jellyfinItems); - }); + const items: Record = {}; - return { - movies, - totalMovies: movies?.length || 0, - series, - totalSeries: series?.length || 0, - getMovie: (tmdbId: number) => movies.find((m) => m.tmdbId === tmdbId), - getSeries: (tmdbId: number) => series.find((s) => s.tmdbId === tmdbId) - }; -} - -export const library = writable>(getLibrary()); - -async function getLibraryMovies( - radarrMovies: RadarrMovie[], - radarrDownloads: RadarrDownload[], - jellyfinContinueWatching: JellyfinItem[], - jellyfinItems: JellyfinItem[] -): Promise { - const playableMoviesPromises = radarrMovies.map(async (m) => { - const radarrDownload = radarrDownloads.find((d) => d.movie.tmdbId === m.tmdbId); - const jellyfinItem = jellyfinItems.find((i) => i.ProviderIds?.Tmdb === String(m.tmdbId)); + const moviesPromise: Promise[] = radarrMovies.map(async (radarrMovie) => { + const itemRadarrDownloads = radarrDownloads.filter( + (d) => d.movie.tmdbId === radarrMovie.tmdbId + ); + const radarrDownload = itemRadarrDownloads[0]; + const jellyfinItem = jellyfinLibraryItems.find( + (i) => i.ProviderIds?.Tmdb === String(radarrMovie.tmdbId) + ); const downloadProgress = radarrDownload?.sizeleft && radarrDownload?.size @@ -122,32 +101,33 @@ async function getLibraryMovies( ? { length, progress: watchingProgress } : undefined; - const backdropUrl = await fetchTmdbMovieImages(String(m.tmdbId)).then( + const backdropUrl = await fetchTmdbMovieImages(String(radarrMovie.tmdbId)).then( (r) => r.backdrops.find((b) => b.iso_639_1 === 'en')?.file_path ); return { - ...m, + type: 'movie' as const, + tmdbId: radarrMovie.tmdbId || 0, + tmdbRating: radarrMovie.ratings?.tmdb?.value || 0, cardBackdropUrl: backdropUrl || '', download, continueWatching, isPlayed: jellyfinItem?.UserData?.Played || false, - jellyfinId: jellyfinItem?.Id + jellyfinId: jellyfinItem?.Id, + jellyfinItem, + radarrMovie, + radarrDownloads: itemRadarrDownloads }; }); - return await Promise.all(playableMoviesPromises); -} - -async function getLibrarySeries( - sonarrSeries: SonarrSeries[], - sonarrDownloads: SonarrDownload[], - jellyfinContinueWatching: JellyfinItem[], - jellyfinItems: JellyfinItem[] -): Promise { - const playableSeriesPromises = sonarrSeries.map(async (s) => { - const sonarrDownload = sonarrDownloads.find((d) => d.series.tvdbId === s.tvdbId); - const jellyfinItem = jellyfinItems.find((i) => i.ProviderIds?.Tvdb === String(s.tvdbId)); + const seriesPromise: Promise[] = sonarrSeries.map(async (sonarrSeries) => { + const itemSonarrDownloads = sonarrDownloads.filter( + (d) => d.series.tvdbId === sonarrSeries.tvdbId + ); + const sonarrDownload = itemSonarrDownloads[0]; + const jellyfinItem = jellyfinLibraryItems.find( + (i) => i.ProviderIds?.Tvdb === String(sonarrSeries.tvdbId) + ); const downloadProgress = sonarrDownload?.sizeleft && sonarrDownload?.size @@ -170,7 +150,9 @@ async function getLibrarySeries( ? { length, progress: watchingProgress } : undefined; - const tmdbItem = s.tvdbId ? await getTmdbSeriesFromTvdbId(s.tvdbId) : undefined; + const tmdbItem = sonarrSeries.tvdbId + ? await getTmdbSeriesFromTvdbId(sonarrSeries.tvdbId) + : undefined; const tmdbId = tmdbItem?.id || undefined; const backdropUrl = tmdbId @@ -180,16 +162,40 @@ async function getLibrarySeries( : undefined; return { - ...s, + type: 'series' as const, tmdbId, + tmdbRating: tmdbItem.vote_average || 0, cardBackdropUrl: backdropUrl || '', download, continueWatching, isPlayed: jellyfinItem?.UserData?.Played || false, - tmdbRating: tmdbItem.vote_average || 0, - jellyfinId: jellyfinItem?.Id + jellyfinId: jellyfinItem?.Id, + jellyfinItem, + sonarrSeries, + sonarrDownloads: itemSonarrDownloads }; }); - return await Promise.all(playableSeriesPromises); + await Promise.all([...moviesPromise, ...seriesPromise]).then((r) => + r.forEach((item) => { + items[item.tmdbId] = item; + }) + ); + + return { + items, + itemsArray: Object.values(items), + continueWatching: Object.values(items).filter((i) => i.continueWatching) + }; } + +function createLibraryStore() { + const { update, set, ...library } = writable>(getLibrary()); //TODO promise to undefined + + return { + ...library, + refresh: async () => getLibrary().then((r) => set(Promise.resolve(r))) + }; +} + +export const library = createLibraryStore(); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index f0cc784..59f1650 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,9 +6,7 @@ import type { LayoutData } from './$types'; import { initialPlayerState } from '$lib/components/VideoPlayer/VideoPlayer'; import SetupRequired from '$lib/components/SetupRequired/SetupRequired.svelte'; - import { library } from '$lib/stores/library.store'; import { settings } from '$lib/stores/settings.store'; - library; setContext('player', initialPlayerState); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 67b3552..511c9a1 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -55,8 +55,8 @@ {/await} {#await $library then libraryData} - {#if libraryData.movies.filter((movie) => movie.continueWatching).length} - {@const continueWatching = libraryData.movies.filter((movie) => movie.continueWatching)} + {#if libraryData.itemsArray.filter((item) => item.continueWatching).length} + {@const continueWatching = libraryData.continueWatching}

Continue Watching

diff --git a/src/routes/discover/+page.svelte b/src/routes/discover/+page.svelte index d61ceba..148ec02 100644 --- a/src/routes/discover/+page.svelte +++ b/src/routes/discover/+page.svelte @@ -17,7 +17,7 @@ const popularMovies = await popularMoviesPromise .then(async (tmdbMovies) => { const libraryData = await $library; - return tmdbMovies.filter((m) => !libraryData.getMovie(m.id)); + return tmdbMovies.filter((m) => !libraryData.items[m.id]); }) .then((tmdbMovies) => { return Promise.all( @@ -44,10 +44,10 @@
For You
{#await discoverPromise} - + {:then { popularMovies: movies }} {#each movies ? [...movies].reverse() : [] as movie (movie.tmdbId)} - + {/each} {/await}
diff --git a/src/routes/library/+page.svelte b/src/routes/library/+page.svelte index 5668a4e..95c9c96 100644 --- a/src/routes/library/+page.svelte +++ b/src/routes/library/+page.svelte @@ -4,11 +4,7 @@ import IconButton from '$lib/components/IconButton.svelte'; import RadarrStats from '$lib/components/SourceStats/RadarrStats.svelte'; import SonarrStats from '$lib/components/SourceStats/SonarrStats.svelte'; - import { - library, - type PlayableRadarrMovie, - type PlayableSonarrSeries - } from '$lib/stores/library.store'; + import { library, type PlayableItem } from '$lib/stores/library.store'; import { ChevronDown, MagnifyingGlass, TextAlignBottom, Trash } from 'radix-icons-svelte'; import type { ComponentProps } from 'svelte'; @@ -26,47 +22,38 @@ let watchedProps: ComponentProps[] = []; let unavailableProps: ComponentProps[] = []; - function itemIsSeries( - item: PlayableRadarrMovie | PlayableSonarrSeries - ): item is PlayableSonarrSeries { - return (item as PlayableSonarrSeries).seasons !== undefined; - } - - function itemIsMovie( - item: PlayableRadarrMovie | PlayableSonarrSeries - ): item is PlayableRadarrMovie { - return (item as PlayableRadarrMovie).isAvailable !== undefined; - } - library.subscribe(async (libraryPromise) => { const libraryData = await libraryPromise; - const items = filterItems(sortItems([...libraryData.movies, ...libraryData.series])); + const items: PlayableItem[] = filterItems(sortItems(libraryData.itemsArray)); for (let item of items) { let props: ComponentProps; - if (itemIsSeries(item)) { - console.log(item); + + const series = item.sonarrSeries; + const movie = item.radarrMovie; + + if (series) { props = { size: 'dynamic', type: 'series', tmdbId: String(item.tmdbId), - title: item.title || '', - genres: item.genres || [], + title: series.title || '', + genres: series.genres || [], backdropUrl: item.cardBackdropUrl, - rating: item.ratings?.value || item.ratings?.value || item.tmdbRating || 0, - seasons: item.seasons?.length || 0 + rating: series.ratings?.value || series.ratings?.value || item.tmdbRating || 0, + seasons: series.seasons?.length || 0 }; - } else if (itemIsMovie(item)) { + } else if (movie) { props = { size: 'dynamic', type: 'movie', tmdbId: String(item.tmdbId), - title: item.title || '', - genres: item.genres || [], + title: movie.title || '', + genres: movie.genres || [], backdropUrl: item.cardBackdropUrl, - rating: item.ratings?.tmdb?.value || item.ratings?.imdb?.value || 0, - runtimeMinutes: item.runtime || 0 + rating: movie.ratings?.tmdb?.value || movie.ratings?.imdb?.value || 0, + runtimeMinutes: movie.runtime || 0 }; } else { continue; @@ -81,10 +68,8 @@ } else if (item.isPlayed) { watchedProps.push({ ...props, available: false }); } else if ( - ((item as PlayableRadarrMovie)?.isAvailable && (item as PlayableRadarrMovie)?.movieFile) || - (item as PlayableSonarrSeries)?.seasons?.find( - (season) => !!season?.statistics?.episodeFileCount - ) + (movie?.isAvailable && movie?.movieFile) || + series?.seasons?.find((season) => !!season?.statistics?.episodeFileCount) ) { availableProps.push(props); } else { @@ -118,7 +103,7 @@
-
+
import ResourceDetails from '$lib/components/ResourceDetails/ResourceDetails.svelte'; + import { library } from '$lib/stores/library.store'; import type { PageData } from './$types'; export let data: PageData; -{#if data.movie} - {@const movie = data.movie} - g.name || '') || []} - runtime={movie?.runtime || 0} - tmdbRating={movie?.vote_average || 0} - starring={movie?.credits?.cast?.slice(0, 5)} - videos={movie.videos?.results || []} - backdropPath={movie?.backdrop_path || ''} - showDetails={true} - /> -{/if} +{#await $library then libraryData} + {#if data.movie} + {@const movie = data.movie} + g.name || '') || []} + runtime={movie?.runtime || 0} + tmdbRating={movie?.vote_average || 0} + starring={movie?.credits?.cast?.slice(0, 5)} + videos={movie.videos?.results || []} + backdropPath={movie?.backdrop_path || ''} + showDetails={true} + jellyfinId={libraryData.items[movie.id]?.jellyfinId} + /> + {/if} +{/await} diff --git a/src/routes/series/[id]/+page.svelte b/src/routes/series/[id]/+page.svelte index bd541ae..e8578c3 100644 --- a/src/routes/series/[id]/+page.svelte +++ b/src/routes/series/[id]/+page.svelte @@ -2,7 +2,6 @@ import ResourceDetails from '$lib/components/ResourceDetails/ResourceDetails.svelte'; import type { PageData } from './$types'; export let data: PageData; - console.log(data.series); {#if data.series}