Merge pull request #301 from arabcoders/dev

Fix premiere video re-queue.
This commit is contained in:
Abdulmohsen 2025-06-15 20:00:15 +03:00 committed by GitHub
commit 4c76aa7123
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 155 additions and 68 deletions

View file

@ -5,13 +5,15 @@ import logging
import time
import uuid
from datetime import UTC, datetime, timedelta
from email.utils import formatdate, parsedate_to_datetime
from email.utils import formatdate
from pathlib import Path
from sqlite3 import Connection
import yt_dlp
from aiohttp import web
from app.library.ag_utils import ag
from .AsyncPool import AsyncPool
from .conditions import Conditions
from .config import Config
@ -32,6 +34,7 @@ from .Utils import (
extract_ytdlp_logs,
is_downloaded,
load_cookies,
str_to_dt,
)
from .YTDLPOpts import YTDLPOpts
@ -109,14 +112,14 @@ class DownloadQueue(metaclass=Singleton):
Scheduler.get_instance().add(
timer="* * * * *",
func=self.monitor_stale,
id=f"{__class__.__name__}.{__class__.monitor_stale.__name__}",
func=self._check_for_stale,
id=f"{__class__.__name__}.{__class__._check_for_stale.__name__}",
)
Scheduler.get_instance().add(
timer="* * * * *",
func=self.monitor_queue_live,
id=f"{__class__.__name__}.{__class__.monitor_queue_live.__name__}",
func=self._check_live,
id=f"{__class__.__name__}.{__class__._check_live.__name__}",
)
# app.on_shutdown.append(self.on_shutdown)
@ -242,11 +245,16 @@ class DownloadQueue(metaclass=Singleton):
live_in: str | None = None
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.
if "live_status" in entry and "is_upcoming" == entry.get("live_status"):
if entry.get("release_timestamp"):
live_in = formatdate(entry.get("release_timestamp"), usegmt=True)
item.extras.update({"live_in": live_in})
if "is_upcoming" == entry.get("live_status"):
if release_in:
live_in = release_in
item.extras["live_in"] = live_in
else:
error = f"No start time is set for {'premiere' if is_premiere else 'live stream'}."
else:
@ -324,12 +332,11 @@ class DownloadQueue(metaclass=Singleton):
if filtered_logs := extract_ytdlp_logs(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
dlInfo.info.status = "not_live"
dlInfo.info.msg = (
f"{'Premiere video' if is_premiere else 'Live Stream' } is not available yet." + text_logs
)
dlInfo.info.msg = f"{'Premiere video' if is_premiere else 'Stream' } is not available yet." + text_logs
await self._notify.emit(Events.LOG_INFO, data=event_info(dlInfo.info.msg, {"lowPriority": True}))
itemDownload: Download = self.done.put(dlInfo)
elif len(entry.get("formats", [])) < 1:
availability: str = entry.get("availability", "public")
@ -343,25 +350,36 @@ class DownloadQueue(metaclass=Singleton):
NotifyEvent = Events.COMPLETED
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:
dlInfo.info.error = (
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.error = "Premiering right now."
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."
),
)
_requeue = True
if release_in:
try:
starts_in = str_to_dt(release_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" 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 {dlInfo.info.error}.", {"lowPriority": True}),
)
else:
NotifyEvent = Events.ADDED
itemDownload = self.queue.put(dlInfo)
@ -788,9 +806,9 @@ class DownloadQueue(metaclass=Singleton):
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():
return
@ -856,7 +874,7 @@ class DownloadQueue(metaclass=Singleton):
LOG.error(f"Failed to cancel staled item '{item_ref}' from worker pool. {e!s}")
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.
"""
@ -878,26 +896,28 @@ class DownloadQueue(metaclass=Singleton):
LOG.debug(f"Item '{item_ref}' is not a live stream.")
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)
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)
if time_now < premiere_ends:
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

View file

@ -1291,3 +1291,36 @@ def parse_tags(text: str) -> tuple[str, dict[str, str | bool]]:
return ""
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'",
# NonWindows
"python-magic>=0.4.27; sys_platform != 'win32'",
# Crossplatform
"python-socketio>=5.11.1",
"aiohttp>=3.9.3",
@ -34,6 +33,7 @@ dependencies = [
"pycryptodome",
"yt-dlp",
"platformdirs",
"dateparser>=1.2.1",
]
[tool.ruff]

View file

@ -107,7 +107,7 @@
</label>
</td>
<td class="is-text-overflow is-vcentered">
<div class="is-inline is-pulled-right" v-if="item.extras?.duration">
<div class="is-inline is-pulled-right" v-if="item.extras?.duration">
<span class="tag is-info" v-if="item.extras?.duration">
{{ formatTime(item.extras.duration) }}
</span>
@ -134,7 +134,8 @@
</td>
<td class="is-vcentered has-text-centered is-unselectable">
<span class="icon-text">
<span class="icon" :class="setIconColor(item)"><i :class="setIcon(item)" /></span>
<span class="icon" :class="setIconColor(item)"><i
:class="[setIcon(item), is_queued(item)]" /></span>
<span>{{ setStatus(item) }}</span>
</span>
</td>
@ -292,7 +293,7 @@
</div>
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
<span class="icon-text">
<span class="icon" :class="setIconColor(item)"><i :class="setIcon(item)" /></span>
<span class="icon" :class="setIconColor(item)"><i :class="[setIcon(item), is_queued(item)]" /></span>
<span>{{ setStatus(item) }}</span>
</span>
</div>
@ -842,4 +843,12 @@ const removeFromArchive = async (item, opts) => {
socket.emit('item_delete', { id: item._id, remove_file: false })
}
}
const is_queued = item => {
if (!item?.status || 'not_live' !== item.status) {
return ''
}
return item.live_in || item.extras?.live_in || item.extras?.release_in ? 'fa-spin' : ''
}
</script>

View file

@ -14,10 +14,11 @@ export interface Notification {
export interface notificationOptions {
timeout?: number,
force?: boolean,
store?: boolean,
closeOnClick?: boolean,
position?: POSITION
onClick?: (closeToast: Function) => void
store?: boolean,
lowPriority?: boolean
}
const allowToast = useStorage<boolean>('allow_toasts', true)
@ -28,15 +29,20 @@ const toast = useToast()
function notify(type: notificationType, message: string, opts?: notificationOptions): void {
const notificationStore = useNotificationStore()
if (!opts) {
opts = {}
}
let id: string = ''
const force = opts?.force || false;
const store = opts?.store || true;
const lowPriority = opts?.lowPriority || false;
if (store && notificationStore) {
if (notificationStore && (store || true === lowPriority)) {
id = notificationStore.add(type, message, false)
}
if (false === allowToast.value && false === force) {
if (true === lowPriority || (false === allowToast.value && false === force)) {
return;
}
@ -44,14 +50,6 @@ function notify(type: notificationType, message: string, opts?: notificationOpti
return;
}
if (!opts) {
opts = {}
}
if (opts?.force) {
delete opts.force
}
opts.closeOnClick = toastDismissOnClick.value
opts.position = toastPosition.value ?? POSITION.TOP_RIGHT
opts.onClick = (closeToast: Function) => {
@ -88,5 +86,6 @@ export default function useNotification() {
success: (message: string, opts?: notificationOptions) => notify('success', message, opts),
warning: (message: string, opts?: notificationOptions) => notify('warning', message, opts),
error: (message: string, opts?: notificationOptions) => notify('error', message, opts),
notify
}
}

View file

@ -51,27 +51,27 @@ export const useSocketStore = defineStore('socket', () => {
socket.value.on('error', stream => {
const json = JSON.parse(stream);
toast.error(`${json.data?.id ?? json?.type}: ${json?.message}`);
toast.error(`${json.data?.id ?? json?.type}: ${json?.message}`, json.data || {});
});
socket.value.on('log_info', stream => {
const json = JSON.parse(stream);
toast.info(json?.message);
toast.info(json?.message, json.data || {});
});
socket.value.on('log_success', stream => {
const json = JSON.parse(stream);
toast.success(json?.message);
toast.success(json?.message, json.data || {});
});
socket.value.on('log_warning', stream => {
const json = JSON.parse(stream);
toast.warning(json?.message);
toast.warning(json?.message, json.data || {});
});
socket.value.on('log_error', stream => {
const json = JSON.parse(stream);
toast.error(json?.message);
toast.error(json?.message, json.data || {});
});
socket.value.on('completed', stream => {

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 },
]
[[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]]
name = "debugpy"
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 },
]
[[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]]
name = "pywebview"
version = "5.4"
@ -1352,6 +1376,7 @@ dependencies = [
{ name = "caribou" },
{ name = "coloredlogs" },
{ name = "curl-cffi" },
{ name = "dateparser" },
{ name = "debugpy" },
{ name = "httpx" },
{ name = "mutagen" },
@ -1387,6 +1412,7 @@ requires-dist = [
{ name = "caribou", specifier = ">=0.3.0" },
{ name = "coloredlogs", specifier = ">=15.0.1" },
{ name = "curl-cffi", specifier = "==0.7.1" },
{ name = "dateparser", specifier = ">=1.2.1" },
{ name = "debugpy", specifier = ">=1.8.1" },
{ name = "httpx" },
{ name = "mutagen" },