add ability to import unknown tv shows

This commit is contained in:
maxid
2025-12-07 17:38:37 +01:00
parent aca60cd9b7
commit 13b32e7104
6 changed files with 326 additions and 12 deletions

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card/index.js';
let {
directory,
isTv,
children
}: {
directory: string;
isTv: boolean;
children: any;
} = $props();
</script>
<Card.Root class="col-span-full flex h-full flex-col overflow-x-hidden sm:col-span-1">
<Card.Header>
<Card.Title class="flex h-12 items-center leading-tight">
An importable {isTv ? 'TV show' : 'movie'} was detected!
</Card.Title>
<Card.Description>
The detected {isTv ? 'TV show' : 'movie'} is in this directory:
<code
class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold"
>
{directory}
</code>
</Card.Description>
</Card.Header>
<Card.Content class="flex flex-1 items-center justify-center">
{@render children?.()}
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,99 @@
<script lang="ts">
import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { toast } from 'svelte-sonner';
import client from '$lib/api';
import type { components } from '$lib/api/api';
import { Spinner } from '$lib/components/ui/spinner';
import SuggestedMediaCard from '$lib/components/import-media/suggested-media-card.svelte';
let {
isTv,
name,
candidates,
children
}: {
isTv: boolean;
name: string;
candidates: components['schemas']['MetaDataProviderSearchResult'][];
children: any;
} = $props();
let dialogOpen = $state(false);
let submitRequestError = $state<string | null>(null);
let isImporting = $state<boolean>(false);
async function handleImportMedia(media: components['schemas']['MetaDataProviderSearchResult']) {
isImporting = true;
submitRequestError = null;
let { data } = await client.POST('/api/v1/tv/shows', {
params: {
query: {
metadata_provider: media.metadata_provider as 'tmdb' | 'tvdb',
show_id: media.external_id
}
}
});
console.log('oida:', data);
let showId = data?.id ?? 'no_id';
const { error } = await client.POST('/api/v1/tv/importable/{show_id}', {
params: {
path: {
show_id: showId
},
query: {
directory: name
}
}
});
isImporting = false;
if (error) {
toast.error('Failed to import');
} else {
dialogOpen = false;
toast.success('Imported successfully!');
}
}
</script>
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Trigger
class={buttonVariants({ variant: 'default' })}
onclick={() => {
dialogOpen = true;
}}
>
{@render children?.()}
</Dialog.Trigger>
<Dialog.Content class="max-h-[90vh] w-fit min-w-[80vw] overflow-y-auto">
<Dialog.Header>
<Dialog.Title>Import unknown {isTv ? 'show' : 'movie'} "{name}"</Dialog.Title>
<Dialog.Description
>Select the {isTv ? 'show' : 'movie'} that is in this directory to import it!
</Dialog.Description>
</Dialog.Header>
<div
class="grid w-full auto-rows-min gap-4 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4"
>
{#if !isImporting}
{#each candidates as candidate (candidate.external_id)}
<SuggestedMediaCard result={candidate} action={() => handleImportMedia(candidate)}
></SuggestedMediaCard>
{:else}
No {isTv ? 'shows' : 'movies'} were found, change the directory's name for better search results!
{/each}
{:else}
<Spinner class="size-8"></Spinner>
{/if}
{#if submitRequestError}
<p class="col-span-full text-center text-sm text-red-500">{submitRequestError}</p>
{/if}
</div>
<Dialog.Footer>
<Button disabled={isImporting} onclick={() => (dialogOpen = false)} variant="outline"
>Cancel
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { ImageOff } from 'lucide-svelte';
import type { components } from '$lib/api/api';
let {
result,
action
}: {
result: components['schemas']['MetaDataProviderSearchResult'];
action: () => void;
} = $props();
</script>
<Card.Root class="col-span-full flex h-full flex-col overflow-x-hidden sm:col-span-1">
<Card.Header>
<Card.Title class="flex h-12 items-center leading-tight">
{result.name}
{#if result.year != null}
({result.year})
{/if}
</Card.Title>
<Card.Description class="truncate">
{result.overview !== '' ? result.overview : 'No overview available'}
</Card.Description>
</Card.Header>
<Card.Content class="flex flex-1 items-center justify-center">
{#if result.poster_path != null}
<img
class="h-full w-full rounded-lg object-contain"
src={result.poster_path}
alt="{result.name}'s Poster Image"
/>
{:else}
<div class="flex h-full w-full items-center justify-center">
<ImageOff class="h-12 w-12 text-gray-400" />
</div>
{/if}
</Card.Content>
<Card.Footer class="flex flex-col items-start gap-2 rounded-b-lg border-t bg-card p-4">
<Button class="w-full font-semibold" onclick={() => action()}>
Import using this metadata
</Button>
<div class="flex w-full items-center gap-2">
{#if result.vote_average != null}
<span class="flex items-center text-sm font-medium text-yellow-600">
<svg class="mr-1 h-4 w-4 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"
><path
d="M10 15l-5.878 3.09 1.122-6.545L.488 6.91l6.561-.955L10 0l2.951 5.955 6.561.955-4.756 4.635 1.122 6.545z"
/></svg
>
Rating: {Math.round(result.vote_average)}/10
</span>
{/if}
</div>
</Card.Footer>
</Card.Root>