Project Refactoring

This commit is contained in:
Aleksi Lassila
2023-07-09 15:50:04 +03:00
parent 56ef4ee865
commit 494a3bf85a
83 changed files with 319 additions and 276 deletions

2
.idea/reiverr.iml generated
View File

@@ -5,6 +5,8 @@
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/.svelte-kit/generated" />
<excludeFolder url="file://$MODULE_DIR$/.svelte-kit/output" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@@ -1,8 +1,8 @@
import createClient from 'openapi-fetch';
import type { paths } from '$lib/jellyfin/jellyfin-types';
import type { paths } from '$lib/apis/jellyfin/jellyfin.generated';
import { PUBLIC_JELLYFIN_API_KEY, PUBLIC_JELLYFIN_URL } from '$env/static/public';
import { request } from '$lib/utils';
import type { DeviceProfile } from '$lib/jellyfin/playback-profiles';
import type { DeviceProfile } from '$lib/apis/jellyfin/playback-profiles';
export const JELLYFIN_DEVICE_ID = 'Reiverr Client';
export const JELLYFIN_USER_ID = '75dcb061c9404115a7acdc893ea6bbbc';

View File

@@ -1,13 +1,14 @@
import createClient from 'openapi-fetch';
import { log, request } from '$lib/utils';
import type { paths } from '$lib/radarr/radarr-types';
import type { components } from '$lib/radarr/radarr-types';
import { fetchTmdbMovie } from '$lib/tmdb-api';
import { RADARR_API_KEY, RADARR_BASE_URL } from '$env/static/private';
import type { paths } from '$lib/apis/radarr/radarr.generated';
import type { components } from '$lib/apis/radarr/radarr.generated';
import { fetchTmdbMovie } from '$lib/apis/tmdbApi';
import { PUBLIC_RADARR_API_KEY, PUBLIC_RADARR_BASE_URL } from '$env/static/public';
export type RadarrMovie = components['schemas']['MovieResource'];
export type MovieFileResource = components['schemas']['MovieFileResource'];
export type ReleaseResource = components['schemas']['ReleaseResource'];
export type RadarrDownload = components['schemas']['QueueResource'] & { movie: RadarrMovie };
export interface RadarrMovieOptions {
title: string;
@@ -23,20 +24,20 @@ export interface RadarrMovieOptions {
}
export const RadarrApi = createClient<paths>({
baseUrl: RADARR_BASE_URL,
baseUrl: PUBLIC_RADARR_BASE_URL,
headers: {
'X-Api-Key': RADARR_API_KEY
'X-Api-Key': PUBLIC_RADARR_API_KEY
}
});
export const getRadarrMovies = () =>
export const getRadarrMovies = (): Promise<RadarrMovie[]> =>
RadarrApi.get('/api/v3/movie', {
params: {}
}).then((r) => r.data);
}).then((r) => r.data || []);
export const requestRadarrMovie = () => request(getRadarrMovie);
export const getRadarrMovie = (tmdbId: string) =>
export const getRadarrMovie = (tmdbId: string): Promise<RadarrMovie | undefined> =>
RadarrApi.get('/api/v3/movie', {
params: {
query: {
@@ -121,16 +122,17 @@ export const deleteRadarrMovie = (id: number) =>
export const requestRadarrQueuedById = () => request(getRadarrDownload);
export const getRadarrDownload = (id: string) =>
export const getRadarrDownloads = (): Promise<RadarrDownload[]> =>
RadarrApi.get('/api/v3/queue', {
params: {
query: {
includeMovie: true
}
}
})
.then((r) => r.data)
.then((queue) => queue?.records?.filter((r) => (r?.movie?.id as any) == id));
}).then((r) => (r.data?.records?.filter((record) => record.movie) as RadarrDownload[]) || []);
export const getRadarrDownload = (id: string) =>
getRadarrDownloads().then((downloads) => downloads.find((d) => d.movie.id === Number(id)));
const getMovieByTmdbIdByTmdbId = (tmdbId: string) =>
RadarrApi.get('/api/v3/movie/lookup/tmdb', {

View File

@@ -0,0 +1,10 @@
import createClient from 'openapi-fetch';
import type { paths } from '$lib/apis/sonarr/sonarr.generated';
import { PUBLIC_SONARR_API_KEY, PUBLIC_SONARR_BASE_URL } from '$env/static/public';
export const SonarrApi = createClient<paths>({
baseUrl: PUBLIC_SONARR_BASE_URL,
headers: {
'X-Api-Key': PUBLIC_SONARR_API_KEY
}
});

View File

@@ -14,7 +14,6 @@
export let available = true;
export let progress = 0;
export let progressType: 'watched' | 'downloading' = 'watched';
export let type: 'dynamic' | 'normal' | 'large' = 'normal';
export let randomProgress = false;
if (randomProgress) {

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import type { TmdbMovie } from '$lib/tmdb-api';
import type { TmdbMovie } from '$lib/apis/tmdbApi';
import { onMount } from 'svelte';
import { fetchTmdbMovie, fetchTmdbMovieImages } from '$lib/tmdb-api';
import { fetchTmdbMovie, fetchTmdbMovieImages } from '$lib/apis/tmdbApi';
import { TMDB_IMAGES } from '$lib/constants';
import { getJellyfinItemByTmdbId } from '$lib/jellyfin/jellyfin';
import { getJellyfinItemByTmdbId } from '$lib/apis/jellyfin/jellyfinApi';
import CardPlaceholder from './CardPlaceholder.svelte';
import Card from './Card.svelte';

View File

@@ -1,6 +1,6 @@
import type { RadarrMovie } from '$lib/radarr/radarr';
import { fetchTmdbMovieImages } from '$lib/tmdb-api';
import type { TmdbMovie } from '$lib/tmdb-api';
import type { RadarrMovie } from '$lib/apis/radarr/radarrApi';
import { fetchTmdbMovieImages } from '$lib/apis/tmdbApi';
import type { TmdbMovie } from '$lib/apis/tmdbApi';
export interface CardProps {
tmdbId: string;

View File

@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { MagnifyingGlass, Person } from 'radix-icons-svelte';
import classNames from 'classnames';
import { page } from '$app/stores';
@@ -11,7 +11,7 @@
let isSearchVisible = false;
function getLinkStyle(path) {
function getLinkStyle(path: string) {
return $page.url.pathname === path ? 'text-amber-200' : 'hover:text-zinc-50 cursor-pointer';
}
@@ -46,10 +46,10 @@
</div>
<div class="flex gap-2 items-center">
<IconButton on:click={() => (isSearchVisible = true)}>
<MagnifyingGlass size="20" />
<MagnifyingGlass size={20} />
</IconButton>
<IconButton>
<Person size="20" />
<Person size={20} />
</IconButton>
</div>
</div>

View File

@@ -3,8 +3,8 @@
import ModalContent from '../Modal/ModalContent.svelte';
import { Cross1, Cross2, MagnifyingGlass } from 'radix-icons-svelte';
import IconButton from '../IconButton.svelte';
import { TmdbApi } from '$lib/tmdb-api';
import type { MultiSearchResponse } from '$lib/tmdb-api';
import { TmdbApi } from '$lib/apis/tmdbApi';
import type { MultiSearchResponse } from '$lib/apis/tmdbApi';
import { TMDB_IMAGES } from '$lib/constants';
import ModalHeader from '../Modal/ModalHeader.svelte';
export let visible = false;

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import { TmdbApi } from '$lib/tmdb-api';
import type { TmdbMovie } from '$lib/tmdb-api';
import { TmdbApi } from '$lib/apis/tmdbApi';
import type { TmdbMovie } from '$lib/apis/tmdbApi';
import { getContext, onMount } from 'svelte';
import { TMDB_IMAGES } from '$lib/constants';
import { formatMinutesToTime } from '$lib/utils';
import { getJellyfinItemByTmdbId } from '$lib/jellyfin/jellyfin';
import { getJellyfinItemByTmdbId } from '$lib/apis/jellyfin/jellyfinApi';
export let tmdbId;
export let progress = 0;

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { formatMinutesToTime, formatSize } from '$lib/utils';
import Button from '../Ui/Button.svelte';
import { DotFilled, Plus, Update } from 'radix-icons-svelte';
import Button from '$lib/components/Button.svelte';
import { DotFilled, Minus, Plus, Trash, Update } from 'radix-icons-svelte';
import RequestModal from '../RequestModal/RequestModal.svelte';
import IconButton from '../IconButton.svelte';
import classNames from 'classnames';
@@ -110,10 +110,13 @@
<div class="flex items-center gap-2">
<div class={headerStyle}>Local Library</div>
<IconButton on:click={openRequestModal}>
<Plus size="20" />
<Plus size={20} />
</IconButton>
<IconButton>
<Trash size={20} />
</IconButton>
<IconButton disabled={isRefetching} on:click={refetch}>
<Update size="15" />
<Update size={15} />
</IconButton>
</div>
{#each data.radarrDownloads || [] as downloadingFile}

View File

@@ -3,13 +3,14 @@
import classNames from 'classnames';
import { fade, fly } from 'svelte/transition';
import { TMDB_IMAGES } from '$lib/constants';
import Button from '../Ui/Button.svelte';
import type { CastMember, TmdbMovie, Video } from '$lib/tmdb-api';
import { fetchTmdbMovieCredits, fetchTmdbMovieVideos } from '$lib/tmdb-api';
import Button from '$lib/components/Button.svelte';
import type { CastMember, TmdbMovie, Video } from '$lib/apis/tmdbApi';
import { fetchTmdbMovieCredits, fetchTmdbMovieVideos } from '$lib/apis/tmdbApi';
import LibraryDetails from './LibraryDetails.svelte';
import { getJellyfinItemByTmdbId } from '$lib/jellyfin/jellyfin';
import { getJellyfinItemByTmdbId } from '$lib/apis/jellyfin/jellyfinApi';
import HeightHider from '../HeightHider.svelte';
import { getContext } from 'svelte';
import type { PlayerState } from '../VideoPlayer/VideoPlayer';
export let movie: TmdbMovie;
export let videos: Video[];
@@ -22,12 +23,12 @@
let trailerStartTime = 0;
let detailsVisible = showDetails;
let streamButtonDisabled = true;
let jellyfinId;
let jellyfinId: string;
let video;
let video: Video;
$: video = videos?.filter((v) => v.site === 'YouTube' && v.type === 'Trailer')?.[0];
let opacityStyle;
let opacityStyle: string;
$: opacityStyle =
(focusTrailer ? 'opacity: 0;' : 'opacity: 100;') + 'transition: opacity 0.3s ease-in-out;';
@@ -48,18 +49,18 @@
'December'
];
const releaseDate = new Date(movie.release_date);
const { playerState, close, streamJellyfinId } = getContext('player');
const { playerState, close, streamJellyfinId } = getContext<PlayerState>('player');
function openTrailer() {
window
.open(
?.open(
'https://www.youtube.com/watch?v=' +
video.key +
'&autoplay=1&t=' +
(trailerStartTime === 0 ? 0 : Math.floor((Date.now() - trailerStartTime) / 1000)),
'_blank'
)
.focus();
?.focus();
}
let fadeIndex = -1;
@@ -70,7 +71,7 @@
// onMount(() => {});
let timeout;
let timeout: NodeJS.Timeout;
$: {
fadeIndex = 0;
streamButtonDisabled = true;
@@ -101,7 +102,7 @@
}
}
let localDetailsTop;
let localDetailsTop: HTMLElement;
</script>
<div class="grid">
@@ -196,7 +197,7 @@
<div
class="hidden items-center justify-center border-2 border-white w-10 cursor-pointer hover:bg-white hover:text-zinc-900 transition-colors"
>
<ChevronDown size="20" />
<ChevronDown size={20} />
</div>
</div>
<div style={opacityStyle} class:hidden={showDetails}>
@@ -219,29 +220,29 @@
</div>
</div>
<div class="flex flex-col gap-6 max-w-[14rem] row-span-full" style={opacityStyle}>
<h3 class="text-xs tracking-wide uppercase" in:fade={getFade(0)}>Details</h3>
<h3 class="text-xs tracking-wide uppercase" in:fade={getFade()}>Details</h3>
<div class="flex flex-col gap-1">
<div class="tracking-widest font-extralight text-sm" in:fade={getFade(1)}>
<div class="tracking-widest font-extralight text-sm" in:fade={getFade()}>
{movie.genres.map((g) => g.name.charAt(0).toUpperCase() + g.name.slice(1)).join(', ')}
</div>
<div class="flex gap-1.5 items-center" in:fade={getFade(2)}>
<Clock size="14" />
<div class="flex gap-1.5 items-center" in:fade={getFade()}>
<Clock size={14} />
<div class="tracking-widest font-extralight text-sm">
{Math.floor(movie.runtime / 60)}h {movie.runtime % 60}m
</div>
</div>
<div class="tracking-widest font-extralight text-sm" in:fade={getFade(3)}>
<div class="tracking-widest font-extralight text-sm" in:fade={getFade()}>
Currently <b>Streaming</b>
</div>
<a
href={'https://www.themoviedb.org/movie/' + movie.id}
target="_blank"
class="tracking-widest font-extralight text-sm"
in:fade={getFade(4)}
in:fade={getFade()}
>
<b>{movie.vote_average.toFixed(1)}</b> TMDB
</a>
<div class="flex mt-4" in:fade={getFade(5)}>
<div class="flex mt-4" in:fade={getFade()}>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="text-white w-4"
><g
><path d="M0 0h24v24H0z" fill="none" /><path
@@ -254,21 +255,21 @@
</div>
</div>
{#if castMembers?.length > 0}
<h3 class="text-xs tracking-wide uppercase" in:fade={getFade(6)}>Starring</h3>
<h3 class="text-xs tracking-wide uppercase" in:fade={getFade()}>Starring</h3>
<div class="flex flex-col gap-1">
{#each castMembers.slice(0, 5) as a}
<a
href={'https://www.themoviedb.org/person/' + a.id}
target="_blank"
class="tracking-widest font-extralight text-sm"
in:fade={getFade(7)}>{a.name}</a
in:fade={getFade()}>{a.name}</a
>
{/each}
<a
href={'https://www.themoviedb.org/movie/' + movie.id + '/cast'}
target="_blank"
class="tracking-widest font-extralight text-sm"
in:fade={getFade(8)}>View all...</a
in:fade={getFade()}>View all...</a
>
</div>
{/if}
@@ -285,7 +286,7 @@
<LibraryDetails
openJellyfinStream={() => jellyfinId && streamJellyfinId(jellyfinId)}
jellyfinStreamDisabled={streamButtonDisabled}
tmdbId={movie.id}
tmdbId={String(movie.id)}
/>
{/key}
</HeightHider>

View File

@@ -1,24 +1,22 @@
<script lang="ts">
import { formatSize } from '$lib/utils.js';
import { onMount } from 'svelte';
import { formatSize, log } from '$lib/utils.js';
import StatsPlaceholder from './StatsPlaceholder.svelte';
import StatsContainer from './StatsContainer.svelte';
import RadarrIcon from '../svgs/RadarrIcon.svelte';
export let large = false;
let statsRequest: Promise<{ moviesAmount: number }> = new Promise((_) => {}) as any;
onMount(() => {
statsRequest = fetch('/radarr/stats')
async function fetchStats() {
return fetch('/radarr/stats')
.then((res) => res.json())
.then(log)
.then((data) => ({
moviesAmount: data?.movies?.length
}));
});
}
</script>
{#await statsRequest}
{#await fetchStats()}
<StatsPlaceholder {large} />
{:then { moviesAmount }}
<StatsContainer

View File

@@ -34,7 +34,7 @@
>
<div class="flex flex-col">
<h3 class="text-zinc-400 font-medium text-xs tracking-wider">{subtitle}</h3>
<a href="/" class="text-zinc-200 font-bold text-xl tracking-wide">{title}</a>
<a href="/static" class="text-zinc-200 font-bold text-xl tracking-wide">{title}</a>
</div>
<div class="flex gap-8">
{#each stats as { title, value }}

View File

@@ -5,7 +5,7 @@
reportJellyfinPlaybackProgress,
reportJellyfinPlaybackStarted,
reportJellyfinPlaybackStopped
} from '$lib/jellyfin/jellyfin';
} from '$lib/apis/jellyfin/jellyfinApi';
import Hls from 'hls.js';
import Modal from '../Modal/Modal.svelte';
import IconButton from '../IconButton.svelte';
@@ -13,10 +13,10 @@
import classNames from 'classnames';
import { getContext, onDestroy } from 'svelte';
import { PUBLIC_JELLYFIN_URL } from '$env/static/public';
import getDeviceProfile from '$lib/jellyfin/playback-profiles';
import getDeviceProfile from '$lib/apis/jellyfin/playback-profiles';
import type { PlayerState, PlayerStateValue } from './VideoPlayer';
const { playerState, close }: PlayerState = getContext('player');
const { playerState, close } = getContext<PlayerState>('player');
let video: HTMLVideoElement;
@@ -44,7 +44,7 @@
video.currentTime = item?.UserData?.PlaybackPositionTicks / 10_000_000;
}
});
await reportJellyfinPlaybackStarted(itemId, sessionId, mediaSourceId);
if (mediaSourceId) await reportJellyfinPlaybackStarted(itemId, sessionId, mediaSourceId);
progressInterval = setInterval(() => {
reportJellyfinPlaybackProgress(
itemId,

View File

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 684 B

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,16 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 30 30">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5229 6.47715 22 12 22C17.5229 22 22 17.5229 22 12C22 6.47715 17.5229 2 12 2ZM0 12C0 5.3726 5.3726 0 12 0C18.6274 0 24 5.3726 24 12C24 18.6274 18.6274 24 12 24C5.3726 24 0 18.6274 0 12Z"
fill="rgba(0,0,0,0.7)"
stroke="none"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.59162 22.7357C9.49492 22.6109 9.49492 21.4986 9.59162 19.399C8.55572 19.4347 7.90122 19.3628 7.62812 19.1833C7.21852 18.9139 6.80842 18.0833 6.44457 17.4979C6.08072 16.9125 5.27312 16.8199 4.94702 16.6891C4.62091 16.5582 4.53905 16.0247 5.84562 16.4282C7.15222 16.8316 7.21592 17.9303 7.62812 18.1872C8.04032 18.4441 9.02572 18.3317 9.47242 18.1259C9.91907 17.9201 9.88622 17.1538 9.96587 16.8503C10.0666 16.5669 9.71162 16.5041 9.70382 16.5018C9.26777 16.5018 6.97697 16.0036 6.34772 13.7852C5.71852 11.5669 6.52907 10.117 6.96147 9.49369C7.24972 9.07814 7.22422 8.19254 6.88497 6.83679C8.11677 6.67939 9.06732 7.06709 9.73672 7.99999C9.73737 8.00534 10.6143 7.47854 12.0001 7.47854C13.386 7.47854 13.8777 7.90764 14.2571 7.99999C14.6365 8.09234 14.94 6.36699 17.2834 6.83679C16.7942 7.79839 16.3844 8.99999 16.6972 9.49369C17.0099 9.98739 18.2372 11.5573 17.4833 13.7852C16.9807 15.2706 15.9927 16.1761 14.5192 16.5018C14.3502 16.5557 14.2658 16.6427 14.2658 16.7627C14.2658 16.9427 14.4942 16.9624 14.8233 17.8058C15.0426 18.368 15.0585 19.9739 14.8708 22.6234C14.3953 22.7445 14.0254 22.8257 13.7611 22.8673C13.2924 22.9409 12.7835 22.9822 12.2834 22.9982C11.7834 23.0141 11.6098 23.0123 10.9185 22.948C10.4577 22.9051 10.0154 22.8343 9.59162 22.7357Z"
fill="rgba(0,0,0,0.7)"
stroke="none"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.1566,22.8189c-10.4-14.8851-30.94-19.2971-45.7914-9.8348L22.2825,29.6078A29.9234,29.9234,0,0,0,8.7639,49.6506a31.5136,31.5136,0,0,0,3.1076,20.2318A30.0061,30.0061,0,0,0,7.3953,81.0653a31.8886,31.8886,0,0,0,5.4473,24.1157c10.4022,14.8865,30.9423,19.2966,45.7914,9.8348L84.7167,98.3921A29.9177,29.9177,0,0,0,98.2353,78.3493,31.5263,31.5263,0,0,0,95.13,58.117a30,30,0,0,0,4.4743-11.1824,31.88,31.88,0,0,0-5.4473-24.1157" style="fill:#ff3e00"/><path d="M45.8171,106.5815A20.7182,20.7182,0,0,1,23.58,98.3389a19.1739,19.1739,0,0,1-3.2766-14.5025,18.1886,18.1886,0,0,1,.6233-2.4357l.4912-1.4978,1.3363.9815a33.6443,33.6443,0,0,0,10.203,5.0978l.9694.2941-.0893.9675a5.8474,5.8474,0,0,0,1.052,3.8781,6.2389,6.2389,0,0,0,6.6952,2.485,5.7449,5.7449,0,0,0,1.6021-.7041L69.27,76.281a5.4306,5.4306,0,0,0,2.4506-3.631,5.7948,5.7948,0,0,0-.9875-4.3712,6.2436,6.2436,0,0,0-6.6978-2.4864,5.7427,5.7427,0,0,0-1.6.7036l-9.9532,6.3449a19.0329,19.0329,0,0,1-5.2965,2.3259,20.7181,20.7181,0,0,1-22.2368-8.2427,19.1725,19.1725,0,0,1-3.2766-14.5024,17.9885,17.9885,0,0,1,8.13-12.0513L55.8833,23.7472a19.0038,19.0038,0,0,1,5.3-2.3287A20.7182,20.7182,0,0,1,83.42,29.6611a19.1739,19.1739,0,0,1,3.2766,14.5025,18.4,18.4,0,0,1-.6233,2.4357l-.4912,1.4978-1.3356-.98a33.6175,33.6175,0,0,0-10.2037-5.1l-.9694-.2942.0893-.9675a5.8588,5.8588,0,0,0-1.052-3.878,6.2389,6.2389,0,0,0-6.6952-2.485,5.7449,5.7449,0,0,0-1.6021.7041L37.73,51.719a5.4218,5.4218,0,0,0-2.4487,3.63,5.7862,5.7862,0,0,0,.9856,4.3717,6.2437,6.2437,0,0,0,6.6978,2.4864,5.7652,5.7652,0,0,0,1.602-.7041l9.9519-6.3425a18.978,18.978,0,0,1,5.2959-2.3278,20.7181,20.7181,0,0,1,22.2368,8.2427,19.1725,19.1725,0,0,1,3.2766,14.5024,17.9977,17.9977,0,0,1-8.13,12.0532L51.1167,104.2528a19.0038,19.0038,0,0,1-5.3,2.3287" style="fill:#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -1,9 +0,0 @@
import { writable } from 'svelte/store';
async function fetchJellyfinState() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('true'), 2000);
});
}
export const jellyfinState = writable(fetchJellyfinState());

View File

@@ -1,10 +0,0 @@
import createClient from 'openapi-fetch';
import type { paths } from '$lib/sonarr/sonarr-types';
import { SONARR_API_KEY, SONARR_BASE_URL } from '$env/static/private';
export const SonarrApi = createClient<paths>({
baseUrl: SONARR_BASE_URL,
headers: {
'X-Api-Key': SONARR_API_KEY
}
});

View File

@@ -0,0 +1,68 @@
import {
getRadarrDownloads,
getRadarrMovies,
RadarrApi,
type RadarrMovie
} from '$lib/apis/radarr/radarrApi';
import type { CardProps } from '$lib/components/Card/card';
import { writable } from 'svelte/store';
import { fetchCardProps } from '$lib/components/Card/card';
import { fetchTmdbMovieImages } from '$lib/apis/tmdbApi';
interface PlayableRadarrMovie extends RadarrMovie {
cardBackdropUrl: string;
download?: {
progress: number;
completionTime: string;
};
}
export interface Library {
movies: PlayableRadarrMovie[];
totalMovies: number;
}
interface DownloadingCardProps extends CardProps {
progress: number;
completionTime: string;
}
async function getLibrary(): Promise<Library> {
const radarrMoviesPromise = getRadarrMovies();
const radarrDownloadsPromise = getRadarrDownloads();
const movies: PlayableRadarrMovie[] = await radarrMoviesPromise.then(async (radarrMovies) => {
const radarrDownloads = await radarrDownloadsPromise;
const playableMoviePromises = radarrMovies.map(async (m) => {
const radarrDownload = radarrDownloads.find((d) => d.movie.tmdbId === m.tmdbId);
const progress = radarrDownload
? radarrDownload.sizeleft && radarrDownload.size
? ((radarrDownload.size - radarrDownload.sizeleft) / radarrDownload.size) * 100
: 0
: undefined;
const completionTime = radarrDownload ? radarrDownload.estimatedCompletionTime : undefined;
const download = progress && completionTime ? { progress, completionTime } : undefined;
const backdropUrl = await fetchTmdbMovieImages(String(m.tmdbId)).then(
(r) => r.backdrops.filter((b) => b.iso_639_1 === 'en')[0].file_path
);
return {
...m,
cardBackdropUrl: backdropUrl,
download
};
});
return await Promise.all(playableMoviePromises);
});
return {
movies,
totalMovies: movies?.length || 0
};
}
export const library = writable<Promise<Library>>(getLibrary());

View File

@@ -1,3 +1,3 @@
import type { components as sonarrComponents } from '$lib/sonarr/sonarr-types';
import type { components as sonarrComponents } from '$lib/apis/sonarr/sonarr-types';
export type SeriesResource = sonarrComponents['schemas']['SeriesResource'];

View File

@@ -1,4 +1,4 @@
import type { Genre } from '$lib/tmdb-api';
import type { Genre } from '$lib/apis/tmdbApi';
import { writable } from 'svelte/store';
export function formatMinutesToTime(minutes: number) {

View File

@@ -1,37 +1,37 @@
import type { LayoutServerLoad } from './$types';
import {
RADARR_API_KEY,
RADARR_BASE_URL,
SONARR_API_KEY,
SONARR_BASE_URL
} from '$env/static/private';
PUBLIC_RADARR_API_KEY,
PUBLIC_RADARR_BASE_URL,
PUBLIC_SONARR_API_KEY,
PUBLIC_SONARR_BASE_URL
} from '$env/static/public';
import { PUBLIC_JELLYFIN_API_KEY, PUBLIC_JELLYFIN_URL } from '$env/static/public';
export type MissingEnvironmentVariables = {
RADARR_API_KEY: boolean;
RADARR_BASE_URL: boolean;
SONARR_API_KEY: boolean;
SONARR_BASE_URL: boolean;
PUBLIC_RADARR_API_KEY: boolean;
PUBLIC_RADARR_BASE_URL: boolean;
PUBLIC_SONARR_API_KEY: boolean;
PUBLIC_SONARR_BASE_URL: boolean;
PUBLIC_JELLYFIN_API_KEY: boolean;
PUBLIC_JELLYFIN_URL: boolean;
};
export const load = (async () => {
const isApplicationSetUp =
!!RADARR_API_KEY &&
!!RADARR_BASE_URL &&
!!SONARR_API_KEY &&
!!SONARR_BASE_URL &&
!!PUBLIC_RADARR_API_KEY &&
!!PUBLIC_RADARR_BASE_URL &&
!!PUBLIC_SONARR_API_KEY &&
!!PUBLIC_SONARR_BASE_URL &&
!!PUBLIC_JELLYFIN_API_KEY &&
!!PUBLIC_JELLYFIN_URL;
return {
isApplicationSetUp,
missingEnvironmentVariables: {
RADARR_API_KEY: !RADARR_API_KEY,
RADARR_BASE_URL: !RADARR_BASE_URL,
SONARR_API_KEY: !SONARR_API_KEY,
SONARR_BASE_URL: !SONARR_BASE_URL,
PUBLIC_RADARR_API_KEY: !PUBLIC_RADARR_API_KEY,
PUBLIC_RADARR_BASE_URL: !PUBLIC_RADARR_BASE_URL,
PUBLIC_SONARR_API_KEY: !PUBLIC_SONARR_API_KEY,
PUBLIC_SONARR_BASE_URL: !PUBLIC_SONARR_BASE_URL,
PUBLIC_JELLYFIN_API_KEY: !PUBLIC_JELLYFIN_API_KEY,
PUBLIC_JELLYFIN_URL: !PUBLIC_JELLYFIN_URL
}

View File

@@ -1,11 +1,13 @@
<script lang="ts">
import '../app.css';
import Navbar from './components/Navbar/Navbar.svelte';
import VideoPlayer from './components/VideoPlayer/VideoPlayer.svelte';
import Navbar from '$lib/components/Navbar/Navbar.svelte';
import VideoPlayer from '$lib/components/VideoPlayer/VideoPlayer.svelte';
import { setContext } from 'svelte';
import type { LayoutData } from './$types';
import { initialPlayerState } from './components/VideoPlayer/VideoPlayer';
import SetupRequired from './components/SetupRequired/SetupRequired.svelte';
import { initialPlayerState } from '$lib/components/VideoPlayer/VideoPlayer';
import SetupRequired from '$lib/components/SetupRequired/SetupRequired.svelte';
import { library } from '$lib/stores/libraryStore';
library;
setContext('player', initialPlayerState);

View File

@@ -1,6 +1,6 @@
import { fetchTmdbMovie, TmdbApi } from '$lib/tmdb-api';
import type { TmdbMovie } from '$lib/tmdb-api';
import { getJellyfinContinueWatching } from '$lib/jellyfin/jellyfin';
import { fetchTmdbMovie, TmdbApi } from '$lib/apis/tmdbApi';
import type { TmdbMovie } from '$lib/apis/tmdbApi';
import { getJellyfinContinueWatching } from '$lib/apis/jellyfin/jellyfinApi';
import type { PageServerLoad } from './$types';
export const load = (async () => {

View File

@@ -1,14 +1,13 @@
<script lang="ts">
import SmallPoster from './components/Poster/Poster.svelte';
import type { PageData } from './$types';
import ResourceDetails from './components/ResourceDetails/ResourceDetails.svelte';
import ResourceDetailsControls from './ResourceDetailsControls.svelte';
import { TMDB_IMAGES } from '$lib/constants';
import { fetchTmdbMovie } from '$lib/tmdb-api';
import { fetchTmdbMovie } from '$lib/apis/tmdbApi';
import SmallPoster from '$lib/components/Poster/Poster.svelte';
import ResourceDetails from '$lib/components/ResourceDetails/ResourceDetails.svelte';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import ResourceDetailsControls from './ResourceDetailsControls.svelte';
export let data: PageData;
let movies = [];
let movies: ReturnType<typeof fetchTmdbMovie>[] = [];
let index = 0;

View File

@@ -1,30 +1,32 @@
<script lang="ts">
import { ChevronDown, ChevronLeft, ChevronRight, DotFilled } from 'radix-icons-svelte';
import IconButton from './components/IconButton.svelte';
import IconButton from '$lib/components/IconButton.svelte';
export let onNext: () => void;
export let onPrevious: () => void;
export let index = 0;
export let length = 0;
// TODO: Control pages using horizontal scroll
</script>
<div class="relative z-[1] flex gap-8 justify-between items-center col-span-2">
<IconButton on:click={onPrevious}>
<ChevronLeft size="24" />
<ChevronLeft size={24} />
</IconButton>
<div class="flex gap-2">
{#each Array.from({ length }, (_, i) => i) as i}
{#if i === index}
<DotFilled size="15" class="opacity-100" />
<DotFilled size={15} class="opacity-100" />
{:else}
<DotFilled size="15" class="opacity-20" />
<DotFilled size={15} class="opacity-20" />
{/if}
{/each}
</div>
<IconButton on:click={onNext}>
<ChevronRight size="24" />
<ChevronRight size={24} />
</IconButton>
</div>
<!--<div class="absolute inset-x-0 bottom-6 flex justify-center mx-auto opacity-50">-->

View File

@@ -1,6 +1,6 @@
import type { PageServerLoad } from './$types';
import { fetchTmdbMovie, fetchTmdbPopularMovies } from '$lib/tmdb-api';
import { fetchCardPropsTmdb } from '../components/Card/card';
import { fetchTmdbMovie, fetchTmdbPopularMovies } from '$lib/apis/tmdbApi';
import { fetchCardPropsTmdb } from '$lib/components/Card/card';
export const load = (() => {
const popularMoviesPromise = fetchTmdbPopularMovies();

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import Card from '../components/Card/Card.svelte';
import Carousel from '../components/Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
import Card from '$lib/components/Card/Card.svelte';
import Carousel from '$lib/components/Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '$lib/components/Carousel/CarouselPlaceholderItems.svelte';
import NetflixCard from './NetflixCard.svelte';
import HboCard from './HboCard.svelte';
import DisneyCard from './DisneyCard.svelte';

View File

@@ -1,6 +1,6 @@
import type { RequestHandler } from '@sveltejs/kit';
import { error, json } from '@sveltejs/kit';
import { getJellyfinItemByTmdbId } from '$lib/jellyfin/jellyfin';
import { getJellyfinItemByTmdbId } from '$lib/apis/jellyfin/jellyfinApi';
export const GET = (async ({ params, request }) => {
const body = await request.json();

View File

@@ -1,95 +1,28 @@
import type { PageServerLoad } from './$types';
import { RadarrApi, getRadarrMovies } from '$lib/radarr/radarr';
import type { CardProps } from '../components/Card/card';
import { fetchCardProps } from '../components/Card/card';
import { RadarrApi, getRadarrMovies } from '$lib/apis/radarr/radarrApi';
import type { CardProps } from '$lib/components/Card/card';
import { fetchCardProps } from '$lib/components/Card/card';
interface DownloadingCardProps extends CardProps {
progress: number;
completionTime: string;
}
// interface DownloadingCardProps extends CardProps {
// progress: number;
// completionTime: string;
// }
export const load = (() => {
const [downloading, available, unavailable] = getLibraryItems();
// export const load = (() => {
// const [downloading, available, unavailable] = getLibraryItems();
// radarrMovies.then((d) => console.log(d.map((m) => m.ratings)));
// // radarrMovies.then((d) => console.log(d.map((m) => m.ratings)));
const libraryInfo = getLibraryInfo();
// const libraryInfo = getLibraryInfo();
return {
streamed: {
libraryInfo,
downloading,
available,
unavailable
}
};
}) satisfies PageServerLoad;
// return {
// streamed: {
// libraryInfo,
// downloading,
// available,
// unavailable
// }
// };
// }) satisfies PageServerLoad;
async function getLibraryInfo(): Promise<any> { }
function getLibraryItems(): [Promise<DownloadingCardProps[]>, Promise<CardProps[]>, Promise<CardProps[]>] {
const radarrMovies = getRadarrMovies();
const downloadingRadarrMovies = RadarrApi.get('/api/v3/queue', {
params: {
query: {
includeMovie: true
}
}
}).then((r) => r.data?.records?.filter((record) => record.movie));
const unavailable: Promise<CardProps[]> = radarrMovies.then(async (movies) => {
const downloadingMovies = await downloadingRadarrMovies;
return await Promise.all(
movies
?.filter(
(m) =>
(!m.movieFile || !m.movieFile || !m.isAvailable) &&
!downloadingMovies?.find((d) => d.movie?.tmdbId === m.tmdbId)
)
.map(async (m) => fetchCardProps(m)) || []
);
});
const available: Promise<CardProps[]> = radarrMovies.then(async (movies) => {
const downloadingMovies = await downloading;
const unavailableMovies = await unavailable;
if (!downloadingMovies || !movies) return [];
return await Promise.all(
movies
.filter((movie) => {
return !downloadingMovies.find(
(downloadingMovie) => downloadingMovie.tmdbId === String(movie.tmdbId)
);
})
.filter(
(movie) =>
!unavailableMovies?.find(
(unavailableMovie) => unavailableMovie.tmdbId === String(movie.tmdbId)
)
)
.map(async (m) => fetchCardProps(m)) || []
);
});
const downloading: Promise<DownloadingCardProps[]> = downloadingRadarrMovies.then(
async (movies) => {
return Promise.all(
movies
?.filter((m) => m?.movie?.tmdbId)
?.map(
async (m) =>
({
...(await fetchCardProps(m.movie as any)),
progress: m.sizeleft && m.size ? ((m.size - m.sizeleft) / m.size) * 100 : 0,
completionTime: m.estimatedCompletionTime
} as DownloadingCardProps)
) || []
);
}
);
return [downloading, available, unavailable];
}
// async function getLibraryInfo(): Promise<any> {}

View File

@@ -1,17 +1,26 @@
<script lang="ts">
import { MagnifyingGlass, TextAlignBottom, Trash } from 'radix-icons-svelte';
import Card from '../components/Card/Card.svelte';
import CardPlaceholder from '../components/Card/CardPlaceholder.svelte';
import IconButton from '../components/IconButton.svelte';
import RadarrStats from '../components/SourceStats/RadarrStats.svelte';
import SonarrStats from '../components/SourceStats/SonarrStats.svelte';
import {
ChevronDown,
ChevronUp,
Download,
MagnifyingGlass,
TextAlignBottom,
Trash
} from 'radix-icons-svelte';
import Card from '$lib/components/Card/Card.svelte';
import CardPlaceholder from '$lib/components/Card/CardPlaceholder.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import RadarrStats from '$lib/components/SourceStats/RadarrStats.svelte';
import SonarrStats from '$lib/components/SourceStats/SonarrStats.svelte';
import type { PageData } from './$types';
export let data: PageData;
import { library } from '$lib/stores/libraryStore';
const watched = [];
const posterGridStyle =
'grid gap-x-4 gap-y-8 grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6';
const headerStyle = 'uppercase tracking-widest font-bold mt-2';
'grid gap-x-4 gap-y-8 grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5';
const headerStyle = 'uppercase tracking-widest font-bold';
const headerContaienr = 'flex items-center justify-between mt-2';
</script>
<div class="pt-24 pb-8 px-8 bg-black">
@@ -66,49 +75,91 @@
</IconButton>
</div>
</div> -->
{#await Promise.all( [data.streamed.available, data.streamed.unavailable, data.streamed.downloading] )}
{#await $library}
<div class={posterGridStyle}>
{#each [...Array(20).keys()] as index (index)}
<CardPlaceholder type="dynamic" {index} />
{/each}
</div>
{:then [available, unavailable, downloading]}
{:then libraryData}
{@const downloading = libraryData.movies.filter((m) => !!m.download)}
{@const available = libraryData.movies.filter(
(m) => !m.download && m.movieFile && m.isAvailable
)}
{@const unavailable = libraryData.movies.filter(
(m) => !m.download && (!m.movieFile || !m.isAvailable)
)}
{#if downloading.length > 0}
<h1 class={headerStyle}>Downloading</h1>
<div class={headerContaienr}>
<h1 class={headerStyle}>Downloading</h1>
<IconButton>
<ChevronDown size={24} />
</IconButton>
</div>
<div class={posterGridStyle}>
{#each downloading as movie (movie)}
<Card
type="dynamic"
{...movie}
progress={movie.progress}
progressType="downloading"
progress={movie.download?.progress || 0}
completionTime={movie.download?.completionTime}
available={false}
tmdbId={String(movie.tmdbId)}
title={movie.title || ''}
genres={movie.genres || []}
runtimeMinutes={movie.runtime || 0}
backdropUrl={movie.cardBackdropUrl}
rating={movie.ratings?.tmdb?.value || movie.ratings?.imdb?.value || 0}
/>
{/each}
</div>
{/if}
{#if available.length > 0}
<h1 class={headerStyle}>Available</h1>
<div class={headerContaienr}>
<h1 class={headerStyle}>Available</h1>
<IconButton>
<ChevronDown size={24} />
</IconButton>
</div>
<div class={posterGridStyle}>
{#each available as movie (movie.tmdbId)}
<Card type="dynamic" {...movie} randomProgress={false} />
<Card
type="dynamic"
tmdbId={String(movie.tmdbId)}
title={movie.title || ''}
genres={movie.genres || []}
runtimeMinutes={movie.runtime || 0}
backdropUrl={movie.cardBackdropUrl}
rating={movie.ratings?.tmdb?.value || movie.ratings?.imdb?.value || 0}
/>
{/each}
</div>
{/if}
{#if unavailable.length > 0}
<h1 class={headerStyle}>Unavailable</h1>
<div class={headerContaienr}>
<h1 class={headerStyle}>Unavailable</h1>
<IconButton>
<ChevronDown size={24} />
</IconButton>
</div>
<div class={posterGridStyle}>
{#each unavailable as movie (movie.tmdbId)}
<Card type="dynamic" {...movie} available={false} />
<Card
type="dynamic"
available={false}
tmdbId={String(movie.tmdbId)}
title={movie.title || ''}
genres={movie.genres || []}
runtimeMinutes={movie.runtime || 0}
backdropUrl={movie.cardBackdropUrl}
rating={movie.ratings?.tmdb?.value || movie.ratings?.imdb?.value || 0}
/>
{/each}
</div>
{/if}
{#if watched.length > 0}
<h1 class={headerStyle}>Watched</h1>
{/if}
{/await}
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { fetchTmdbMovie } from '$lib/tmdb-api';
import { fetchTmdbMovie } from '$lib/apis/tmdbApi';
import type { PageServerLoad } from './$types';
export const load = (async ({ params }) => {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { PageData } from './$types';
import ResourceDetails from '../../components/ResourceDetails/ResourceDetails.svelte';
import ResourceDetails from '$lib/components/ResourceDetails/ResourceDetails.svelte';
export let data: PageData;
</script>

View File

@@ -1,7 +1,7 @@
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';
import { getJellyfinItemByTmdbId } from '$lib/jellyfin/jellyfin';
import { getRadarrMovie, getRadarrDownload } from '$lib/radarr/radarr';
import { getJellyfinItemByTmdbId } from '$lib/apis/jellyfin/jellyfinApi';
import { getRadarrMovie, getRadarrDownload } from '$lib/apis/radarr/radarrApi';
export const parseMovieId = (params: any) => {
const { id: tmdbId } = params;

View File

@@ -1,7 +1,7 @@
import type { RequestHandler } from '@sveltejs/kit';
import { json } from '@sveltejs/kit';
import { parseMovieId } from '../+server';
import { addRadarrMovie, deleteRadarrMovie } from '$lib/radarr/radarr';
import { addRadarrMovie, deleteRadarrMovie } from '$lib/apis/radarr/radarrApi';
// Delete download
export const DELETE = (async ({ params }) => {

View File

@@ -1,5 +1,5 @@
import { parseMovieId } from '../+server';
import { addRadarrMovie, getRadarrMovie } from '$lib/radarr/radarr';
import { addRadarrMovie, getRadarrMovie } from '$lib/apis/radarr/radarrApi';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';

View File

@@ -6,7 +6,7 @@ import {
addRadarrMovie,
fetchRadarrReleases,
downloadRadarrMovie
} from '$lib/radarr/radarr';
} from '$lib/apis/radarr/radarrApi';
export const GET = (async ({ params }) => {
const radarrId = parseMovieId(params);

View File

@@ -1,4 +1,4 @@
import { RadarrApi, getRadarrMovies } from '$lib/radarr/radarr';
import { RadarrApi, getRadarrMovies } from '$lib/apis/radarr/radarrApi';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import RadarrIcon from '../components/svgs/RadarrIcon.svelte';
import SonarrIcon from '../components/svgs/SonarrIcon.svelte';
import RadarrIcon from '$lib/components/svgs/RadarrIcon.svelte';
import SonarrIcon from '$lib/components/svgs/SonarrIcon.svelte';
import { formatSize } from '$lib/utils.js';
import RadarrStats from '../components/SourceStats/RadarrStats.svelte';
import SonarrStats from '../components/SourceStats/SonarrStats.svelte';
import RadarrStats from '$lib/components/SourceStats/RadarrStats.svelte';
import SonarrStats from '$lib/components/SourceStats/SonarrStats.svelte';
</script>
<div class="pt-24 px-8 min-h-screen flex justify-center">
@@ -15,7 +15,7 @@
>
<h2 class="font-medium">Sonarr is not set up</h2>
<p class="text-sm">
To set up Sonarr, define the <code>SONARR_API_KEY</code> and <code>SONARR_URL</code> environment
To set up Sonarr, define the <code>PUBLIC_SONARR_API_KEY</code> and <code>SONARR_URL</code> environment
variables.
</p>
</div>

View File

@@ -12,6 +12,15 @@ const config = {
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
},
vitePlugin: {
experimental: {
inspector: {
toggleKeyCombo: 'meta',
holdMode: true
}
}
}
};