From fbdeec6a9980270f2d9958b8f8fd75e46cb10e91 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Sat, 8 Mar 2025 18:22:50 +0300 Subject: [PATCH 1/6] add different icon for embeded content --- ui/assets/css/style.css | 7 ++++++- ui/components/History.vue | 2 +- ui/components/Queue.vue | 2 +- ui/public/images/embed-icon.png | Bin 0 -> 4548 bytes 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 ui/public/images/embed-icon.png diff --git a/ui/assets/css/style.css b/ui/assets/css/style.css index 58aa328c..51790731 100644 --- a/ui/assets/css/style.css +++ b/ui/assets/css/style.css @@ -226,7 +226,8 @@ hr { cursor: pointer; } -.play-icon { +.play-icon, +.embed-icon { position: absolute; z-index: 2; @@ -245,6 +246,10 @@ hr { opacity: 0.4; } +.embed-icon { + background-image: url(/images/embed-icon.png); +} + .play-icon:hover, .play-icon:focus, .play-active { diff --git a/ui/components/History.vue b/ui/components/History.vue index 131cd808..4bd0fe81 100644 --- a/ui/components/History.vue +++ b/ui/components/History.vue @@ -104,7 +104,7 @@ -
+
diff --git a/ui/components/Queue.vue b/ui/components/Queue.vue index 8277e38f..cff11603 100644 --- a/ui/components/Queue.vue +++ b/ui/components/Queue.vue @@ -54,7 +54,7 @@
-
+
diff --git a/ui/public/images/embed-icon.png b/ui/public/images/embed-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a6970dd834b63b87c42f50c5a8736c4119b5d49b GIT binary patch literal 4548 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 literal 0 HcmV?d00001 From e4794eda7cde5f6768428b126c9d1fee14d59150 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Sat, 8 Mar 2025 19:53:15 +0300 Subject: [PATCH 2/6] Updated the worker pool to be dynamic, and make it so that live stream downloading doesn't count towards the worker pool limit. --- .vscode/launch.json | 1 - app/library/AsyncPool.py | 228 ++++++++++++++++++++++------------- app/library/DownloadQueue.py | 2 +- 3 files changed, 146 insertions(+), 85 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index e2486ea9..1e9f7c07 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -35,7 +35,6 @@ "YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads", "YTP_TEMP_PATH": "${workspaceFolder}/var/tmp", "PYDEVD_DISABLE_FILE_VALIDATION": "1", - "YTP_MAX_WORKERS": "2", "YTP_IGNORE_UI": "true", "YTP_PIP_IGNORE_UPDATES": "true", "YTP_LOG_LEVEL": "DEBUG", diff --git a/app/library/AsyncPool.py b/app/library/AsyncPool.py index f6b58a9a..2c0581ad 100644 --- a/app/library/AsyncPool.py +++ b/app/library/AsyncPool.py @@ -1,5 +1,6 @@ import asyncio import logging +import uuid from datetime import UTC, datetime @@ -27,18 +28,20 @@ class AsyncPool: against each item retrieved from the queue. If any exceptions are raised out of worker_co, self.exceptions will be set to True. - :param loop: asyncio loop to use - :param num_workers: number of async tasks which will pull from the internal queue - :param name: name of the worker pool (used for logging) - :param logger: logger to use - :param worker_co: async coroutine to call when an item is retrieved from the queue - :param load_factor: multiplier used for number of items in queue - :param max_task_time: maximum time allowed for each task before a CancelledError is raised in the task. - Set to None for no limit. - :param return_futures: set to reture to return a future for each `push` (imposes CPU overhead) - :param raise_on_join: raise on join if any exceptions have occurred, default is False + Args: + num_workers (int): number of async tasks which will pull from the internal queue + worker_co (coroutine): async coroutine to call when an item is retrieved from the queue + name (str): name of the worker pool (used for logging) + logger (logging.Logger): logger to use + loop (asyncio.AbstractEventLoop|None): asyncio loop to use + load_factor (int): multiplier used for number of items in queue + max_task_time (int|None): maximum time allowed for each task before a CancelledError is raised in the task. + return_futures (bool): set to reture to return a future for each `push` (imposes CPU overhead) + raise_on_join (bool): raise on join if any exceptions have occurred, default is False + + Returns: + AsyncPool: instance of AsyncPool - :return: instance of AsyncWorkerPool """ self._loop = loop if loop else asyncio.get_event_loop() self._num_workers = num_workers @@ -53,77 +56,100 @@ class AsyncPool: self._worker_co = worker_co self._status: dict[str, dict | None] = {} - async def _worker_loop(self, worker_id: str): - """ - This is the main loop for each worker. It will pull from the queue and call the worker_co - coroutine with the arguments passed to `push`. - :param worker_id: id of the worker - :return: None - """ - while True: - got_obj = False - future = None - try: - item = await self._queue.get() - got_obj = True - - if item.__class__ is Terminator: - break - - future, args, kwargs = item - - self._status[worker_id] = { - "started": self._time().isoformat(), - "data": kwargs.get("entry", {"info": {}}).info.__dict__, - } - - # the wait_for will cancel the task (task sees CancelledError) and raises a TimeoutError from here - # so be wary of catching TimeoutErrors in this loop - result = await asyncio.wait_for(self._worker_co(*args, **kwargs), timeout=self._max_task_time) - - if future: - future.set_result(result) - except (KeyboardInterrupt, MemoryError, SystemExit) as e: - if future: - future.set_exception(e) - self._exceptions = True - raise - except BaseException as e: - if isinstance(e, asyncio.exceptions.CancelledError): - raise - - self._exceptions = True - - if future: - # don't log the failure when the client is receiving the future - future.set_exception(e) - else: - self._logger.exception(f"Worker call failed. {e!s}") - finally: - self._status[worker_id] = None - - if got_obj: - self._queue.task_done() - @property def exceptions(self): return self._exceptions + async def _worker_loop(self, worker_id: str): + """ + The main persistent worker loop that continuously processes jobs from the shared queue. + + Args: + worker_id (str): the id of the worker processing this job. + + """ + while True: + item = await self._queue.get() + should_continue = await self._process_item(worker_id, item, from_queue=True) + if not should_continue: + break + + async def _process_item(self, worker_id: str, item: tuple | Terminator, from_queue: bool = True) -> bool: + """ + Processes a single job item. + + Args: + worker_id (str): the id of the worker processing this job. + item (tuple|Terminator): the job item (tuple of (future, args, kwargs)) or a Terminator. + from_queue (bool): indicates whether the job came from the shared queue; if True, task_done() is called. + + Returns: + bool: False if the item is a Terminator (indicating termination), True otherwise. + + """ + future = None + is_terminator = isinstance(item, Terminator) + + try: + if is_terminator: + return False + + future, args, kwargs = item + + self._status[worker_id] = { + "started": self._time().isoformat(), + "data": kwargs.get("entry", {"info": {}}).info.__dict__, + } + + result = await asyncio.wait_for(self._worker_co(*args, **kwargs), timeout=self._max_task_time) + + if future: + future.set_result(result) + except (KeyboardInterrupt, MemoryError, SystemExit) as e: + if future: + future.set_exception(e) + self._exceptions = True + raise + except Exception as e: + self._exceptions = True + if future: + future.set_exception(e) + else: + self._logger.exception(f"Worker call failed. {e!s}") + finally: + self._status[worker_id] = None + if not is_terminator and from_queue: + self._queue.task_done() + + return True + def has_open_workers(self) -> bool: """ - :return: True if there are open workers. + Check if there are open workers. + + Returns: + bool: True if there are open workers, False otherwise. + """ return self.get_available_workers() > 0 def get_available_workers(self) -> int: """ - :return: number of available workers. + Get the number of available workers. + + Returns: + int: number of available workers. + """ return sum(1 for worker_status in self._status.values() if worker_status is None) def get_workers_status(self) -> dict[str, datetime | None]: """ - :return: dictionary of worker status. + Get the status of all workers. + + Returns: + dict: dictionary of worker status. + """ return self._status @@ -134,36 +160,65 @@ class AsyncPool: async def __aexit__(self, exc_type, exc_val, exc_tb): await self.join() - async def push(self, *args, **kwargs) -> asyncio.Future: + async def push(self, *args, is_temp: bool = False, **kwargs) -> asyncio.Future: """ - Method to push work to `worker_co` passed initially to `__init__`. + Push work to the worker_co. If is_temp is True, a temporary worker is spawned - :param args: position arguments to be passed to `worker_co` - :param kwargs: keyword arguments to be passed to `worker_co` + Args: + args: position arguments to be passed to `worker_co` + is_temp (bool): flag to indicate if the job is temporary, default is False + kwargs (dict): keyword arguments to be passed to `worker_co` + + Returns: + asyncio.Future: future of result. - :return: future of result. """ future = asyncio.futures.Future(loop=self._loop) if self._return_futures else None + + if is_temp: + temp_worker_id = f"temp_worker_{uuid.uuid4()!s}" + self._logger.info(f"Creating temporary worker '{temp_worker_id}'.") + # Spawn a temporary worker that processes this one job. + task = asyncio.ensure_future( + self._process_item(temp_worker_id, (future, args, kwargs), from_queue=False), loop=self._loop + ) + self._workers[temp_worker_id] = task + + # Attach a callback to clean up temporary worker entries when done. + def cleanup_temp_worker(_): + self._workers.pop(temp_worker_id, None) + self._status.pop(temp_worker_id, None) + self._logger.info(f"Temporary worker '{temp_worker_id}' has terminated.") + + task.add_done_callback(cleanup_temp_worker) + + return future + await self._queue.put((future, args, kwargs)) - self._logger.debug(f"'{self._name}' pool has received a new job. {args} {kwargs}") - return future def start(self): - """Will start up worker pool and reset exception state""" + """ + Will start up worker pool and reset exception state + """ self._exceptions = False for worker_number in range(self._num_workers): worker_id = f"worker_{worker_number+1}" self._create_worker(worker_id) - async def restart(self, worker_id: str, msg: str = None) -> bool: + async def restart(self, worker_id: str, msg: str | None = None) -> bool: """ Will restart the worker pool - :param worker_id: worker id to restart - :param msg: message to send to the worker - :return: True if worker was restarted + + Args: + worker_id (str): worker id to restart + msg (str|None): message to send to the worker, default is None + + Returns: + bool: True if worker was restarted. + """ if worker_id not in self._workers: self._logger.warning(f"Worker {worker_id} does not exist.") @@ -185,18 +240,25 @@ class AsyncPool: return True async def on_shutdown(self, _) -> None: - """Will shutdown the worker pool""" + """ + Will shutdown the worker pool + """ try: await asyncio.wait_for(asyncio.gather(*[self.stop(worker_id) for worker_id in self._workers]), timeout=10) except Exception: self._logger.error(f"Exception shutting down {self._name}") - async def stop(self, worker_id: str, msg: str = None) -> bool: + async def stop(self, worker_id: str, msg: str | None = None) -> bool: """ Will stop the worker - :param worker_id: worker id to stop - :param msg: message to send to the worker - :return: True if worker was stopped + + Args: + worker_id (str): worker id to stop + msg (str|None): message to send to the worker, default is None + + Returns: + bool: True if worker was stopped. + """ if worker_id not in self._workers: self._logger.warning(f"Worker {worker_id} does not exist.") diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index 2da63433..cbb6b810 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -640,7 +640,7 @@ class DownloadQueue(metaclass=Singleton): LOG.debug(f"Pushing {entry=} to executor.") if entry.started() is False and entry.is_cancelled() is False: - await self.pool.push(id=entry.info._id, entry=entry) + await self.pool.push(is_temp=entry.is_live, id=entry.info._id, entry=entry) LOG.debug(f"Pushed {entry=} to executor.") await asyncio.sleep(1) From 65e7d3e6fc086e8a87829b0c5327f91b415777b4 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Sat, 8 Mar 2025 19:56:55 +0300 Subject: [PATCH 3/6] reduce log spam --- app/library/DownloadQueue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index cbb6b810..757b22c1 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -614,9 +614,9 @@ class DownloadQueue(metaclass=Singleton): while True: if self.pool.has_open_workers() is True: break - if self.config.max_workers > 1 and time.time() - lastLog > 120: + if self.config.max_workers > 1 and time.time() - lastLog > 600: lastLog = time.time() - LOG.info(f"Waiting for worker to be free. {self.pool.get_workers_status()}") + LOG.info("Waiting for worker to be free.", extra={"workers": self.pool.get_available_workers()}) await asyncio.sleep(1) while not self.queue.has_downloads(): From bb507c62b31bda36eed06c2f3e43b65114e1d257 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Sat, 8 Mar 2025 20:20:54 +0300 Subject: [PATCH 4/6] added filter to presets, so we can reduce data usage when creating a iOS shortcut --- app/library/HttpAPI.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/library/HttpAPI.py b/app/library/HttpAPI.py index 67535b8c..8ce7b562 100644 --- a/app/library/HttpAPI.py +++ b/app/library/HttpAPI.py @@ -661,20 +661,25 @@ class HttpAPI(Common): ) @route("GET", "api/presets") - async def presets(self, _: Request) -> Response: + async def presets(self, request: Request) -> Response: """ Get the presets. Args: - _: The request object. + request (Request): The request object. Returns: Response: The response object. """ - return web.json_response( - data=Presets.get_instance().get_all(), status=web.HTTPOk.status_code, dumps=self.encoder.encode - ) + data = Presets.get_instance().get_all() + filter_fields = request.query.get("filter", None) + + if filter_fields: + fields = [field.strip() for field in filter_fields.split(",")] + data = [{key: value for key, value in preset.__dict__.items() if key in fields} for preset in data] + + return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=self.encoder.encode) @route("PUT", "api/presets") async def presets_add(self, request: Request) -> Response: From c7ace294a9998165bd893b95a7d43681c28cbe61 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Sun, 9 Mar 2025 01:15:00 +0300 Subject: [PATCH 5/6] updated README with bookmarklets and updated the favicon. --- README.md | 20 ++++++++++++++++++++ ui/public/favicon.ico | Bin 3262 -> 4286 bytes 2 files changed, 20 insertions(+) diff --git a/README.md b/README.md index 1b48a99d..c0e377c9 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,26 @@ Certain configuration values can be set via environment variables, using the `-e | YTP_PIP_IGNORE_UPDATES | Do not update the custom pip packages | `false` | | YTP_BASIC_MODE | Whether to run WebUI in basic mode | `false` | +## Bookmarklets and browser extensions + +### For simple bookmarklets + +```javascript +javascript:(() => { const url = "https://ytptube.example.org"; const preset = "default"; const mUrl = new URL(url); mUrl.pathname = "/api/history"; fetch(mUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: document.location.href, preset: preset }) }).then(res => alert(res.ok ? "URL sent!" : "Failed to send URL.")); })() +``` + +Change the the variable `url` and `preset` variables to match your YTPTube instance and preset name. + +### Browser Extensions Store + +- For Firefox [Firefox](https://addons.mozilla.org/en-US/firefox/addon/ytptube-extension/) +- For [Chrome/Chromium Browsers](https://chromewebstore.google.com/detail/ytptube-extension/kiepfnpeflemfokokgjiaelddchglfil) + +### iOS Shortcuts + +not yet available. + + ## Running behind a reverse proxy It's advisable to run YTPTube behind a reverse proxy, if authentication and/or HTTPS support are required. diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico index 1c765b50f5648f2a2a57f90463d3959c7d583962..8117f894429a416a42a7d3be3fb4b647e8c323af 100644 GIT binary patch literal 4286 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 J#WGp;e*jJRAp8IT literal 3262 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 V)hGEtrW4J)ihu6^g8$e2e*i2~F1!E$ From 20ddbb1a1f42849fd560e768f540e178c2d49172 Mon Sep 17 00:00:00 2001 From: ArabCoders Date: Sun, 9 Mar 2025 01:15:30 +0300 Subject: [PATCH 6/6] typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c0e377c9..4e8fc85c 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,8 @@ Change the the variable `url` and `preset` variables to match your YTPTube insta ### Browser Extensions Store -- For Firefox [Firefox](https://addons.mozilla.org/en-US/firefox/addon/ytptube-extension/) -- For [Chrome/Chromium Browsers](https://chromewebstore.google.com/detail/ytptube-extension/kiepfnpeflemfokokgjiaelddchglfil) +- For Firefox via [Firefox Store](https://addons.mozilla.org/en-US/firefox/addon/ytptube-extension/) +- For Chrome/Chromium Browsers via [Chrome Store](https://chromewebstore.google.com/detail/ytptube-extension/kiepfnpeflemfokokgjiaelddchglfil) ### iOS Shortcuts