Compare commits

..

8 Commits

Author SHA1 Message Date
Alex Shnitman 51fd203b71 upgrade to Angular 22 2026-06-28 21:12:20 +03:00
Alex Shnitman d136344c26 upgrade dependencies 2026-06-28 21:08:08 +03:00
dependabot[bot] 33f1412fac Bump actions/checkout from 6 to 7 in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-28 21:06:58 +03:00
Alex Shnitman ce897ee009 fix open download of cookie files 2026-06-20 09:52:34 +03:00
Alex Shnitman dd1b4c2436 upgrade dependencies 2026-06-20 09:52:34 +03:00
Alex 8752b500d6 Merge pull request #1004 from akeeton/docker-audio-download-dir
Create AUDIO_DOWNLOAD_DIR in Docker image
2026-06-18 06:45:50 +03:00
Andrew Keeton 04b9366764 Incorporate PR feedback
Move the default assignment of AUDIO_DOWNLOAD_DIR from the Dockerfile to docker-entrypoint.sh, and change the default value from "/downloads" to $DOWNLOAD_DIR.
2026-06-17 17:17:44 -04:00
Andrew Keeton d157444877 Create AUDIO_DOWNLOAD_DIR in Docker image 2026-06-11 17:22:55 -04:00
13 changed files with 1877 additions and 1735 deletions
+3 -3
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
@@ -59,7 +59,7 @@ jobs:
run: echo "date=$(date +'%Y.%m.%d')" >> "$GITHUB_OUTPUT" run: echo "date=$(date +'%Y.%m.%d')" >> "$GITHUB_OUTPUT"
- -
name: Checkout name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- -
name: Set up QEMU name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v4
@@ -118,7 +118,7 @@ jobs:
id: date id: date
run: echo "date=$(date +'%Y.%m.%d')" >> $GITHUB_OUTPUT run: echo "date=$(date +'%Y.%m.%d')" >> $GITHUB_OUTPUT
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Get commits since last release - name: Get commits since last release
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
token: ${{ secrets.AUTOUPDATE_PAT }} token: ${{ secrets.AUTOUPDATE_PAT }}
- -
+1 -1
View File
@@ -9,7 +9,7 @@ If an addition would exceed the limit, trim existing prose elsewhere — prefer
## Tech stack ## Tech stack
- **Backend:** Python 3.13+, aiohttp, python-socketio 5.x, yt-dlp - **Backend:** Python 3.13+, aiohttp, python-socketio 5.x, yt-dlp
- **Frontend:** Angular 21, TypeScript, Bootstrap 5, SASS, ngx-socket-io - **Frontend:** Angular 22, TypeScript, Bootstrap 5, SASS, ngx-socket-io
- **Package managers:** uv (Python), pnpm (frontend) - **Package managers:** uv (Python), pnpm (frontend)
- **Container:** Multi-stage Docker (Node builder + Python runtime), multi-arch (amd64/arm64) - **Container:** Multi-stage Docker (Node builder + Python runtime), multi-arch (amd64/arm64)
+25 -2
View File
@@ -16,7 +16,7 @@ import logging
import json import json
import pathlib import pathlib
import re import re
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from urllib.parse import parse_qs, unquote, urlencode, urlparse, urlunparse
from watchfiles import DefaultFilter, Change, awatch from watchfiles import DefaultFilter, Change, awatch
from ytdl import DownloadQueueNotifier, DownloadQueue, Download from ytdl import DownloadQueueNotifier, DownloadQueue, Download
@@ -286,7 +286,30 @@ class ObjectSerializer(json.JSONEncoder):
return json.JSONEncoder.default(self, obj) return json.JSONEncoder.default(self, obj)
serializer = ObjectSerializer() serializer = ObjectSerializer()
app = web.Application()
_STATE_DIR_REAL = os.path.realpath(config.STATE_DIR)
def _is_within_state_dir(real_target: str) -> bool:
return real_target == _STATE_DIR_REAL or real_target.startswith(_STATE_DIR_REAL + os.sep)
@web.middleware
async def state_dir_guard(request, handler):
for prefix, base in (
(config.URL_PREFIX + 'download/', config.DOWNLOAD_DIR),
(config.URL_PREFIX + 'audio_download/', config.AUDIO_DOWNLOAD_DIR),
):
if request.path.startswith(prefix):
rel = unquote(request.path[len(prefix):])
target = os.path.realpath(os.path.join(base, rel))
if _is_within_state_dir(target):
raise web.HTTPNotFound()
break
return await handler(request)
app = web.Application(middlewares=[state_dir_guard])
_cors_origins = [o.strip() for o in config.CORS_ALLOWED_ORIGINS.split(',') if o.strip()] if config.CORS_ALLOWED_ORIGINS else [] _cors_origins = [o.strip() for o in config.CORS_ALLOWED_ORIGINS.split(',') if o.strip()] if config.CORS_ALLOWED_ORIGINS else []
sio = socketio.AsyncServer(cors_allowed_origins=_cors_origins if _cors_origins else []) sio = socketio.AsyncServer(cors_allowed_origins=_cors_origins if _cors_origins else [])
sio.attach(app, socketio_path=config.URL_PREFIX + 'socket.io') sio.attach(app, socketio_path=config.URL_PREFIX + 'socket.io')
+41
View File
@@ -3,10 +3,13 @@
from __future__ import annotations from __future__ import annotations
import json import json
import os
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
from aiohttp import web from aiohttp import web
from aiohttp.test_utils import TestClient, TestServer
import main import main
@@ -306,3 +309,41 @@ async def test_subscribe_rejects_clip_options(mock_dqueue, monkeypatch):
with pytest.raises(web.HTTPBadRequest): with pytest.raises(web.HTTPBadRequest):
await main.subscribe(req) await main.subscribe(req)
main.submgr.add_subscription.assert_not_awaited() main.submgr.add_subscription.assert_not_awaited()
def test_is_within_state_dir_blocks_state_subtree():
state_dir = main._STATE_DIR_REAL
assert main._is_within_state_dir(state_dir)
assert main._is_within_state_dir(os.path.join(state_dir, "cookies.txt"))
assert main._is_within_state_dir(os.path.join(state_dir, "queue", "item.json"))
def test_is_within_state_dir_allows_sibling_downloads():
download_dir = os.path.realpath(main.config.DOWNLOAD_DIR)
assert not main._is_within_state_dir(os.path.join(download_dir, "video.mp4"))
assert not main._is_within_state_dir("/tmp/unrelated/video.mp4")
@pytest.mark.asyncio
async def test_download_blocks_state_dir_files(monkeypatch):
download_dir = Path(main.config.DOWNLOAD_DIR)
state_dir = download_dir / ".metube"
state_dir.mkdir(parents=True, exist_ok=True)
(state_dir / "cookies.txt").write_text("# Netscape HTTP Cookie File\n", encoding="utf-8")
(download_dir / "video.mp4").write_bytes(b"video")
monkeypatch.setattr(main.config, "STATE_DIR", str(state_dir))
monkeypatch.setattr(main, "_STATE_DIR_REAL", os.path.realpath(str(state_dir)))
try:
async with TestClient(TestServer(main.app)) as client:
blocked = await client.get("/download/.metube/cookies.txt")
assert blocked.status == 404
allowed = await client.get("/download/video.mp4")
assert allowed.status == 200
assert await allowed.read() == b"video"
finally:
(state_dir / "cookies.txt").unlink(missing_ok=True)
(download_dir / "video.mp4").unlink(missing_ok=True)
state_dir.rmdir()
+4 -3
View File
@@ -2,11 +2,12 @@
PUID="${UID:-$PUID}" PUID="${UID:-$PUID}"
PGID="${GID:-$PGID}" PGID="${GID:-$PGID}"
AUDIO_DOWNLOAD_DIR="${AUDIO_DOWNLOAD_DIR:-$DOWNLOAD_DIR}"
echo "Setting umask to ${UMASK}" echo "Setting umask to ${UMASK}"
umask ${UMASK} umask ${UMASK}
echo "Creating download directory (${DOWNLOAD_DIR}), state directory (${STATE_DIR}), and temp dir (${TEMP_DIR})" echo "Creating download directory (${DOWNLOAD_DIR}), audio download directory (${AUDIO_DOWNLOAD_DIR}), state directory (${STATE_DIR}), and temp dir (${TEMP_DIR})"
mkdir -p "${DOWNLOAD_DIR}" "${STATE_DIR}" "${TEMP_DIR}" mkdir -p "${DOWNLOAD_DIR}" "${AUDIO_DOWNLOAD_DIR}" "${STATE_DIR}" "${TEMP_DIR}"
do_upgrade() { do_upgrade() {
echo "Upgrading yt-dlp to nightly channel..." echo "Upgrading yt-dlp to nightly channel..."
@@ -56,7 +57,7 @@ if [ `id -u` -eq 0 ] && [ `id -g` -eq 0 ]; then
fi fi
if [ "${CHOWN_DIRS:-true}" != "false" ]; then if [ "${CHOWN_DIRS:-true}" != "false" ]; then
echo "Changing ownership of download and state directories to ${PUID}:${PGID}" echo "Changing ownership of download and state directories to ${PUID}:${PGID}"
chown -R "${PUID}":"${PGID}" /app "${DOWNLOAD_DIR}" "${STATE_DIR}" "${TEMP_DIR}" chown -R "${PUID}":"${PGID}" /app "${DOWNLOAD_DIR}" "${AUDIO_DOWNLOAD_DIR}" "${STATE_DIR}" "${TEMP_DIR}"
fi fi
if nightly_enabled; then if nightly_enabled; then
echo "YTDL_NIGHTLY_UPDATE_TIME is set to ${YTDL_NIGHTLY_UPDATE_TIME}; upgrading yt-dlp on startup" echo "YTDL_NIGHTLY_UPDATE_TIME is set to ${YTDL_NIGHTLY_UPDATE_TIME}; upgrading yt-dlp on startup"
+23 -23
View File
@@ -23,41 +23,41 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^21.2.17", "@angular/animations": "^22.0.4",
"@angular/common": "^21.2.17", "@angular/common": "^22.0.4",
"@angular/compiler": "^21.2.17", "@angular/compiler": "^22.0.4",
"@angular/core": "^21.2.17", "@angular/core": "^22.0.4",
"@angular/forms": "^21.2.17", "@angular/forms": "^22.0.4",
"@angular/platform-browser": "^21.2.17", "@angular/platform-browser": "^22.0.4",
"@angular/platform-browser-dynamic": "^21.2.17", "@angular/platform-browser-dynamic": "^22.0.4",
"@angular/service-worker": "^21.2.17", "@angular/service-worker": "^22.0.4",
"@fortawesome/angular-fontawesome": "~4.0.0", "@fortawesome/angular-fontawesome": "~4.0.0",
"@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/fontawesome-svg-core": "^7.3.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0", "@fortawesome/free-brands-svg-icons": "^7.3.0",
"@fortawesome/free-regular-svg-icons": "^7.2.0", "@fortawesome/free-regular-svg-icons": "^7.3.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.3.0",
"@ng-bootstrap/ng-bootstrap": "^20.0.0", "@ng-bootstrap/ng-bootstrap": "^21.0.0",
"@ng-select/ng-select": "^21.8.2", "@ng-select/ng-select": "^23.2.0",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"ngx-cookie-service": "^21.3.1", "ngx-cookie-service": "^22.0.0",
"ngx-socket-io": "~4.10.0", "ngx-socket-io": "~4.10.0",
"rxjs": "~7.8.2", "rxjs": "~7.8.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"zone.js": "0.15.0" "zone.js": "0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-eslint/builder": "21.1.0", "@angular-eslint/builder": "22.0.0",
"@angular/build": "^21.2.15", "@angular/build": "^22.0.4",
"@angular/cli": "^21.2.15", "@angular/cli": "^22.0.4",
"@angular/compiler-cli": "^21.2.17", "@angular/compiler-cli": "^22.0.4",
"@angular/localize": "^21.2.17", "@angular/localize": "^22.0.4",
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.4",
"angular-eslint": "21.1.0", "angular-eslint": "22.0.0",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"jsdom": "^27.4.0", "jsdom": "^27.4.0",
"typescript": "~5.9.3", "typescript": "~6.0.3",
"typescript-eslint": "8.47.0", "typescript-eslint": "8.62.0",
"vitest": "^4.1.9" "vitest": "^4.1.9"
} }
} }
+1736 -1673
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, isDevMode, provideZoneChangeDetection } from '@angular/core'; import { ApplicationConfig, provideBrowserGlobalErrorListeners, isDevMode, provideZoneChangeDetection } from '@angular/core';
import { provideServiceWorker } from '@angular/service-worker'; import { provideServiceWorker } from '@angular/service-worker';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { provideHttpClient, withInterceptorsFromDi, withXhr } from '@angular/common/http';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@@ -12,6 +12,6 @@ export const appConfig: ApplicationConfig = {
// or after 30 seconds (whichever comes first). // or after 30 seconds (whichever comes first).
registrationStrategy: 'registerWhenStable:30000' registrationStrategy: 'registerWhenStable:30000'
}), }),
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withXhr(), withInterceptorsFromDi()),
] ]
}; };
@@ -1,4 +1,4 @@
import { Component, ElementRef, viewChild, output, input } from "@angular/core"; import { Component, ElementRef, viewChild, output, input, ChangeDetectionStrategy } from "@angular/core";
import { Checkable } from "../interfaces"; import { Checkable } from "../interfaces";
import { FormsModule } from "@angular/forms"; import { FormsModule } from "@angular/forms";
@@ -10,7 +10,10 @@ import { FormsModule } from "@angular/forms";
<label class="form-check-label visually-hidden" for="{{id()}}-select-all">Select all</label> <label class="form-check-label visually-hidden" for="{{id()}}-select-all">Select all</label>
</div> </div>
`, `,
imports: [ // Shared Checkable objects are mutated in place; Eager preserves pre-v22 behavior.
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
changeDetection: ChangeDetectionStrategy.Eager,
imports: [
FormsModule FormsModule
] ]
}) })
@@ -1,4 +1,4 @@
import { Component, input } from '@angular/core'; import { Component, input, ChangeDetectionStrategy } from '@angular/core';
import { SelectAllCheckboxComponent } from './master-checkbox.component'; import { SelectAllCheckboxComponent } from './master-checkbox.component';
import { Checkable } from '../interfaces'; import { Checkable } from '../interfaces';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@@ -11,7 +11,10 @@ import { FormsModule } from '@angular/forms';
<label class="form-check-label visually-hidden" for="{{master().id()}}-{{id()}}-select">Select item</label> <label class="form-check-label visually-hidden" for="{{master().id()}}-{{id()}}-select">Select item</label>
</div> </div>
`, `,
imports: [ // Shared Checkable objects are mutated in place; Eager preserves pre-v22 behavior.
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
changeDetection: ChangeDetectionStrategy.Eager,
imports: [
FormsModule FormsModule
] ]
}) })
+9 -1
View File
@@ -12,5 +12,13 @@
], ],
"exclude": [ "exclude": [
"src/**/*.spec.ts" "src/**/*.spec.ts"
] ],
"angularCompilerOptions": {
"extendedDiagnostics": {
"checks": {
"nullishCoalescingNotNullable": "suppress",
"optionalChainNotNullable": "suppress"
}
}
}
} }
Generated
+22 -22
View File
@@ -106,14 +106,14 @@ wheels = [
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.14.0" version = "4.14.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "idna" }, { name = "idna" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" },
] ]
[[package]] [[package]]
@@ -194,11 +194,11 @@ wheels = [
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.5.20" version = "2026.6.17"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
] ]
[[package]] [[package]]
@@ -347,15 +347,15 @@ wheels = [
[[package]] [[package]]
name = "deno" name = "deno"
version = "2.8.3" version = "2.9.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/db/ec/98f972cca4ca734534947bdf28b86a99e9ed8a5a31a061be2dec9b4105c2/deno-2.8.3.tar.gz", hash = "sha256:5413f4a1814ac3b8e441ca7fe9eb677a4152a42eef05efd56a61d750088a7fc8", size = 8163, upload-time = "2026-06-11T16:13:53.129Z" } sdist = { url = "https://files.pythonhosted.org/packages/9f/96/098c84eff4b8ec7da8fb1b43d3371c22f021b7642d5b2bdad8d1780cd605/deno-2.9.0.tar.gz", hash = "sha256:7548d37528eaca993a2d4483ce092eca971fa206faadb0f4c8af8c3f039677d2", size = 8168, upload-time = "2026-06-25T15:03:34.872Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/52/42/71f88d6c04c7f4335f94c1c9eb5aee7e928e1a53056bb9b20c0c4d889f48/deno-2.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:57825615e8d4182160401b56907b2e36712aed55a5dfdef2c0e3089379bb6988", size = 42314540, upload-time = "2026-06-11T16:13:39.134Z" }, { url = "https://files.pythonhosted.org/packages/0a/64/ebd8bb0fc2ef57b0f8cff53cd9b43261c9c16dfaf0a07db37b9fcb2ec186/deno-2.9.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f53bbb11a9a7eeae3865470baa1d5cb02da105ac073019953e9d59e0588bc10e", size = 42301281, upload-time = "2026-06-25T15:03:18.675Z" },
{ url = "https://files.pythonhosted.org/packages/df/95/032101e28532fa9ed28ca7dc737934d4631544fa3b11f4efe3573970a05a/deno-2.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:05dbbbd8edf1907abcbcf55395e63f00d74c2672b6039dd1ecd5656baebaa56c", size = 38101511, upload-time = "2026-06-11T16:13:42.192Z" }, { url = "https://files.pythonhosted.org/packages/63/3c/1cc1c8c731e5dada562640dc42c0603c017bc1ba731a69255cecd9b25be0/deno-2.9.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e58aa71c144dcd91ef9fd5b044dd465d1be571b6763c3a777924d4d54f7033e7", size = 37950190, upload-time = "2026-06-25T15:03:21.958Z" },
{ url = "https://files.pythonhosted.org/packages/23/24/68c1d2d79933738acb8e5ef3a4584f57c6d5deb56c84073a5f4079c24647/deno-2.8.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:9fdb7574a6437fdd59f668e745dc3b3e32725fa360b64204a6b000689fa534e9", size = 42053660, upload-time = "2026-06-11T16:13:45.006Z" }, { url = "https://files.pythonhosted.org/packages/30/86/e5487fe222840aca26c292cfa2532fb0ba4b080b6106f89cce530a7470a5/deno-2.9.0-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:3401337f5c3b75e8f7c1b07589612cddac66ddc17a0a18e7eff6e55f23a1b53b", size = 42017185, upload-time = "2026-06-25T15:03:25.328Z" },
{ url = "https://files.pythonhosted.org/packages/d2/56/0d23c6daa1b139c31591df801feda3ac7d0d6225f1686cbf5caaa5db5c27/deno-2.8.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:9ca5727e5650f8459f39875f0af4b2242ead77da4ce8a8d52e86647036f3d63a", size = 43800396, upload-time = "2026-06-11T16:13:47.821Z" }, { url = "https://files.pythonhosted.org/packages/5a/d0/7f73a8d5f77947c327272147d89d005426888d4d5b3d6575181c1a7271e9/deno-2.9.0-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:63988d645e591bc171a574aa6b3ada2ddfc9bcf59f0178412a9fde1991bfc111", size = 43869469, upload-time = "2026-06-25T15:03:28.311Z" },
{ url = "https://files.pythonhosted.org/packages/6f/aa/e35b736205b89c98f31ed4abeebaa7846774e51006b14601e4edb7728e67/deno-2.8.3-py3-none-win_amd64.whl", hash = "sha256:37da75ee91448e4e6f5626f0d5cb18e295dbb6cff5cf780fab1f92ebbbd44071", size = 41552180, upload-time = "2026-06-11T16:13:50.711Z" }, { url = "https://files.pythonhosted.org/packages/01/34/ca326c53558f99792eb4e2c19ca88763e6890ce1ffb44b9536ecd7ae983f/deno-2.9.0-py3-none-win_amd64.whl", hash = "sha256:29f6f616b47e5babd9604bcd1a76ec3576895275f8696dedfb33ab6545f6ed85", size = 41589185, upload-time = "2026-06-25T15:03:32.002Z" },
] ]
[[package]] [[package]]
@@ -628,11 +628,11 @@ wheels = [
[[package]] [[package]]
name = "mutagen" name = "mutagen"
version = "1.47.0" version = "1.48.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", size = 1274186, upload-time = "2023-09-03T16:33:33.411Z" } sdist = { url = "https://files.pythonhosted.org/packages/df/70/1675da133ea92227da41bf5b24e1c66be597ff736a1533ade41da986852f/mutagen-1.48.1.tar.gz", hash = "sha256:8f95637ab9f6f305cec6bd1294e197debe207998e3e068596563c74f86b0a173", size = 1276978, upload-time = "2026-06-25T09:47:32.443Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391, upload-time = "2023-09-03T16:33:29.955Z" }, { url = "https://files.pythonhosted.org/packages/47/d8/a29e4e3991765e7ce4ed1f7e4074fe1ba9da03e0048639734de60f9cadb9/mutagen-1.48.1-py3-none-any.whl", hash = "sha256:4f077fe87d3fc7fba259aa63d8c026b18382ca6a42ef37c61e16f1b1b5b82fe7", size = 195706, upload-time = "2026-06-25T09:47:30.296Z" },
] ]
[[package]] [[package]]
@@ -807,7 +807,7 @@ wheels = [
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "9.1.0" version = "9.1.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
@@ -816,9 +816,9 @@ dependencies = [
{ name = "pluggy" }, { name = "pluggy" },
{ name = "pygments" }, { name = "pygments" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" } sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" }, { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
] ]
[[package]] [[package]]
@@ -849,14 +849,14 @@ wheels = [
[[package]] [[package]]
name = "python-engineio" name = "python-engineio"
version = "4.13.2" version = "4.13.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "simple-websocket" }, { name = "simple-websocket" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/fa/6d/4384c2723adad93a3d6de4297e6d9c8b93be7f778a407f34f6ee0b2bea3e/python_engineio-4.13.2.tar.gz", hash = "sha256:a7732e99cfb7db6ed1aee31f18d7f73bbae086a92f31dee019bc646155d9684e", size = 79639, upload-time = "2026-05-21T21:45:07.578Z" } sdist = { url = "https://files.pythonhosted.org/packages/fb/a0/f75491f942184d9960b15e763270f765fe9f239745ca5f9e16289011aed4/python_engineio-4.13.3.tar.gz", hash = "sha256:572b7783e341fed21edbc7cea297ccd378dad79265fdde96aa4664420a7c06c9", size = 79734, upload-time = "2026-06-20T22:53:52.197Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/28/180bfc5c95e83d40cb2abce512684ccad44e4819ec899fc36cb404a19061/python_engineio-4.13.2-py3-none-any.whl", hash = "sha256:8c101cd170e400dc4e970cd523325cde22df8fc25140953f379327055d701a6b", size = 59993, upload-time = "2026-05-21T21:45:06.162Z" }, { url = "https://files.pythonhosted.org/packages/5b/96/82f6328e410515fab21d5602ba35b9377a47b5a141a0c1f9efa00ce21eb4/python_engineio-4.13.3-py3-none-any.whl", hash = "sha256:1f60ecaf1358190f0e26c48c578a60428dc02a8f1295bc3dbf53d1b31116821f", size = 59993, upload-time = "2026-06-20T22:53:50.775Z" },
] ]
[[package]] [[package]]