Merge pull request #395 from arabcoders/dev
Some checks failed
Build Native wrappers / build (amd64, ubuntu-latest) (push) Has been cancelled
Build Native wrappers / build (amd64, windows-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, macos-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, ubuntu-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, windows-latest) (push) Has been cancelled

Implement a workaround for slowness in status update
This commit is contained in:
Abdulmohsen 2025-08-27 01:02:17 +03:00 committed by GitHub
commit 3de3471bd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 154 additions and 96 deletions

View file

@ -6,6 +6,10 @@ on:
tag: tag:
required: true required: true
description: "Ref to build from (e.g. v1.0.0)" description: "Ref to build from (e.g. v1.0.0)"
useWorkflowSpec:
type: boolean
default: false
description: "Use app.spec from workflow branch instead of tag"
push: push:
tags: tags:
- "v*" - "v*"
@ -42,6 +46,21 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ env.TAG_NAME }} ref: ${{ env.TAG_NAME }}
- name: Overwrite app.spec from workflow branch
if: ${{ github.event.inputs.useWorkflowSpec == 'true' }}
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
sparse-checkout: |
app.spec
sparse-checkout-cone-mode: false
path: temp-spec
- name: Replace app.spec with version from workflow branch
if: ${{ github.event.inputs.useWorkflowSpec == 'true' }}
run: cp temp-spec/app.spec ./app.spec
- name: Cache Python venv - name: Cache Python venv
id: cache-python id: cache-python
uses: actions/cache@v4 uses: actions/cache@v4

View file

@ -6,6 +6,7 @@ import os
import re import re
import signal import signal
import time import time
from copy import deepcopy
from datetime import UTC, datetime from datetime import UTC, datetime
from email.utils import formatdate from email.utils import formatdate
from pathlib import Path from pathlib import Path
@ -64,6 +65,7 @@ class Download:
update_task = None update_task = None
cancel_in_progress: bool = False cancel_in_progress: bool = False
final_update = False
bad_live_options: list = [ bad_live_options: list = [
"concurrent_fragment_downloads", "concurrent_fragment_downloads",
@ -130,36 +132,40 @@ class Download:
def _progress_hook(self, data: dict): def _progress_hook(self, data: dict):
if self.debug: if self.debug:
from copy import deepcopy
d_copy = deepcopy(data) d_copy = deepcopy(data)
for k in ["formats", "thumbnails", "description", "tags", "_format_sort_fields"]: for k in ["formats", "thumbnails", "description", "tags", "_format_sort_fields"]:
d_copy["info_dict"].pop(k, None) d_copy["info_dict"].pop(k, None)
self.logger.debug(f"Progress hook: {d_copy}") self.logger.debug(f"PG Hook: {d_copy}")
dataDict = {k: v for k, v in data.items() if k in self._ytdlp_fields} self.status_queue.put(
{
if "finished" == data.get("status") and data.get("info_dict", {}).get("filename", None): "id": self.id,
dataDict["filename"] = data["info_dict"]["filename"] "action": "progress",
**{k: v for k, v in data.items() if k in self._ytdlp_fields},
self.status_queue.put({"id": self.id, **dataDict}) }
)
def _postprocessor_hook(self, data: dict): def _postprocessor_hook(self, data: dict):
if self.debug: if self.debug:
self.logger.debug(f"Postprocessor hook: {data}") d_copy = deepcopy(data)
for k in ["formats", "thumbnails", "description", "tags", "_format_sort_fields"]:
d_copy["info_dict"].pop(k, None)
if "MoveFiles" != data.get("postprocessor") or "finished" != data.get("status"): self.logger.debug(f"PP Hook: {d_copy}")
dataDict = {k: v for k, v in data.items() if k in self._ytdlp_fields}
self.status_queue.put({"id": self.id, **dataDict, "status": "postprocessing"}) if "MoveFiles" == data.get("postprocessor") and "finished" == data.get("status"):
if "__finaldir" in data.get("info_dict", {}) and "filepath" in data.get("info_dict", {}):
filename = str(Path(data["info_dict"]["__finaldir"]) / Path(data["info_dict"]["filepath"]).name)
else:
filename = data.get("info_dict", {}).get("filepath", data.get("filename"))
self.logger.debug(f"Final filename: '{filename}'.")
self.status_queue.put({"id": self.id, "action": "moved", "status": "finished", "final_name": filename})
return return
if "__finaldir" in data["info_dict"]: dataDict = {k: v for k, v in data.items() if k in self._ytdlp_fields}
filename = str(Path(data["info_dict"]["__finaldir"]) / Path(data["info_dict"]["filepath"]).name) self.status_queue.put({"id": self.id, "action": "postprocessing", **dataDict, "status": "postprocessing"})
else:
filename = data["info_dict"]["filepath"]
self.status_queue.put({"id": self.id, "status": "finished", "filename": filename})
def post_hooks(self, filename: str | None = None): def post_hooks(self, filename: str | None = None):
if not filename: if not filename:
@ -353,7 +359,32 @@ class Download:
await self._notify.emit(Events.ITEM_UPDATED, data=self.info) await self._notify.emit(Events.ITEM_UPDATED, data=self.info)
asyncio.create_task(self.progress_update(), name=f"update-{self.id}") asyncio.create_task(self.progress_update(), name=f"update-{self.id}")
return await asyncio.get_running_loop().run_in_executor(None, self.proc.join) ret = await asyncio.get_running_loop().run_in_executor(None, self.proc.join)
if self.final_update:
return ret
self.status_queue.put(Terminator())
self.logger.debug("Draining status queue.")
try:
drain_count: int = 50 + (self.status_queue.qsize() if hasattr(self.status_queue, "qsize") else 5)
except Exception:
drain_count = 55
for i in range(drain_count):
try:
self.logger.debug(f"(50/{i}) Draining the status queue...")
if self.final_update:
self.logger.debug("(50/{i}) Draining stopped. Final update received.")
break
next_status = self.status_queue.get(timeout=0.1)
if next_status is None or isinstance(next_status, Terminator):
continue
await self._process_status_update(next_status)
except Exception: # noqa: S112
continue
return ret
def started(self) -> bool: def started(self) -> bool:
return self.proc is not None return self.proc is not None
@ -480,6 +511,85 @@ class Download:
else: else:
self.logger.info(f"Temp folder '{self.temp_path}' deletion is {'success' if status else 'failed'}.") self.logger.info(f"Temp folder '{self.temp_path}' deletion is {'success' if status else 'failed'}.")
async def _process_status_update(self, status):
if status.get("id") != self.id or len(status) < 2:
self.logger.warning(f"Received invalid status update. {status}")
return
if self.debug:
self.logger.debug(f"Status Update: {self.info._id=} {status=}")
if isinstance(status, str):
await self._notify.emit(Events.ITEM_UPDATED, data=self.info)
return
self.tmpfilename = status.get("tmpfilename")
fl = None
if "final_name" in status:
fl = Path(status.get("final_name"))
try:
self.info.filename = str(fl.relative_to(Path(self.download_dir)))
except ValueError as ve:
self.logger.debug(f"Failed to get relative path for '{fl}' from '{self.download_dir}'. {ve}")
if self.temp_path:
self.info.filename = str(fl.relative_to(Path(self.temp_path)))
else:
self.info.filename = str(fl)
if fl.is_file() and fl.exists():
self.final_update = True
self.logger.debug(f"Final file name: '{fl}'.")
try:
self.info.file_size = fl.stat().st_size
except FileNotFoundError:
self.info.file_size = 0
self.info.status = status.get("status", self.info.status)
self.info.msg = status.get("msg")
if "error" == self.info.status and "error" in status:
self.info.error = status.get("error")
await self._notify.emit(
Events.LOG_ERROR,
data=self.info,
title="Download Error",
message=f"'{self.info.title}' failed to download. {self.info.error}",
)
if "downloaded_bytes" in status and status.get("downloaded_bytes", 0) > 0:
self.info.downloaded_bytes = status.get("downloaded_bytes")
total = status.get("total_bytes") or status.get("total_bytes_estimate")
if total:
try:
self.info.percent = status["downloaded_bytes"] / total * 100
except ZeroDivisionError:
self.info.percent = 0
self.info.total_bytes = total
self.info.speed = status.get("speed")
self.info.eta = status.get("eta")
if "finished" == self.info.status and fl and fl.is_file() and fl.exists():
self.info.file_size = fl.stat().st_size
self.info.datetime = str(formatdate(time.time()))
try:
ff = await ffprobe(str(fl))
self.info.extras["is_video"] = ff.has_video()
self.info.extras["is_audio"] = ff.has_audio()
if (ff.has_video() or ff.has_audio()) and not self.info.extras.get("duration"):
self.info.extras["duration"] = int(float(ff.metadata.get("duration", 0.0)))
except Exception as e:
self.info.extras["is_video"] = True
self.info.extras["is_audio"] = True
self.logger.exception(e)
self.logger.error(f"Failed to run ffprobe. {status.get}. {e}")
if not self.final_update or fl:
await self._notify.emit(Events.ITEM_UPDATED, data=self.info)
async def progress_update(self): async def progress_update(self):
""" """
Update status of download task and notify the client. Update status of download task and notify the client.
@ -488,85 +598,12 @@ class Download:
try: try:
self.update_task = asyncio.get_running_loop().run_in_executor(None, self.status_queue.get) self.update_task = asyncio.get_running_loop().run_in_executor(None, self.status_queue.get)
status = await self.update_task status = await self.update_task
if status is None or isinstance(status, Terminator):
return
await self._process_status_update(status)
except (asyncio.CancelledError, OSError, FileNotFoundError): except (asyncio.CancelledError, OSError, FileNotFoundError):
return return
if status is None or isinstance(status, Terminator):
return
if status.get("id") != self.id or len(status) < 2:
continue
if self.debug:
self.logger.debug(f"Status Update: {self.info._id=} {status=}")
if isinstance(status, str):
await self._notify.emit(Events.ITEM_UPDATED, data=self.info)
continue
self.tmpfilename = status.get("tmpfilename")
if "filename" in status:
fl = Path(status.get("filename"))
try:
self.info.filename = str(Path(status.get("filename")).relative_to(Path(self.download_dir)))
except ValueError:
if self.temp_path:
self.info.filename = str(Path(status.get("filename")).relative_to(Path(self.temp_path)))
else:
self.info.filename = str(fl)
if fl.is_file() and fl.exists():
try:
self.info.file_size = fl.stat().st_size
except FileNotFoundError:
self.info.file_size = 0
self.info.status = status.get("status", self.info.status)
self.info.msg = status.get("msg")
if "error" == self.info.status and "error" in status:
self.info.error = status.get("error")
await self._notify.emit(
Events.LOG_ERROR,
data=self.info,
title="Download Error",
message=f"'{self.info.title}' failed to download: {self.info.error}",
)
if "downloaded_bytes" in status and status.get("downloaded_bytes") > 0:
self.info.downloaded_bytes = status.get("downloaded_bytes")
total = status.get("total_bytes") or status.get("total_bytes_estimate")
if total:
try:
self.info.percent = status["downloaded_bytes"] / total * 100
except ZeroDivisionError:
self.info.percent = 0
self.info.total_bytes = total
self.info.speed = status.get("speed")
self.info.eta = status.get("eta")
fl = Path(status.get("filename")) if status and "filename" in status else None
if "finished" == self.info.status and fl and fl.is_file() and fl.exists():
self.info.file_size = fl.stat().st_size
self.info.datetime = str(formatdate(time.time()))
try:
ff = await ffprobe(status.get("filename"))
self.info.extras["is_video"] = ff.has_video()
self.info.extras["is_audio"] = ff.has_audio()
if (ff.has_video() or ff.has_audio()) and not self.info.extras.get("duration"):
self.info.extras["duration"] = int(float(ff.metadata.get("duration", 0.0)))
except Exception as e:
self.info.extras["is_video"] = True
self.info.extras["is_audio"] = True
self.logger.exception(e)
self.logger.error(f"Failed to run ffprobe. {status.get}. {e}")
await self._notify.emit(Events.ITEM_UPDATED, data=self.info)
def is_stale(self) -> bool: def is_stale(self) -> bool:
""" """
Check if the download task is stale. Check if the download task is stale.

View file

@ -981,6 +981,8 @@ class DownloadQueue(metaclass=Singleton):
await entry.start() await entry.start()
if entry.info.status not in ("finished", "skip"): if entry.info.status not in ("finished", "skip"):
if not entry.info.error:
entry.info.error = f"Download failed with status '{entry.info.status}'."
entry.info.status = "error" entry.info.status = "error"
except Exception as e: except Exception as e:
entry.info.status = "error" entry.info.status = "error"