Merge pull request #482 from arabcoders/dev

Fix: handle errors when fetching remote thumbnails
This commit is contained in:
Abdulmohsen 2025-11-11 23:45:47 +03:00 committed by GitHub
commit 8ff6582316
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 247 additions and 47 deletions

44
FAQ.md
View file

@ -47,6 +47,7 @@ or the `environment:` section in `compose.yaml` file.
| YTP_ALLOW_INTERNAL_URLS | Allow requests to internal URLs | `false` | | YTP_ALLOW_INTERNAL_URLS | Allow requests to internal URLs | `false` |
| YTP_SIMPLE_MODE | Switch default interface to Simple mode. | `false` | | YTP_SIMPLE_MODE | Switch default interface to Simple mode. | `false` |
| YTP_STATIC_UI_PATH | Path to custom static UI files. | `(not_set)` | | YTP_STATIC_UI_PATH | Path to custom static UI files. | `(not_set)` |
| YTP_AUTO_CLEAR_HISTORY_DAYS | Number of days after which completed download history is cleared. | `0` |
> [!NOTE] > [!NOTE]
> To raise the maximum workers for specific extractor, you need to add a ENV variable that follows the pattern `YTP_MAX_WORKERS_FOR_<EXTRACTOR_NAME>`. > To raise the maximum workers for specific extractor, you need to add a ENV variable that follows the pattern `YTP_MAX_WORKERS_FOR_<EXTRACTOR_NAME>`.
@ -54,8 +55,12 @@ or the `environment:` section in `compose.yaml` file.
> The limit should not exceed the `YTP_MAX_WORKERS` value as it will be ignored. > The limit should not exceed the `YTP_MAX_WORKERS` value as it will be ignored.
> [!IMPORTANT] > [!IMPORTANT]
> The env variable `YTP_SIMPLE_MODE` only control what being displayed for first time visitor, the users can still switch between the two modes via the WebUI settings page. > The env variable `YTP_SIMPLE_MODE` only control what being displayed for first time visitor, the users can still switch between the two modes via the WebUI settings page.
## Notes about YTP_AUTO_CLEAR_HISTORY_DAYS
- `0` days means no automatic clearing of the download history. lowest value that will trigger the clearing is `1` day.
- This setting will **NOT** delete the downloaded files, it will only clear the history from the database.
# Browser extensions & bookmarklets # Browser extensions & bookmarklets
@ -507,3 +512,40 @@ Thats it, the `main.yml` will now disable the docker/github container registries
naming, your container name will be named `REGISTRY/ytptube` and the tags will be the same as the ones used in the github registry. naming, your container name will be named `REGISTRY/ytptube` and the tags will be the same as the ones used in the github registry.
Unfortunately, the `native-builder.yml` workflow doesn't support self-hosted repositories at the moment. Unfortunately, the `native-builder.yml` workflow doesn't support self-hosted repositories at the moment.
# Getting No space left on device error
If you encounter this error: `OSError: [Errno 28] No space left on device` This indicates that either
the `/tmp` or `/downloads` directory has run out of available space.
This issue commonly occurs when:
- `/tmp` is mounted as `tmpfs` (memory-based storage)
- Your system has limited RAM
- You're downloading large video files
Since videos are temporarily stored in `/tmp` before being moved to the final download location, memory-based storage
may be insufficient for large downloads.
To fix the issue, modify your `compose.yaml` to use a disk-based directory for temporary files:
```yaml
services:
ytptube:
user: "${UID:-1000}:${UID:-1000}"
image: ghcr.io/arabcoders/ytptube:latest
container_name: ytptube
restart: unless-stopped
ports:
- "8081:8081"
volumes:
- ./config:/config:rw
- ./downloads:/downloads/local:rw
- ./temp:/tmp:rw
```
> [!NOTE]
> Replace the `tmpfs` mount with a local directory volume (`./temp:/tmp:rw`). This allows temporary files to use disk space instead of RAM.
After making the changes, restart your container. This should resolve the "No space left on device"
error during download.

View file

