add the ability to prevent downloading youtube premiere initial stream.

This commit is contained in:
arabcoders 2025-06-14 00:59:57 +03:00
parent a478e8e58d
commit bb41eadde7
8 changed files with 240 additions and 153 deletions

View file

@ -274,41 +274,42 @@ If you feel like donating and appreciate my work, you can do so by donating to c
Certain configuration values can be set via environment variables, using the `-e` parameter on the docker command line, or the `environment:` section in `compose.yaml` file.
| Environment Variable | Description | Default |
| ------------------------ | ---------------------------------------------------------------- | ---------------------------------- |
| YTP_OUTPUT_TEMPLATE | The template for the filenames of the downloaded videos | `%(title)s.%(ext)s` |
| YTP_DEFAULT_PRESET | The default preset to use for the download | `default` |
| YTP_INSTANCE_TITLE | The title of the instance | `empty string` |
| YTP_FILE_LOGGING | Whether to log to file | `false` |
| YTP_DOWNLOAD_PATH | Path to where the downloads will be saved | `/downloads` |
| YTP_MAX_WORKERS | How many works to use for downloads | `1` |
| YTP_AUTH_USERNAME | Username for basic authentication | `empty string` |
| YTP_AUTH_PASSWORD | Password for basic authentication | `empty string` |
| YTP_CONSOLE_ENABLED | Whether to enable the console | `false` |
| YTP_REMOVE_FILES | Remove the actual file when clicking the remove button | `false` |
| YTP_CONFIG_PATH | Path to where the config files will be stored. | `/config` |
| YTP_TEMP_PATH | Path to where tmp files are stored. | `/tmp` |
| YTP_TEMP_KEEP | Whether to keep the Individual video temp directory or remove it | `false` |
| YTP_KEEP_ARCHIVE | Keep history of downloaded videos | `true` |
| YTP_YTDL_DEBUG | Whether to turn debug logging for the internal `yt-dlp` package | `false` |
| YTP_HOST | Which IP address to bind to | `0.0.0.0` |
| YTP_PORT | Which port to bind to | `8081` |
| YTP_LOG_LEVEL | Log level | `info` |
| YTP_STREAMER_VCODEC | The video codec to use for in-browser streaming | `libx264` |
| YTP_STREAMER_ACODEC | The audio codec to use for in-browser streaming | `aac` |
| YTP_ACCESS_LOG | Whether to log access to the web server | `true` |
| YTP_DEBUG | Whether to turn on debug mode | `false` |
| YTP_DEBUGPY_PORT | The port to use for the debugpy debugger | `5678` |
| YTP_SOCKET_TIMEOUT | The timeout for the yt-dlp socket connection variable | `30` |
| YTP_EXTRACT_INFO_TIMEOUT | The timeout for extracting video information | `70` |
| YTP_DB_FILE | The path to the SQLite database file | `{config_path}/ytptube.db` |
| YTP_MANUAL_ARCHIVE | The path to the manual archive file | `{config_path}/manual_archive.log` |
| YTP_UI_UPDATE_TITLE | Whether to update the title of the page with the current stats | `true` |
| YTP_PIP_PACKAGES | A space separated list of pip packages to install | `empty string` |
| YTP_PIP_IGNORE_UPDATES | Do not update the custom pip packages | `false` |
| YTP_BASIC_MODE | Whether to run WebUI in basic mode | `false` |
| YTP_PICTURES_BACKENDS | A comma separated list of pictures urls to use. | `empty string` |
| YTP_BROWSER_ENABLED | Whether to enable the file browser | `false` |
| YTP_YTDLP_AUTO_UPDATE | Whether to enable the auto update for yt-dlp | `true` |
| YTP_BASE_PATH | Set this if you are serving YTPTube from sub-folder | `/` |
| Environment Variable | Description | Default |
| ------------------------- | ------------------------------------------------------------------ | ---------------------------------- |
| YTP_OUTPUT_TEMPLATE | The template for the filenames of the downloaded videos | `%(title)s.%(ext)s` |
| YTP_DEFAULT_PRESET | The default preset to use for the download | `default` |
| YTP_INSTANCE_TITLE | The title of the instance | `empty string` |
| YTP_FILE_LOGGING | Whether to log to file | `false` |
| YTP_DOWNLOAD_PATH | Path to where the downloads will be saved | `/downloads` |
| YTP_MAX_WORKERS | How many works to use for downloads | `1` |
| YTP_AUTH_USERNAME | Username for basic authentication | `empty string` |
| YTP_AUTH_PASSWORD | Password for basic authentication | `empty string` |
| YTP_CONSOLE_ENABLED | Whether to enable the console | `false` |
| YTP_REMOVE_FILES | Remove the actual file when clicking the remove button | `false` |
| YTP_CONFIG_PATH | Path to where the config files will be stored. | `/config` |
| YTP_TEMP_PATH | Path to where tmp files are stored. | `/tmp` |
| YTP_TEMP_KEEP | Whether to keep the Individual video temp directory or remove it | `false` |
| YTP_KEEP_ARCHIVE | Keep history of downloaded videos | `true` |
| YTP_YTDL_DEBUG | Whether to turn debug logging for the internal `yt-dlp` package | `false` |
| YTP_HOST | Which IP address to bind to | `0.0.0.0` |
| YTP_PORT | Which port to bind to | `8081` |
| YTP_LOG_LEVEL | Log level | `info` |
| YTP_STREAMER_VCODEC | The video codec to use for in-browser streaming | `libx264` |
| YTP_STREAMER_ACODEC | The audio codec to use for in-browser streaming | `aac` |
| YTP_ACCESS_LOG | Whether to log access to the web server | `true` |
| YTP_DEBUG | Whether to turn on debug mode | `false` |
| YTP_DEBUGPY_PORT | The port to use for the debugpy debugger | `5678` |
| YTP_SOCKET_TIMEOUT | The timeout for the yt-dlp socket connection variable | `30` |
| YTP_EXTRACT_INFO_TIMEOUT | The timeout for extracting video information | `70` |
| YTP_DB_FILE | The path to the SQLite database file | `{config_path}/ytptube.db` |
| YTP_MANUAL_ARCHIVE | The path to the manual archive file | `{config_path}/manual_archive.log` |
| YTP_UI_UPDATE_TITLE | Whether to update the title of the page with the current stats | `true` |
| YTP_PIP_PACKAGES | A space separated list of pip packages to install | `empty string` |
| YTP_PIP_IGNORE_UPDATES | Do not update the custom pip packages | `false` |
| YTP_BASIC_MODE | Whether to run WebUI in basic mode | `false` |
| YTP_PICTURES_BACKENDS | A comma separated list of pictures urls to use. | `empty string` |
| YTP_BROWSER_ENABLED | Whether to enable the file browser | `false` |
| YTP_YTDLP_AUTO_UPDATE | Whether to enable the auto update for yt-dlp | `true` |
| YTP_BASE_PATH | Set this if you are serving YTPTube from sub-folder | `/` |
| YTP_PREVENT_LIVE_PREMIERE | Prevents the initial youtube premiere stream from being downloaded | `false` |

View file

@ -18,6 +18,7 @@ from .config import Config
from .DataStore import DataStore
from .Download import Download
from .Events import EventBus, Events
from .Events import info as event_info
from .Events import warning as event_warning
from .ItemDTO import Item, ItemDTO
from .Presets import Presets
@ -239,6 +240,7 @@ class DownloadQueue(metaclass=Singleton):
options: dict = {}
error: str | None = None
live_in: str | None = None
is_premiere: bool = bool(entry.get("is_premiere", False))
# check if the video is live stream.
if "live_status" in entry and "is_upcoming" == entry.get("live_status"):
@ -246,7 +248,7 @@ class DownloadQueue(metaclass=Singleton):
live_in = formatdate(entry.get("release_timestamp"), usegmt=True)
item.extras.update({"live_in": live_in})
else:
error = "Live stream not yet started. And no date is set."
error = f"No start time is set for {'premiere' if is_premiere else 'live stream'}."
else:
error = entry.get("msg")
@ -274,7 +276,7 @@ class DownloadQueue(metaclass=Singleton):
is_live = bool(entry.get("is_live") or live_in or entry.get("live_status") in live_status)
try:
download_dir = calc_download_path(base_path=self.config.download_path, folder=item.folder)
download_dir: str = calc_download_path(base_path=self.config.download_path, folder=item.folder)
except Exception as e:
LOG.exception(e)
return {"status": "error", "msg": str(e)}
@ -287,6 +289,14 @@ class DownloadQueue(metaclass=Singleton):
if isinstance(key, str) and key.startswith("playlist") and entry.get(key):
item.extras[key] = entry.get(key)
item.extras["duration"] = entry.get("duration", item.extras.get("duration"))
if not item.extras.get("live_in") and live_in:
item.extras["live_in"] = live_in
if not item.extras.get("live_in") and is_premiere:
item.extras["is_premiere"] = is_premiere
dl = ItemDTO(
id=str(entry.get("id")),
title=str(entry.get("title")),
@ -310,18 +320,20 @@ class DownloadQueue(metaclass=Singleton):
try:
dlInfo: Download = Download(info=dl, info_dict=entry, logs=logs)
text_logs = ""
text_logs: str = ""
if filtered_logs := extract_ytdlp_logs(logs):
text_logs = f" Logs: {', '.join(filtered_logs)}"
if live_in or "is_upcoming" == entry.get("live_status"):
NotifyEvent = Events.COMPLETED
dlInfo.info.status = "not_live"
dlInfo.info.msg = "Stream is not live yet." + text_logs
itemDownload = self.done.put(dlInfo)
dlInfo.info.msg = (
f"{'Premiere video' if is_premiere else 'Live Stream' } is not available yet." + text_logs
)
itemDownload: Download = self.done.put(dlInfo)
elif len(entry.get("formats", [])) < 1:
availability = entry.get("availability", "public")
msg = "No formats found."
availability: str = entry.get("availability", "public")
msg: str = "No formats found."
if availability and availability not in ("public",):
msg += f" Availability is set for '{availability}'."
@ -329,7 +341,27 @@ class DownloadQueue(metaclass=Singleton):
dlInfo.info.status = "error"
itemDownload = self.done.put(dlInfo)
NotifyEvent = Events.COMPLETED
await self._notify.emit(Events.LOG_WARNING, data=event_warning(msg))
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.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:
NotifyEvent = Events.ADDED
itemDownload = self.queue.put(dlInfo)
@ -470,12 +502,10 @@ class DownloadQueue(metaclass=Singleton):
if not entry:
return {"status": "error", "msg": "Unable to extract info." + "\n".join(logs)}
if not item.requeued:
condition = Conditions.get_instance().match(info=entry)
if condition is not None:
already.pop()
LOG.info(f"Condition '{condition}' matched for '{item.url}'.")
return await self.add(item=item.new_with(requeued=True, cli=condition.cli), already=already)
if not item.requeued and (condition := Conditions.get_instance().match(info=entry)):
already.pop()
LOG.info(f"Condition '{condition.name}' matched for '{item.url}'.")
return await self.add(item=item.new_with(requeued=True, cli=condition.cli), already=already)
end_time = time.perf_counter() - started
LOG.debug(f"extract_info: for 'URL: {item.url}' is done in '{end_time:.3f}'. Length: '{len(entry)}/keys'.")
@ -833,32 +863,44 @@ class DownloadQueue(metaclass=Singleton):
if self.is_paused() or self.done.empty():
return
LOG.debug("Checking for live stream items in the history queue.")
LOG.debug("Checking history queue for queued live stream links.")
time_now = datetime.now(tz=UTC)
status = ["not_live", "is_upcoming", "is_live"]
status: list[str] = ["not_live", "is_upcoming", "is_live"]
for id, item in list(self.done.items()):
if item.info.status not in status:
continue
item_ref = f"{id=} {item.info.id=} {item.info.title=}"
item_ref: str = f"{id=} {item.info.id=} {item.info.title=}"
if not item.is_live:
LOG.debug(f"Item '{item_ref}' is not a live stream.")
continue
if not item.info.live_in:
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(item.info.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)
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:
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()}'"
)
continue
LOG.info(f"Re-queuing item '{item_ref} {item.info.extras=}' for download.")
try:
@ -868,17 +910,18 @@ class DownloadQueue(metaclass=Singleton):
continue
try:
info = item.info
new_queue = Item(
url=info.url,
preset=info.preset,
folder=info.folder,
cookies=info.cookies,
template=info.template,
cli=item.info.cli,
extras=item.info.extras,
await self.add(
item=Item(
url=item.info.url,
preset=item.info.preset,
folder=item.info.folder,
cookies=item.info.cookies,
template=item.info.template,
cli=item.info.cli,
extras=item.info.extras,
)
)
await self.add(item=new_queue)
except Exception as e:
LOG.error(f"Failed to re-queue item '{item_ref}'. {e!s}")
self.done.put(item)
LOG.exception(e)
LOG.error(f"Failed to re-queue item '{item_ref}'. {e!s}")

View file

@ -17,6 +17,7 @@ from typing import TypeVar
import yt_dlp
from Crypto.Cipher import AES
from yt_dlp.utils import match_str
from .LogWrapper import LogWrapper
@ -59,7 +60,8 @@ FILES_TYPE: list = [
{"rx": re.compile(r"\.(nfo|json|jpg|torrent|\.info\.json)$", re.IGNORECASE), "type": "metadata"},
]
DATETIME_PATTERN = re.compile(r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2}))\s?")
TAG_REGEX: re.Pattern[str] = re.compile(r"%{([^:}]+)(?::([^}]*))?}c")
DT_PATTERN: re.Pattern[str] = re.compile(r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2}))\s?")
T = TypeVar("T")
@ -204,6 +206,10 @@ def extract_info(
if not data:
return data
data["is_premiere"] = match_str("media_type=video & duration & is_live", data)
if not data["is_premiere"]:
data["is_premiere"] = "video" == data.get("media_type") and "is_upcoming" == data.get("live_status")
return yt_dlp.YoutubeDL.sanitize_info(data) if sanitize_info else data
@ -963,7 +969,7 @@ async def read_logfile(file: Path, offset: int = 0, limit: int = 50) -> dict:
for line in lines[-(offset + limit) : -offset] if offset else lines[-limit:]:
line_bytes = line if isinstance(line, bytes) else line.encode()
msg = line.decode(errors="replace")
dt_match = DATETIME_PATTERN.match(msg)
dt_match = DT_PATTERN.match(msg)
result.append(
{
"id": sha256(line_bytes).hexdigest(),
@ -1005,7 +1011,7 @@ async def tail_log(file: Path, emitter: callable, sleep_time: float = 0.5):
continue
msg = line.decode(errors="replace")
dt_match = DATETIME_PATTERN.match(msg)
dt_match = DT_PATTERN.match(msg)
await emitter(
{
@ -1228,3 +1234,25 @@ def load_modules(root_path: Path, directory: Path):
importlib.import_module(full_name)
except ImportError as e:
LOG.error(f"Failed to import module '{full_name}': {e}")
def parse_tags(text: str) -> tuple[str, dict[str, str | bool]]:
"""
Parse tags from a string formatted with %{tag_name[:value]}c.
Args:
text (str): The input string containing tags.
Returns:
tuple[str, dict[str, str | bool]]: A tuple containing the string with tags removed and a dictionary of tags.
"""
tags: dict[str, str | bool] = {}
def replacer(match: re.Match) -> str:
name = match.group(1)
value = match.group(2)
tags[name] = value if value is not None else True
return ""
return TAG_REGEX.sub(replacer, text).strip(), tags

View file

@ -158,6 +158,9 @@ class Config:
is_native: bool = False
"Is the application running in webview."
prevent_live_premiere: bool = False
"""Prevent downloading of the initial premiere live broadcast."""
pictures_backends: list[str] = [
"https://unsplash.it/1920/1080?random",
"https://picsum.photos/1920/1080",
@ -209,6 +212,7 @@ class Config:
"console_enabled",
"browser_enabled",
"ytdlp_auto_update",
"prevent_premiere_live",
)
"The variables that are booleans."

View file

@ -537,6 +537,9 @@ const setIcon = item => {
if (!item.filename) {
return 'fa-solid fa-exclamation'
}
if (item.extras?.is_premiere) {
return 'fa-solid fa-star'
}
return item.is_live ? 'fa-solid fa-globe' : 'fa-solid fa-circle-check'
}
@ -549,7 +552,7 @@ const setIcon = item => {
}
if ('not_live' === item.status) {
return 'fa-solid fa-headset'
return item.extras?.is_premiere ? 'fa-solid fa-star' : 'fa-solid fa-headset'
}
return 'fa-solid fa-circle'
@ -579,6 +582,11 @@ const setStatus = item => {
if (!item.filename) {
return 'Skipped?'
}
if (item.extras?.is_premiere) {
return 'Premiere'
}
return item.is_live ? 'Live Ended' : 'Completed'
}
@ -591,6 +599,9 @@ const setStatus = item => {
}
if ('not_live' === item.status) {
if (item.extras?.is_premiere){
return 'Premiere'
}
return display_style.value === 'cards' ? 'Live Stream' : 'Live'
}

View file

@ -32,7 +32,7 @@
<hr class="navbar-divider">
</template>
<div class="notification-list">
<div v-for="n in store.notifications" :key="n.id" class="navbar-item is-flex is-align-items-start"
<div v-for="n in store.notifications" :key="n.id" class="pr-1 pl-1 navbar-item is-flex is-align-items-start"
:class="['notification-item', 'notification-' + n.level]">
<div class="is-flex-grow-1">
<p class="is-size-7 mb-1 notification-message" :class="{ expanded: expandedId === n.id }"

View file

@ -12,7 +12,7 @@
"web-types": "./web-types.json",
"dependencies": {
"@pinia/nuxt": "^0.11.1",
"@sentry/nuxt": "^9.28.1",
"@sentry/nuxt": "^9.29.0",
"@vueuse/core": "^13.3.0",
"@vueuse/nuxt": "^13.3.0",
"@xterm/addon-fit": "^0.10.0",

View file

@ -12,8 +12,8 @@ importers:
specifier: ^0.11.1
version: 0.11.1(magicast@0.3.5)(pinia@3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3)))
'@sentry/nuxt':
specifier: ^9.28.1
version: 9.28.1(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)(magicast@0.3.5)(nuxt@3.17.5(@parcel/watcher@2.5.1)(@types/node@24.0.1)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)(rollup@4.43.0)(terser@5.42.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(yaml@2.8.0))(pinia@3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3)))(rollup@4.43.0)(vue@3.5.16(typescript@5.8.3))
specifier: ^9.29.0
version: 9.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)(magicast@0.3.5)(nuxt@3.17.5(@parcel/watcher@2.5.1)(@types/node@24.0.1)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)(rollup@4.43.0)(terser@5.42.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(yaml@2.8.0))(pinia@3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3)))(rollup@4.43.0)(vue@3.5.16(typescript@5.8.3))
'@vueuse/core':
specifier: ^13.3.0
version: 13.3.0(vue@3.5.16(typescript@5.8.3))
@ -1066,40 +1066,40 @@ packages:
cpu: [x64]
os: [win32]
'@sentry-internal/browser-utils@9.28.1':
resolution: {integrity: sha512-P/FEZkT7UqTw9P/2n/Y4Aa1OtGP6dnCvyqzPPkjiRdVa7Ep7S5ElBJloGv7077TLLBtAfCsEUVRlM1F6/jQoaA==}
'@sentry-internal/browser-utils@9.29.0':
resolution: {integrity: sha512-Wp6UJCDVV2KVK+TG8GwdLZyDy4GtUYDmVhGMpHKPS3G/Qgpf36cY/XHwChwaHZ5P9Bk1sjS9Ok698J59S8L2nw==}
engines: {node: '>=18'}
'@sentry-internal/feedback@9.28.1':
resolution: {integrity: sha512-HOk/c26D3nlClO/xEefev8fIJzRA621PFQvNFPu/y0Z5HujEqSmIsrff0cXszPPYD95h4Mwk63E0ZYdspdeXcw==}
'@sentry-internal/feedback@9.29.0':
resolution: {integrity: sha512-ADvetGrtr+RfYcQKrQxah4fHs/xDJ/VjbStVMSuaNllzwWPYNkWIGFE6YjQ7wZszj0DQIu5/H+B6lZKsFYk4xw==}
engines: {node: '>=18'}
'@sentry-internal/replay-canvas@9.28.1':
resolution: {integrity: sha512-RtkogfcIpXLFCyV8CTnXmVTH2QauT/KwmUAXBbeOz3rRWsM19yjN1moHrsjxn7OdjTv+D4qWSCA8Ka1aKSpr7g==}
'@sentry-internal/replay-canvas@9.29.0':
resolution: {integrity: sha512-TrQYhSAVPhyenvu0fNkon7BznFibu1mzS5bCudxhgOWajZluUVrXcbp8Q3WZ3R+AogrcgA3Vy6aumP/+fMKdwg==}
engines: {node: '>=18'}
'@sentry-internal/replay@9.28.1':
resolution: {integrity: sha512-Tv9pkfAX+1bmhxF42TL0c4uTiK2+rp5LMYEPdz6JBfpfvG/Z1unPGsuB7fQmHYKyfHBQJmi92DZV+smljm7w/g==}
'@sentry-internal/replay@9.29.0':
resolution: {integrity: sha512-we/1JPRje8sNowQCyogOV1OYWuDOP/3XmDi48XoFG2HB0XMl2HfL5LI8AvgAvC/5nrqVAAo4ktbjoVLm1fb7rg==}
engines: {node: '>=18'}
'@sentry/babel-plugin-component-annotate@3.2.4':
resolution: {integrity: sha512-yBzRn3GEUSv1RPtE4xB4LnuH74ZxtdoRJ5cmQ9i6mzlmGDxlrnKuvem5++AolZTE9oJqAD3Tx2rd1PqmpWnLoA==}
engines: {node: '>= 14'}
'@sentry/babel-plugin-component-annotate@3.4.0':
resolution: {integrity: sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg==}
'@sentry/babel-plugin-component-annotate@3.5.0':
resolution: {integrity: sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==}
engines: {node: '>= 14'}
'@sentry/browser@9.28.1':
resolution: {integrity: sha512-XAS46iQSq8lXTnv9udQP025JTf3PwSVRE9ePJVQhx25QBWxedqGhEOv5qqX9b1Ijf8KiZYXXhBWMQxBBXVzUaw==}
'@sentry/browser@9.29.0':
resolution: {integrity: sha512-+GFX/yb+rh6V1fSgTYM6ttAgledl2aUR3T3Rg86HNuegbdX8ym6lOtUOIZ0j9jPK015HR47KIPyIZVZZJ7Rj9g==}
engines: {node: '>=18'}
'@sentry/bundler-plugin-core@3.2.4':
resolution: {integrity: sha512-YMj9XW5W2JA89EeweE7CPKLDz245LBsI1JhCmqpt/bjSvmsSIAAPsLYnvIJBS3LQFm0OhtG8NB54PTi96dAcMA==}
engines: {node: '>= 14'}
'@sentry/bundler-plugin-core@3.4.0':
resolution: {integrity: sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw==}
'@sentry/bundler-plugin-core@3.5.0':
resolution: {integrity: sha512-zDzPrhJqAAy2VzV4g540qAZH4qxzisstK2+NIJPZUUKztWRWUV2cMHsyUtdctYgloGkLyGpZJBE3RE6dmP/xqQ==}
engines: {node: '>= 14'}
'@sentry/cli-darwin@2.42.2':
@ -1148,22 +1148,22 @@ packages:
engines: {node: '>= 10'}
hasBin: true
'@sentry/core@9.28.1':
resolution: {integrity: sha512-6q59r/71MeE+4StkvwdKAAyhBBNpWcii0HeiWBZ3l1gaFYQlb6bChjZJRZmxSzF5dnvkdF4duQbAC3JmjeIbPA==}
'@sentry/core@9.29.0':
resolution: {integrity: sha512-wDyNe45PM+RCGtUn1tK7LzJ08ksv8i8KRUHrst7lsinEfRm83YH+wbWrPmwkVNEngUZvYkHwGLbNXM7xgFUuDQ==}
engines: {node: '>=18'}
'@sentry/node@9.28.1':
resolution: {integrity: sha512-1bC8ywFPs1EIKzRlUhLoreTR/fUU45O+ezVNvOu0I6JURMwjyC8uT23f0Xkiolx4WjKZXER+d0el3HojPaYkYw==}
'@sentry/node@9.29.0':
resolution: {integrity: sha512-oABipgC/fClRuvyMeK43rigv9F+OAaoR84UaMKB7aPXN6iz634wBRVsaoZAwiR3xLL+R7MafEPPA/s9XqlG7ag==}
engines: {node: '>=18'}
'@sentry/nuxt@9.28.1':
resolution: {integrity: sha512-+QhmFywTq5BL9tK7Yq7j6o6/B31l2fAn/ZULmIlJLAlgnKsexVS/Dbhlbal/deMi0tLLQPxbEtOtZ86PbQpKdg==}
'@sentry/nuxt@9.29.0':
resolution: {integrity: sha512-bstQwiqgB6n4/23NYDb8pl7B7wICSJaZoIbTjAhceiog1yVtdQZsQa8iRyCwDpdzU7NyOhfCT1ECQ98EdwDJog==}
engines: {node: '>=18.19.1'}
peerDependencies:
nuxt: '>=3.7.0 || 4.x'
'@sentry/opentelemetry@9.28.1':
resolution: {integrity: sha512-1xxriB2diNSlNpkMosdgIKzPOPwAiyFg7XgEWKpLtDYWj7WVbLNyLkUWriFr4JgU40uZp3LW+sGgnshA48/zQQ==}
'@sentry/opentelemetry@9.29.0':
resolution: {integrity: sha512-QTUmre8i5+832RjzQW+g8IQ3UmBe5fbQXGbCF5hQ0UNuHle9r3Z8UZcIff5W8tm5AXMxPqvptTnDEZUUXHgBiA==}
engines: {node: '>=18'}
peerDependencies:
'@opentelemetry/api': ^1.9.0
@ -1173,8 +1173,8 @@ packages:
'@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0
'@opentelemetry/semantic-conventions': ^1.34.0
'@sentry/rollup-plugin@3.4.0':
resolution: {integrity: sha512-oqDcjV+aaTZZ7oOadk90KlShOYfKEEQsvbZtzHl7HPHNt5kmtTaQyWphPIDt2Z9OCK8QF5T8GLsr1MCOXJ6vqA==}
'@sentry/rollup-plugin@3.5.0':
resolution: {integrity: sha512-aMPCvdNMkv//LZYjYCJsEcNiNiaQFinBO75+9NJVEe1OrKNdGqDi3hky2ll7zuY+xozEtZCZcUKJJz/aAYAS8A==}
engines: {node: '>= 14'}
peerDependencies:
rollup: '>=3.2.0'
@ -1183,8 +1183,8 @@ packages:
resolution: {integrity: sha512-ZRn5TLlq5xtwKOqaWP+XqS1PYVfbBCgsbMk7wW2Ly6EgF9wYePvtLqKgYnE3hwPg2LpBnRPR2ti1ohlUkR+wXA==}
engines: {node: '>= 14'}
'@sentry/vue@9.28.1':
resolution: {integrity: sha512-o6b2rQpbEjvNgyUDd7dVjjKwFfembf0ltd4MW78F2CoAYMS/YkHAum/iZ/5wOB4pYhutPbeDDZf0s6aM5KCOZQ==}
'@sentry/vue@9.29.0':
resolution: {integrity: sha512-wwKHgW2RFpn91UQuClYiI6Tt8VkQ36dGxiVT8l98uGC0uPXzwz40ikdBgnOMt3qwbRZX8ziEXHjjTtAxdETe3w==}
engines: {node: '>=18'}
peerDependencies:
pinia: 2.x || 3.x
@ -1595,8 +1595,8 @@ packages:
caniuse-api@3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
caniuse-lite@1.0.30001722:
resolution: {integrity: sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==}
caniuse-lite@1.0.30001723:
resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
@ -2337,8 +2337,8 @@ packages:
image-meta@0.2.1:
resolution: {integrity: sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==}
import-in-the-middle@1.14.0:
resolution: {integrity: sha512-g5zLT0HaztRJWysayWYiUq/7E5H825QIiecMD2pI5QO7Wzr847l6GDvPvmZaDIdrDtS2w7qRczywxiK6SL5vRw==}
import-in-the-middle@1.14.2:
resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==}
impound@1.0.0:
resolution: {integrity: sha512-8lAJ+1Arw2sMaZ9HE2ZmL5zOcMnt18s6+7Xqgq2aUVy4P1nlzAyPtzCDxsk51KVFwHEEdc6OWvUyqwHwhRYaug==}
@ -4090,8 +4090,8 @@ packages:
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
engines: {node: '>= 14'}
zod@3.25.63:
resolution: {integrity: sha512-3ttCkqhtpncYXfP0f6dsyabbYV/nEUW+Xlu89jiXbTBifUfjaSqXOG6JnQPLtqt87n7KAmnMqcjay6c0Wq0Vbw==}
zod@3.25.64:
resolution: {integrity: sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==}
snapshots:
@ -4532,7 +4532,7 @@ snapshots:
unixify: 1.0.0
urlpattern-polyfill: 8.0.2
yargs: 17.7.2
zod: 3.25.63
zod: 3.25.64
transitivePeerDependencies:
- encoding
- rollup
@ -4966,7 +4966,7 @@ snapshots:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.57.2
'@types/shimmer': 1.2.0
import-in-the-middle: 1.14.0
import-in-the-middle: 1.14.2
require-in-the-middle: 7.5.2
semver: 7.7.2
shimmer: 1.2.1
@ -5264,35 +5264,35 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.43.0':
optional: true
'@sentry-internal/browser-utils@9.28.1':
'@sentry-internal/browser-utils@9.29.0':
dependencies:
'@sentry/core': 9.28.1
'@sentry/core': 9.29.0
'@sentry-internal/feedback@9.28.1':
'@sentry-internal/feedback@9.29.0':
dependencies:
'@sentry/core': 9.28.1
'@sentry/core': 9.29.0
'@sentry-internal/replay-canvas@9.28.1':
'@sentry-internal/replay-canvas@9.29.0':
dependencies:
'@sentry-internal/replay': 9.28.1
'@sentry/core': 9.28.1
'@sentry-internal/replay': 9.29.0
'@sentry/core': 9.29.0
'@sentry-internal/replay@9.28.1':
'@sentry-internal/replay@9.29.0':
dependencies:
'@sentry-internal/browser-utils': 9.28.1
'@sentry/core': 9.28.1
'@sentry-internal/browser-utils': 9.29.0
'@sentry/core': 9.29.0
'@sentry/babel-plugin-component-annotate@3.2.4': {}
'@sentry/babel-plugin-component-annotate@3.4.0': {}
'@sentry/babel-plugin-component-annotate@3.5.0': {}
'@sentry/browser@9.28.1':
'@sentry/browser@9.29.0':
dependencies:
'@sentry-internal/browser-utils': 9.28.1
'@sentry-internal/feedback': 9.28.1
'@sentry-internal/replay': 9.28.1
'@sentry-internal/replay-canvas': 9.28.1
'@sentry/core': 9.28.1
'@sentry-internal/browser-utils': 9.29.0
'@sentry-internal/feedback': 9.29.0
'@sentry-internal/replay': 9.29.0
'@sentry-internal/replay-canvas': 9.29.0
'@sentry/core': 9.29.0
'@sentry/bundler-plugin-core@3.2.4':
dependencies:
@ -5308,10 +5308,10 @@ snapshots:
- encoding
- supports-color
'@sentry/bundler-plugin-core@3.4.0':
'@sentry/bundler-plugin-core@3.5.0':
dependencies:
'@babel/core': 7.27.4
'@sentry/babel-plugin-component-annotate': 3.4.0
'@sentry/babel-plugin-component-annotate': 3.5.0
'@sentry/cli': 2.42.2
dotenv: 16.5.0
find-up: 5.0.0
@ -5362,9 +5362,9 @@ snapshots:
- encoding
- supports-color
'@sentry/core@9.28.1': {}
'@sentry/core@9.29.0': {}
'@sentry/node@9.28.1':
'@sentry/node@9.29.0':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0)
@ -5396,23 +5396,23 @@ snapshots:
'@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.34.0
'@prisma/instrumentation': 6.8.2(@opentelemetry/api@1.9.0)
'@sentry/core': 9.28.1
'@sentry/opentelemetry': 9.28.1(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)
import-in-the-middle: 1.14.0
'@sentry/core': 9.29.0
'@sentry/opentelemetry': 9.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)
import-in-the-middle: 1.14.2
minimatch: 9.0.5
transitivePeerDependencies:
- supports-color
'@sentry/nuxt@9.28.1(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)(magicast@0.3.5)(nuxt@3.17.5(@parcel/watcher@2.5.1)(@types/node@24.0.1)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)(rollup@4.43.0)(terser@5.42.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(yaml@2.8.0))(pinia@3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3)))(rollup@4.43.0)(vue@3.5.16(typescript@5.8.3))':
'@sentry/nuxt@9.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)(magicast@0.3.5)(nuxt@3.17.5(@parcel/watcher@2.5.1)(@types/node@24.0.1)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)(rollup@4.43.0)(terser@5.42.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(yaml@2.8.0))(pinia@3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3)))(rollup@4.43.0)(vue@3.5.16(typescript@5.8.3))':
dependencies:
'@nuxt/kit': 3.17.5(magicast@0.3.5)
'@sentry/browser': 9.28.1
'@sentry/core': 9.28.1
'@sentry/node': 9.28.1
'@sentry/opentelemetry': 9.28.1(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)
'@sentry/rollup-plugin': 3.4.0(rollup@4.43.0)
'@sentry/browser': 9.29.0
'@sentry/core': 9.29.0
'@sentry/node': 9.29.0
'@sentry/opentelemetry': 9.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)
'@sentry/rollup-plugin': 3.5.0(rollup@4.43.0)
'@sentry/vite-plugin': 3.2.4
'@sentry/vue': 9.28.1(pinia@3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3)))(vue@3.5.16(typescript@5.8.3))
'@sentry/vue': 9.29.0(pinia@3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3)))(vue@3.5.16(typescript@5.8.3))
nuxt: 3.17.5(@parcel/watcher@2.5.1)(@types/node@24.0.1)(db0@0.3.2)(ioredis@5.6.1)(magicast@0.3.5)(rollup@4.43.0)(terser@5.42.0)(typescript@5.8.3)(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(yaml@2.8.0)
transitivePeerDependencies:
- '@opentelemetry/api'
@ -5428,7 +5428,7 @@ snapshots:
- supports-color
- vue
'@sentry/opentelemetry@9.28.1(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)':
'@sentry/opentelemetry@9.29.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0)
@ -5436,11 +5436,11 @@ snapshots:
'@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.34.0
'@sentry/core': 9.28.1
'@sentry/core': 9.29.0
'@sentry/rollup-plugin@3.4.0(rollup@4.43.0)':
'@sentry/rollup-plugin@3.5.0(rollup@4.43.0)':
dependencies:
'@sentry/bundler-plugin-core': 3.4.0
'@sentry/bundler-plugin-core': 3.5.0
rollup: 4.43.0
unplugin: 1.0.1
transitivePeerDependencies:
@ -5455,10 +5455,10 @@ snapshots:
- encoding
- supports-color
'@sentry/vue@9.28.1(pinia@3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3)))(vue@3.5.16(typescript@5.8.3))':
'@sentry/vue@9.29.0(pinia@3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3)))(vue@3.5.16(typescript@5.8.3))':
dependencies:
'@sentry/browser': 9.28.1
'@sentry/core': 9.28.1
'@sentry/browser': 9.29.0
'@sentry/core': 9.29.0
vue: 3.5.16(typescript@5.8.3)
optionalDependencies:
pinia: 3.0.3(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))
@ -5864,7 +5864,7 @@ snapshots:
autoprefixer@10.4.21(postcss@8.5.5):
dependencies:
browserslist: 4.25.0
caniuse-lite: 1.0.30001722
caniuse-lite: 1.0.30001723
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.1.1
@ -5900,7 +5900,7 @@ snapshots:
browserslist@4.25.0:
dependencies:
caniuse-lite: 1.0.30001722
caniuse-lite: 1.0.30001723
electron-to-chromium: 1.5.167
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.0)
@ -5956,11 +5956,11 @@ snapshots:
caniuse-api@3.0.0:
dependencies:
browserslist: 4.25.0
caniuse-lite: 1.0.30001722
caniuse-lite: 1.0.30001723
lodash.memoize: 4.1.2
lodash.uniq: 4.5.0
caniuse-lite@1.0.30001722: {}
caniuse-lite@1.0.30001723: {}
chokidar@3.6.0:
dependencies:
@ -6708,7 +6708,7 @@ snapshots:
image-meta@0.2.1: {}
import-in-the-middle@1.14.0:
import-in-the-middle@1.14.2:
dependencies:
acorn: 8.15.0
acorn-import-attributes: 1.9.5(acorn@8.15.0)
@ -8592,4 +8592,4 @@ snapshots:
compress-commons: 6.0.2
readable-stream: 4.7.0
zod@3.25.63: {}
zod@3.25.64: {}