diff --git a/media_manager/torrent/router.py b/media_manager/torrent/router.py index ce90c83..2df2edd 100644 --- a/media_manager/torrent/router.py +++ b/media_manager/torrent/router.py @@ -2,18 +2,13 @@ from fastapi import APIRouter from fastapi import status from fastapi.params import Depends -from media_manager.auth.users import current_active_user +from media_manager.auth.users import current_active_user, current_superuser from media_manager.torrent.dependencies import torrent_service_dep, torrent_dep from media_manager.torrent.schemas import Torrent router = APIRouter() -@router.get("/{torrent_id}", status_code=status.HTTP_200_OK, response_model=Torrent) -def get_torrent(service: torrent_service_dep, torrent: torrent_dep): - return service.get_torrent_by_id(torrent_id=torrent.id) - - @router.get( "", status_code=status.HTTP_200_OK, @@ -22,3 +17,39 @@ def get_torrent(service: torrent_service_dep, torrent: torrent_dep): ) def get_all_torrents(service: torrent_service_dep): return service.get_all_torrents() + + +@router.get("/{torrent_id}", status_code=status.HTTP_200_OK, response_model=Torrent) +def get_torrent(service: torrent_service_dep, torrent: torrent_dep): + return service.get_torrent_by_id(torrent_id=torrent.id) + + +@router.delete( + "/{torrent_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(current_superuser)], +) +def delete_torrent( + service: torrent_service_dep, + torrent: torrent_dep, + delete_files: bool = False, +): + try: + service.cancel_download(torrent=torrent, delete_files=delete_files) + except RuntimeError: + pass + + service.delete_torrent(torrent_id=torrent.id) + + +@router.post( + "/{torrent_id}/retry", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(current_superuser)], +) +def retry_torrent_download( + service: torrent_service_dep, + torrent: torrent_dep, +): + service.pause_download(torrent=torrent) + service.resume_download(torrent=torrent) diff --git a/media_manager/torrent/service.py b/media_manager/torrent/service.py index 751eb11..1322bca 100644 --- a/media_manager/torrent/service.py +++ b/media_manager/torrent/service.py @@ -105,13 +105,9 @@ class TorrentService: self.torrent_repository.get_torrent_by_id(torrent_id=torrent_id) ) - # TODO: extract deletion logic to tv module - # def delete_torrent(self, torrent_id: TorrentId): - # t = self.torrent_repository.get_torrent_by_id(torrent_id=torrent_id) - # if not t.imported: - # from media_manager.tv.repository import remove_season_files_by_torrent_id - # remove_season_files_by_torrent_id(db=self.db, torrent_id=torrent_id) - # media_manager.torrent.repository.delete_torrent(db=self.db, torrent_id=t.id) + def delete_torrent(self, torrent_id: TorrentId): + t = self.torrent_repository.get_torrent_by_id(torrent_id=torrent_id) + self.torrent_repository.delete_torrent(torrent_id=t.id) def get_movie_files_of_torrent(self, torrent: Torrent): return self.torrent_repository.get_movie_files_of_torrent(torrent_id=torrent.id) diff --git a/web/src/lib/api/api.d.ts b/web/src/lib/api/api.d.ts index 42a3394..9e32386 100644 --- a/web/src/lib/api/api.d.ts +++ b/web/src/lib/api/api.d.ts @@ -248,6 +248,43 @@ export interface paths { patch?: never; trace?: never; }; + '/api/v1/auth/oauth/authorize': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Oauth:Oauth2.Cookie.Authorize */ + get: operations['oauth_oauth2_cookie_authorize_api_v1_auth_oauth_authorize_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/auth/oauth/callback': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Oauth:Oauth2.Cookie.Callback + * @description The response varies based on the authentication backend used. + */ + get: operations['oauth_oauth2_cookie_callback_api_v1_auth_oauth_callback_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/api/v1/tv/shows': { parameters: { query?: never; @@ -544,23 +581,6 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/torrent/{torrent_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get Torrent */ - get: operations['get_torrent_api_v1_torrent__torrent_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; '/api/v1/torrent': { parameters: { query?: never; @@ -578,6 +598,41 @@ export interface paths { patch?: never; trace?: never; }; + '/api/v1/torrent/{torrent_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Torrent */ + get: operations['get_torrent_api_v1_torrent__torrent_id__get']; + put?: never; + post?: never; + /** Delete Torrent */ + delete: operations['delete_torrent_api_v1_torrent__torrent_id__delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/torrent/{torrent_id}/retry': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Retry Torrent Download */ + post: operations['retry_torrent_download_api_v1_torrent__torrent_id__retry_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/api/v1/movies': { parameters: { query?: never; @@ -1221,6 +1276,11 @@ export interface components { */ timestamp?: string; }; + /** OAuth2AuthorizeResponse */ + OAuth2AuthorizeResponse: { + /** Authorization Url */ + authorization_url: string; + }; /** PublicMovie */ PublicMovie: { /** @@ -2275,6 +2335,80 @@ export interface operations { }; }; }; + oauth_oauth2_cookie_authorize_api_v1_auth_oauth_authorize_get: { + parameters: { + query?: { + scopes?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['OAuth2AuthorizeResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + oauth_oauth2_cookie_callback_api_v1_auth_oauth_callback_get: { + parameters: { + query?: { + code?: string | null; + code_verifier?: string | null; + state?: string | null; + error?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorModel']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; get_all_shows_api_v1_tv_shows_get: { parameters: { query?: never; @@ -2905,6 +3039,26 @@ export interface operations { }; }; }; + get_all_torrents_api_v1_torrent_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Torrent'][]; + }; + }; + }; + }; get_torrent_api_v1_torrent__torrent_id__get: { parameters: { query?: never; @@ -2936,22 +3090,62 @@ export interface operations { }; }; }; - get_all_torrents_api_v1_torrent_get: { + delete_torrent_api_v1_torrent__torrent_id__delete: { parameters: { - query?: never; + query?: { + delete_files?: boolean; + }; header?: never; - path?: never; + path: { + torrent_id: string; + }; cookie?: never; }; requestBody?: never; responses: { /** @description Successful Response */ - 200: { + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation Error */ + 422: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['Torrent'][]; + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + retry_torrent_download_api_v1_torrent__torrent_id__retry_post: { + parameters: { + query?: never; + header?: never; + path: { + torrent_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']; }; }; }; diff --git a/web/src/lib/components/delete-torrent-dialog.svelte b/web/src/lib/components/delete-torrent-dialog.svelte new file mode 100644 index 0000000..fec3895 --- /dev/null +++ b/web/src/lib/components/delete-torrent-dialog.svelte @@ -0,0 +1,61 @@ + + + + Delete Torrent + + + Delete a Torrent + + Delete Torrent "{torrentName}". This action cannot be undone! + + +
+ +
+ +
+
+ + + + + +
+
diff --git a/web/src/lib/components/torrent-table.svelte b/web/src/lib/components/torrent-table.svelte index b61cd5d..e95ab04 100644 --- a/web/src/lib/components/torrent-table.svelte +++ b/web/src/lib/components/torrent-table.svelte @@ -6,8 +6,43 @@ } from '$lib/utils.js'; import CheckmarkX from '$lib/components/checkmark-x.svelte'; import * as Table from '$lib/components/ui/table/index.js'; + import type { components } from '$lib/api/api'; + import { getContext } from 'svelte'; + import { Button } from '$lib/components/ui/button/index.js'; + import client from '$lib/api'; + import { toast } from 'svelte-sonner'; + import DeleteTorrentDialog from '$lib/components/delete-torrent-dialog.svelte'; - let { torrents, isShow = true } = $props(); + let { + torrents, + isShow = true + }: { + torrents: + | components['schemas']['MovieTorrent'][] + | components['schemas']['RichSeasonTorrent'][]; + isShow: boolean; + } = $props(); + + let user: () => components['schemas']['UserRead'] = getContext('user'); + + async function retryTorrentDownload( + torrent: components['schemas']['MovieTorrent'] | components['schemas']['RichSeasonTorrent'] + ) { + console.log(`Retrying download for torrent ${torrent.torrent_title}`); + const { error } = await client.POST('/api/v1/torrent/{torrent_id}/retry', { + params: { + path: { + torrent_id: torrent.torrent_id! + } + } + }); + if (error) { + toast.error(`Failed on retrying download: ${error}`); + } else { + console.log(`Successfully retried download for torrent ${torrent.torrent_title}`); + toast.success('Trying to download torrent...'); + } + } @@ -22,6 +57,9 @@ Quality File Path Suffix Imported + {#if user().is_superuser} + Actions + {/if} @@ -32,7 +70,9 @@ {#if isShow} - {convertTorrentSeasonRangeToIntegerRange(torrent)} + {convertTorrentSeasonRangeToIntegerRange( + (torrent as components['schemas']['RichSeasonTorrent']).seasons! + )} {/if} @@ -47,6 +87,17 @@ + {#if user().is_superuser} + + {#if 'finished' !== getTorrentStatusString(torrent.status)} + + {/if} + + + {/if} {/each} diff --git a/web/src/routes/dashboard/movies/torrents/+page.svelte b/web/src/routes/dashboard/movies/torrents/+page.svelte index 05fda7a..e0f717f 100644 --- a/web/src/routes/dashboard/movies/torrents/+page.svelte +++ b/web/src/routes/dashboard/movies/torrents/+page.svelte @@ -6,16 +6,8 @@ import * as Accordion from '$lib/components/ui/accordion/index.js'; import * as Card from '$lib/components/ui/card/index.js'; import TorrentTable from '$lib/components/torrent-table.svelte'; - import { onMount } from 'svelte'; import { base } from '$app/paths'; - import client from '$lib/api'; - import type { components } from '$lib/api/api'; - - let torrents: components['schemas']['RichMovieTorrent'][] = []; - onMount(async () => { - const { data } = await client.GET('/api/v1/movies/torrents'); - torrents = data as components['schemas']['RichMovieTorrent'][]; - }); + import { page } from '$app/state'; @@ -54,7 +46,7 @@ Movie Torrents - {#each torrents as movie (movie.movie_id)} + {#each page.data.torrents as movie (movie.movie_id)}
diff --git a/web/src/routes/dashboard/movies/torrents/+page.ts b/web/src/routes/dashboard/movies/torrents/+page.ts new file mode 100644 index 0000000..e8a81d5 --- /dev/null +++ b/web/src/routes/dashboard/movies/torrents/+page.ts @@ -0,0 +1,8 @@ +import type { PageLoad } from './$types'; +import client from '$lib/api'; + +export const load: PageLoad = async ({ fetch }) => { + const { data } = await client.GET('/api/v1/movies/torrents', { fetch: fetch }); + + return { torrents: data }; +}; diff --git a/web/src/routes/dashboard/tv/torrents/+page.svelte b/web/src/routes/dashboard/tv/torrents/+page.svelte index 07decc8..5650812 100644 --- a/web/src/routes/dashboard/tv/torrents/+page.svelte +++ b/web/src/routes/dashboard/tv/torrents/+page.svelte @@ -8,8 +8,6 @@ import * as Card from '$lib/components/ui/card/index.js'; import TorrentTable from '$lib/components/torrent-table.svelte'; import { resolve } from '$app/paths'; - import type { components } from '$lib/api/api'; - let showsPromise: Promise = $state(page.data.shows); @@ -47,26 +45,22 @@

TV Torrents

- {#await showsPromise} - Loading... - {:then shows} - - {#each shows as show (show.show_id)} -
- - - - {getFullyQualifiedMediaName(show)} - - - - - - -
- {:else} -
No Torrents added yet.
- {/each} -
- {/await} + + {#each page.data.torrents as show (show.show_id)} +
+ + + + {getFullyQualifiedMediaName(show)} + + + + + + +
+ {:else} +
No Torrents added yet.
+ {/each} +
diff --git a/web/src/routes/dashboard/tv/torrents/+page.ts b/web/src/routes/dashboard/tv/torrents/+page.ts index 112e143..7369ba1 100644 --- a/web/src/routes/dashboard/tv/torrents/+page.ts +++ b/web/src/routes/dashboard/tv/torrents/+page.ts @@ -3,5 +3,5 @@ import client from '$lib/api'; export const load: PageLoad = async ({ fetch }) => { const { data } = await client.GET('/api/v1/tv/shows/torrents', { fetch: fetch }); - return { shows: data }; + return { torrents: data }; };