diff --git a/backend/src/auth/config.py b/backend/src/auth/config.py
index 597e732..0be560e 100644
--- a/backend/src/auth/config.py
+++ b/backend/src/auth/config.py
@@ -1,3 +1,4 @@
+from pydantic import EmailStr
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -7,6 +8,7 @@ class AuthConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix='AUTH_')
token_secret: str
session_lifetime: int = 60 * 60 * 24
+ admin_email: str | list[str]
@property
def jwt_signing_key(self):
return self._jwt_signing_key
diff --git a/backend/src/auth/users.py b/backend/src/auth/users.py
index cc133ea..1d9e639 100644
--- a/backend/src/auth/users.py
+++ b/backend/src/auth/users.py
@@ -14,6 +14,7 @@ from httpx_oauth.oauth2 import OAuth2
import auth.config
from auth.db import User, get_user_db
+from auth.schemas import UserUpdate
config = auth.config.AuthConfig()
SECRET = config.token_secret
@@ -40,6 +41,9 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
async def on_after_register(self, user: User, request: Optional[Request] = None):
print(f"User {user.id} has registered.")
+ if user.email in config.admin_email:
+ updated_user = UserUpdate(is_superuser=True, is_verified=True)
+ await self.update(user=user, user_update=updated_user)
async def on_after_forgot_password(
self, user: User, token: str, request: Optional[Request] = None
@@ -84,5 +88,5 @@ cookie_auth_backend = AuthenticationBackend(
fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [bearer_auth_backend, cookie_auth_backend])
-current_active_user = fastapi_users.current_user(active=True)
-current_superuser = fastapi_users.current_user(active=True, superuser=True)
+current_active_user = fastapi_users.current_user(active=True, verified=True)
+current_superuser = fastapi_users.current_user(active=True, verified=True, superuser=True)
diff --git a/backend/src/main.py b/backend/src/main.py
index 6f731b3..440a34b 100644
--- a/backend/src/main.py
+++ b/backend/src/main.py
@@ -4,6 +4,8 @@ from logging.config import dictConfig
from pythonjsonlogger.json import JsonFormatter
+import router
+
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
@@ -113,6 +115,11 @@ app.include_router(
prefix="/auth",
tags=["auth"],
)
+# Misc Router
+app.include_router(
+ router.router,
+ tags=["users"]
+)
# User Router
app.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
diff --git a/backend/src/metadataProvider/__init__.py b/backend/src/metadataProvider/__init__.py
index 8e29f95..859cb92 100644
--- a/backend/src/metadataProvider/__init__.py
+++ b/backend/src/metadataProvider/__init__.py
@@ -1,6 +1,7 @@
import logging
import metadataProvider.tmdb
+import metadataProvider.tvdb
from metadataProvider.abstractMetaDataProvider import metadata_providers
from metadataProvider.schemas import MetaDataProviderShowSearchResult
from tv.schemas import Show
diff --git a/backend/src/metadataProvider/tmdb.py b/backend/src/metadataProvider/tmdb.py
index baefdc2..79b8098 100644
--- a/backend/src/metadataProvider/tmdb.py
+++ b/backend/src/metadataProvider/tmdb.py
@@ -69,17 +69,15 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
metadata_provider=self.name,
)
- # TODO: convert images automatically to .jpg
# downloading the poster
+ # NOTE: all pictures from TMDB should already be jpeg, so no need to convert
if show_metadata["poster_path"] is not None:
poster_url = "https://image.tmdb.org/t/p/original" + show_metadata["poster_path"]
- res = requests.get(poster_url, stream=True)
- content_type = res.headers["content-type"]
- file_extension = mimetypes.guess_extension(content_type)
- if res.status_code == 200:
- with open(self.storage_path.joinpath(str(show.id) + file_extension), 'wb') as f:
- f.write(res.content)
- log.info(f"image for show {show.name} successfully downloaded")
+ if metadataProvider.utils.download_poster_image(storage_path=self.storage_path, poster_url=poster_url,
+ show=show):
+ log.info("Successfully downloaded poster image for show " + show.name)
+ else:
+ log.warning(f"download for image of show {show.name} failed")
else:
log.warning(f"image for show {show.name} could not be downloaded")
diff --git a/backend/src/metadataProvider/tvdb.py b/backend/src/metadataProvider/tvdb.py
new file mode 100644
index 0000000..79e0c64
--- /dev/null
+++ b/backend/src/metadataProvider/tvdb.py
@@ -0,0 +1,98 @@
+import pprint
+
+import tvdb_v4_official
+import logging
+import mimetypes
+
+import requests
+import tmdbsimple
+from pydantic_settings import BaseSettings
+from tmdbsimple import TV, TV_Seasons
+
+import metadataProvider.utils
+from metadataProvider.abstractMetaDataProvider import AbstractMetadataProvider, register_metadata_provider
+from metadataProvider.schemas import MetaDataProviderShowSearchResult
+from tv.schemas import Episode, Season, Show, SeasonNumber, EpisodeNumber
+
+
+class TvdbConfig(BaseSettings):
+ TVDB_API_KEY: str | None = None
+
+
+config = TvdbConfig()
+log = logging.getLogger(__name__)
+
+
+class TvdbMetadataProvider(AbstractMetadataProvider):
+ name = "tvdb"
+ tvdb_client: tvdb_v4_official.TVDB
+
+ def __init__(self, api_key: str = None):
+ self.tvdb_client = tvdb_v4_official.TVDB(api_key)
+
+ def get_show_metadata(self, id: int = None) -> Show:
+ """
+
+ :param id: the external id of the show
+ :type id: int
+ :return: returns a ShowMetadata object
+ :rtype: ShowMetadata
+ """
+ series = self.tvdb_client.get_series_extended(id)
+ seasons = []
+ for season in series["seasons"]:
+ s = self.tvdb_client.get_season_extended(season["id"])
+ episodes = [Episode(number=episode['number'], external_id=episode['id'], title=episode['name']) for episode
+ in s["episodes"]]
+ seasons.append(Season(number=s['number'], name="TVDB doesn't provide Season Names",
+ overview="TVDB doesn't provide Season Overviews", external_id=s['id'],
+ episodes=episodes))
+ try:
+ year = series['year']
+ except KeyError:
+ year = None
+ show = Show(name=series['name'], overview=series['overview'], year=year,
+ external_id=series['id'], metadata_provider=self.name, seasons=seasons)
+
+ if series["image"] is not None:
+ metadataProvider.utils.download_poster_image(storage_path=self.storage_path,
+ poster_url=series['image'], show=show)
+ else:
+ log.warning(f"image for show {show.name} could not be downloaded")
+
+ return show
+
+ def search_show(self, query: str) -> list[MetaDataProviderShowSearchResult]:
+ results = self.tvdb_client.search(query)
+ formatted_results = []
+ for result in results:
+ if result['type'] == 'series':
+ try:
+ year = result['year']
+ except KeyError:
+ year = None
+
+ formatted_results.append(
+ MetaDataProviderShowSearchResult(
+ poster_path=result["image_url"],
+ overview=result["overview"],
+ name=result["name"],
+ external_id=result["tvdb_id"],
+ year=year,
+ metadata_provider=self.name,
+ added=False,
+ )
+ )
+ return formatted_results
+
+
+if config.TVDB_API_KEY is not None:
+ log.info("Registering TVDB as metadata provider")
+ register_metadata_provider(metadata_provider=TvdbMetadataProvider(config.TVDB_API_KEY))
+
+if __name__ == "__main__":
+ tvdb = TvdbMetadataProvider(config.TVDB_API_KEY)
+ # show_metadata = tvdb.get_show_metadata(id=328724) # Replace with a valid TVDB ID
+ # pprint.pprint(dict(show_metadata))
+ search_results = tvdb.search_show("Simpsons Declassified")
+ pprint.pprint(search_results)
diff --git a/backend/src/metadataProvider/utils.py b/backend/src/metadataProvider/utils.py
index 6940706..e031307 100644
--- a/backend/src/metadataProvider/utils.py
+++ b/backend/src/metadataProvider/utils.py
@@ -1,5 +1,23 @@
+import mimetypes
+
+import requests
+
+
+
def get_year_from_first_air_date(first_air_date: str | None) -> int | None:
if first_air_date:
return int(first_air_date.split('-')[0])
else:
return None
+
+
+def download_poster_image(storage_path=None, poster_url=None, show=None) -> bool:
+ res = requests.get(poster_url, stream=True)
+ content_type = res.headers["content-type"]
+ file_extension = mimetypes.guess_extension(content_type)
+ if res.status_code == 200:
+ with open(storage_path.joinpath(str(show.id) + file_extension), 'wb') as f:
+ f.write(res.content)
+ return True
+ else:
+ return False
diff --git a/backend/src/router.py b/backend/src/router.py
new file mode 100644
index 0000000..59c1d48
--- /dev/null
+++ b/backend/src/router.py
@@ -0,0 +1,17 @@
+from fastapi import APIRouter, Depends
+from fastapi import status
+from sqlalchemy import select
+
+from auth.db import User
+from auth.schemas import UserRead
+from auth.users import current_superuser
+from database import DbSessionDependency
+
+router = APIRouter()
+
+
+@router.get("/users/all", status_code=status.HTTP_200_OK, dependencies=[Depends(current_superuser)])
+def get_all_users(db: DbSessionDependency) -> list[UserRead]:
+ stmt = select(User)
+ result = db.execute(stmt).scalars().unique()
+ return [UserRead.model_validate(user) for user in result]
diff --git a/web/src/hooks.server.ts b/web/src/hooks.server.ts
index 6d5b65e..cc86da6 100644
--- a/web/src/hooks.server.ts
+++ b/web/src/hooks.server.ts
@@ -1,6 +1,7 @@
import {createImageOptimizer} from 'sveltekit-image-optimize';
import type {Handle} from '@sveltejs/kit';
import {createFileSystemCache} from 'sveltekit-image-optimize/cache-adapters';
+import type {HandleServerError} from '@sveltejs/kit';
const cache = createFileSystemCache('./cache');
const imageHandler = createImageOptimizer({
diff --git a/web/src/lib/components/app-sidebar.svelte b/web/src/lib/components/app-sidebar.svelte
index eeee2b5..1aea417 100644
--- a/web/src/lib/components/app-sidebar.svelte
+++ b/web/src/lib/components/app-sidebar.svelte
@@ -22,23 +22,16 @@
url: '/dashboard/tv/add-show'
},
{
+ title: 'Torrents',
+ url: '/dashboard/tv/torrents'
+ },
+ {
title: 'Settings',
url: '#'
}
- ]
+
+ ]
},
- {
- title: 'Torrents',
- url: '#',
- icon: DownloadIcon,
- isActive: true,
- items: [
- {
- title: 'Show Torrents',
- url: '/dashboard/torrents'
- }
- ]
- }
],
navSecondary: [
{
diff --git a/web/src/lib/components/login-form.svelte b/web/src/lib/components/login-form.svelte
index 1beda79..2011474 100644
--- a/web/src/lib/components/login-form.svelte
+++ b/web/src/lib/components/login-form.svelte
@@ -1,99 +1,199 @@
+{#snippet tabSwitcher()}
+
+
+
+
+{/snippet}
+
+ Your account has been successfully created, but activation by an + administrator is required. +
++ The above button will only work once your account is verified. +
++ If you have any questions, please contact an administrator.
+