Fix premiere re-queue.
This commit is contained in:
parent
367d4a1aeb
commit
784671cb24
4 changed files with 129 additions and 47 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ dependencies = [
|
||||||
"python-magic-bin; sys_platform == 'win32'",
|
"python-magic-bin; sys_platform == 'win32'",
|
||||||
# Non‐Windows
|
# Non‐Windows
|
||||||
"python-magic>=0.4.27; sys_platform != 'win32'",
|
"python-magic>=0.4.27; sys_platform != 'win32'",
|
||||||
|
|
||||||
# Cross‐platform
|
# Cross‐platform
|
||||||
"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
26
uv.lock
|
|
@ -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" },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue