refactor: update API URL handling for client and server environments

This commit is contained in:
maxDorninger
2025-05-30 12:14:24 +02:00
parent 71cbed8a6f
commit 6150080ac7
27 changed files with 100 additions and 50 deletions

View File

@@ -2,6 +2,13 @@ FROM ghcr.io/astral-sh/uv:debian-slim
ARG VERSION
LABEL version=${VERSION}
LABEL description="Docker image for the backend of MediaManager"
ENV IMAGE_DIRECTORY=/data/images
ENV TV_SHOW_DIRECTORY=/data/tv
ENV MOVIE_DIRECTORY=/data/movies
ENV TORRENT_DIRECTORY=/data/torrents
ENV OAUTH_ENABLED=FALSE
WORKDIR /app
COPY media_manager ./media_manager
COPY alembic ./alembic

View File

@@ -7,7 +7,7 @@ class AuthConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="AUTH_")
token_secret: str
session_lifetime: int = 60 * 60 * 24
admin_email: str | list[str]
admin_email: str
@property
def jwt_signing_key(self):

View File

@@ -10,5 +10,5 @@ class BasicConfig(BaseSettings):
movie_directory: Path = "./movie"
torrent_directory: Path = "./torrent"
FRONTEND_URL: AnyHttpUrl
CORS_URLS: str = ""
DEVELOPMENT: bool = False

View File

