Fix premiere re-queue.

This commit is contained in:
arabcoders 2025-06-15 18:45:18 +03:00
parent 367d4a1aeb
commit 784671cb24
4 changed files with 129 additions and 47 deletions

View file

@ -5,13 +5,15 @@ import logging
import time import time
import uuid import uuid
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from email.utils import formatdate, parsedate_to_datetime from email.utils import formatdate
from pathlib import Path from pathlib import Path
from sqlite3 import Connection from sqlite3 import Connection
import yt_dlp import yt_dlp
from aiohttp import web from aiohttp import web
from app.library.ag_utils import ag
from .AsyncPool import AsyncPool from .AsyncPool import AsyncPool
from .conditions import Conditions from .conditions import Conditions
from .config import Config from .config import Config
@ -32,6 +34,7 @@ from .Utils import (
extract_ytdlp_logs, extract_ytdlp_logs,
is_downloaded, is_downloaded,
load_cookies, load_cookies,
str_to_dt,
) )
from .YTDLPOpts import YTDLPOpts from .YTDLPOpts import YTDLPOpts
@ -109,14 +112,14 @@ class DownloadQueue(metaclass=Singleton):
Scheduler.get_instance().add( Scheduler.get_instance().add(
timer="* * * * *", timer="* * * * *",
func=self.monitor_stale, func=self._check_for_stale,
id=f"{__class__.__name__}.{__class__.monitor_stale.__name__}", id=f"{__class__.__name__}.{__class__._check_for_stale.__name__}",
) )
Scheduler.get_instance().add( Scheduler.get_instance().add(
timer="* * * * *", timer="* * * * *",
func=self.monitor_queue_live, func=self._check_live,
id=f"{__class__.__name__}.{__class__.monitor_queue_live.__name__}", id=f"{__class__.__name__}.{__class__._check_live.__name__}",
) )
# app.on_shutdown.append(self.on_shutdown) # app.on_shutdown.append(self.on_shutdown)
@ -242,11 +245,16 @@ class DownloadQueue(metaclass=Singleton):
live_in: str | None = None live_in: str | None = None
is_premiere: bool = bool(entry.get("is_premiere", False)) is_premiere: bool = bool(entry.get("is_premiere", False))
release_in: str | None = None
if entry.get("release_timestamp"):
release_in = formatdate(entry.get("release_timestamp"), usegmt=True)
item.extras["release_in"] = release_in
# check if the video is live stream. # check if the video is live stream.
if "live_status" in entry and "is_upcoming" == entry.get("live_status"): if "is_upcoming" == entry.get("live_status"):
if entry.get("release_timestamp"): if release_in:
live_in = formatdate(entry.get("release_timestamp"), usegmt=True) live_in = release_in
item.extras.update({"live_in": live_in}) item.extras["live_in"] = live_in
else: else:
error = f"No start time is set for {'premiere' if is_premiere else 'live stream'}." error = f"No start time is set for {'premiere' if is_premiere else 'live stream'}."
else: else:
@ -324,11 +332,11 @@ class DownloadQueue(metaclass=Singleton):
if filtered_logs := extract_ytdlp_logs(logs): if filtered_logs := extract_ytdlp_logs(logs):
text_logs = f" Logs: {', '.join(filtered_logs)}" text_logs = f" Logs: {', '.join(filtered_logs)}"
if live_in or "is_upcoming" == entry.get("live_status"): if "is_upcoming" == entry.get("live_status"):
NotifyEvent = Events.COMPLETED NotifyEvent = Events.COMPLETED
dlInfo.info.status = "not_live" dlInfo.info.status = "not_live"
dlInfo.info.msg = ( dlInfo.info.msg = (
f"{'Premiere video' if is_premiere else 'Live Stream' } is not available yet." + text_logs f"{'Premiere video' if is_premiere else 'Stream' } is not available yet." + text_logs
) )
itemDownload: Download = self.done.put(dlInfo) itemDownload: Download = self.done.put(dlInfo)
elif len(entry.get("formats", [])) < 1: elif len(entry.get("formats", [])) < 1:
@ -343,25 +351,38 @@ class DownloadQueue(metaclass=Singleton):
NotifyEvent = Events.COMPLETED NotifyEvent = Events.COMPLETED
await self._notify.emit(Events.LOG_WARNING, data=event_warning(f"No formats found for '{dl.title}'.")) await self._notify.emit(Events.LOG_WARNING, data=event_warning(f"No formats found for '{dl.title}'."))
elif is_premiere and self.config.prevent_live_premiere: elif is_premiere and self.config.prevent_live_premiere:
dlInfo.info.error = ( dlInfo.info.error = "Premiering right now."
f"Premiering right now. Delaying download by '{300+dl.extras.get('duration',0)}' seconds."
)
_live_in = live_in or item.extras.get("live_in", None)
if _live_in:
starts_in = parsedate_to_datetime(live_in)
starts_in = starts_in.replace(tzinfo=UTC) if starts_in.tzinfo is None else starts_in.astimezone(UTC)
starts_in = starts_in + timedelta(minutes=5, seconds=dl.extras.get("duration", 0))
dlInfo.info.error += f" Starts in {starts_in.isoformat()}."
dlInfo.info.status = "not_live" _requeue = True
itemDownload = self.done.put(dlInfo) if release_in:
NotifyEvent = Events.COMPLETED try:
await self._notify.emit( starts_in = str_to_dt(release_in)
Events.LOG_INFO, starts_in = (
data=event_info( starts_in.replace(tzinfo=UTC) if starts_in.tzinfo is None else starts_in.astimezone(UTC)
f"'{dl.title}' is premiering. Download delayed by '{300+dl.extras.get('duration')}'s." )
), starts_in = starts_in + timedelta(minutes=5, seconds=dl.extras.get("duration", 0))
) dlInfo.info.error += f" Download will start at {starts_in.isoformat()}."
_requeue = False
except Exception as e:
LOG.error(f"Failed to parse live_in date '{release_in}'. {e!s}")
dlInfo.info.error += f" Failed to parse live_in date '{release_in}'."
else:
dlInfo.info.error += f" Delaying download by '{300+dl.extras.get('duration',0)}' seconds."
if _requeue:
NotifyEvent = Events.ADDED
itemDownload = self.queue.put(dlInfo)
self.event.set()
else:
dlInfo.info.status = "not_live"
itemDownload = self.done.put(dlInfo)
NotifyEvent = Events.COMPLETED
await self._notify.emit(
Events.LOG_INFO,
data=event_info(
f"'{dl.title}' is premiering. Download delayed by '{300+dl.extras.get('duration')}'s."
),
)
else: else:
NotifyEvent = Events.ADDED NotifyEvent = Events.ADDED
itemDownload = self.queue.put(dlInfo) itemDownload = self.queue.put(dlInfo)
@ -788,9 +809,9 @@ class DownloadQueue(metaclass=Singleton):
return is_downloaded(file, url) return is_downloaded(file, url)
async def monitor_stale(self): async def _check_for_stale(self):
""" """
Monitor the queue and pool for stale downloads and cancel them if needed. Monitor pool for stale downloads and cancel them if needed.
""" """
if self.is_paused(): if self.is_paused():
return return
@ -856,7 +877,7 @@ class DownloadQueue(metaclass=Singleton):
LOG.error(f"Failed to cancel staled item '{item_ref}' from worker pool. {e!s}") LOG.error(f"Failed to cancel staled item '{item_ref}' from worker pool. {e!s}")
LOG.exception(e) LOG.exception(e)
async def monitor_queue_live(self): async def _check_live(self):
""" """
Monitor the queue for items marked as live events and queue them when time is reached. Monitor the queue for items marked as live events and queue them when time is reached.
""" """
@ -878,26 +899,28 @@ class DownloadQueue(metaclass=Singleton):
LOG.debug(f"Item '{item_ref}' is not a live stream.") LOG.debug(f"Item '{item_ref}' is not a live stream.")
continue continue
live_in: str | None = item.info.live_in or item.info.extras.get("live_in", None)
if not live_in:
LOG.debug(f"Item '{item_ref}' marked as live stream, but no date is set.")
continue
starts_in = parsedate_to_datetime(live_in)
starts_in = starts_in.replace(tzinfo=UTC) if starts_in.tzinfo is None else starts_in.astimezone(UTC)
if time_now < (starts_in + timedelta(minutes=1)):
LOG.debug(f"Item '{item_ref}' is not yet live. will start in '{dt_delta(starts_in-time_now)}'.")
continue
duration: int | None = item.info.extras.get("duration", None) duration: int | None = item.info.extras.get("duration", None)
is_premiere: bool = item.info.extras.get("is_premiere", False) is_premiere: bool = item.info.extras.get("is_premiere", False)
if is_premiere and duration and self.config.prevent_live_premiere: live_in: str | None = item.info.live_in or ag(item.info.extras, ["live_in", "release_in"], None)
if not live_in:
LOG.debug(
f"Item '{item_ref}' marked as {'premiere video' if is_premiere else 'live stream'}, but no date is set."
)
continue
starts_in = str_to_dt(live_in)
starts_in = starts_in.replace(tzinfo=UTC) if starts_in.tzinfo is None else starts_in.astimezone(UTC)
if time_now < (starts_in + timedelta(minutes=1)):
LOG.debug(f"Item '{item_ref}' is not yet live. will start at '{dt_delta(starts_in - time_now)}'.")
continue
if self.config.prevent_live_premiere and is_premiere and duration:
premiere_ends: datetime = starts_in + timedelta(minutes=5, seconds=duration) premiere_ends: datetime = starts_in + timedelta(minutes=5, seconds=duration)
if time_now < premiere_ends: if time_now < premiere_ends:
LOG.debug( LOG.debug(
f"Item '{item_ref}' is premiering and download is delayed by '{300+duration}' seconds. Will start at '{premiere_ends.isoformat()}'" f"Item '{item_ref}' is premiering, download will start in '{(starts_in.astimezone() + timedelta(minutes=5, seconds=duration)).isoformat()}'"
) )
continue continue

View file

@ -1291,3 +1291,36 @@ def parse_tags(text: str) -> tuple[str, dict[str, str | bool]]:
return "" return ""
return TAG_REGEX.sub(replacer, text).strip(), tags return TAG_REGEX.sub(replacer, text).strip(), tags
def str_to_dt(time_str: str, now=None) -> datetime:
"""
Convert a string representation of time into a datetime object.
Args:
time_str (str): The string representation of time.
now (datetime, optional): The base datetime to use for relative times. Defaults to None, which uses the current UTC time.
Returns:
datetime: A datetime object representing the parsed time.
Raises:
ValueError: If the time string cannot be parsed.
"""
from dateparser import parse as _parse
dt = _parse(
time_str,
settings={
"RELATIVE_BASE": now or datetime.now(tz=UTC),
"RETURN_AS_TIMEZONE_AWARE": True,
"TO_TIMEZONE": "UTC",
},
)
if dt is None:
msg = f"Couldn't parse date: {time_str!r}"
raise ValueError(msg)
return dt

View file

@ -12,7 +12,6 @@ dependencies = [
"python-magic-bin; sys_platform == 'win32'", "python-magic-bin; sys_platform == 'win32'",
# NonWindows # NonWindows
"python-magic>=0.4.27; sys_platform != 'win32'", "python-magic>=0.4.27; sys_platform != 'win32'",
# Crossplatform # Crossplatform
"python-socketio>=5.11.1", "python-socketio>=5.11.1",
"aiohttp>=3.9.3", "aiohttp>=3.9.3",
@ -34,6 +33,7 @@ dependencies = [
"pycryptodome", "pycryptodome",
"yt-dlp", "yt-dlp",
"platformdirs", "platformdirs",
"dateparser>=1.2.1",
] ]
[tool.ruff] [tool.ruff]

26
uv.lock
View file

@ -352,6 +352,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/16/773159bbac84807f0be78d19b832f109bf8bd46f0bc2d79a21849d0c385f/curl_cffi-0.7.1-cp38-abi3-win_amd64.whl", hash = "sha256:0eb5b08f562749639529e6990ff1b10a40e53ed45115e15f00b239230eabb927", size = 3990082 }, { url = "https://files.pythonhosted.org/packages/cb/16/773159bbac84807f0be78d19b832f109bf8bd46f0bc2d79a21849d0c385f/curl_cffi-0.7.1-cp38-abi3-win_amd64.whl", hash = "sha256:0eb5b08f562749639529e6990ff1b10a40e53ed45115e15f00b239230eabb927", size = 3990082 },
] ]
[[package]]
name = "dateparser"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "regex" },
{ name = "tzlocal" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bd/3f/d3207a05f5b6a78c66d86631e60bfba5af163738a599a5b9aa2c2737a09e/dateparser-1.2.1.tar.gz", hash = "sha256:7e4919aeb48481dbfc01ac9683c8e20bfe95bb715a38c1e9f6af889f4f30ccc3", size = 309924 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/0a/981c438c4cd84147c781e4e96c1d72df03775deb1bc76c5a6ee8afa89c62/dateparser-1.2.1-py3-none-any.whl", hash = "sha256:bdcac262a467e6260030040748ad7c10d6bacd4f3b9cdb4cfd2251939174508c", size = 295658 },
]
[[package]] [[package]]
name = "debugpy" name = "debugpy"
version = "1.8.14" version = "1.8.14"
@ -1062,6 +1077,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl", hash = "sha256:f6702d694d5d5b163c9f3f5cc34e0bed8d6857150237fae411fefb883a656d20", size = 297506 }, { url = "https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl", hash = "sha256:f6702d694d5d5b163c9f3f5cc34e0bed8d6857150237fae411fefb883a656d20", size = 297506 },
] ]
[[package]]
name = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
]
[[package]] [[package]]
name = "pywebview" name = "pywebview"
version = "5.4" version = "5.4"
@ -1352,6 +1376,7 @@ dependencies = [
{ name = "caribou" }, { name = "caribou" },
{ name = "coloredlogs" }, { name = "coloredlogs" },
{ name = "curl-cffi" }, { name = "curl-cffi" },
{ name = "dateparser" },
{ name = "debugpy" }, { name = "debugpy" },
{ name = "httpx" }, { name = "httpx" },
{ name = "mutagen" }, { name = "mutagen" },
@ -1387,6 +1412,7 @@ requires-dist = [
{ name = "caribou", specifier = ">=0.3.0" }, { name = "caribou", specifier = ">=0.3.0" },
{ name = "coloredlogs", specifier = ">=15.0.1" }, { name = "coloredlogs", specifier = ">=15.0.1" },
{ name = "curl-cffi", specifier = "==0.7.1" }, { name = "curl-cffi", specifier = "==0.7.1" },
{ name = "dateparser", specifier = ">=1.2.1" },
{ name = "debugpy", specifier = ">=1.8.1" }, { name = "debugpy", specifier = ">=1.8.1" },
{ name = "httpx" }, { name = "httpx" },
{ name = "mutagen" }, { name = "mutagen" },