diff --git a/backend/src/tv/repository.py b/backend/src/tv/repository.py
index 952d0c1..fc409be 100644
--- a/backend/src/tv/repository.py
+++ b/backend/src/tv/repository.py
@@ -4,6 +4,7 @@ from sqlalchemy.orm import Session, joinedload
from torrent.models import Torrent
from torrent.schemas import TorrentId, Torrent as TorrentSchema
+from tv import log
from tv.models import Season, Show, Episode, SeasonRequest, SeasonFile
from tv.schemas import Season as SeasonSchema, SeasonId, Show as ShowSchema, ShowId, \
SeasonRequest as SeasonRequestSchema, SeasonFile as SeasonFileSchema, SeasonNumber, SeasonRequestId, \
@@ -110,12 +111,22 @@ def get_season(season_id: SeasonId, db: Session) -> SeasonSchema:
return SeasonSchema.model_validate(db.get(Season, season_id))
-def add_season_to_requested_list(season_request: SeasonRequestSchema, db: Session) -> None:
+def add_season_request(season_request: SeasonRequestSchema, db: Session) -> None:
"""
Adds a Season to the SeasonRequest table, which marks it as requested.
"""
- db.add(SeasonRequest(**season_request.model_dump()))
+ log.debug(f"Adding season request {season_request.model_dump()}")
+ db_model = SeasonRequest(
+ id=season_request.id,
+ season_id=season_request.season_id,
+ wanted_quality=season_request.wanted_quality,
+ min_quality=season_request.min_quality,
+ requested_by_id=season_request.requested_by.id if season_request.requested_by else None,
+ authorized=season_request.authorized,
+ authorized_by_id=season_request.authorized_by.id if season_request.authorized_by else None
+ )
+ db.add(db_model)
db.commit()
@@ -144,7 +155,7 @@ def get_season_requests(db: Session) -> list[RichSeasonRequestSchema]:
return [RichSeasonRequestSchema(min_quality=x.min_quality,
wanted_quality=x.wanted_quality, show=x.season.show, season=x.season,
requested_by=x.requested_by, authorized_by=x.authorized_by, authorized=x.authorized,
- id=x.id)
+ id=x.id, season_id=x.season.id)
for x in result]
diff --git a/backend/src/tv/router.py b/backend/src/tv/router.py
index 1e65abe..fdf96f5 100644
--- a/backend/src/tv/router.py
+++ b/backend/src/tv/router.py
@@ -1,3 +1,4 @@
+import logging
from typing import Annotated
from fastapi import APIRouter, Depends, status
@@ -12,6 +13,7 @@ from backend.src.database import DbSessionDependency
from indexer.schemas import PublicIndexerQueryResult, IndexerQueryResultId
from metadataProvider.schemas import MetaDataProviderShowSearchResult
from torrent.schemas import Torrent
+from tv import log
from tv.exceptions import MediaAlreadyExists
from tv.schemas import Show, SeasonRequest, ShowId, RichShowTorrent, PublicShow, PublicSeasonFile, SeasonNumber, \
CreateSeasonRequest, SeasonRequestId, UpdateSeasonRequest, RichSeasonRequest
@@ -91,8 +93,7 @@ def request_a_season(db: DbSessionDependency, user: Annotated[User, Depends(curr
"""
request: SeasonRequest = SeasonRequest.model_validate(season_request)
request.requested_by = UserRead.model_validate(user)
-
- tv.service.request_season(db=db, season_request=request)
+ tv.service.add_season_request(db=db, season_request=request)
return
@@ -102,6 +103,14 @@ def get_season_requests(db: DbSessionDependency) -> list[RichSeasonRequest]:
return tv.service.get_all_season_requests(db=db)
+@router.delete("/seasons/requests/{request_id}", status_code=status.HTTP_204_NO_CONTENT,
+ dependencies=[Depends(current_active_user)])
+def delete_season_request(db: DbSessionDependency, request_id: SeasonRequestId):
+ tv.service.delete_season_request(db=db, season_request_id=request_id)
+ return
+
+
+
@router.patch("/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT)
def authorize_request(db: DbSessionDependency, user: Annotated[User, Depends(current_superuser)],
season_request_id: SeasonRequestId, authorized_status: bool = False):
@@ -115,7 +124,7 @@ def authorize_request(db: DbSessionDependency, user: Annotated[User, Depends(cur
return
-@router.put("/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT)
+@router.put("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT)
def update_request(db: DbSessionDependency, user: Annotated[User, Depends(current_superuser)],
season_request: UpdateSeasonRequest):
season_request: SeasonRequest = SeasonRequest.model_validate(season_request)
@@ -123,13 +132,6 @@ def update_request(db: DbSessionDependency, user: Annotated[User, Depends(curren
tv.service.update_season_request(db=db, season_request=season_request)
return
-
-@router.delete("/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT,
- dependencies=[Depends(current_active_user)])
-def delete_season_request(db: DbSessionDependency, request_id: SeasonRequestId):
- tv.service.delete_season_request(db=db, season_request_id=request_id)
-
-
# --------------------------------
# MANAGE TORRENTS
# --------------------------------
diff --git a/backend/src/tv/schemas.py b/backend/src/tv/schemas.py
index 868a95d..1f6ced8 100644
--- a/backend/src/tv/schemas.py
+++ b/backend/src/tv/schemas.py
@@ -2,7 +2,7 @@ import typing
import uuid
from uuid import UUID
-from pydantic import BaseModel, Field, ConfigDict
+from pydantic import BaseModel, Field, ConfigDict, model_validator
from tvdb_v4_official import Request
from auth.schemas import UserRead
@@ -59,6 +59,12 @@ class SeasonRequestBase(BaseModel):
min_quality: Quality
wanted_quality: Quality
+ @model_validator(mode="after")
+ def ensure_wanted_quality_is_eq_or_gt_min_quality(self) -> "SeasonRequestBase":
+ if self.min_quality.value < self.wanted_quality.value:
+ raise ValueError("Error text")
+ return self
+
class CreateSeasonRequest(SeasonRequestBase):
season_id: SeasonId
@@ -73,8 +79,8 @@ class SeasonRequest(SeasonRequestBase):
model_config = ConfigDict(from_attributes=True)
id: SeasonRequestId = Field(default_factory=uuid.uuid4)
- season: Season
+ season_id: SeasonId
requested_by: UserRead | None = None
authorized: bool = False
authorized_by: UserRead | None = None
@@ -82,6 +88,8 @@ class SeasonRequest(SeasonRequestBase):
class RichSeasonRequest(SeasonRequest):
show: Show
+ season: Season
+
class SeasonFile(BaseModel):
model_config = ConfigDict(from_attributes=True)
diff --git a/backend/src/tv/service.py b/backend/src/tv/service.py
index 97d704c..1f2f4bf 100644
--- a/backend/src/tv/service.py
+++ b/backend/src/tv/service.py
@@ -27,8 +27,8 @@ def add_show(db: Session, external_id: int, metadata_provider: str) -> Show | No
return saved_show
-def request_season(db: Session, season_request: SeasonRequest) -> None:
- tv.repository.add_season_to_requested_list(db=db, season_request=season_request)
+def add_season_request(db: Session, season_request: SeasonRequest) -> None:
+ tv.repository.add_season_request(db=db, season_request=season_request)
def update_season_request(db: Session, season_request: SeasonRequest) -> None:
diff --git a/web/components.json b/web/components.json
index a429661..1b00ff9 100644
--- a/web/components.json
+++ b/web/components.json
@@ -1,8 +1,6 @@
{
"$schema": "https://next.shadcn-svelte.com/schema.json",
- "style": "new-york",
"tailwind": {
- "config": "tailwind.config.ts",
"css": "src\\app.css",
"baseColor": "zinc"
},
@@ -10,8 +8,9 @@
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
- "hooks": "$lib/hooks"
+ "hooks": "$lib/hooks",
+ "lib": "$lib"
},
"typescript": true,
- "registry": "https://next.shadcn-svelte.com/registry"
+ "registry": "https://tw3.shadcn-svelte.com/registry/new-york"
}
diff --git a/web/package-lock.json b/web/package-lock.json
index a4dc302..daa1d44 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -12,7 +12,6 @@
"@sveltejs/adapter-node": "^5.2.12",
"eslint-plugin-unused-imports": "^4.1.4",
"lucide-svelte": "^0.507.0",
- "mode-watcher": "^0.5.1",
"sharp": "^0.34.1",
"sveltekit-image-optimize": "^0.0.7",
"uuid": "^11.1.0"
@@ -36,11 +35,13 @@
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.14.0",
+ "mode-watcher": "^1.0.7",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.10",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
+ "svelte-sonner": "^0.3.28",
"tailwind-merge": "^3.2.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^3.4.17",
@@ -4794,11 +4795,32 @@
}
},
"node_modules/mode-watcher": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.5.1.tgz",
- "integrity": "sha512-adEC6T7TMX/kzQlaO/MtiQOSFekZfQu4MC+lXyoceQG+U5sKpJWZ4yKXqw846ExIuWJgedkOIPqAYYRk/xHm+w==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.0.7.tgz",
+ "integrity": "sha512-ZGA7ZGdOvBJeTQkzdBOnXSgTkO6U6iIFWJoyGCTt6oHNg9XP9NBvS26De+V4W2aqI+B0yYXUskFG2VnEo3zyMQ==",
+ "dev": true,
+ "dependencies": {
+ "runed": "^0.25.0",
+ "svelte-toolbelt": "^0.7.1"
+ },
"peerDependencies": {
- "svelte": "^4.0.0 || ^5.0.0-next.1"
+ "svelte": "^5.27.0"
+ }
+ },
+ "node_modules/mode-watcher/node_modules/runed": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/runed/-/runed-0.25.0.tgz",
+ "integrity": "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/huntabyte",
+ "https://github.com/sponsors/tglide"
+ ],
+ "dependencies": {
+ "esm-env": "^1.0.0"
+ },
+ "peerDependencies": {
+ "svelte": "^5.7.0"
}
},
"node_modules/mri": {
@@ -5920,6 +5942,15 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/svelte-sonner": {
+ "version": "0.3.28",
+ "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.28.tgz",
+ "integrity": "sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==",
+ "dev": true,
+ "peerDependencies": {
+ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
+ }
+ },
"node_modules/svelte-toolbelt": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz",
diff --git a/web/package.json b/web/package.json
index 35d1345..19b78ff 100644
--- a/web/package.json
+++ b/web/package.json
@@ -32,11 +32,13 @@
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.14.0",
+ "mode-watcher": "^1.0.7",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.10",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
+ "svelte-sonner": "^0.3.28",
"tailwind-merge": "^3.2.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^3.4.17",
@@ -50,7 +52,6 @@
"@sveltejs/adapter-node": "^5.2.12",
"eslint-plugin-unused-imports": "^4.1.4",
"lucide-svelte": "^0.507.0",
- "mode-watcher": "^0.5.1",
"sharp": "^0.34.1",
"sveltekit-image-optimize": "^0.0.7",
"uuid": "^11.1.0"
diff --git a/web/src/lib/components/app-sidebar.svelte b/web/src/lib/components/app-sidebar.svelte
index 900004d..789f4b5 100644
--- a/web/src/lib/components/app-sidebar.svelte
+++ b/web/src/lib/components/app-sidebar.svelte
@@ -22,8 +22,8 @@
url: '/dashboard/tv/torrents'
},
{
- title: 'Settings',
- url: '#'
+ title: 'Requests',
+ url: '/dashboard/tv/requests'
}
]
diff --git a/web/src/lib/components/request-season-dialog.svelte b/web/src/lib/components/request-season-dialog.svelte
new file mode 100644
index 0000000..35dd6bc
--- /dev/null
+++ b/web/src/lib/components/request-season-dialog.svelte
@@ -0,0 +1,162 @@
+
+
+ {submitRequestError}