@@ -4,7 +4,6 @@ from logging.config import dictConfig
from pythonjsonlogger.json import JsonFormatter
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
@@ -52,23 +51,9 @@ import media_manager.torrent.router as torrent_router
init_db()
log.info("Database initialized")
from media_manager.auth.users import oauth_client
from media_manager.auth.users import SECRET as AUTH_USERS_SECRET
from media_manager.config import BasicConfig
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from media_manager.auth.schemas import UserCreate, UserRead, UserUpdate
from media_manager.auth.users import (
bearer_auth_backend,
fastapi_users,
cookie_auth_backend,
oauth_cookie_auth_backend,
)
from media_manager.auth.router import users_router as custom_users_router
from media_manager.auth.router import auth_metadata_router
basic_config = BasicConfig()
if basic_config.DEVELOPMENT:
@@ -87,9 +72,10 @@ if basic_config.DEVELOPMENT:
"*",
]
else:
origins = [
basic_config.FRONTEND_URL,
]
origins = basic_config.CORS_URLS.split(",")
log.info("CORS URLs activated for following origins:")
for origin in origins:
log.info(f" - {origin}")
app.add_middleware(
CORSMiddleware,
@@ -99,6 +85,21 @@ app.add_middleware(
allow_headers=["*"],
)
import uvicorn
from fastapi.staticfiles import StaticFiles
from media_manager.auth.users import oauth_client
from media_manager.auth.users import SECRET as AUTH_USERS_SECRET
from media_manager.auth.router import users_router as custom_users_router
from media_manager.auth.router import auth_metadata_router
from media_manager.auth.schemas import UserCreate, UserRead, UserUpdate
from media_manager.auth.users import (
bearer_auth_backend,
fastapi_users,
cookie_auth_backend,
oauth_cookie_auth_backend,
)
# Standard Auth Routers
app.include_router(
fastapi_users.get_auth_router(bearer_auth_backend),

View File

@@ -14,6 +14,7 @@ LABEL version=${VERSION}
LABEL description="Docker image for the web frontend of MediaManager"
ENV PUBLIC_VERSION=${VERSION}
ENV PUBLIC_SSR_WEB=false
WORKDIR /app
COPY --chown=node:node package*.json ./

View File

@@ -7,7 +7,9 @@
import {base} from '$app/paths';
import type {MetaDataProviderShowSearchResult} from '$lib/types.js';
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
import {browser} from "$app/environment";
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
let loading = $state(false);
let errorMessage = $state(null);
let {result}: { result: MetaDataProviderShowSearchResult } = $props();
@@ -15,7 +17,7 @@
async function addShow() {
loading = true;
let url = new URL(env.PUBLIC_API_URL + '/tv/shows');
let url = new URL(apiUrl + '/tv/shows');
url.searchParams.append('show_id', String(result.external_id));
url.searchParams.append('metadata_provider', result.metadata_provider);
const response = await fetch(url, {

View File

@@ -12,7 +12,9 @@
import * as Tabs from '$lib/components/ui/tabs/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import {browser} from "$app/environment";
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
let {show} = $props();
let dialogueState = $state(false);
let selectedSeasonNumber: number = $state(1);
@@ -23,7 +25,7 @@
let filePathSuffix: string = $state('');
async function downloadTorrent(result_id: string) {
let url = new URL(env.PUBLIC_API_URL + '/tv/torrents');
let url = new URL(apiUrl + '/tv/torrents');
url.searchParams.append('public_indexer_result_id', result_id);
url.searchParams.append('show_id', show.id);
if (filePathSuffix !== '') {
@@ -67,7 +69,7 @@
torrentsError = null;
torrents = [];
let url = new URL(env.PUBLIC_API_URL + '/tv/torrents');
let url = new URL(apiUrl + '/tv/torrents');
url.searchParams.append('show_id', show.id);
if (override) {
url.searchParams.append('search_query_override', queryOverride);

View File

@@ -8,8 +8,9 @@
import * as Tabs from '$lib/components/ui/tabs/index.js';
import {toast} from 'svelte-sonner';
import LoadingBar from '$lib/components/loading-bar.svelte';
import {browser} from "$app/environment";
let apiUrl = env.PUBLIC_API_URL;
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
let {oauthProvider} = $props();
let oauthProviderName = $derived(oauthProvider.oauth_name);

View File

@@ -8,7 +8,9 @@
import type {CreateSeasonRequest, PublicShow, Quality} from '$lib/types.js';
import {getFullyQualifiedShowName, getTorrentQualityString} from '$lib/utils.js';
import {toast} from 'svelte-sonner';
import {browser} from "$app/environment";
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
let {show}: { show: PublicShow } = $props();
let dialogOpen = $state(false);
@@ -41,7 +43,7 @@
}));
for (const payload of payloads) {
try {
const response = await fetch(`${env.PUBLIC_API_URL}/tv/seasons/requests`, {
const response = await fetch(`${apiUrl}/tv/seasons/requests`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@@ -9,7 +9,9 @@
import {toast} from 'svelte-sonner';
import {goto} from '$app/navigation';
import {base} from '$app/paths';
import {browser} from "$app/environment";
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
let {
requests,
filter = () => {
@@ -21,7 +23,7 @@
async function approveRequest(requestId: string, currentAuthorizedStatus: boolean) {
try {
const response = await fetch(
`${env.PUBLIC_API_URL}/tv/seasons/requests/${requestId}?authorized_status=${!currentAuthorizedStatus}`,
`${apiUrl}/tv/seasons/requests/${requestId}?authorized_status=${!currentAuthorizedStatus}`,
{
method: 'PATCH',
headers: {
@@ -56,7 +58,7 @@
async function deleteRequest(requestId: string) {
try {
const response = await fetch(`${env.PUBLIC_API_URL}/tv/seasons/requests/${requestId}`, {
const response = await fetch(`${apiUrl}/tv/seasons/requests/${requestId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'

View File

@@ -9,7 +9,9 @@
import {Label} from '$lib/components/ui/label/index.js';
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
import {Input} from '$lib/components/ui/input/index.js';
import {browser} from "$app/environment";
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
let {users}: { users: User[] } = $props();
let sortedUsers = $derived(users.sort((a, b) => a.email.localeCompare(b.email)));
let selectedUser: User | null = $state(null);
@@ -20,7 +22,7 @@
if (!selectedUser) return;
try {
const response = await fetch(`${env.PUBLIC_API_URL}/users/${selectedUser.id}`, {
const response = await fetch(`${apiUrl}/users/${selectedUser.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'

View File

@@ -5,14 +5,16 @@
import * as Dialog from '$lib/components/ui/dialog/index.js';
import {Label} from '$lib/components/ui/label/index.js';
import {Input} from '$lib/components/ui/input/index.js';
import {browser} from "$app/environment";
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
let newPassword: string = $state('');
let newEmail: string = $state('');
let dialogOpen = $state(false);
async function saveUser() {
try {
const response = await fetch(`${env.PUBLIC_API_URL}/users/me`, {
const response = await fetch(`${apiUrl}/users/me`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'

View File

@@ -4,8 +4,9 @@ import {env} from '$env/dynamic/public';
import {goto} from '$app/navigation';
import {base} from '$app/paths';
import {toast} from 'svelte-sonner';
import {browser} from "$app/environment";
const apiUrl = env.PUBLIC_API_URL;
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
export const qualityMap: { [key: number]: string } = {
1: 'high',

View File

@@ -2,11 +2,14 @@
import {goto} from '$app/navigation';
import {base} from '$app/paths';
import {onMount} from 'svelte';
import {browser} from "$app/environment";
import {redirect} from "@sveltejs/kit";
onMount(() => {
if (browser)
goto(base + '/dashboard');
});
else
throw redirect(307, '/login');
</script>
<svelte:head>

View File

@@ -1,11 +1,13 @@
import {env} from '$env/dynamic/public';
import type {LayoutServerLoad} from './$types';
import type {LayoutLoad} from './$types';
import {redirect} from '@sveltejs/kit';
import {base} from '$app/paths';
import {browser} from "$app/environment";
import {goto} from '$app/navigation';
const apiUrl = env.PUBLIC_API_URL;
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
export const load: LayoutServerLoad = async ({fetch}) => {
export const load: LayoutLoad = async ({fetch}) => {
const response = await fetch(apiUrl + '/users/me', {
method: 'GET',
headers: {
@@ -15,7 +17,11 @@ export const load: LayoutServerLoad = async ({fetch}) => {
});
if (!response.ok) {
console.log('unauthorized, redirecting to login');
throw redirect(303, base + '/login');
if (browser) {
await goto(base + '/login');
} else {
throw redirect(303, base + '/login');
}
}
return {user: await response.json()};
};

View File

@@ -1,7 +1,8 @@
import {env} from '$env/dynamic/public';
import type {PageLoad} from './$types';
import {browser} from "$app/environment";
const apiUrl = env.PUBLIC_API_URL;
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
export const load: PageLoad = async ({fetch}) => {
const response = await fetch(apiUrl + '/tv/recommended', {

View File

@@ -1,9 +1,11 @@
import {env} from '$env/dynamic/public';
import type {PageLoad} from './$types';
import {browser} from "$app/environment";
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
export const load: PageLoad = async ({fetch}) => {
try {
const users = await fetch(env.PUBLIC_API_URL + '/users/all', {
const users = await fetch(apiUrl + '/users/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json'

View File

@@ -10,6 +10,7 @@
import logo from '$lib/images/svelte-logo.svg';
import LoadingBar from '$lib/components/loading-bar.svelte';
const apiUrl = env.PUBLIC_SSR_API_URL
let tvShowsPromise = page.data.tvShows;
</script>
@@ -64,7 +65,7 @@
<Card.Content>
<img
class="aspect-9/16 center h-auto max-w-full rounded-lg object-cover"
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show.id}.jpg`)}
src={toOptimizedURL(`${apiUrl}/static/image/${show.id}.jpg`)}
alt="{getFullyQualifiedShowName(show)}'s Poster Image"
on:error={(e) => {
e.target.src = logo;

View File

@@ -1,7 +1,8 @@
import {env} from '$env/dynamic/public';
import type {PageLoad} from './$types';
import {browser} from "$app/environment";
const apiUrl = env.PUBLIC_API_URL;
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
export const load: PageLoad = async ({fetch}) => {
const response = fetch(apiUrl + '/tv/shows', {

View File

@@ -1,6 +1,8 @@
import {env} from '$env/dynamic/public';
import type {LayoutLoad} from './$types';
import {browser} from "$app/environment";
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
export const load: LayoutLoad = async ({params, fetch}) => {
const showId = params.showId;
@@ -12,7 +14,7 @@ export const load: LayoutLoad = async ({params, fetch}) => {
}
try {
const show = await fetch(`${env.PUBLIC_API_URL}/tv/shows/${showId}`, {
const show = await fetch(`${apiUrl}/tv/shows/${showId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
@@ -20,7 +22,7 @@ export const load: LayoutLoad = async ({params, fetch}) => {
credentials: 'include'
});
const torrents = await fetch(`${env.PUBLIC_API_URL}/tv/shows/${showId}/torrents`, {
const torrents = await fetch(`${apiUrl}/tv/shows/${showId}/torrents`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'

View File

@@ -15,7 +15,9 @@
import {page} from '$app/state';
import TorrentTable from '$lib/components/torrent-table.svelte';
import RequestSeasonDialog from '$lib/components/request-season-dialog.svelte';
import {browser} from "$app/environment";
const apiUrl = env.PUBLIC_SSR_API_URL;
let show: Show = getContext('show');
let user: User = getContext('user');
let torrents: RichShowTorrent = page.data.torrentsData;
@@ -55,7 +57,7 @@
{#if show().id}
<img
class="aspect-9/16 h-auto w-full rounded-lg object-cover"
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show().id}.jpg`)}
src={toOptimizedURL(`${apiUrl}/static/image/${show().id}.jpg`)}
alt="{show().name}'s Poster Image"
/>
{:else}

View File

@@ -10,7 +10,9 @@
import {getFullyQualifiedShowName, getTorrentQualityString} from '$lib/utils';
import {toOptimizedURL} from "sveltekit-image-optimize/components";
import {env} from "$env/dynamic/public";
import {browser} from "$app/environment";
const apiUrl = env.PUBLIC_SSR_API_URL;
const SeasonNumber = page.params.SeasonNumber;
let seasonFiles: PublicSeasonFile[] = $state(page.data.files);
let show: Show = getContext('show');
@@ -62,7 +64,7 @@
<img
class="aspect-9/16 h-auto w-full rounded-lg object-cover"
alt="{show().name}'s Poster Image"
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show().id}.jpg`)}
src={toOptimizedURL(`${apiUrl}/static/image/${show().id}.jpg`)}
/>
</div>
<div class="h-full w-1/4 flex-auto rounded-xl bg-muted/50 p-4">

View File

@@ -1,7 +1,8 @@
import {env} from '$env/dynamic/public';
import type {PageLoad} from './$types';
import {browser} from "$app/environment";
const apiUrl = env.PUBLIC_API_URL;
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
export const load: PageLoad = async ({fetch, params}) => {
const url = `${apiUrl}/tv/shows/${params.showId}/${params.SeasonNumber}/files`;

View File

@@ -12,14 +12,16 @@
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
import AddShowCard from '$lib/components/add-show-card.svelte';
import {toast} from 'svelte-sonner';
import {browser} from "$app/environment";
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
let searchTerm: string = $state('');
let metadataProvider: string = $state('tmdb');
let results: MetaDataProviderShowSearchResult[] | null = $state(null);
async function search() {
if (searchTerm.length > 0) {
let url = new URL(env.PUBLIC_API_URL + '/tv/search');
let url = new URL(apiUrl + '/tv/search');
url.searchParams.append('query', searchTerm);
url.searchParams.append('metadata_provider', metadataProvider);
toast.info(`Searching for "${searchTerm}" using ${metadataProvider.toUpperCase()}...`);

View File

@@ -1,9 +1,11 @@
import {env} from '$env/dynamic/public';
import type {LayoutLoad} from './$types';
import {browser} from "$app/environment";
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
export const load: LayoutLoad = async ({fetch}) => {
try {
const requests = await fetch(`${env.PUBLIC_API_URL}/tv/seasons/requests`, {
const requests = await fetch(`${apiUrl}/tv/seasons/requests`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'

View File

@@ -1,7 +1,8 @@
import {env} from '$env/dynamic/public';
import type {PageLoad} from './$types';
import {browser} from "$app/environment";
const apiUrl = env.PUBLIC_API_URL;
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
export const load: PageLoad = async ({fetch}) => {
const response = await fetch(apiUrl + '/tv/shows/torrents', {

View File

@@ -1,7 +1,8 @@
import {env} from '$env/dynamic/public';
import type {PageLoad} from './$types';
import {browser} from "$app/environment";
const apiUrl = env.PUBLIC_API_URL;
const apiUrl = browser ? env.PUBLIC_API_URL : env.PUBLIC_SSR_API_URL;
export const load: PageLoad = async ({fetch}) => {
const response = await fetch(apiUrl + '/auth/metadata', {