From 288c10d3bf11c6c36cd7badec098385cc8114f26 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Wed, 27 Nov 2024 04:41:13 +0100 Subject: [PATCH] Add Config class reading from DEFAULTS, environ, and config file. --- src/serve.py | 7 ++++--- src/sync.py | 18 ++++++++--------- src/ytplom/misc.py | 49 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/serve.py b/src/serve.py index 368a9f1..f7a3aed 100755 --- a/src/serve.py +++ b/src/serve.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 """Minimalistic download-focused YouTube interface.""" -from ytplom.misc import HTTP_PORT, Server, TaskHandler +from ytplom.misc import Config, Server, TaskHandler if __name__ == '__main__': - server = Server(('0.0.0.0', HTTP_PORT), TaskHandler) - print(f'running at port {HTTP_PORT}') + config = Config() + server = Server(config, (config.host, config.port), TaskHandler) + print(f'running at port {config.port}') try: server.serve_forever() except KeyboardInterrupt: diff --git a/src/sync.py b/src/sync.py index 7bba29e..fdeced8 100755 --- a/src/sync.py +++ b/src/sync.py @@ -4,7 +4,7 @@ # included libs from typing import Any, Callable from json import loads as json_loads -from os import environ, remove as os_remove +from os import remove as os_remove from os.path import join as path_join from urllib.request import urlopen # non-included libs @@ -12,14 +12,10 @@ from paramiko import SSHClient # type: ignore from scp import SCPClient # type: ignore from ytplom.misc import ( PAGE_NAMES, PATH_DB, PATH_DOWNLOADS, PATH_TEMP, - DatabaseConnection, PathStr, QuotaLog, VideoFile, + Config, DatabaseConnection, PathStr, QuotaLog, VideoFile, YoutubeQuery, YoutubeVideo) -# what we might want to manually define per environs -YTPLOM_REMOTE = environ.get('YTPLOM_REMOTE') -YTPLOM_PORT = environ.get('YTPLOM_PORT') - PATH_DB_REMOTE = PathStr(path_join(PATH_TEMP, 'remote_db.sql')) ATTR_NAME_LAST_UPDATE = 'last_update' @@ -76,9 +72,10 @@ def sync_relations(host_names: tuple[str, str], def main(): """Connect to remote, sync local+remote DBs, + downloads where missing.""" + config = Config() ssh = SSHClient() ssh.load_system_host_keys() - ssh.connect(YTPLOM_REMOTE) + ssh.connect(config.remote) scp = SCPClient(ssh.get_transport()) scp.get(PATH_DB, PATH_DB_REMOTE) local_db = DatabaseConnection(PATH_DB) @@ -92,9 +89,10 @@ def main(): remote_db.commit_close() scp.put(PATH_DB_REMOTE, PATH_DB) os_remove(PATH_DB_REMOTE) - for host, direction, mover in ((YTPLOM_REMOTE, 'local->remote', scp.put), - ('localhost', 'remote->local', scp.get)): - url_missing = f'http://{host}:{YTPLOM_PORT}/{PAGE_NAMES["missing"]}' + for host, port, direction, mover in ( + (config.remote, config.port_remote, 'local->remote', scp.put), + (config.host, config.port, 'remote->local', scp.get)): + url_missing = f'http://{host}:{port}/{PAGE_NAMES["missing"]}' with urlopen(url_missing) as response: missing = json_loads(response.read()) for path in (path_join(PATH_DOWNLOADS, path) for path in missing): diff --git a/src/ytplom/misc.py b/src/ytplom/misc.py index 2618fe9..f654657 100644 --- a/src/ytplom/misc.py +++ b/src/ytplom/misc.py @@ -8,7 +8,7 @@ from os.path import (dirname, isdir, isfile, exists as path_exists, from random import shuffle from time import time, sleep from datetime import datetime, timedelta -from json import dumps as json_dumps +from json import dumps as json_dumps, load as json_load from uuid import uuid4 from sqlite3 import connect as sql_connect, Cursor, Row from http.server import HTTPServer, BaseHTTPRequestHandler @@ -23,9 +23,12 @@ from mpv import MPV # type: ignore from yt_dlp import YoutubeDL # type: ignore import googleapiclient.discovery # type: ignore -# what we might want to manually define per environs -API_KEY = environ.get('GOOGLE_API_KEY') -HTTP_PORT = int(environ.get('YTPLOM_PORT', 8084)) +# default configuration +DEFAULTS = { + 'host': '127.0.0.1', # NB: to be found remotely, use '0.0.0.0'! + 'port': 8090, + 'port_remote': 8090 +} # type definitions for mypy DatetimeStr = NewType('DatetimeStr', str) @@ -58,6 +61,7 @@ PATH_DOWNLOADS = PathStr(path_join(PATH_HOME, 'ytplom_downloads')) PATH_DB = PathStr(path_join(PATH_APP_DATA, 'db.sql')) PATH_TEMP = PathStr(path_join(PATH_CACHE, 'temp')) PATH_THUMBNAILS = PathStr(path_join(PATH_CACHE, 'thumbnails')) +PATH_CONFFILE = PathStr(path_join(PATH_HOME, '.config/ytplom/config.json')) # template paths PATH_TEMPLATES = PathStr(path_join(PATH_APP_DATA, 'templates')) @@ -96,10 +100,6 @@ THUMBNAIL_URL_SUFFIX = PathStr('/default.jpg') QUOTA_COST_YOUTUBE_SEARCH = QuotaCost(100) QUOTA_COST_YOUTUBE_DETAILS = QuotaCost(1) -# local expectations -TIMESTAMP_FMT = '%Y-%m-%d %H:%M:%S.%f' -LEGAL_EXTENSIONS = {'webm', 'mp4', 'mkv'} - # database stuff EXPECTED_DB_VERSION = 1 SQL_DB_VERSION = SqlText('PRAGMA user_version') @@ -108,6 +108,9 @@ PATH_DB_SCHEMA = PathStr(path_join(PATH_MIGRATIONS, f'init_{EXPECTED_DB_VERSION}.sql')) # other +ENVIRON_PREFIX = 'YTPLOM_' +TIMESTAMP_FMT = '%Y-%m-%d %H:%M:%S.%f' +LEGAL_EXTENSIONS = {'webm', 'mp4', 'mkv'} NAME_INSTALLER = PathStr('install.sh') VIDEO_FLAGS: dict[FlagName, FlagsInt] = { FlagName('delete'): FlagsInt(1 << 62) @@ -139,6 +142,29 @@ def get_db_version(db_path: PathStr) -> int: return list(conn.execute(SQL_DB_VERSION))[0][0] +class Config: + """Collects user-configurable settings.""" + host: str + remote: str + port: int + port_remote: int + api_key: str + + def __init__(self): + def set_attrs_from_dict(d): + for attr_name, type_ in self.__class__.__annotations__.items(): + if attr_name in d: + setattr(self, attr_name, type_(d[attr_name])) + set_attrs_from_dict(DEFAULTS) + if isfile(PATH_CONFFILE): + with open(PATH_CONFFILE, 'r', encoding='utf8') as f: + conffile = json_load(f) + set_attrs_from_dict(conffile) + set_attrs_from_dict({k[len(ENVIRON_PREFIX):].lower(): v + for k, v in environ.items() + if k.isupper() and k.startswith(ENVIRON_PREFIX)}) + + class DatabaseConnection: """Wrapped sqlite3.Connection.""" @@ -635,8 +661,9 @@ class DownloadsManager: class Server(HTTPServer): """Extension of HTTPServer providing for Player and DownloadsManager.""" - def __init__(self, *args, **kwargs) -> None: + def __init__(self, config: Config, *args, **kwargs) -> None: super().__init__(*args, **kwargs) + self.config = config self.jinja = JinjaEnv(loader=JinjaFSLoader(PATH_TEMPLATES)) self.player = Player() self.downloads = DownloadsManager() @@ -712,8 +739,8 @@ class TaskHandler(BaseHTTPRequestHandler): def collect_results(query_txt: QueryText) -> list[YoutubeVideo]: _ensure_expected_dirs([PATH_THUMBNAILS]) - youtube = googleapiclient.discovery.build('youtube', 'v3', - developerKey=API_KEY) + youtube = googleapiclient.discovery.build( + 'youtube', 'v3', developerKey=self.server.config.api_key) QuotaLog.update(conn, QUOTA_COST_YOUTUBE_SEARCH) search_request = youtube.search().list( q=query_txt, -- 2.30.2