diff --git a/.dockerignore b/.dockerignore index c33d6bf..c58ee53 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,3 +28,4 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* +.idea \ No newline at end of file diff --git a/.gitignore b/.gitignore index 699266e..252f3c4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* +.idea \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 3c4586e..a0f8a74 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,4 +2,5 @@ pnpm-lock.yaml package-lock.json yarn.lock -CHANGELOG.md \ No newline at end of file +CHANGELOG.md +.idea \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 2e7cd4c..b422dc9 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,9 +1,13 @@ #!/bin/sh -# Export the ORIGIN and BACKEND_URL environment variables -export ORIGIN=${ORIGIN} +if [ -z "$ORIGIN" ]; then + echo "ORIGIN is not set" + export PROTOCOL_HEADER=x-forwarded-proto + export HOST_HEADER=x-forwarded-host +else + export ORIGIN=${ORIGIN} +fi + export BACKEND_URL=${BACKEND_URL} export DIALECT=${DIALECT} export DATABASE_URL=${DATABASE_URL} - -# Execute the command provided to the script exec "$@" \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 1d362fb..3decb1c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -48,6 +48,6 @@ export default [ } }, { - ignores: ['build/', '.svelte-kit/', 'dist/', 'src/lib/components/ui/'] + ignores: ['build/', '.svelte-kit/', 'dist/', 'src/lib/components/ui/', '.idea/'] } ]; diff --git a/src/lib/forms/general-form.svelte b/src/lib/forms/general-form.svelte index b377853..b9ecf38 100644 --- a/src/lib/forms/general-form.svelte +++ b/src/lib/forms/general-form.svelte @@ -266,6 +266,65 @@ {/if} + + + + + {#if $formData.subliminal_enabled} +
+ + {#each $formData.subliminal_languages as _, i} + + +
+ + +
+ { + removeField('subliminal_languages', i); + }} + > + + +
+
+
+
+ {/each} + +
+

Add subtitle languages

+ { + addField('subliminal_languages'); + }} + > + + +
+
+
+ {/if} +
diff --git a/src/lib/forms/helpers.ts b/src/lib/forms/helpers.ts index e8ab170..ca6b34f 100644 --- a/src/lib/forms/helpers.ts +++ b/src/lib/forms/helpers.ts @@ -8,7 +8,8 @@ export const generalSettingsToGet: string[] = [ 'symlink', 'downloaders', 'database', - 'notifications' + 'notifications', + 'post_processing' ]; export const generalSettingsSchema = z.object({ @@ -38,7 +39,9 @@ export const generalSettingsSchema = z.object({ notifications_enabled: z.boolean().default(false), notifications_title: z.string().optional().default('Riven completed something'), notifications_on_item_type: z.string().array().optional().default([]), - notifications_service_urls: z.string().array().optional().default([]) + notifications_service_urls: z.string().array().optional().default([]), + subliminal_enabled: z.boolean().default(false), + subliminal_languages: z.string().array().optional().default([]) }); export type GeneralSettingsSchema = typeof generalSettingsSchema; @@ -68,7 +71,9 @@ export function generalSettingsToPass(data: any) { notifications_enabled: data.data.notifications.enabled, notifications_title: data.data.notifications.title, notifications_on_item_type: data.data.notifications.on_item_type, - notifications_service_urls: data.data.notifications.service_urls + notifications_service_urls: data.data.notifications.service_urls, + subliminal_enabled: data.data.post_processing.subliminal.enabled, + subliminal_languages: data.data.post_processing.subliminal?.languages }; } @@ -129,6 +134,15 @@ export function generalSettingsToSet(form: SuperValidated; - blacklisted: boolean; infohash: string; lev_ratio: number; - parent_id: number; parsed_title: string; rank: number; raw_title: string; } +export interface StreamBlacklistRelation { + _id: Generated; + media_item_id: number; + stream_id: number; +} + +export interface StreamRelation { + _id: Generated; + child_id: number; + parent_id: number; +} + +export interface Subtitle { + _id: Generated; + file: string | null; + language: string; + parent_id: number; +} + export interface DB { alembic_version: AlembicVersion; Episode: Episode; @@ -96,4 +113,7 @@ export interface DB { Season: Season; Show: Show; Stream: Stream; + StreamBlacklistRelation: StreamBlacklistRelation; + StreamRelation: StreamRelation; + Subtitle: Subtitle; } diff --git a/src/routes/[type]/[id]/+page.svelte b/src/routes/[type]/[id]/+page.svelte index 9ae2010..d6f391d 100644 --- a/src/routes/[type]/[id]/+page.svelte +++ b/src/routes/[type]/[id]/+page.svelte @@ -2,7 +2,16 @@ import type { PageData } from './$types'; import Header from '$lib/components/header.svelte'; import { Badge } from '$lib/components/ui/badge'; - import { Star, Trash2, Download, ArrowUpRight, Tag, Wrench } from 'lucide-svelte'; + import { + Star, + Trash2, + Download, + ArrowUpRight, + Tag, + Wrench, + RotateCcw, + CirclePower + } from 'lucide-svelte'; import * as Carousel from '$lib/components/ui/carousel/index.js'; import { Button } from '$lib/components/ui/button'; import * as Tooltip from '$lib/components/ui/tooltip'; @@ -38,6 +47,32 @@ } } + async function retryItem(_id: number) { + const response = await fetch(`/api/media/${_id}/retry`, { + method: 'POST' + }); + + if (response.ok) { + toast.success('Media retried successfully'); + invalidateAll(); + } else { + toast.error('An error occurred while retrying the media'); + } + } + + async function resetItem(_id: number) { + const response = await fetch(`/api/media/${_id}/reset`, { + method: 'POST' + }); + + if (response.ok) { + toast.success('Media reset successfully'); + invalidateAll(); + } else { + toast.error('An error occurred while resetting the media'); + } + } + async function requestItem(imdb: number) { const response = await fetch(`/api/media/${imdb}`, { method: 'POST' @@ -225,6 +260,7 @@ builders={[builder]} class="flex items-center gap-1" variant="destructive" + disabled={true} > Delete @@ -250,6 +286,69 @@ + + + + + + + + Are you absolutely sure? + + This action will remove the item from queue and insert it back + + + + Cancel + { + if (data.db) { + await retryItem(data.db._id); + } + }}>Continue + + + + + + + + + + + Are you absolutely sure? + + This action will reset the media to its initial state and blacklist the + torrent + + + + Cancel + { + if (data.db) { + await resetItem(data.db._id); + } + }}>Continue + + + {/if}
{#if data.details.belongs_to_collection} diff --git a/src/routes/api/media/[id]/+server.ts b/src/routes/api/media/[id]/+server.ts index 1557ac6..85fd22f 100644 --- a/src/routes/api/media/[id]/+server.ts +++ b/src/routes/api/media/[id]/+server.ts @@ -54,7 +54,7 @@ export const DELETE: RequestHandler = async ({ params, locals }) => { const id = params.id; try { - const itemDeleteResponse = await fetch(`${locals.BACKEND_URL}/items/remove?${id}`, { + const itemDeleteResponse = await fetch(`${locals.BACKEND_URL}/items?${id}`, { method: 'DELETE' }); diff --git a/src/routes/api/media/[id]/reset/+server.ts b/src/routes/api/media/[id]/reset/+server.ts new file mode 100644 index 0000000..88b9f90 --- /dev/null +++ b/src/routes/api/media/[id]/reset/+server.ts @@ -0,0 +1,51 @@ +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ params, locals }) => { + const id = params.id; + + try { + const response = await fetch(`${locals.BACKEND_URL}/items/reset?ids=${id}`, { + method: 'POST' + }); + + if (response.ok) { + const data = await response.json(); + return new Response( + JSON.stringify({ + success: 'Media item reset', + data + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } else { + return new Response( + JSON.stringify({ + error: 'Failed to reset media item' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } + } catch { + return new Response( + JSON.stringify({ + error: 'Failed to reset media item' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } +}; diff --git a/src/routes/api/media/[id]/retry/+server.ts b/src/routes/api/media/[id]/retry/+server.ts new file mode 100644 index 0000000..d9402a3 --- /dev/null +++ b/src/routes/api/media/[id]/retry/+server.ts @@ -0,0 +1,51 @@ +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ params, locals }) => { + const id = params.id; + + try { + const response = await fetch(`${locals.BACKEND_URL}/items/retry?ids=${id}`, { + method: 'POST' + }); + + if (response.ok) { + const data = await response.json(); + return new Response( + JSON.stringify({ + success: 'Media item retried', + data + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } else { + return new Response( + JSON.stringify({ + error: 'Failed to retry media item' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } + } catch { + return new Response( + JSON.stringify({ + error: 'Failed to retry media item' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } +}; diff --git a/src/routes/browse/+page.svelte b/src/routes/browse/+page.svelte index 917c318..11607cb 100644 --- a/src/routes/browse/+page.svelte +++ b/src/routes/browse/+page.svelte @@ -32,6 +32,7 @@
{ const query = url.searchParams.get('query'); if (query && query.length > 0) { const moviesRes = await getMovieSearch(fetch, query, false, 'en-US', null, 1, null, null); - const tvRes = await getTVSearch(fetch, query, null, false, 'en-US', 1, null); + const collectionRes = await getCollectionSearch(fetch, query, false, 'en-US', 1, null); return { movies: moviesRes.results, - shows: tvRes.results + shows: tvRes.results, + collections: collectionRes.results }; } else { return { diff --git a/src/routes/library/+page.server.ts b/src/routes/library/+page.server.ts index befab77..46a9faf 100644 --- a/src/routes/library/+page.server.ts +++ b/src/routes/library/+page.server.ts @@ -52,8 +52,7 @@ export const load = (async ({ url, locals }) => { if (query && query.length > 0) { const fuse = new Fuse(await dbQuery.execute(), fuseOptions); - const searchResults = fuse.search(query).map((result) => result.item); - return searchResults; + return fuse.search(query).map((result) => result.item); } return await dbQuery.execute(); diff --git a/src/routes/library/+page.svelte b/src/routes/library/+page.svelte index 7bf13d8..b247f78 100644 --- a/src/routes/library/+page.svelte +++ b/src/routes/library/+page.svelte @@ -198,7 +198,7 @@ {:else} - + {page.value} diff --git a/src/routes/settings/all.json/+server.ts b/src/routes/settings/all.json/+server.ts new file mode 100644 index 0000000..460413d --- /dev/null +++ b/src/routes/settings/all.json/+server.ts @@ -0,0 +1,53 @@ +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ fetch, locals }) => { + try { + const response = await fetch(`${locals.BACKEND_URL}/settings/get/all`, { + method: 'GET' + }); + + if (response.ok) { + const data = await response.json(); + return new Response( + JSON.stringify( + { + data + }, + null, + 2 + ), + { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } else { + return new Response( + JSON.stringify({ + error: 'Failed to get settings' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } + } catch (e) { + console.error(e); + return new Response( + JSON.stringify({ + error: 'Failed to get settings' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } +};