Merge pull request #301 from arabcoders/dev
Fix premiere video re-queue.
This commit is contained in:
commit
4c76aa7123
7 changed files with 155 additions and 68 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,12 +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 'Stream' } is not available yet." + text_logs
|
||||||
f"{'Premiere video' if is_premiere else 'Live 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)
|
itemDownload: Download = self.done.put(dlInfo)
|
||||||
elif len(entry.get("formats", [])) < 1:
|
elif len(entry.get("formats", [])) < 1:
|
||||||
availability: str = entry.get("availability", "public")
|
availability: str = entry.get("availability", "public")
|
||||||
|
|
@ -343,25 +350,36 @@ 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 {dlInfo.info.error}.", {"lowPriority": True}),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
NotifyEvent = Events.ADDED
|
NotifyEvent = Events.ADDED
|
||||||
itemDownload = self.queue.put(dlInfo)
|
itemDownload = self.queue.put(dlInfo)
|
||||||
|
|
@ -788,9 +806,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 +874,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 +896,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]
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="is-text-overflow is-vcentered">
|
<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">
|
<span class="tag is-info" v-if="item.extras?.duration">
|
||||||
{{ formatTime(item.extras.duration) }}
|
{{ formatTime(item.extras.duration) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -134,7 +134,8 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="is-vcentered has-text-centered is-unselectable">
|
<td class="is-vcentered has-text-centered is-unselectable">
|
||||||
<span class="icon-text">
|
<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>{{ setStatus(item) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -292,7 +293,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
|
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable">
|
||||||
<span class="icon-text">
|
<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>{{ setStatus(item) }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -842,4 +843,12 @@ const removeFromArchive = async (item, opts) => {
|
||||||
socket.emit('item_delete', { id: item._id, remove_file: false })
|
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>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,11 @@ export interface Notification {
|
||||||
export interface notificationOptions {
|
export interface notificationOptions {
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
force?: boolean,
|
force?: boolean,
|
||||||
store?: boolean,
|
|
||||||
closeOnClick?: boolean,
|
closeOnClick?: boolean,
|
||||||
position?: POSITION
|
position?: POSITION
|
||||||
onClick?: (closeToast: Function) => void
|
onClick?: (closeToast: Function) => void
|
||||||
|
store?: boolean,
|
||||||
|
lowPriority?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowToast = useStorage<boolean>('allow_toasts', true)
|
const allowToast = useStorage<boolean>('allow_toasts', true)
|
||||||
|
|
@ -28,15 +29,20 @@ const toast = useToast()
|
||||||
function notify(type: notificationType, message: string, opts?: notificationOptions): void {
|
function notify(type: notificationType, message: string, opts?: notificationOptions): void {
|
||||||
const notificationStore = useNotificationStore()
|
const notificationStore = useNotificationStore()
|
||||||
|
|
||||||
|
if (!opts) {
|
||||||
|
opts = {}
|
||||||
|
}
|
||||||
|
|
||||||
let id: string = ''
|
let id: string = ''
|
||||||
const force = opts?.force || false;
|
const force = opts?.force || false;
|
||||||
const store = opts?.store || true;
|
const store = opts?.store || true;
|
||||||
|
const lowPriority = opts?.lowPriority || false;
|
||||||
|
|
||||||
if (store && notificationStore) {
|
if (notificationStore && (store || true === lowPriority)) {
|
||||||
id = notificationStore.add(type, message, false)
|
id = notificationStore.add(type, message, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === allowToast.value && false === force) {
|
if (true === lowPriority || (false === allowToast.value && false === force)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,14 +50,6 @@ function notify(type: notificationType, message: string, opts?: notificationOpti
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts) {
|
|
||||||
opts = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts?.force) {
|
|
||||||
delete opts.force
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.closeOnClick = toastDismissOnClick.value
|
opts.closeOnClick = toastDismissOnClick.value
|
||||||
opts.position = toastPosition.value ?? POSITION.TOP_RIGHT
|
opts.position = toastPosition.value ?? POSITION.TOP_RIGHT
|
||||||
opts.onClick = (closeToast: Function) => {
|
opts.onClick = (closeToast: Function) => {
|
||||||
|
|
@ -88,5 +86,6 @@ export default function useNotification() {
|
||||||
success: (message: string, opts?: notificationOptions) => notify('success', message, opts),
|
success: (message: string, opts?: notificationOptions) => notify('success', message, opts),
|
||||||
warning: (message: string, opts?: notificationOptions) => notify('warning', message, opts),
|
warning: (message: string, opts?: notificationOptions) => notify('warning', message, opts),
|
||||||
error: (message: string, opts?: notificationOptions) => notify('error', message, opts),
|
error: (message: string, opts?: notificationOptions) => notify('error', message, opts),
|
||||||
|
notify
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,27 +51,27 @@ export const useSocketStore = defineStore('socket', () => {
|
||||||
|
|
||||||
socket.value.on('error', stream => {
|
socket.value.on('error', stream => {
|
||||||
const json = JSON.parse(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 => {
|
socket.value.on('log_info', stream => {
|
||||||
const json = JSON.parse(stream);
|
const json = JSON.parse(stream);
|
||||||
toast.info(json?.message);
|
toast.info(json?.message, json.data || {});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.value.on('log_success', stream => {
|
socket.value.on('log_success', stream => {
|
||||||
const json = JSON.parse(stream);
|
const json = JSON.parse(stream);
|
||||||
toast.success(json?.message);
|
toast.success(json?.message, json.data || {});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.value.on('log_warning', stream => {
|
socket.value.on('log_warning', stream => {
|
||||||
const json = JSON.parse(stream);
|
const json = JSON.parse(stream);
|
||||||
toast.warning(json?.message);
|
toast.warning(json?.message, json.data || {});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.value.on('log_error', stream => {
|
socket.value.on('log_error', stream => {
|
||||||
const json = JSON.parse(stream);
|
const json = JSON.parse(stream);
|
||||||
toast.error(json?.message);
|
toast.error(json?.message, json.data || {});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.value.on('completed', stream => {
|
socket.value.on('completed', stream => {
|
||||||
|
|
|
||||||
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