From 99e914557ac02066effa85d9a6f205a20c324122 Mon Sep 17 00:00:00 2001 From: sherl Date: Fri, 20 Jun 2025 23:23:00 +0200 Subject: [PATCH] fix: backport fixes from a sister project --- config.default.toml | 1 + ythdd.py | 3 +++ ythdd_api.py | 15 +++++++++++++-- ythdd_api_v1.py | 18 ++++++++++++++++-- ythdd_globals.py | 11 +++++++++-- ythdd_inv_tl.py | 2 +- 6 files changed, 43 insertions(+), 7 deletions(-) diff --git a/config.default.toml b/config.default.toml index 216fc38..85ef49f 100644 --- a/config.default.toml +++ b/config.default.toml @@ -3,6 +3,7 @@ db_file_path = "/path/to/ythdd_db.sqlite" # Preferably stored on an SSD video_storage_directory_path = "/path/to/videos/" # Path to video vault. is_proxied = false # Set to true if running behind reverse proxy. public_facing_url = "http://localhost:5000/" # Used for URL rewriting. Note the trailing backslash /. +debug = false # Whether to print verbose, debug info on API endpoints. [api] api_key = "" # Leave empty API key for public access to non-sensitive backend diff --git a/ythdd.py b/ythdd.py index f1ea867..4d7cfb4 100644 --- a/ythdd.py +++ b/ythdd.py @@ -53,6 +53,9 @@ def setup(): sanity_string += f" If you're running a reverse proxy, set {colors.OKCYAN}is_proxied{colors.ENDC} to true to silence this message.\n" print(sanity_string) + # Should work around disconnects: https://stackoverflow.com/a/61739721 + app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {"pool_pre_ping": True} + app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{config['general']['db_file_path']}" app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.add_url_rule('/', view_func=views.index) diff --git a/ythdd_api.py b/ythdd_api.py index 21cfe0d..c4f7bc4 100644 --- a/ythdd_api.py +++ b/ythdd_api.py @@ -4,6 +4,7 @@ from markupsafe import escape import requests, time, json import ythdd_globals import ythdd_api_v1, ythdd_inv_tl +import traceback def api_greeting(): string = {'status': 200, 'msg': f"ok (ythdd {ythdd_globals.version})", 'latest_api': f"v{ythdd_globals.apiVersion}"} @@ -25,10 +26,15 @@ def api_global_catchall(received_request): #return api_greeting() resp = api_greeting() try: - status, received, data = ythdd_api_v1.lookup(request_list) + status, received, data = ythdd_api_v1.lookup(request_list, request) except Exception as e: ythdd_globals.apiFailedRequests += 1 + stripped_filename = __file__[max(__file__.rfind("/"), __file__.rfind("\\")) + 1:] + print(f"\n{c.FAIL}Error! /api/{received_request} -> {stripped_filename}:L{e.__traceback__.tb_lineno} -> {type(e).__name__}{c.ENDC}:" + + f"{traceback.format_exc()}") status, received, data = 500, f"internal server error: call ended in failure: {e}", [] + if ythdd_globals.config["general"]["debug"]: + status, received, data = 500, f"internal server error: call ended in failure: {e} ({stripped_filename}:L{e.__traceback__.tb_lineno})", [] resp = Response(json.dumps({'status': status, 'msg': received, 'data': data}), mimetype='application/json', status=status) elif request_list[0] == 'invidious': # drop 'invidious' from the list @@ -43,12 +49,17 @@ def api_global_catchall(received_request): # if a path has been supplied try to get appropriate data try: # lookup and construct a response - resp = ythdd_inv_tl.lookup(request_list) + resp = ythdd_inv_tl.lookup(request_list, request) #print(resp) # for debugging purposes # unless an error occurs except Exception as e: ythdd_globals.apiFailedRequests += 1 + stripped_filename = __file__[max(__file__.rfind("/"), __file__.rfind("\\")) + 1:] + print(f"\n{c.FAIL}Error! /api/{received_request} -> {stripped_filename}:L{e.__traceback__.tb_lineno} -> {type(e).__name__}{c.ENDC}:" + + f"{traceback.format_exc()}") status, received, data = 500, f"internal server error: invidious translation call ended in failure: {e}", [] + if ythdd_globals.config["general"]["debug"]: + status, received, data = 500, f"internal server error: invidious translation call ended in failure: {e} ({stripped_filename}:L{e.__traceback__.tb_lineno})", [] resp = Response(json.dumps({'status': status, 'msg': received, 'data': data}), mimetype='application/json', status=status) else: ythdd_globals.apiFailedRequests += 1 diff --git a/ythdd_api_v1.py b/ythdd_api_v1.py index 777fab7..8b7b71e 100644 --- a/ythdd_api_v1.py +++ b/ythdd_api_v1.py @@ -8,6 +8,20 @@ import ythdd_globals, ythdd_extractor #from flask_sqlalchemy import SQLAlchemy #import ythdd_api_v1_stats, ythdd_api_v1_user, ythdd_api_v1_info, ythdd_api_v1_query, ythdd_api_v1_meta, ythdd_api_v1_admin +def requireAuthentication(func): + @wraps(func) + def wrapper(*args, **kwargs): + token = kwargs["r"].args.get('token') + if token == lewy_globals.config['api']['api_key']: + try: + status, received, data = func(*args, **kwargs) + return status, received, data + except: + raise AssertionError(f"Function \"{func.__name__}\" does not return status, code, and data as it should!") + else: + return 401, "error", {'error_msg': "Unauthorized"} + return wrapper + def incrementBadRequests(): ythdd_globals.apiFailedRequests += 1 @@ -29,7 +43,7 @@ def stats(): "outside_api_requests": ythdd_globals.outsideApiHits, "local_api_requests": ythdd_globals.apiRequests - ythdd_globals.outsideApiHits } - return 200, "OK", data_to_send + return 200, "ok", data_to_send def videoIdSanityCheck(videoId: str): if len(videId) != 11: @@ -129,7 +143,7 @@ def hot(data): incrementBadRequests() return notImplemented([data[1]]) # workaround before notImplemented is reworked -def lookup(data): +def lookup(data, request): match data[0]: case 'stats': return stats() diff --git a/ythdd_globals.py b/ythdd_globals.py index 3638acd..f96c533 100644 --- a/ythdd_globals.py +++ b/ythdd_globals.py @@ -30,7 +30,7 @@ def getConfig(configfile): global randomly_generated_passcode if not os.path.exists(configfile): - dummy_config = {'general': {'db_file_path': 'ythdd_db.sqlite', 'video_storage_directory_path': 'videos/', 'is_proxied': False, 'public_facing_url': 'http://localhost:5000/'}, 'api': {'api_key': 'CHANGEME'}, 'extractor': {'user-agent': '', 'cookies_path': ''}, 'admin': {'admins': ['admin']}, 'yt_dlp': {}, 'postprocessing': {'presets': [{'name': 'recommended: [N][<=720p] best V+A', 'format': 'bv[height<=720]+ba', 'reencode': ''}, {'name': '[N][1080p] best V+A', 'format': 'bv[height=1080]+ba', 'reencode': ''}, {'name': '[R][1080p] webm', 'format': 'bv[height=1080]+ba', 'reencode': 'webm'}, {'name': '[N][720p] best V+A', 'format': 'bv[height=720]+ba', 'reencode': ''}, {'name': '[R][720p] webm', 'format': 'bv[height=720]+ba', 'reencode': 'webm'}, {'name': '[N][480p] best V+A', 'format': 'bv[height=480]+ba', 'reencode': ''}, {'name': '[480p] VP9 webm/reencode', 'format': 'bv*[height=480][ext=webm]+ba/bv[height=480]+ba', 'reencode': 'webm'}, {'name': '[N][1080p] best video only', 'format': 'bv[height=1080]', 'reencode': ''}, {'name': '[N][opus] best audio only', 'format': 'ba', 'reencode': 'opus'}]}} + dummy_config = {'general': {'db_file_path': 'ythdd_db.sqlite', 'video_storage_directory_path': 'videos/', 'is_proxied': False, 'public_facing_url': 'http://localhost:5000/', 'debug': False}, 'api': {'api_key': 'CHANGEME'}, 'extractor': {'user-agent': '', 'cookies_path': ''}, 'admin': {'admins': ['admin']}, 'yt_dlp': {}, 'postprocessing': {'presets': [{'name': 'recommended: [N][<=720p] best V+A', 'format': 'bv[height<=720]+ba', 'reencode': ''}, {'name': '[N][1080p] best V+A', 'format': 'bv[height=1080]+ba', 'reencode': ''}, {'name': '[R][1080p] webm', 'format': 'bv[height=1080]+ba', 'reencode': 'webm'}, {'name': '[N][720p] best V+A', 'format': 'bv[height=720]+ba', 'reencode': ''}, {'name': '[R][720p] webm', 'format': 'bv[height=720]+ba', 'reencode': 'webm'}, {'name': '[N][480p] best V+A', 'format': 'bv[height=480]+ba', 'reencode': ''}, {'name': '[480p] VP9 webm/reencode', 'format': 'bv*[height=480][ext=webm]+ba/bv[height=480]+ba', 'reencode': 'webm'}, {'name': '[N][1080p] best video only', 'format': 'bv[height=1080]', 'reencode': ''}, {'name': '[N][opus] best audio only', 'format': 'ba', 'reencode': 'opus'}]}} # if a passcode has not been provided by the user (config file doesn't exist, and user didn't specify it using an argument) print(f"{colors.WARNING}WARNING{colors.ENDC}: Using default, baked in config data. {colors.ENDL}" f" Consider copying and editing the provided example file ({colors.OKCYAN}config.default.toml{colors.ENDC}).") @@ -99,8 +99,15 @@ def safeTraverse(obj: dict, path: list, default=None): for x in path: #print(f"traversing {result} with respect to {x}") result = result[x] - except KeyError: + except (KeyError, TypeError): result = default print(f"error reading: {' -> '.join(path)} - returning: {default}") finally: return result + +def getCommit() -> str | None: + try: + return Repo(search_parent_directories=True).head.object.hexsha + except Exception as e: + return None + diff --git a/ythdd_inv_tl.py b/ythdd_inv_tl.py index 71dd92d..8c502ab 100644 --- a/ythdd_inv_tl.py +++ b/ythdd_inv_tl.py @@ -451,7 +451,7 @@ def videos(data): return send(status_code, response) -def lookup(data): +def lookup(data, request): # possibly TODO: rewrite this mess if len(data) > 2: if (data[0], data[1]) == ("api", "v1"):