mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-21 16:25:36 +02:00
add ability to import unknown tv shows
This commit is contained in:
122
web/src/lib/api/api.d.ts
vendored
122
web/src/lib/api/api.d.ts
vendored
@@ -341,6 +341,46 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/tv/importable': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Get All Importable Shows
|
||||
* @description get a list of unknown shows that were detected in the tv directory and are importable
|
||||
*/
|
||||
get: operations['get_all_importable_shows_api_v1_tv_importable_get'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/tv/importable/{show_id}': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Import Detected Show
|
||||
* @description get a list of unknown shows that were detected in the tv directory and are importable
|
||||
*/
|
||||
post: operations['import_detected_show_api_v1_tv_importable__show_id__post'];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/api/v1/tv/shows/torrents': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1625,6 +1665,16 @@ export interface components {
|
||||
* @enum {integer}
|
||||
*/
|
||||
TorrentStatus: 1 | 2 | 3 | 4;
|
||||
/** TvShowImportSuggestion */
|
||||
TvShowImportSuggestion: {
|
||||
/**
|
||||
* Directory
|
||||
* Format: path
|
||||
*/
|
||||
directory: string;
|
||||
/** Candidates */
|
||||
candidates: components['schemas']['MetaDataProviderSearchResult'][];
|
||||
};
|
||||
/** UpdateSeasonRequest */
|
||||
UpdateSeasonRequest: {
|
||||
min_quality: components['schemas']['Quality'];
|
||||
@@ -2489,15 +2539,6 @@ export interface operations {
|
||||
'application/json': components['schemas']['Show'];
|
||||
};
|
||||
};
|
||||
/** @description Show already exists */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': string;
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
@@ -2591,6 +2632,69 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
get_all_importable_shows_api_v1_tv_importable_get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
metadata_provider?: 'tmdb' | 'tvdb';
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['TvShowImportSuggestion'][];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['HTTPValidationError'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
import_detected_show_api_v1_tv_importable__show_id__post: {
|
||||
parameters: {
|
||||
query: {
|
||||
directory: string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
/** @description The ID of the show */
|
||||
show_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['HTTPValidationError'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
get_shows_with_torrents_api_v1_tv_shows_torrents_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user