@ -77,6 +77,7 @@ services:
> [!IMPORTANT] > [!IMPORTANT]
> Make sure to change the `user` line to match your user id and group id > Make sure to change the `user` line to match your user id and group id
> If you have low RAM, remove the `tmpfs` and mount a disk-based directory to `/tmp` instead. See [FAQ](FAQ.md#getting-no-space-left-on-device-error) for more information.
```bash ```bash
$ mkdir -p ./{config,downloads} && docker compose -f compose.yaml up -d $ mkdir -p ./{config,downloads} && docker compose -f compose.yaml up -d

View file

@ -140,6 +140,15 @@ class DownloadQueue(metaclass=Singleton):
func=self._check_live, func=self._check_live,
id=f"{__class__.__name__}.{__class__._check_live.__name__}", id=f"{__class__.__name__}.{__class__._check_live.__name__}",
) )
if self.config.auto_clear_history_days > 0:
Scheduler.get_instance().add(
# timer="8 */1 * * *",
timer="* * * * *",
func=self._delete_old_history,
id=f"{__class__.__name__}.{__class__._delete_old_history.__name__}",
)
# app.on_shutdown.append(self.on_shutdown) # app.on_shutdown.append(self.on_shutdown)
async def test(self) -> bool: async def test(self) -> bool:
@ -1262,6 +1271,48 @@ class DownloadQueue(metaclass=Singleton):
LOG.exception(e) LOG.exception(e)
LOG.error(f"Failed to retry item '{item_ref}'. {e!s}") LOG.error(f"Failed to retry item '{item_ref}'. {e!s}")
async def _delete_old_history(self) -> None:
"""
Automatically delete old download history based on user specified days.
0 or negative value disables this feature.
"""
if self.config.auto_clear_history_days < 0 or self.is_paused() or self.done.empty():
return
cutoff_date: datetime = datetime.now(UTC) - timedelta(days=self.config.auto_clear_history_days)
items_to_delete: list[tuple[str, ItemDTO]] = []
for key, download in self.done.items():
info: ItemDTO = download.info
if not info or not info.timestamp:
continue
if "finished" != info.status or not info.filename:
continue
try:
timestamp_seconds: float = info.timestamp / 1e9
item_datetime: datetime = datetime.fromtimestamp(timestamp_seconds, tz=UTC)
if item_datetime < cutoff_date:
items_to_delete.append((key, info))
except (OSError, ValueError, OverflowError) as e:
LOG.error(f"Failed to parse timestamp '{info.timestamp}' for item '{info.title}': {e}")
titles: list[str] = []
for key, info in items_to_delete:
item_name: str = info.title or info.id or info._id
self._notify.emit(
Events.ITEM_DELETED,
data=info,
title="Download Cleared",
message=f"'{item_name}' record removed from history.",
)
titles.append(item_name)
self.done.delete(key)
if titles:
LOG.info(f"Automatically cleared '{', '.join(titles)}' from download history due to age.")
def _handle_task_exception(self, task: asyncio.Task) -> None: def _handle_task_exception(self, task: asyncio.Task) -> None:
if task.cancelled(): if task.cancelled():
return return

View file

