From 9399c515c41d6ae0c23df7040917ea2aa9bf2ae3 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Wed, 1 Jan 2025 17:09:51 +0300 Subject: [PATCH 1/6] fixe yt-dlp Impersonate Target --- app/library/Utils.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/library/Utils.py b/app/library/Utils.py index 52cc7776..26f0b21e 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -12,6 +12,7 @@ from functools import lru_cache from typing import Any import yt_dlp +from yt_dlp.networking.impersonate import ImpersonateTarget LOG = logging.getLogger("Utils") @@ -157,7 +158,16 @@ def mergeDict(source: dict, destination: dict) -> dict: def mergeConfig(config: dict, new_config: dict) -> dict: - """Merge user provided config into default config""" + """ + Merge user provided config into default config + + Args: + config (dict): Default config + new_config (dict): User provided config + + Returns: + dict: Merged config + """ ignored_keys: tuple = ( "cookiefile", @@ -171,7 +181,12 @@ def mergeConfig(config: dict, new_config: dict) -> dict: if key in new_config: del new_config[key] - return mergeDict(new_config, config) + conf = mergeDict(new_config, config) + + if "impersonate" in conf: + conf["impersonate"] = ImpersonateTarget.from_str(conf["impersonate"]) + + return conf def isDownloaded(archive_file: str, url: str) -> tuple[bool, dict[str | None, str | None, str | None]]: From f4668aab60538e179ccfd6f8e738bb21ea7cc2a8 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Wed, 1 Jan 2025 23:25:58 +0300 Subject: [PATCH 2/6] Fixed webhook not firing --- app/library/Webhooks.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/app/library/Webhooks.py b/app/library/Webhooks.py index a3cb4152..636cb87e 100644 --- a/app/library/Webhooks.py +++ b/app/library/Webhooks.py @@ -29,20 +29,24 @@ class Webhooks: LOG.info(f"Loading webhooks from '{file}'.") from .Utils import load_file - (target, status, error) = load_file(file, list) + (targets, status, error) = load_file(file, list) if not status: raise Exception(f"{error}") - self.targets = target + for target in targets: + LOG.info(f"Will send '{target.get('on',['all'])}' events to '{target.get('name')}'.") + + self.targets = targets except Exception as e: LOG.error(f"Error loading webhooks from '{file}'. '{e}'") pass - async def send(self, event: str, item: ItemDTO) -> list[dict]: + async def send(self, event: str, item: ItemDTO | dict) -> list[dict]: if len(self.targets) == 0: return [] - if not isinstance(item, ItemDTO): + if not isinstance(item, ItemDTO) and not isinstance(item, dict): + LOG.debug(f"Received invalid item type '{type(item)}' with event '{event}'.") return [] tasks = [] @@ -56,12 +60,13 @@ class Webhooks: return await asyncio.gather(*tasks) - async def __send(self, event: str, target: dict, item: ItemDTO) -> dict: + async def __send(self, event: str, target: dict, item: ItemDTO | dict) -> dict: from .config import Config req: dict = target.get("request") + itemId = item.get("id", item.get("_id", "??")) try: - LOG.info(f"Sending event '{event}' id '{item.id}' to '{target.get('name')}'.") + LOG.info(f"Sending event '{event}' id '{itemId}' to '{target.get('name')}'.") async with httpx.AsyncClient() as client: request_type = req.get("type", "json") @@ -76,17 +81,17 @@ class Webhooks: match request_type: case "json": - reqBody["json"] = item.__dict__ + reqBody["json"] = item.__dict__ if isinstance(item, ItemDTO) else item reqBody["headers"]["Content-Type"] = "application/json" case _: - reqBody["data"] = item.__dict__ + reqBody["data"] = item.__dict__ if isinstance(item, ItemDTO) else item reqBody["headers"]["Content-Type"] = "application/x-www-form-urlencoded" response = await client.request(**reqBody) respData = {"url": req.get("url"), "status": response.status_code, "text": response.text} - msg = f"Webhook target '{target.get('name')}' Responded to event '{event}' id '{item.id}' with status '{response.status_code}'." + msg = f"Webhook target '{target.get('name')}' Responded to event '{event}' id '{itemId}' with status '{response.status_code}'." if Config.get_instance().debug and respData.get("text"): msg += f" body '{respData.get('text','??')}'." @@ -94,7 +99,7 @@ class Webhooks: return respData except Exception as e: - LOG.error(f"Error sending event '{event}' id '{item.id}' to '{target.get('name')}'. '{e}'") + LOG.error(f"Error sending event '{event}' id '{itemId}' to '{target.get('name')}'. '{e}'") return {"url": req.get("url"), "status": 500, "text": str(e)} def emit(self, event, data, **kwargs): From 531ffc7eb65b9c53cfb260577e45791a0e3a4296 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Thu, 2 Jan 2025 17:44:17 +0300 Subject: [PATCH 3/6] added env variable and file to be able to install python packages on start a better approach for installing packages. --- README.md | 2 ++ app/library/PackageInstaller.py | 60 +++++++++++++++++++++++++++++++++ app/library/config.py | 3 ++ app/main.py | 8 +++++ 4 files changed, 73 insertions(+) create mode 100644 app/library/PackageInstaller.py diff --git a/README.md b/README.md index 1420b88e..7cb28666 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ Certain values can be set via environment variables, using the `-e` parameter on * __YTP_DB_FILE__: The path to the SQLite database file. Defaults to `{config_path}/ytptube.db`. * __YTP_MANUAL_ARCHIVE__: The path to the manual archive file. Defaults to `{config_path}/manual_archive.log`. * __YTP_UI_UPDATE_TITLE__: Whether to update the title of the page with the current stats. Defaults to `true`. +* __YTP_PIP_PACKAGES__: a space separated list of pip packages to install. Defaults to `""`, you can also use `{config_path}/pip.txt` to install the packages. +* __YTP_PIP_IGNORE_UPDATES__: Do not update the custom pip packages. Defaults to `false`. ## Running behind a reverse proxy diff --git a/app/library/PackageInstaller.py b/app/library/PackageInstaller.py new file mode 100644 index 00000000..177d473d --- /dev/null +++ b/app/library/PackageInstaller.py @@ -0,0 +1,60 @@ +import logging +import os +import subprocess +import sys +import importlib +from library.config import Config + +LOG = logging.getLogger("package_installer") + + +class PackageInstaller: + """ + This class is responsible for installing and upgrading pip packages. + """ + + def __init__(self, config: Config): + self.config: Config = config + + def from_env(self): + return self.config.pip_packages.split() if self.config.pip_packages else [] + + def from_file(self, file_path): + if not os.path.exists(file_path): + return [] + + if os.access(file_path, os.R_OK): + with open(file_path, "r") as f: + return [pkg.strip() for pkg in f.readlines() if pkg.strip()] + else: + LOG.error(f"Could not read pip packages from '{file_path}'.") + return [] + + def action(self, pkg: str): + try: + importlib.import_module(pkg) + if self.config.pip_ignore_updates: + LOG.info(f"'{pkg}' is already installed. Skipping upgrades. as requested.") + return + LOG.info(f"'{pkg}' is already installed. Checking for upgrades...") + subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", pkg], check=True) + except ImportError: + LOG.info(f"'{pkg}' is not installed. Installing...") + subprocess.run([sys.executable, "-m", "pip", "install", pkg], check=True) + + def check(self): + """ + Checks for user supplied pip packages and installs them if they are not already installed. + """ + pip_file = os.path.join(self.config.config_path, "pip.txt") + packages = list(set(self.from_env() + self.from_file(pip_file))) + + if not packages: + return + + for package in packages: + try: + self.action(package) + except Exception as e: + LOG.error(f"Failed to install or upgrade package '{package}'. Error message: {str(e)}") + LOG.exception(e) diff --git a/app/library/config.py b/app/library/config.py index ce4c1c36..db6ae847 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -56,6 +56,8 @@ class Config: db_file: str = "{config_path}/ytptube.db" manual_archive: str = "{config_path}/archive.manual.log" ui_update_title: bool = True + pip_packages: str = "" + pip_ignore_updates: bool = False # immutable config vars. version: str = APP_VERSION @@ -146,6 +148,7 @@ class Config: "remove_files", "ignore_ui", "ui_update_title", + "pip_ignore_updates", ) _frontend_vars: tuple = ( diff --git a/app/main.py b/app/main.py index 1511b0e9..5325f3f7 100644 --- a/app/main.py +++ b/app/main.py @@ -19,6 +19,7 @@ from library.encoder import Encoder from library.HttpAPI import HttpAPI, LOG as http_logger from library.HttpSocket import HttpSocket from library.Webhooks import Webhooks +from library.PackageInstaller import PackageInstaller LOG = logging.getLogger("app") MIME = magic.Magic(mime=True) @@ -37,6 +38,13 @@ class Main: self.encoder = Encoder() self.checkFolders() + + try: + PackageInstaller(self.config).check() + except Exception as e: + LOG.error(f"Failed to check for packages. Error message '{str(e)}'.") + LOG.exception(e) + caribou.upgrade(self.config.db_file, os.path.join(self.rootPath, "migrations")) connection = sqlite3.connect(database=self.config.db_file, isolation_level=None) From 62c457df6592090ac898bcfa3230bee2466137a7 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Thu, 2 Jan 2025 18:20:09 +0300 Subject: [PATCH 4/6] embeded optional yt-dlp packages --- Pipfile | 3 + Pipfile.lock | 176 +++++++++++++++++++++++++++++++- app/library/PackageInstaller.py | 1 + 3 files changed, 179 insertions(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 9edb7d20..285e02cc 100644 --- a/Pipfile +++ b/Pipfile @@ -19,6 +19,9 @@ curl_cffi = "*" pysubs2 = "*" regex = "*" yt-dlp = "*" +mutagen = "*" +brotli = "*" +brotlicffi = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 0ad69e78..9e0db3e8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d132a1657486171e8fddcc57ff4eb235886b74892116c54795c1d8122700e2f9" + "sha256": "cd9a56440fe36b901993ced28ee6ef8d301a5125344cda86519fb7c54ede1449" }, "pipfile-spec": 6, "requires": { @@ -156,6 +156,171 @@ "markers": "python_version >= '3.8'", "version": "==0.23.1" }, + "brotli": { + "hashes": [ + "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", + "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", + "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354", + "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419", + "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a", + "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", + "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c", + "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088", + "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", + "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", + "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", + "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", + "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", + "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438", + "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", + "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b", + "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b", + "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68", + "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", + "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d", + "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943", + "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", + "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", + "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", + "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", + "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", + "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", + "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", + "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547", + "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", + "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", + "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", + "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a", + "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb", + "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112", + "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", + "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2", + "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", + "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", + "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95", + "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", + "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", + "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", + "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38", + "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914", + "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", + "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", + "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", + "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", + "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c", + "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", + "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f", + "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", + "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", + "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", + "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", + "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", + "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", + "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", + "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", + "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", + "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", + "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", + "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", + "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", + "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", + "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", + "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", + "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", + "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", + "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74", + "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", + "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60", + "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", + "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1", + "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", + "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", + "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", + "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", + "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460", + "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", + "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", + "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2", + "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", + "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", + "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", + "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75", + "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5", + "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", + "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", + "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", + "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", + "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", + "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", + "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", + "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", + "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01", + "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", + "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", + "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", + "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", + "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579", + "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84", + "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7", + "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c", + "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", + "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52", + "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", + "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", + "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", + "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", + "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", + "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", + "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", + "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2", + "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", + "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", + "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", + "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643", + "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", + "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", + "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985", + "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", + "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2", + "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064" + ], + "index": "pypi", + "version": "==1.1.0" + }, + "brotlicffi": { + "hashes": [ + "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b", + "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171", + "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb", + "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979", + "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33", + "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca", + "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f", + "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6", + "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca", + "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112", + "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391", + "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8", + "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0", + "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35", + "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820", + "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838", + "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613", + "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5", + "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851", + "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814", + "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc", + "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", + "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990", + "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6", + "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d", + "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808", + "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.1.0.0" + }, "caribou": { "hashes": [ "sha256:13a66fc9ef9b1c9e9ef220876d59a44ee5eff9e579655252271b7514fc0ad787", @@ -549,6 +714,15 @@ "markers": "python_version >= '3.8'", "version": "==6.1.0" }, + "mutagen": { + "hashes": [ + "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", + "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.47.0" + }, "propcache": { "hashes": [ "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4", diff --git a/app/library/PackageInstaller.py b/app/library/PackageInstaller.py index 177d473d..2c71ece4 100644 --- a/app/library/PackageInstaller.py +++ b/app/library/PackageInstaller.py @@ -52,6 +52,7 @@ class PackageInstaller: if not packages: return + LOG.info(f"Checking for user pip packages: {', '.join(packages)}") for package in packages: try: self.action(package) From d8f73a618aa09956b2d5267ebfb1e76a5aeade9d Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Thu, 2 Jan 2025 19:11:22 +0300 Subject: [PATCH 5/6] Fixed audio only streaming --- .vscode/launch.json | 2 + app/library/M3u8.py | 11 ++- app/library/Playlist.py | 11 +-- app/library/Segments.py | 38 +++++---- app/library/ffprobe.py | 167 ++++++++++++++++++++++++---------------- 5 files changed, 135 insertions(+), 94 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d6dcc102..929cc427 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,6 +30,7 @@ "console": "internalConsole", "justMyCode": true, "env": { + "PYDEVD_DISABLE_FILE_VALIDATION": "1", "YTP_CONFIG_PATH": "${workspaceFolder}/var/config", "YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads", "YTP_TEMP_PATH": "${workspaceFolder}/var/tmp", @@ -39,6 +40,7 @@ "YTP_YTDL_DEBUG": "true", "YTP_MAX_WORKERS": "2", "YTP_IGNORE_UI": "true", + "YTP_PIP_IGNORE_UPDATES": "true", } }, { diff --git a/app/library/M3u8.py b/app/library/M3u8.py index 3b6c95db..215c82d4 100644 --- a/app/library/M3u8.py +++ b/app/library/M3u8.py @@ -2,7 +2,7 @@ import math import os from urllib.parse import quote from .Utils import calcDownloadPath, StreamingError -from .ffprobe import FFProbe +from .ffprobe import ffprobe class M3u8: @@ -30,15 +30,14 @@ class M3u8: raise StreamingError(f"File '{realFile}' does not exist.") try: - ffprobe = FFProbe(realFile) - await ffprobe.run() + ff = await ffprobe(realFile) except UnicodeDecodeError: pass - if "duration" not in ffprobe.metadata: + if "duration" not in ff.metadata: raise StreamingError(f"Unable to get '{realFile}' play duration.") - duration: float = float(ffprobe.metadata.get("duration")) + duration: float = float(ff.metadata.get("duration")) m3u8 = [] @@ -53,7 +52,7 @@ class M3u8: segmentParams: dict = {} - for stream in ffprobe.streams: + for stream in ff.streams(): if stream.is_video(): if stream.codec_name not in self.ok_vcodecs: segmentParams["vc"] = 1 diff --git a/app/library/Playlist.py b/app/library/Playlist.py index 27aa1c47..5ea5ae3b 100644 --- a/app/library/Playlist.py +++ b/app/library/Playlist.py @@ -2,10 +2,8 @@ import glob import re from pathlib import Path from urllib.parse import quote - from aiohttp.web import Response - -from .ffprobe import FFProbe +from .ffprobe import ffprobe from .Subtitle import Subtitle from .Utils import calcDownloadPath, checkId, StreamingError @@ -32,15 +30,14 @@ class Playlist: ) try: - ffprobe = FFProbe(rFile) - await ffprobe.run() + ff = await ffprobe(rFile) except UnicodeDecodeError: pass - if "duration" not in ffprobe.metadata: + if "duration" not in ff.metadata: raise StreamingError(f"Unable to get '{rFile}' duration.") - duration: float = float(ffprobe.metadata.get("duration")) + duration: float = float(ff.metadata.get("duration")) playlist = [] playlist.append("#EXTM3U") diff --git a/app/library/Segments.py b/app/library/Segments.py index e4e7b3d4..8c96f3e4 100644 --- a/app/library/Segments.py +++ b/app/library/Segments.py @@ -4,6 +4,7 @@ import logging import os import tempfile +from .ffprobe import ffprobe from .config import Config from .Utils import calcDownloadPath, StreamingError @@ -21,6 +22,7 @@ class Segments: self.acodec = config.streamer_acodec # sadly due to unforeseen circumstances, we have to convert the video for now. self.vconvert = True + self.aconvert = True async def stream(self, path: str, file: str) -> bytes: realFile: str = calcDownloadPath(basePath=path, folder=file, createPath=False) @@ -28,6 +30,11 @@ class Segments: if not os.path.exists(realFile): raise StreamingError(f"File {realFile} does not exist.") + try: + ff = await ffprobe(realFile) + except UnicodeDecodeError: + pass + tmpDir: str = tempfile.gettempdir() tmpFile = os.path.join(tmpDir, f'ytptube_stream.{hashlib.md5(realFile.encode("utf-8")).hexdigest()}') @@ -57,24 +64,27 @@ class Segments: fargs.append("-map_metadata") fargs.append("-1") - fargs.append("-pix_fmt") - fargs.append("yuv420p") - fargs.append("-g") - fargs.append("52") + # video section. + if ff.has_video(): + fargs.append("-pix_fmt") + fargs.append("yuv420p") + fargs.append("-g") + fargs.append("52") - fargs.append("-map") - fargs.append("0:v:0") - fargs.append("-strict") - fargs.append("-2") + fargs.append("-map") + fargs.append("0:v:0") + fargs.append("-strict") + fargs.append("-2") - fargs.append("-codec:v") - fargs.append(self.vcodec if self.vconvert else "copy") + fargs.append("-codec:v") + fargs.append(self.vcodec if self.vconvert else "copy") # audio section. - fargs.append("-map") - fargs.append("0:a:0") - fargs.append("-codec:a") - fargs.append(self.acodec if self.aconvert else "copy") + if ff.has_audio(): + fargs.append("-map") + fargs.append("0:a:0") + fargs.append("-codec:a") + fargs.append(self.acodec if self.aconvert else "copy") fargs.append("-sn") diff --git a/app/library/ffprobe.py b/app/library/ffprobe.py index 72b6e548..25924646 100644 --- a/app/library/ffprobe.py +++ b/app/library/ffprobe.py @@ -7,6 +7,31 @@ import functools import json import operator import os +from functools import lru_cache +from typing import TypedDict + + +# parameter-less decorator +def async_lru_cache_decorator(async_function): + @functools.lru_cache + def cached_async_function(*args, **kwargs): + coroutine = async_function(*args, **kwargs) + return asyncio.ensure_future(coroutine) + + return cached_async_function + + +# decorator with options +def async_lru_cache(*lru_cache_args, **lru_cache_kwargs): + def async_lru_cache_decorator(async_function): + @functools.lru_cache(*lru_cache_args, **lru_cache_kwargs) + def cached_async_function(*args, **kwargs): + coroutine = async_function(*args, **kwargs) + return asyncio.ensure_future(coroutine) + + return cached_async_function + + return async_lru_cache_decorator class FFProbeError(Exception): @@ -163,78 +188,86 @@ class FFStream: raise FFProbeError("None integer bit_rate") -class FFProbe: - """ - FFProbe wraps the ffprobe command and pulls the data into an object form:: - metadata=FFProbe('multimedia-file.mov') - """ +class FFProbeResult: + def __init__(self): + self.metadata: dict = {} + self.video: list[FFStream] = [] + self.audio: list[FFStream] = [] + self.subtitle: list[FFStream] = [] + self.attachment: list[FFStream] = [] - audio: list[FFStream] = [] - attachment: list[FFStream] = [] - streams: list[FFStream] = [] - subtitle: list[FFStream] = [] - video: list[FFStream] = [] - metadata: dict = {} - path_to_video: str = "" + def get(self, key: str, default=None): + return getattr(self, key) if hasattr(self, key) else default - def __init__(self, path_to_video): - self.path_to_video = path_to_video + def streams(self) -> list[FFStream]: + "List of all streams." + return self.video + self.audio + self.subtitle + self.attachment - async def run(self): - try: - with open(os.devnull, "w") as tempf: - await asyncio.create_subprocess_exec("ffprobe", "-h", stdout=tempf, stderr=tempf) - except FileNotFoundError: - raise IOError("ffprobe not found.") + def has_video(self): + "Is there a video stream?" + return len(self.video) > 0 - if not os.path.isfile(self.path_to_video): - raise IOError(f"No such media file '{self.path_to_video}'.") + def has_audio(self): + "Is there an audio stream?" + return len(self.audio) > 0 - args = [ - "-v", - "quiet", - "-of", - "json", - "-show_streams", - "-show_format", - self.path_to_video, - ] - p = await asyncio.create_subprocess_exec( - "ffprobe", - *args, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - exitCode = await p.wait() - - data, err = await p.communicate() - if 0 == exitCode: - parsed: dict = json.loads(data.decode("utf-8")) - else: - raise FFProbeError(f"FFProbe return with non-0 exit code. '{err.decode('utf-8')}'") - - stream = False - self.streams = [] - self.video = [] - self.audio = [] - self.subtitle = [] - self.attachment = [] - - for stream in parsed.get("streams", []): - self.streams.append(FFStream(stream)) - - self.metadata = parsed.get("format", {}) - - for stream in self.streams: - if stream.is_audio(): - self.audio.append(stream) - elif stream.is_video(): - self.video.append(stream) - elif stream.is_subtitle(): - self.subtitle.append(stream) - elif stream.is_attachment(): - self.attachment.append(stream) + def has_subtitle(self): + "Is there a subtitle stream?" + return len(self.subtitle) > 0 def __repr__(self): return "".format(**vars(self)) + + +@async_lru_cache(maxsize=512) +async def ffprobe(file: str) -> FFProbeResult: + """ + Run ffprobe on a file and return the parsed data as a dictionary. + + Args: + file (str): The path to the media file. + + Returns: + dict: A dictionary containing the parsed data. + """ + try: + with open(os.devnull, "w") as tempf: + await asyncio.create_subprocess_exec("ffprobe", "-h", stdout=tempf, stderr=tempf) + except FileNotFoundError: + raise IOError("ffprobe not found.") + + if not os.path.isfile(file): + raise IOError(f"No such media file '{file}'.") + + args = ["-v", "quiet", "-of", "json", "-show_streams", "-show_format", file] + + p = await asyncio.create_subprocess_exec( + "ffprobe", + *args, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + exitCode = await p.wait() + + data, err = await p.communicate() + if 0 == exitCode: + parsed: dict = json.loads(data.decode("utf-8")) + else: + raise FFProbeError(f"FFProbe return with non-0 exit code. '{err.decode('utf-8')}'") + + result = FFProbeResult() + result.metadata = parsed.get("format", {}) + + for stream in parsed.get("streams", []): + stream = FFStream(stream) + if stream.is_audio(): + result.audio.append(stream) + elif stream.is_video(): + result.video.append(stream) + elif stream.is_subtitle(): + result.subtitle.append(stream) + elif stream.is_attachment(): + result.attachment.append(stream) + + return result From 9dd7e3c35286b8fc35df10fce137ef1ec7aeb6a6 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Thu, 2 Jan 2025 19:11:56 +0300 Subject: [PATCH 6/6] removed no longer used imports --- app/library/ffprobe.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/library/ffprobe.py b/app/library/ffprobe.py index 25924646..c396c3df 100644 --- a/app/library/ffprobe.py +++ b/app/library/ffprobe.py @@ -7,8 +7,6 @@ import functools import json import operator import os -from functools import lru_cache -from typing import TypedDict # parameter-less decorator