diff --git a/app/main.py b/app/main.py index 5d3a185d..9fe28d8c 100644 --- a/app/main.py +++ b/app/main.py @@ -12,8 +12,6 @@ import logging import caribou import sqlite3 -log = logging.getLogger('main') - class Main: config: Config = None @@ -87,7 +85,6 @@ class Main: self.start() def start(self): - log.info(f"Listening on {self.config.host}:{self.config.port}") web.run_app( self.app, host=self.config.host, @@ -118,7 +115,7 @@ class Main: if ytdlp_config is None: ytdlp_config = {} - status = await self.dqueue.add( + status = await self.add( url=url, quality=quality, format=format, @@ -130,6 +127,33 @@ class Main: return web.Response(text=self.serializer.encode(status)) + @self.routes.post(self.config.url_prefix + 'add_batch') + async def add_batch(request: Request) -> Response: + status = {} + + post = await request.json() + if not isinstance(post, list): + raise web.HTTPBadRequest() + + for item in post: + if not isinstance(item, dict): + raise web.HTTPBadRequest( + 'Invalid request body expecting list with dicts.') + if not item.get('url'): + raise web.HTTPBadRequest(reason='url is required') + + status[item.get('url')] = await self.add( + url=item.get('url'), + quality=item.get('quality'), + format=item.get('format'), + folder=item.get('folder'), + ytdlp_cookies=item.get('ytdlp_cookies'), + ytdlp_config=item.get('ytdlp_config'), + output_template=item.get('output_template') + ) + + return web.Response(text=self.serializer.encode(status)) + @self.routes.delete(self.config.url_prefix + 'delete') async def delete(request: Request) -> Response: post = await request.json() @@ -208,6 +232,31 @@ class Main: 'Could not find the frontend UI static assets. Please run `node_modules/.bin/ng build` inside the frontend folder.') from e raise e + async def add( + self, + url: str, + quality: str, + format: str, + folder: str, + ytdlp_cookies: str, + ytdlp_config: dict, + output_template: str + ) -> dict[str, str]: + if ytdlp_config is None: + ytdlp_config = {} + + status = await self.dqueue.add( + url=url, + quality=quality, + format=format, + folder=folder, + ytdlp_cookies=ytdlp_cookies, + ytdlp_config=ytdlp_config, + output_template=output_template + ) + + return status + if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) diff --git a/app/src/Config.py b/app/src/Config.py index 500c726c..49bd92ed 100644 --- a/app/src/Config.py +++ b/app/src/Config.py @@ -3,9 +3,7 @@ import logging import os import re import sys - -log = logging.getLogger('config') - +import coloredlogs class Config: config_path: str = '.' @@ -32,6 +30,8 @@ class Config: base_path: str = '' + logging_level: str = 'info' + _boolean_vars: tuple = ('keep_archive', 'ytdl_debug') def __init__(self): @@ -59,7 +59,7 @@ class Config: for key in re.findall(r'\{.*?\}', v): localKey: str = key[1:-1] if localKey not in self.__dict__: - log.error( + logging.error( f'Config variable "{k}" had non exisitng config reference "{key}"') sys.exit(1) v = v.replace(key, getattr(self, localKey)) @@ -68,27 +68,41 @@ class Config: if k in self._boolean_vars: if str(v).lower() not in (True, False, 'true', 'false', 'on', 'off', '1', '0'): - log.error( + raise ValueError( f'Config variable "{k}" is set to a non-boolean value "{v}".') - sys.exit(1) + setattr(self, k, str(v).lower() in (True, 'true', 'on', '1')) if not self.url_prefix.endswith('/'): self.url_prefix += '/' + numeric_level = getattr( + logging, self.logging_level.upper(), None) + + if not isinstance(numeric_level, int): + raise ValueError(f"Invalid log level: {self.logging_level}") + + logging.basicConfig( + force=True, + level=numeric_level, + format="%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s" + ) + + coloredlogs.install() + if isinstance(self.ytdl_options, str): try: self.ytdl_options = json.loads(self.ytdl_options) assert isinstance(self.ytdl_options, dict) except (json.decoder.JSONDecodeError, AssertionError) as e: - log.error(f'JSON error in "YTP_YTDL_OPTIONS": {e}') + logging.error(f'JSON error in "YTP_YTDL_OPTIONS": {e}') sys.exit(1) if self.ytdl_options_file: - log.info( + logging.info( f'Loading yt-dlp custom options from "{self.ytdl_options_file}"') if not os.path.exists(self.ytdl_options_file): - log.error( + logging.error( f'"YTP_YTDL_OPTIONS_FILE" ENV points to non-existent file: "{self.ytdl_options_file}"') else: try: @@ -97,7 +111,8 @@ class Config: assert isinstance(opts, dict) self.ytdl_options.update(opts) except (json.decoder.JSONDecodeError, AssertionError) as e: - log.error(f'JSON error in "{self.ytdl_options_file}": {e}') + logging.error( + f'JSON error in "{self.ytdl_options_file}": {e}') sys.exit(1) if self.keep_archive: diff --git a/app/src/DataStore.py b/app/src/DataStore.py index c14d4f23..b6b17d68 100644 --- a/app/src/DataStore.py +++ b/app/src/DataStore.py @@ -1,6 +1,5 @@ from collections import OrderedDict import json -import logging import sqlite3 from src.Utils import calcDownloadPath from src.Config import Config @@ -55,7 +54,6 @@ class DataStore: ) for row in cursor: - logging.debug(row) data: dict = json.loads(row['data']) key: str = data.pop('_id') item: ItemDTO = ItemDTO(**data) diff --git a/app/src/Utils.py b/app/src/Utils.py index 3fbc0730..e8055506 100644 --- a/app/src/Utils.py +++ b/app/src/Utils.py @@ -77,7 +77,6 @@ def get_opts(format: str, quality: str, ytdl_opts: dict) -> dict: Returns: ytdl_opts: Extra options """ - logging.debug(f"get_opts: {format} {quality} {ytdl_opts}") opts = copy.deepcopy(ytdl_opts) if not opts: opts: dict = { @@ -107,8 +106,6 @@ def get_opts(format: str, quality: str, ytdl_opts: dict) -> dict: postprocessors.append( {"key": "FFmpegThumbnailsConvertor", "format": "jpg", "when": "before_dl"}) - logging.debug( - f"Postprocessors: {opts['postprocessors']} + {postprocessors}") opts["postprocessors"] = postprocessors + \ (opts["postprocessors"] if "postprocessors" in opts else []) return opts @@ -119,7 +116,6 @@ def getVideoInfo(url: str, ytdlp_opts: dict = None) -> (Any | dict[str, Any] | N 'quiet': True, 'no_color': True, 'extract_flat': True, - } if ytdlp_opts: