home · contact · privacy
Add Config class reading from DEFAULTS, environ, and config file.
authorChristian Heller <c.heller@plomlompom.de>
Wed, 27 Nov 2024 03:41:13 +0000 (04:41 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Wed, 27 Nov 2024 03:41:13 +0000 (04:41 +0100)
src/serve.py
src/sync.py
src/ytplom/misc.py

index 368a9f1ec6d0d391048f4136df2d11020b3236fd..f7a3aede80a4dd12eef0bba7f8332c8c8cf5fbe1 100755 (executable)
@@ -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:
index 7bba29ea154c936a45ba2efd0d48562771bbed9c..fdeced8693e12fe568c4106b2dabcb7710ce945f 100755 (executable)
@@ -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):
index 2618fe9cfb444aae09b93126e3fd8313bbb3a6f4..f65465781e25df5a5f78f6719d4de01e111b5da6 100644 (file)
@@ -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,