Skip to content

Commit 7b0aa01

Browse files
authored
Use Pydantic for settings validation data model (#204)
* Use Pydantic data models for settings * Rename Scraping to Scraper to be consistent across platform * Revert "Rename Scraping to Scraper to be consistent across platform" This reverts commit db29f42. * fix data model * Fix data model and references * Add url field to torrentio model * Correct docstring
1 parent 2de2f54 commit 7b0aa01

25 files changed

+326
-330
lines changed

backend/controllers/default.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi import APIRouter, Request
22
import requests
3-
from utils.settings import settings_manager
3+
from program.settings.manager import settings_manager
44

55

66
router = APIRouter(
@@ -26,7 +26,7 @@ async def health(request: Request):
2626

2727
@router.get("/user")
2828
async def get_rd_user():
29-
api_key = settings_manager.get("real_debrid.api_key")
29+
api_key = settings_manager.settings.real_debrid.api_key
3030
headers = {"Authorization": f"Bearer {api_key}"}
3131
response = requests.get(
3232
"https://api.real-debrid.com/rest/1.0/user", headers=headers

backend/controllers/settings.py

+46-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from copy import copy
2-
from fastapi import APIRouter
3-
from utils.settings import settings_manager
2+
from fastapi import APIRouter, HTTPException
3+
from program.settings.manager import settings_manager
44
from pydantic import BaseModel
55
from typing import Any, List
66

@@ -39,14 +39,25 @@ async def save_settings():
3939
async def get_all_settings():
4040
return {
4141
"success": True,
42-
"data": copy(settings_manager.get_all()),
42+
"data": copy(settings_manager.settings),
4343
}
4444

4545

46-
@router.get("/get/{keys}")
47-
async def get_settings(keys: str):
48-
keys = keys.split(",")
49-
data = {key: settings_manager.get(key) for key in keys}
46+
@router.get("/get/{paths}")
47+
async def get_settings(paths: str):
48+
current_settings = settings_manager.settings.dict()
49+
data = {}
50+
for path in paths.split(","):
51+
keys = path.split('.')
52+
current_obj = current_settings
53+
54+
for k in keys:
55+
if k not in current_obj:
56+
return None
57+
current_obj = current_obj[k]
58+
59+
data[path] = current_obj
60+
5061
return {
5162
"success": True,
5263
"data": data,
@@ -55,8 +66,33 @@ async def get_settings(keys: str):
5566

5667
@router.post("/set")
5768
async def set_settings(settings: List[SetSettings]):
58-
settings_manager.set(settings)
69+
current_settings = settings_manager.settings.dict()
70+
71+
for setting in settings:
72+
keys = setting.key.split('.')
73+
current_obj = current_settings
74+
75+
# Navigate to the last key's parent object, similar to the getter.
76+
for k in keys[:-1]:
77+
if k not in current_obj:
78+
# If a key in the path does not exist, raise an exception or optionally create a new dict.
79+
raise HTTPException(status_code=400, detail=f"Path '{'.'.join(keys[:-1])}' does not exist.")
80+
current_obj = current_obj[k]
81+
82+
# Set the value at the final key.
83+
if keys[-1] in current_obj:
84+
current_obj[keys[-1]] = setting.value
85+
else:
86+
# If the final key does not exist, raise an exception.
87+
raise HTTPException(status_code=400, detail=f"Key '{keys[-1]}' does not exist in path '{'.'.join(keys[:-1])}'.")
88+
89+
90+
settings_manager.load(settings_dict=current_settings)
91+
92+
# Notify observers about the update.
93+
settings_manager.notify_observers()
94+
5995
return {
6096
"success": True,
61-
"message": "Settings saved!",
62-
}
97+
"message": "Settings updated successfully."
98+
}

backend/program/__init__.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
from program.realdebrid import Debrid
88
from program.symlink import Symlinker
99
from program.media.container import MediaItemContainer
10-
from utils.logger import logger, get_data_path
10+
from utils.logger import logger
1111
from program.plex import Plex
1212
from program.content import Content
1313
from utils.utils import Pickly
14-
from utils.settings import settings_manager as settings
14+
from utils import data_dir_path
15+
from program.settings.manager import settings_manager
1516
from utils.service_manager import ServiceManager
1617

1718

@@ -22,14 +23,17 @@ def __init__(self, args):
2223
super().__init__(name="Iceberg")
2324
self.running = False
2425
self.startup_args = args
26+
logger.configure_logger(
27+
debug=settings_manager.settings.debug,
28+
log=settings_manager.settings.log
29+
)
2530

2631
def start(self):
27-
logger.info("Iceberg v%s starting!", settings.get("version"))
32+
logger.info("Iceberg v%s starting!", settings_manager.settings.version)
2833
self.initialized = False
2934
self.media_items = MediaItemContainer(items=[])
30-
self.data_path = get_data_path()
31-
if not os.path.exists(self.data_path):
32-
os.mkdir(self.data_path)
35+
self.data_path = data_dir_path
36+
os.makedirs(self.data_path, exist_ok=True)
3337
if not self.startup_args.dev:
3438
self.pickly = Pickly(self.media_items, self.data_path)
3539
self.pickly.start()

backend/program/content/listrr.py

+6-13
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,23 @@
1-
"""Mdblist content module"""
1+
"""Listrr content module"""
22
from time import time
33
from typing import Optional
4-
from pydantic import BaseModel
5-
from utils.settings import settings_manager
4+
5+
from requests.exceptions import HTTPError
6+
7+
from program.settings.manager import settings_manager
68
from utils.logger import logger
79
from utils.request import get, ping
8-
from requests.exceptions import HTTPError
910
from program.media.container import MediaItemContainer
1011
from program.updaters.trakt import Updater as Trakt, get_imdbid_from_tmdb, get_imdbid_from_tvdb
1112

1213

13-
class ListrrConfig(BaseModel):
14-
enabled: bool
15-
movie_lists: Optional[list]
16-
show_lists: Optional[list]
17-
api_key: Optional[str]
18-
update_interval: int # in seconds
19-
20-
2114
class Listrr:
2215
"""Content class for Listrr"""
2316

2417
def __init__(self, media_items: MediaItemContainer):
2518
self.key = "listrr"
2619
self.url = "https://listrr.pro/api"
27-
self.settings = ListrrConfig(**settings_manager.get(f"content.{self.key}"))
20+
self.settings = settings_manager.settings.content.listrr
2821
self.headers = {"X-Api-Key": self.settings.api_key}
2922
self.initialized = self.validate_settings()
3023
if not self.initialized:

backend/program/content/mdblist.py

+3-8
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
11
"""Mdblist content module"""
22
from typing import Optional
3-
from pydantic import BaseModel
4-
from utils.settings import settings_manager
3+
4+
from program.settings.manager import settings_manager
55
from utils.logger import logger
66
from utils.request import RateLimitExceeded, RateLimiter, get, ping
77
from program.media.container import MediaItemContainer
88
from program.updaters.trakt import Updater as Trakt
99

1010

11-
class MdblistConfig(BaseModel):
12-
enabled: bool
13-
api_key: Optional[str]
14-
lists: Optional[list]
15-
1611
class Mdblist:
1712
"""Content class for mdblist"""
1813

1914
def __init__(self, media_items: MediaItemContainer):
2015
self.key = "mdblist"
21-
self.settings = MdblistConfig(**settings_manager.get(f"content.{self.key}"))
16+
self.settings = settings_manager.settings.content.mdblist
2217
self.initialized = self.validate_settings()
2318
if not self.initialized:
2419
return

backend/program/content/overseerr.py

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
11
"""Mdblist content module"""
22
from typing import Optional
3-
from pydantic import BaseModel
4-
from utils.settings import settings_manager
3+
4+
5+
from program.settings.manager import settings_manager
56
from utils.logger import logger
67
from utils.request import get, ping
78
from program.media.container import MediaItemContainer
89
from program.updaters.trakt import Updater as Trakt, get_imdbid_from_tmdb, get_imdbid_from_tvdb
910

1011

11-
class OverseerrConfig(BaseModel):
12-
enabled: bool
13-
url: Optional[str]
14-
api_key: Optional[str]
15-
16-
1712
class Overseerr:
1813
"""Content class for overseerr"""
1914

2015
def __init__(self, media_items: MediaItemContainer):
2116
self.key = "overseerr"
22-
self.settings = OverseerrConfig(**settings_manager.get(f"content.{self.key}"))
17+
self.settings = settings_manager.settings.content.overseerr
2318
self.headers = {"X-Api-Key": self.settings.api_key}
2419
self.initialized = self.validate_settings()
2520
if not self.initialized:

backend/program/content/plex_watchlist.py

+6-9
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
"""Plex Watchlist Module"""
2+
import json
23
from typing import Optional
3-
from pydantic import BaseModel
4+
45
from requests import ConnectTimeout, HTTPError
6+
57
from utils.request import get, ping
68
from utils.logger import logger
7-
from utils.settings import settings_manager
9+
from program.settings.manager import settings_manager
810
from program.media.container import MediaItemContainer
911
from program.updaters.trakt import Updater as Trakt
10-
import json
11-
1212

13-
class PlexWatchlistConfig(BaseModel):
14-
enabled: bool
15-
rss: Optional[str]
1613

1714

1815
class PlexWatchlist:
@@ -21,11 +18,11 @@ class PlexWatchlist:
2118
def __init__(self, media_items: MediaItemContainer):
2219
self.key = "plex_watchlist"
2320
self.rss_enabled = False
24-
self.settings = PlexWatchlistConfig(**settings_manager.get(f"content.{self.key}"))
21+
self.settings = settings_manager.settings.content.plex_watchlist
2522
self.initialized = self.validate_settings()
2623
if not self.initialized:
2724
return
28-
self.token = settings_manager.get("plex.token")
25+
self.token = settings_manager.settings.plex.token
2926
self.media_items = media_items
3027
self.prev_count = 0
3128
self.updater = Trakt()

backend/program/content/trakt.py

+3-11
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,22 @@
11
"""Mdblist content module"""
22
from time import time
33
from typing import Optional
4-
from pydantic import BaseModel
5-
from utils.settings import settings_manager
4+
5+
from program.settings.manager import settings_manager
66
from utils.logger import logger
77
from utils.request import get, ping
88
from program.media.container import MediaItemContainer
99
from program.updaters.trakt import Updater as Trakt, CLIENT_ID
1010

1111

12-
class TraktConfig(BaseModel):
13-
enabled: bool
14-
watchlist: Optional[list]
15-
collection: Optional[list]
16-
user_lists: Optional[list]
17-
api_key: Optional[str]
18-
update_interval: int # in seconds
19-
2012

2113
class Trakt:
2214
"""Content class for Trakt"""
2315

2416
def __init__(self, media_items: MediaItemContainer):
2517
self.key = "trakt"
2618
self.url = None
27-
self.settings = TraktConfig(**settings_manager.get(f"content.{self.key}"))
19+
self.settings = settings_manager.settings.content.trakt
2820
self.headers = {"X-Api-Key": self.settings.api_key}
2921
self.initialized = self.validate_settings()
3022
if not self.initialized:

backend/program/plex.py

+6-12
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import uuid
77
from datetime import datetime
88
from typing import Optional
9+
910
from plexapi.server import PlexServer
1011
from plexapi.exceptions import BadRequest, Unauthorized
11-
from pydantic import BaseModel
12-
# from program.updaters.trakt import get_imdbid_from_tvdb
12+
1313
from utils.logger import logger
14-
from utils.settings import settings_manager as settings
14+
from program.settings.manager import settings_manager
1515
from program.media.container import MediaItemContainer
1616
from program.media.state import Symlink, Library
1717
from utils.request import get, post
@@ -23,12 +23,6 @@
2323
)
2424

2525

26-
class PlexConfig(BaseModel):
27-
user: Optional[str] = None
28-
token: Optional[str] = None
29-
url: Optional[str] = None
30-
31-
3226
class Plex(threading.Thread):
3327
"""Plex library class"""
3428

@@ -37,12 +31,12 @@ def __init__(self, media_items: MediaItemContainer):
3731
self.key = "plex"
3832
self.initialized = False
3933
self.library_path = os.path.abspath(
40-
os.path.dirname(settings.get("symlink.container_path"))
34+
os.path.dirname(settings_manager.settings.symlink.container_path)
4135
)
4236
self.last_fetch_times = {}
4337

4438
try:
45-
self.settings = PlexConfig(**settings.get(self.key))
39+
self.settings = settings_manager.settings.plex
4640
self.plex = PlexServer(
4741
self.settings.url, self.settings.token, timeout=60
4842
)
@@ -185,7 +179,7 @@ def _oauth(self):
185179
additional_headers={
186180
"X-Plex-Product": "Iceberg",
187181
"X-Plex-Client-Identifier": random_uuid,
188-
"X-Plex-Token": settings.get("plex.token"),
182+
"X-Plex-Token": settings_manager.settings.plex.token,
189183
},
190184
)
191185
if not response.ok:

backend/program/realdebrid.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,25 @@
33
from pathlib import Path
44
import time
55
from typing import Optional
6-
from pydantic import BaseModel
76
from requests import ConnectTimeout
87
from utils.logger import logger
98
from utils.request import get, post, ping
10-
from utils.settings import settings_manager
9+
from program.settings.manager import settings_manager
1110
from utils.parser import parser
1211

1312

1413
WANTED_FORMATS = [".mkv", ".mp4", ".avi"]
1514
RD_BASE_URL = "https://api.real-debrid.com/rest/1.0"
1615

1716

18-
class DebridConfig(BaseModel):
19-
api_key: Optional[str]
20-
21-
2217
class Debrid:
2318
"""Real-Debrid API Wrapper"""
2419

2520
def __init__(self, _):
2621
# Realdebrid class library is a necessity
2722
self.initialized = False
2823
self.key = "real_debrid"
29-
self.settings = DebridConfig(**settings_manager.get(self.key))
24+
self.settings = settings_manager.settings.real_debrid
3025
self.auth_headers = {"Authorization": f"Bearer {self.settings.api_key}"}
3126
self.running = False
3227
if not self._validate_settings():

0 commit comments

Comments
 (0)