@ -24,6 +24,8 @@ class Events:
CONNECTED: str = "connected" CONNECTED: str = "connected"
CONFIGURATION: str = "configuration"
LOG_INFO: str = "log_info" LOG_INFO: str = "log_info"
LOG_WARNING: str = "log_warning" LOG_WARNING: str = "log_warning"
LOG_ERROR: str = "log_error" LOG_ERROR: str = "log_error"
@ -90,6 +92,7 @@ class Events:
""" """
return [ return [
Events.CONFIGURATION,
Events.CONNECTED, Events.CONNECTED,
Events.LOG_INFO, Events.LOG_INFO,
Events.LOG_WARNING, Events.LOG_WARNING,
@ -97,7 +100,6 @@ class Events:
Events.LOG_SUCCESS, Events.LOG_SUCCESS,
Events.ITEM_ADDED, Events.ITEM_ADDED,
Events.ITEM_UPDATED, Events.ITEM_UPDATED,
Events.ITEM_COMPLETED,
Events.ITEM_CANCELLED, Events.ITEM_CANCELLED,
Events.ITEM_DELETED, Events.ITEM_DELETED,
Events.ITEM_MOVED, Events.ITEM_MOVED,

View file

@ -44,7 +44,7 @@ class HttpSocket:
self.sio = sio or socketio.AsyncServer( self.sio = sio or socketio.AsyncServer(
async_handlers=True, async_handlers=True,
async_mode="aiohttp", async_mode="aiohttp",
cors_allowed_origins=[], cors_allowed_origins="*",
transports=["websocket", "polling"], transports=["websocket", "polling"],
logger=self.config.debug, logger=self.config.debug,
engineio_logger=self.config.debug, engineio_logger=self.config.debug,

View file

@ -482,6 +482,9 @@ def check_id(file: Path) -> bool | str:
id: str | None = match.groupdict().get("id") id: str | None = match.groupdict().get("id")
try: try:
if not file.parent.exists():
return False
for f in file.parent.iterdir(): for f in file.parent.iterdir():
if id not in f.stem: if id not in f.stem:
continue continue

View file

@ -192,6 +192,9 @@ class Config(metaclass=Singleton):
playlist_items_concurrency: int = 4 playlist_items_concurrency: int = 4
"""The number of concurrent playlist items to be processed at same time.""" """The number of concurrent playlist items to be processed at same time."""
auto_clear_history_days: int = 0
"""Number of days after which completed download history is automatically cleared. 0 to disable."""
pictures_backends: list[str] = [ pictures_backends: list[str] = [
"https://unsplash.it/1920/1080?random", "https://unsplash.it/1920/1080?random",
"https://picsum.photos/1920/1080", "https://picsum.photos/1920/1080",
@ -230,6 +233,7 @@ class Config(metaclass=Singleton):
"playlist_items_concurrency", "playlist_items_concurrency",
"download_path_depth", "download_path_depth",
"download_info_expires", "download_info_expires",
"auto_clear_history_days",
) )
"The variables that are integers." "The variables that are integers."

View file

@ -71,6 +71,12 @@ async def get_thumbnail(request: Request, config: Config) -> Response:
LOG.debug(f"Fetching thumbnail from '{url}'.") LOG.debug(f"Fetching thumbnail from '{url}'.")
response = await client.request(method="GET", url=url, follow_redirects=True) response = await client.request(method="GET", url=url, follow_redirects=True)
if response.status_code != web.HTTPOk.status_code:
LOG.error(f"Failed to fetch thumbnail from '{url}'. Status code: {response.status_code}.")
return web.json_response(
data={"error": "failed to retrieve the thumbnail."}, status=response.status_code
)
return web.Response( return web.Response(
body=response.content, body=response.content,
headers={ headers={

View file

@ -23,25 +23,31 @@ class _Data:
@route(RouteType.SOCKET, "connect", "socket_connect") @route(RouteType.SOCKET, "connect", "socket_connect")
async def connect(config: Config, queue: DownloadQueue, notify: EventBus, sid: str): async def connect(config: Config, queue: DownloadQueue, notify: EventBus, sid: str):
data = { notify.emit(
**queue.get(), Events.CONFIGURATION,
"config": config.frontend(), data={
"presets": Presets.get_instance().get_all(), "config": config.frontend(),
"dl_fields": DLFields.get_instance().get_all(), "presets": Presets.get_instance().get_all(),
"paused": queue.is_paused(), "dl_fields": DLFields.get_instance().get_all(),
} "paused": queue.is_paused(),
},
data["folders"] = list_folders( title="Client connected",
path=Path(config.download_path), message=f"Client '{sid}' connected.",
base=Path(config.download_path), to=sid,
depth_limit=config.download_path_depth-1,
) )
notify.emit( notify.emit(
Events.CONNECTED, Events.CONNECTED,
data=data, data={
title="Client connected", "folders": list_folders(
message=f"Client '{sid}' connected.", path=Path(config.download_path),
base=Path(config.download_path),
depth_limit=config.download_path_depth - 1,
),
**queue.get(),
},
title="Sending initial download data",
message=f"Sending initial download data to client '{sid}'.",
to=sid, to=sid,
) )

View file

@ -91,7 +91,6 @@ class TestEvents:
Events.LOG_SUCCESS, Events.LOG_SUCCESS,
Events.ITEM_ADDED, Events.ITEM_ADDED,
Events.ITEM_UPDATED, Events.ITEM_UPDATED,
Events.ITEM_COMPLETED,
] ]
for expected in expected_frontend: for expected in expected_frontend:

View file

@ -20,7 +20,7 @@ dependencies = [
"aiocron>=1.8", "aiocron>=1.8",
"python-dotenv>=1.0.1", "python-dotenv>=1.0.1",
"debugpy>=1.8.1", "debugpy>=1.8.1",
"httpx", "httpx[brotli,http2,socks,zstd]",
"async-timeout", "async-timeout",
"pyjson5", "pyjson5",
"curl_cffi>=0.13", "curl_cffi>=0.13",

View file

@ -103,7 +103,7 @@
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item has-dropdown" v-if="config.app.file_logging || config.app.console_enabled"> <div class="navbar-item has-dropdown" v-if="config.app?.file_logging || config.app?.console_enabled">
<a class="navbar-link" @click="(e: MouseEvent) => openMenu(e)"> <a class="navbar-link" @click="(e: MouseEvent) => openMenu(e)">
<span class="icon"><i class="fas fa-tools" /></span> <span class="icon"><i class="fas fa-tools" /></span>
<span>Other</span> <span>Other</span>
@ -111,7 +111,7 @@
<div class="navbar-dropdown"> <div class="navbar-dropdown">
<NuxtLink class="navbar-item" to="/logs" @click.prevent="(e: MouseEvent) => changeRoute(e)" <NuxtLink class="navbar-item" to="/logs" @click.prevent="(e: MouseEvent) => changeRoute(e)"
v-if="config.app.file_logging"> v-if="config.app?.file_logging">
<span class="icon"><i class="fa-solid fa-file-lines" /></span> <span class="icon"><i class="fa-solid fa-file-lines" /></span>
<span>Logs</span> <span>Logs</span>
</NuxtLink> </NuxtLink>

View file

@ -51,11 +51,13 @@ export const useSocketStore = defineStore('socket', () => {
const connect = () => { const connect = () => {
const opts = { const opts = {
transports: ['websocket', 'polling'], transports: ['polling', 'websocket'],
withCredentials: true, withCredentials: true,
reconnection: true, reconnection: true,
reconnectionAttempts: 30, reconnectionAttempts: 50,
reconnectionDelay: 5000 reconnectionDelay: 5000,
tryAllTransports: true,
timeout: 10000 * 5,
} as Partial<ManagerOptions & SocketOptions> } as Partial<ManagerOptions & SocketOptions>
let url = runtimeConfig.public.wss let url = runtimeConfig.public.wss
@ -70,28 +72,32 @@ export const useSocketStore = defineStore('socket', () => {
connectionStatus.value = 'connecting'; connectionStatus.value = 'connecting';
socket.value = io(url, opts) socket.value = io(url, opts)
socket.value.on('connect', () => { on("connect_error", (e: any) => console.error("Socket connection error:", e));
on('connect', () => {
isConnected.value = true isConnected.value = true
connectionStatus.value = 'connected'; connectionStatus.value = 'connected';
}); });
socket.value.on('disconnect', () => { on('disconnect', () => {
isConnected.value = false isConnected.value = false
connectionStatus.value = 'disconnected'; connectionStatus.value = 'disconnected';
}); });
socket.value.on('connected', stream => { on('configuration', stream => {
const json = JSON.parse(stream) const json = JSON.parse(stream)
config.setAll({ config.setAll({
app: json.data.config, app: json.data.config,
tasks: json.data.tasks, tasks: json.data.tasks,
folders: json.data.folders,
presets: json.data.presets, presets: json.data.presets,
dl_fields: json.data.dl_fields, dl_fields: json.data.dl_fields,
paused: Boolean(json.data.paused) paused: Boolean(json.data.paused)
} as Partial<ConfigState>) } as Partial<ConfigState>)
})
on('connected', stream => {
const json = JSON.parse(stream);
config.add('folders', json.data.folders)
stateStore.addAll('queue', json.data.queue || {}) stateStore.addAll('queue', json.data.queue || {})
stateStore.addAll('history', json.data.done || {}) stateStore.addAll('history', json.data.done || {})
}) })
@ -122,21 +128,6 @@ export const useSocketStore = defineStore('socket', () => {
} }
}, true); }, true);
on('item_completed', (stream: string) => {
const json = JSON.parse(stream);
if (true === stateStore.has('queue', json.data._id)) {
stateStore.remove('queue', json.data._id);
}
if (true === stateStore.has('history', json.data._id)) {
stateStore.update('history', json.data._id, json.data);
return;
}
stateStore.add('history', json.data._id, json.data);
});
on('item_cancelled', (stream: string) => { on('item_cancelled', (stream: string) => {
const item = JSON.parse(stream); const item = JSON.parse(stream);
const id = item.data._id const id = item.data._id

99
uv.lock
View file

@ -524,6 +524,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
] ]
[[package]]
name = "h2"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "hpack" },
{ name = "hyperframe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779 },
]
[[package]]
name = "hpack"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 },
]
[[package]] [[package]]
name = "httpcore" name = "httpcore"
version = "1.0.9" version = "1.0.9"
@ -552,6 +574,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
] ]
[package.optional-dependencies]
brotli = [
{ name = "brotli", marker = "platform_python_implementation == 'CPython'" },
{ name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" },
]
http2 = [
{ name = "h2" },
]
socks = [
{ name = "socksio" },
]
zstd = [
{ name = "zstandard" },
]
[[package]] [[package]]
name = "httpx-curl-cffi" name = "httpx-curl-cffi"
version = "0.1.4" version = "0.1.4"
@ -578,6 +615,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 }, { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 },
] ]
[[package]]
name = "hyperframe"
version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.11" version = "3.11"
@ -1490,6 +1536,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
] ]
[[package]]
name = "socksio"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763 },
]
[[package]] [[package]]
name = "sortedcontainers" name = "sortedcontainers"
version = "2.4.0" version = "2.4.0"
@ -1710,7 +1765,7 @@ dependencies = [
{ name = "dateparser" }, { name = "dateparser" },
{ name = "debugpy" }, { name = "debugpy" },
{ name = "defusedxml" }, { name = "defusedxml" },
{ name = "httpx" }, { name = "httpx", extra = ["brotli", "http2", "socks", "zstd"] },
{ name = "httpx-curl-cffi" }, { name = "httpx-curl-cffi" },
{ name = "jmespath" }, { name = "jmespath" },
{ name = "jsonschema" }, { name = "jsonschema" },
@ -1761,7 +1816,7 @@ requires-dist = [
{ name = "dateparser", specifier = ">=1.2.1" }, { name = "dateparser", specifier = ">=1.2.1" },
{ name = "debugpy", specifier = ">=1.8.1" }, { name = "debugpy", specifier = ">=1.8.1" },
{ name = "defusedxml", specifier = ">=0.7.1" }, { name = "defusedxml", specifier = ">=0.7.1" },
{ name = "httpx" }, { name = "httpx", extras = ["brotli", "http2", "socks", "zstd"] },
{ name = "httpx-curl-cffi", specifier = ">=0.1.4" }, { name = "httpx-curl-cffi", specifier = ">=0.1.4" },
{ name = "jmespath", specifier = ">=1.0.1" }, { name = "jmespath", specifier = ">=1.0.1" },
{ name = "jsonschema", specifier = ">=4.23.0" }, { name = "jsonschema", specifier = ">=4.23.0" },
@ -1801,3 +1856,43 @@ sdist = { url = "https://files.pythonhosted.org/packages/11/f2/690a35762cf8366ce
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/de/62/c2da1c495291a52e561257d017585e08906d288035d025ccf636f6b9a266/zipstream_ng-1.9.0-py3-none-any.whl", hash = "sha256:31dc2cf617abdbf28d44f2e08c0d14c8eee2ea0ec26507a7e4d5d5f97c564b7a", size = 24852 }, { url = "https://files.pythonhosted.org/packages/de/62/c2da1c495291a52e561257d017585e08906d288035d025ccf636f6b9a266/zipstream_ng-1.9.0-py3-none-any.whl", hash = "sha256:31dc2cf617abdbf28d44f2e08c0d14c8eee2ea0ec26507a7e4d5d5f97c564b7a", size = 24852 },
] ]
[[package]]
name = "zstandard"
version = "0.25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735 },
{ url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440 },
{ url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070 },
{ url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001 },
{ url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120 },
{ url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230 },
{ url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173 },
{ url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736 },
{ url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368 },
{ url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022 },
{ url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889 },
{ url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952 },
{ url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054 },
{ url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113 },
{ url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936 },
{ url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232 },
{ url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671 },
{ url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887 },
{ url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658 },
{ url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849 },
{ url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095 },
{ url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751 },
{ url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818 },
{ url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402 },
{ url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108 },
{ url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248 },
{ url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330 },
{ url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123 },
{ url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591 },
{ url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513 },
{ url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118 },
{ url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940 },
]