Loads of changes, experimental radarr queuing and integration
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind utilities;
|
||||
|
||||
a {
|
||||
@apply hover:text-amber-200;
|
||||
}
|
||||
113
src/lib/radarr/radarr.ts
Normal file
113
src/lib/radarr/radarr.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import createClient from 'openapi-fetch';
|
||||
import { PUBLIC_RADARR_API_KEY } from '$env/static/public';
|
||||
import { request } from '$lib/utils';
|
||||
import type { paths } from '$lib/radarr/radarr-api';
|
||||
import type { components } from '$lib/radarr/radarr-api';
|
||||
import type { TmdbMovie, TmdbMovieFull } from '$lib/tmdb-api';
|
||||
import { fetchTmdbMovie, TmdbApi } from '$lib/tmdb-api';
|
||||
|
||||
export type MovieResource = components['schemas']['MovieResource'];
|
||||
export type MovieFileResource = components['schemas']['MovieFileResource'];
|
||||
export type ReleaseResource = components['schemas']['ReleaseResource'];
|
||||
|
||||
export interface RadarrMovieOptions {
|
||||
title: string;
|
||||
qualityProfileId: number;
|
||||
minimumAvailability: 'announced' | 'inCinemas' | 'released';
|
||||
tags: number[];
|
||||
profileId: number;
|
||||
year: number;
|
||||
rootFolderPath: string;
|
||||
tmdbId: number;
|
||||
monitored?: boolean;
|
||||
searchNow?: boolean;
|
||||
}
|
||||
|
||||
export const RadarrApi = createClient<paths>({
|
||||
baseUrl: 'http://radarr.home',
|
||||
headers: {
|
||||
'X-Api-Key': PUBLIC_RADARR_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
export const getRadarrMovie = () =>
|
||||
request((tmdbId: string) =>
|
||||
RadarrApi.get('/api/v3/movie', {
|
||||
params: {
|
||||
query: {
|
||||
tmdbId: Number(tmdbId)
|
||||
}
|
||||
}
|
||||
}).then((r) => r.data?.find((m) => (m.tmdbId as any) == tmdbId))
|
||||
);
|
||||
|
||||
export const addRadarrMovie = () =>
|
||||
request(async (tmdbId: string) => {
|
||||
const tmdbMovie = await fetchTmdbMovie(tmdbId);
|
||||
const radarrMovie = await getMovieByTmdbIdByTmdbId(tmdbId);
|
||||
console.log('fetched movies', tmdbMovie, radarrMovie);
|
||||
|
||||
if (radarrMovie?.id) throw new Error('Movie already exists');
|
||||
|
||||
if (!tmdbMovie) throw new Error('Movie not found');
|
||||
|
||||
const qualityProfile = 4;
|
||||
const options: RadarrMovieOptions = {
|
||||
qualityProfileId: qualityProfile,
|
||||
profileId: qualityProfile,
|
||||
rootFolderPath: '/movies',
|
||||
minimumAvailability: 'announced',
|
||||
title: tmdbMovie.title,
|
||||
tmdbId: tmdbMovie.id,
|
||||
year: Number((await tmdbMovie).release_date.slice(0, 4)),
|
||||
monitored: false,
|
||||
tags: [],
|
||||
searchNow: false
|
||||
};
|
||||
|
||||
return RadarrApi.post('/api/v3/movie', {
|
||||
params: {},
|
||||
body: options
|
||||
}).then((r) => r.data);
|
||||
});
|
||||
|
||||
export const getReleases = () =>
|
||||
request((movieId: string) =>
|
||||
RadarrApi.get('/api/v3/release', { params: { query: { movieId: Number(movieId) } } }).then(
|
||||
(r) => r.data
|
||||
)
|
||||
);
|
||||
|
||||
export const queueRelease = () =>
|
||||
request((guid: string) =>
|
||||
RadarrApi.post('/api/v3/release', {
|
||||
params: {},
|
||||
body: {
|
||||
indexerId: 2,
|
||||
guid
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export const getQueuedById = () =>
|
||||
request((id: string) =>
|
||||
getQueue().then((queue) => queue?.records?.filter((r) => (r?.movie?.id as any) == id))
|
||||
);
|
||||
|
||||
const getQueue = () =>
|
||||
RadarrApi.get('/api/v3/queue', {
|
||||
params: {
|
||||
query: {
|
||||
includeMovie: true
|
||||
}
|
||||
}
|
||||
}).then((r) => r.data);
|
||||
|
||||
const getMovieByTmdbIdByTmdbId = (tmdbId: string) =>
|
||||
RadarrApi.get('/api/v3/movie/lookup/tmdb', {
|
||||
params: {
|
||||
query: {
|
||||
tmdbId: Number(tmdbId)
|
||||
}
|
||||
}
|
||||
}).then((r) => r.data as any as MovieResource);
|
||||
@@ -1,18 +0,0 @@
|
||||
import createClient from 'openapi-fetch';
|
||||
import type { paths as radarrPaths } from '$lib/radarr';
|
||||
import type { paths as sonarrPaths } from '$lib/sonarr';
|
||||
import { PUBLIC_RADARR_API_KEY, PUBLIC_SONARR_API_KEY } from '$env/static/public';
|
||||
|
||||
export const radarrApi = createClient<radarrPaths>({
|
||||
baseUrl: 'http://radarr.home',
|
||||
headers: {
|
||||
'X-Api-Key': PUBLIC_RADARR_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
export const sonarrApi = createClient<sonarrPaths>({
|
||||
baseUrl: 'http://sonarr.home',
|
||||
headers: {
|
||||
'X-Api-Key': PUBLIC_SONARR_API_KEY
|
||||
}
|
||||
});
|
||||
10
src/lib/sonarr/sonarr.ts
Normal file
10
src/lib/sonarr/sonarr.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import createClient from 'openapi-fetch';
|
||||
import type { paths as sonarrPaths } from '$lib/sonarr/sonarr';
|
||||
import { PUBLIC_SONARR_API_KEY } from '$env/static/public';
|
||||
|
||||
export const SonarrApi = createClient<sonarrPaths>({
|
||||
baseUrl: 'http://sonarr.home',
|
||||
headers: {
|
||||
'X-Api-Key': PUBLIC_SONARR_API_KEY
|
||||
}
|
||||
});
|
||||
@@ -8,25 +8,26 @@ export const TmdbApi = axios.create({
|
||||
}
|
||||
});
|
||||
|
||||
export async function fetchMovieDetails(imdbId: string | number): Promise<TmdbMovieFull> {
|
||||
export async function fetchFullMovieDetails(tmdbId: string): Promise<TmdbMovieFull> {
|
||||
return {
|
||||
...(await TmdbApi.get('/movie/' + imdbId).then((res) => res.data)),
|
||||
videos: await TmdbApi.get<VideosResponse>('/movie/' + imdbId + '/videos').then(
|
||||
(res) => res.data.results
|
||||
),
|
||||
images: await TmdbApi.get<ImagesResponse>('/movie/' + imdbId + '/images').then((res) => {
|
||||
return {
|
||||
backdrops: res.data.backdrops,
|
||||
logos: res.data.logos,
|
||||
posters: res.data.posters
|
||||
};
|
||||
}),
|
||||
credits: await TmdbApi.get<CreditsResponse>('/movie/' + imdbId + '/credits').then(
|
||||
...(await fetchTmdbMovie(tmdbId)),
|
||||
videos: await fetchTmdbMovieVideos(tmdbId),
|
||||
images: await fetchTmdbMovieImages(tmdbId),
|
||||
credits: await TmdbApi.get<CreditsResponse>('/movie/' + tmdbId + '/credits').then(
|
||||
(res) => res.data.cast
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchTmdbMovie = async (tmdbId: string) =>
|
||||
await TmdbApi.get<TmdbMovie>('/movie/' + tmdbId).then((r) => r.data);
|
||||
|
||||
export const fetchTmdbMovieVideos = async (tmdbId: string) =>
|
||||
await TmdbApi.get<VideosResponse>('/movie/' + tmdbId + '/videos').then((res) => res.data.results);
|
||||
|
||||
export const fetchTmdbMovieImages = async (tmdbId: string) =>
|
||||
await TmdbApi.get<ImagesResponse>('/movie/' + tmdbId + '/images').then((res) => res.data);
|
||||
|
||||
export interface TmdbMovieFull extends TmdbMovie {
|
||||
videos: Video[];
|
||||
images: {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import type { components as radarrComponents } from '$lib/radarr';
|
||||
import type { components as sonarrComponents } from '$lib/sonarr';
|
||||
|
||||
export type MovieResource = radarrComponents['schemas']['MovieResource'];
|
||||
import type { components as sonarrComponents } from '$lib/sonarr/sonarr-api';
|
||||
|
||||
export type SeriesResource = sonarrComponents['schemas']['SeriesResource'];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Genre } from '$lib/tmdb-api';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export function getRuntime(minutes: number) {
|
||||
export function formatMinutes(minutes: number) {
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = Math.floor(minutes % 60);
|
||||
|
||||
@@ -10,3 +11,49 @@ export function getRuntime(minutes: number) {
|
||||
export function formatGenres(genres: Genre[]) {
|
||||
return genres.map((genre) => genre.name.charAt(0).toUpperCase() + genre.name.slice(1)).join(', ');
|
||||
}
|
||||
|
||||
export function formatSize(size: number) {
|
||||
const gbs = size / 1024 / 1024 / 1024;
|
||||
const mbs = size / 1024 / 1024;
|
||||
|
||||
if (gbs >= 1) {
|
||||
return `${gbs.toFixed(2)} GB`;
|
||||
} else {
|
||||
return `${mbs.toFixed(2)} MB`;
|
||||
}
|
||||
}
|
||||
|
||||
export function request<T, A>(fetcher: (arg: A) => Promise<T>, args: A | undefined = undefined) {
|
||||
const loading = writable(args !== undefined);
|
||||
const error = writable<Error | null>(null);
|
||||
const data = writable<T | null>(null);
|
||||
const didLoad = writable(false);
|
||||
|
||||
async function load(arg: A) {
|
||||
loading.set(true);
|
||||
error.set(null);
|
||||
|
||||
fetcher(arg)
|
||||
.then((d) => {
|
||||
console.log('got data', d);
|
||||
data.set(d);
|
||||
})
|
||||
.catch((e) => error.set(e))
|
||||
.finally(() => {
|
||||
loading.set(false);
|
||||
didLoad.set(true);
|
||||
});
|
||||
}
|
||||
|
||||
if (args) {
|
||||
load(args);
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
didLoad,
|
||||
load
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { radarrApi } from '$lib/servarr-api';
|
||||
import { fetchMovieDetails, TmdbApi } from '$lib/tmdb-api';
|
||||
import { fetchFullMovieDetails, TmdbApi } from '$lib/tmdb-api';
|
||||
import { RadarrApi } from '$lib/radarr/radarr';
|
||||
|
||||
export const load = (async () => {
|
||||
const movies = await TmdbApi.get('/movie/popular').then((res) => res.data.results.slice(0, 5));
|
||||
|
||||
const showcases = await Promise.all(movies.map((m: any) => fetchMovieDetails(m.id)));
|
||||
const showcases = await Promise.all(movies.map((m: any) => fetchFullMovieDetails(m.id)));
|
||||
return { showcases };
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div
|
||||
class={classNames('fixed inset-0 bg-[#00000088] justify-center items-center z-20', {
|
||||
hidden: !visible,
|
||||
flex: visible
|
||||
'flex overflow-hidden': visible
|
||||
})}
|
||||
on:click|self={close}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { TmdbApi } from '$lib/tmdb-api';
|
||||
import type { MultiSearchResponse } from '$lib/tmdb-api';
|
||||
import { TMDB_IMAGES } from '$lib/constants';
|
||||
import { onMount } from 'svelte';
|
||||
export let visible = false;
|
||||
let searchValue = '';
|
||||
|
||||
@@ -41,17 +40,10 @@
|
||||
})
|
||||
.finally(() => (fetching = false));
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
searchValue = 'incepti';
|
||||
searchMovie('incepti');
|
||||
});
|
||||
|
||||
$: console.log(results);
|
||||
</script>
|
||||
|
||||
<Modal {visible} {close}>
|
||||
<ModalContent {close}>
|
||||
<ModalContent>
|
||||
<div class="flex text-zinc-200 items-center p-3 px-5 gap-4 border-b border-zinc-700">
|
||||
<MagnifyingGlass size="20" class="text-zinc-400" />
|
||||
<input
|
||||
|
||||
68
src/routes/components/RequestModal/RequestModal.svelte
Normal file
68
src/routes/components/RequestModal/RequestModal.svelte
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import Modal from '../Modal/Modal.svelte';
|
||||
import ModalContent from '../Modal/ModalContent.svelte';
|
||||
import { getReleases, queueRelease } from '$lib/radarr/radarr';
|
||||
import { formatSize } from '$lib/utils';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { Download } from 'radix-icons-svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let visible = false;
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
export let radarrId;
|
||||
|
||||
const { data: releases, load: loadReleases, didLoad: didLoadReleases } = getReleases();
|
||||
const { data, load: downloadRelease } = queueRelease();
|
||||
|
||||
$: if (visible) loadReleases(radarrId);
|
||||
|
||||
let releasesFiltered = [];
|
||||
let releasesSkipped = 0;
|
||||
releases.subscribe((releases: any[]) => {
|
||||
if (!releases) return;
|
||||
releasesFiltered = releases
|
||||
.filter((release) => {
|
||||
if (release.seeders < 5) return false;
|
||||
else return true;
|
||||
})
|
||||
.sort((a, b) => b.size - a.size)
|
||||
.slice(0, 5);
|
||||
releasesSkipped = releases.length - releasesFiltered.length;
|
||||
});
|
||||
|
||||
function handleDownload(guid) {
|
||||
downloadRelease(guid).then(() => {
|
||||
dispatch('download');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {visible} {close}>
|
||||
<ModalContent>
|
||||
{#if releasesFiltered?.length}
|
||||
Releases:
|
||||
<div class="flex flex-col">
|
||||
{#each releasesFiltered as release}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>{formatSize(release.size)}</div>
|
||||
<IconButton on:click={() => handleDownload(release.guid)}>
|
||||
<Download size="20" />
|
||||
</IconButton>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if releasesSkipped > 0}
|
||||
<div>{releasesSkipped} releases hidden</div>
|
||||
{/if}
|
||||
{:else if !$didLoadReleases}
|
||||
Loading...
|
||||
{:else}
|
||||
No releases found
|
||||
{/if}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
@@ -1,14 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { MovieResource } from '$lib/types';
|
||||
import { ChevronDown, Clock } from 'radix-icons-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import classNames from 'classnames';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { TMDB_IMAGES } from '$lib/constants';
|
||||
import Button from '../Ui/Button.svelte';
|
||||
import type { MovieResource } from '$lib/radarr/radarr';
|
||||
import type { TmdbMovie, TmdbMovieFull } from '$lib/tmdb-api';
|
||||
|
||||
export let resource: MovieResource;
|
||||
export let trailer = true;
|
||||
export let remoteResource: any;
|
||||
export let remoteResource: TmdbMovieFull;
|
||||
let tmdbPageUrl = '';
|
||||
$: tmdbPageUrl = 'https://www.themoviedb.org/movie/' + remoteResource.id;
|
||||
|
||||
let video: any = remoteResource.videos.filter(
|
||||
(v) => v.site === 'YouTube' && v.type === 'Trailer'
|
||||
@@ -58,6 +62,7 @@
|
||||
trailerStartTime = Date.now();
|
||||
}
|
||||
}, 2500);
|
||||
console.log(remoteResource);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -75,7 +80,7 @@
|
||||
transition:fade
|
||||
src={'https://www.youtube.com/embed/' +
|
||||
video.key +
|
||||
'?autoplay=1&mute=1&loop=1&color=white&controls=0&modestbranding=1&playsinline=1&rel=0&enablejsapi=1'}
|
||||
'?autoplay=1&mute=1&loop=1&controls=0&modestbranding=1&playsinline=1&rel=0&enablejsapi=1'}
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -131,22 +136,19 @@
|
||||
</div>
|
||||
<div class="flex gap-6 mt-10">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="bg-white border-2 border-white hover:bg-amber-400 hover:border-amber-400 transition-colors text-zinc-900 px-8 py-3.5 uppercase tracking-widest font-extrabold cursor-pointer text-xs"
|
||||
style={opacityStyle}>Stream</button
|
||||
>
|
||||
<Button size="lg" style={opacityStyle}>Stream</Button>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
size="lg"
|
||||
type="secondary"
|
||||
on:mouseover={() => (focusTrailer = trailer)}
|
||||
on:mouseleave={() => (focusTrailer = false)}
|
||||
on:click={openTrailer}
|
||||
class="border-2 border-white cursor-pointer transition-colors px-8 py-3.5 uppercase tracking-widest font-semibold text-xs hover:bg-white hover:text-black opacity-100"
|
||||
>Watch Trailer</button
|
||||
on:click={openTrailer}>Watch Trailer</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,9 +167,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="tracking-widest font-extralight text-sm">Currently <b>Streaming</b></div>
|
||||
<div class="tracking-widest font-extralight text-sm">
|
||||
<b>{remoteResource.vote_average}</b> TMDB
|
||||
</div>
|
||||
<a
|
||||
href={'https://www.themoviedb.org/movie/' + remoteResource.id}
|
||||
target="_blank"
|
||||
class="tracking-widest font-extralight text-sm"
|
||||
>
|
||||
<b>{remoteResource.vote_average.toFixed(1)}</b> TMDB
|
||||
</a>
|
||||
<div class="flex mt-4">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="text-white w-4"
|
||||
><g
|
||||
@@ -183,8 +189,17 @@
|
||||
<h3 class="text-xs tracking-wide uppercase">Starring</h3>
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each remoteResource.credits.slice(0, 5) as a}
|
||||
<div class="tracking-widest font-extralight text-sm">{a.name}</div>
|
||||
<a
|
||||
href={'https://www.themoviedb.org/person/' + a.id}
|
||||
target="_blank"
|
||||
class="tracking-widest font-extralight text-sm">{a.name}</a
|
||||
>
|
||||
{/each}
|
||||
<a
|
||||
href={'https://www.themoviedb.org/movie/' + remoteResource.id + '/cast'}
|
||||
target="_blank"
|
||||
class="tracking-widest font-extralight text-sm">View all...</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="page-controls" />
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { formatSize } from '$lib/utils';
|
||||
import Button from '../Ui/Button.svelte';
|
||||
import { DotFilled, Plus } from 'radix-icons-svelte';
|
||||
import RequestModal from '../RequestModal/RequestModal.svelte';
|
||||
import { addRadarrMovie, getQueuedById, getRadarrMovie } from '$lib/radarr/radarr';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
import { formatMinutes } from '$lib/utils.js';
|
||||
import classNames from 'classnames';
|
||||
|
||||
let isRequestModalVisible = false;
|
||||
export let tmdbId: string;
|
||||
|
||||
const { data: localResource, load, didLoad } = getRadarrMovie();
|
||||
const { data: queueResponse, load: loadQueued } = getQueuedById();
|
||||
const { data: addMovieResponse, loading: addMovieLoading, load: addToRadarr } = addRadarrMovie();
|
||||
|
||||
function refreshRadarrMovie() {
|
||||
if (tmdbId) load(tmdbId);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
refreshRadarrMovie();
|
||||
});
|
||||
|
||||
addMovieResponse.subscribe(() => {
|
||||
if ($addMovieResponse) refreshRadarrMovie();
|
||||
});
|
||||
|
||||
const headerStyle = 'uppercase tracking-widest font-bold';
|
||||
|
||||
function openRequestModal() {
|
||||
if ($localResource) isRequestModalVisible = true;
|
||||
}
|
||||
|
||||
localResource.subscribe((resource) => {
|
||||
if (resource?.id) loadQueued(resource.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-8 p-8">
|
||||
{#if !$didLoad}
|
||||
Loading...
|
||||
{:else if !$localResource?.movieFile}
|
||||
<div>
|
||||
<h1 class="text-lg mb-1 font-medium tracking-wide">No sources found</h1>
|
||||
<p class="text-zinc-300">
|
||||
No local or remote sources found for this title. You can configure your sources on the <a
|
||||
href="/sources"
|
||||
class="text-amber-200 hover:text-amber-100">sources</a
|
||||
> page.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $localResource}
|
||||
<div class="">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class={headerStyle}>Local Library</div>
|
||||
<IconButton on:click={openRequestModal}>
|
||||
<Plus size="20" />
|
||||
</IconButton>
|
||||
</div>
|
||||
{#each $queueResponse || [] as downloadingFile}
|
||||
<div
|
||||
class={classNames('border-l-2 p-1 px-4 flex justify-between items-center py-1', {
|
||||
'border-purple-400': downloadingFile.status === 'downloading',
|
||||
'border-amber-400': downloadingFile.status !== 'downloading'
|
||||
})}
|
||||
>
|
||||
<div class="flex gap-1 items-center">
|
||||
<b class="">{downloadingFile.quality.quality.resolution}p</b>
|
||||
<DotFilled class="text-zinc-300" />
|
||||
<h2 class="text-zinc-200 text-sm">{formatSize(downloadingFile.size)} on disk</h2>
|
||||
<DotFilled class="text-zinc-300" />
|
||||
<h2 class="uppercase text-zinc-200 text-sm">
|
||||
{downloadingFile.quality.quality.source}
|
||||
</h2>
|
||||
<DotFilled class="text-zinc-300" />
|
||||
<h2 class="text-zinc-200 text-sm">
|
||||
Completed in {downloadingFile.timeleft}
|
||||
</h2>
|
||||
</div>
|
||||
<Button size="sm" disabled={true}
|
||||
>{downloadingFile.status === 'downloding' ? 'Downloading...' : 'Importing...'}</Button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
{#each $localResource?.movieFile ? [$localResource.movieFile] : [] as movieFile (movieFile.id)}
|
||||
<div class="border-l-2 border-zinc-200 p-1 px-4 flex justify-between items-center my-1">
|
||||
<div class="flex gap-1 items-center">
|
||||
<b class="">{movieFile.quality.quality.resolution}p</b>
|
||||
<DotFilled class="text-zinc-300" />
|
||||
<h2 class="text-zinc-200 text-sm">{formatSize(movieFile.size)} on disk</h2>
|
||||
<DotFilled class="text-zinc-300" />
|
||||
<h2 class="uppercase text-zinc-200 text-sm">
|
||||
{movieFile.quality.quality.source}
|
||||
</h2>
|
||||
<DotFilled class="text-zinc-300" />
|
||||
<h2 class="uppercase text-zinc-200 text-sm">
|
||||
{movieFile.mediaInfo.videoCodec}
|
||||
</h2>
|
||||
</div>
|
||||
<Button size="sm">Stream</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-4 items-center bg-black p-8 py-4 empty:hidden">
|
||||
{#if !$localResource && $didLoad && tmdbId}
|
||||
<Button on:click={() => addToRadarr(tmdbId)} disabled={$addMovieLoading}>Add to Radarr</Button>
|
||||
{/if}
|
||||
{#if $localResource?.movieFile}
|
||||
<Button type="secondary">Manage Local Files</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $localResource?.id}
|
||||
<RequestModal
|
||||
bind:visible={isRequestModalVisible}
|
||||
radarrId={$localResource.id}
|
||||
on:download={() => refreshRadarrMovie()}
|
||||
/>
|
||||
{/if}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { TmdbMovieFull } from '$lib/tmdb-api';
|
||||
import { formatGenres, getRuntime } from '$lib/utils';
|
||||
import { formatGenres, formatMinutes } from '$lib/utils';
|
||||
import classNames from 'classnames';
|
||||
import { TMDB_IMAGES } from '$lib/constants';
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
{#if progressType === 'watched'}
|
||||
<div class="text-xs font-medium text-zinc-200">
|
||||
{progress
|
||||
? getRuntime(tmdbMovie.runtime - tmdbMovie.runtime * (progress / 100)) + ' left'
|
||||
: getRuntime(tmdbMovie.runtime)}
|
||||
? formatMinutes(tmdbMovie.runtime - tmdbMovie.runtime * (progress / 100)) + ' left'
|
||||
: formatMinutes(tmdbMovie.runtime)}
|
||||
</div>
|
||||
{:else if progressType === 'downloading'}
|
||||
<div class="text-xs font-medium text-zinc-200">
|
||||
|
||||
41
src/routes/components/Ui/Button.svelte
Normal file
41
src/routes/components/Ui/Button.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let size: 'md' | 'sm' | 'lg' = 'md';
|
||||
export let type: 'primary' | 'secondary' | 'tertiary' = 'primary';
|
||||
export let disabled = false;
|
||||
|
||||
export let href: string | undefined = undefined;
|
||||
export let target: string | undefined = undefined;
|
||||
|
||||
const buttonStyle = classNames(
|
||||
'border-2 border-white transition-colors uppercase tracking-widest text-xs',
|
||||
{
|
||||
'bg-white text-zinc-900 font-extrabold': type === 'primary',
|
||||
'hover:bg-amber-400 hover:border-amber-400': type === 'primary' && !disabled,
|
||||
'font-semibold': type === 'secondary',
|
||||
'hover:bg-white hover:text-black': type === 'secondary' && !disabled,
|
||||
'px-8 py-3.5': size === 'lg',
|
||||
'px-6 py-2.5': size === 'md',
|
||||
'px-5 py-2': size === 'sm',
|
||||
'opacity-70': disabled,
|
||||
'cursor-pointer': !disabled
|
||||
}
|
||||
);
|
||||
|
||||
const handleClick = (event) => {
|
||||
if (href) {
|
||||
if (target === '_blank') window.open(href, target).focus();
|
||||
else window.open(href, target as string);
|
||||
} else {
|
||||
dispatch('click', event);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<button class={buttonStyle} on:click={handleClick} on:mouseover on:mouseleave {disabled}>
|
||||
<slot />
|
||||
</button>
|
||||
@@ -1,18 +1,16 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { radarrApi } from '$lib/servarr-api';
|
||||
import { fetchMovieDetails } from '$lib/tmdb-api';
|
||||
import { fetchFullMovieDetails } from '$lib/tmdb-api';
|
||||
import { RadarrApi } from '$lib/radarr/radarr';
|
||||
|
||||
export const load = (async () => {
|
||||
const radarrMovies = await radarrApi
|
||||
.get('/api/v3/movie', {
|
||||
params: {}
|
||||
})
|
||||
.then((r) => r.data);
|
||||
const radarrMovies = await RadarrApi.get('/api/v3/movie', {
|
||||
params: {}
|
||||
}).then((r) => r.data);
|
||||
|
||||
let tmdbMovies;
|
||||
if (radarrMovies) {
|
||||
tmdbMovies = await Promise.all(
|
||||
radarrMovies.filter((m) => m.tmdbId).map((m) => fetchMovieDetails(m.tmdbId as any))
|
||||
radarrMovies.filter((m) => m.tmdbId).map((m) => fetchFullMovieDetails(String(m.tmdbId)))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,14 +19,12 @@ export const load = (async () => {
|
||||
return {
|
||||
radarrMovies,
|
||||
tmdbMovies,
|
||||
downloading: await radarrApi
|
||||
.get('/api/v3/queue', {
|
||||
params: {
|
||||
query: {
|
||||
includeMovie: true
|
||||
}
|
||||
downloading: await RadarrApi.get('/api/v3/queue', {
|
||||
params: {
|
||||
query: {
|
||||
includeMovie: true
|
||||
}
|
||||
})
|
||||
.then((r) => r.data?.records)
|
||||
}
|
||||
}).then((r) => r.data?.records)
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import ResourceDetails from '../../components/ResourceDetails/ResourceDetails.svelte';
|
||||
import ResourceLocalDetails from '../../components/ResourceDetails/ResourceLocalDetails.svelte';
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<ResourceDetails resource={data.movie} remoteResource={data.remoteMovie} />
|
||||
<ResourceLocalDetails tmdbId={data.remoteMovie.id} />
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { radarrApi } from '$lib/servarr-api';
|
||||
import { fetchMovieDetails, TmdbApi } from '$lib/tmdb-api';
|
||||
import { fetchFullMovieDetails, TmdbApi } from '$lib/tmdb-api';
|
||||
import { RadarrApi } from '$lib/radarr/radarr';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
return {
|
||||
movie: await radarrApi
|
||||
.get('/api/v3/movie', {
|
||||
params: {
|
||||
query: {
|
||||
tmdbId: Number(params.id)
|
||||
}
|
||||
movie: await RadarrApi.get('/api/v3/movie', {
|
||||
params: {
|
||||
query: {
|
||||
tmdbId: Number(params.id)
|
||||
}
|
||||
})
|
||||
.then((res) => res.data?.[0]),
|
||||
remoteMovie: fetchMovieDetails(params.id)
|
||||
}
|
||||
}).then((res) => res.data?.[0]),
|
||||
remoteMovie: fetchFullMovieDetails(params.id)
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
||||
0
src/routes/people/+page.svelte
Normal file
0
src/routes/people/+page.svelte
Normal file
0
src/routes/people/+page.ts
Normal file
0
src/routes/people/+page.ts
Normal file
@@ -8,7 +8,7 @@ export default {
|
||||
display: ['Inter', 'system', 'sans-serif']
|
||||
},
|
||||
colors: {
|
||||
darken: '#070501bf',
|
||||
darken: '#07050199',
|
||||
'highlight-dim': '#fde68a20'
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user