diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c4aaa2..d692dfd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ npm run check python tools/build.py ``` -`tools/build.py` собирает небольшой `dist/YouTubeDL-1.4.6.zip` без EXE. +`tools/build.py` собирает небольшой `dist/YouTubeDL-1.4.7.zip` без EXE. Для сборки расширения не требуется предварительно собирать помощник. ## Браузерные проверки diff --git a/INSTALL.md b/INSTALL.md index 9480251..bc14700 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,7 +6,7 @@ Firefox, macOS, Linux и Windows ARM не входят в эту сборку. Скачивания доступны в [релизах проекта](https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/latest). Помощник распространяется **отдельно**: внутри архива расширения EXE нет. -1. Скачайте **YouTubeDL-1.4.6.zip** и распакуйте его в постоянную папку. Не удаляйте и не перемещайте её после установки. +1. Скачайте **YouTubeDL-1.4.7.zip** и распакуйте его в постоянную папку. Не удаляйте и не перемещайте её после установки. 2. Откройте `edge://extensions` или `chrome://extensions`. Включите **Режим разработчика**, нажмите **Загрузить распакованное расширение** и выберите папку **extension** внутри распакованного архива. 3. На странице релиза отдельно скачайте **YouTubeDL-Helper-Setup.exe**. Это автономный комплект: Python, yt-dlp, EJS, Node.js, FFmpeg и FFprobe уже внутри. 4. Запустите EXE вручную и дождитесь окончания установки. Администратор и интернет для установки комплекта не требуются. Программы устанавливаются для текущего пользователя в `%LOCALAPPDATA%\YouTubeDL\Helper`. @@ -59,7 +59,7 @@ Get-FileHash -Algorithm SHA256 -LiteralPath '.\YouTubeDL-Helper-Setup.exe' При разных дисках результат сначала копируется во временную папку на диске сохранения, затем публикуется под итоговым именем. Прогресс копирования виден в уведомлении; частичный файл не появляется под готовым именем. При отмене помощник завершает группу процессов данной задачи и удаляет её временные папки. -Для отдельной рабочей папки, подробного прогресса слияния и исправленной отмены установите помощник 1.3.2 и расширение 1.4.6. Обновление компонентов не заменяет программный код помощника; для этой версии запустите новый установщик помощника. +Для отдельной рабочей папки, подробного прогресса слияния и исправленной отмены установите помощник 1.3.3 и расширение 1.4.7. Обновление компонентов не заменяет программный код помощника; для этой версии запустите новый установщик помощника. ## История и ошибки @@ -69,7 +69,7 @@ Get-FileHash -Algorithm SHA256 -LiteralPath '.\YouTubeDL-Helper-Setup.exe' ## Совместимость версий -В настройках и мастере первого запуска отображаются версии расширения и помощника. Расширение 1.4.6 поддерживает помощник 1.3.2. При старом помощнике появится кнопка «Обновить помощник», при более новом помощнике или устаревшем расширении — «Обновить расширение». Пока совместимость не подтверждена, новая загрузка не запускается. +В настройках и мастере первого запуска отображаются версии расширения и помощника. Расширение 1.4.7 поддерживает помощник 1.3.3. При старом помощнике появится кнопка «Обновить помощник», при более новом помощнике или устаревшем расширении — «Обновить расширение». Пока совместимость не подтверждена, новая загрузка не запускается. После установки помощника нажмите «Проверить»: статус изменится без перезагрузки страницы. После замены файлов расширения нажмите «Обновить» на странице расширений браузера и перезагрузите вкладку YouTube. Обновление компонентов обновляет инструменты внутри комплекта и не заменяет само расширение или код помощника. diff --git a/LICENSE b/LICENSE index 1998fc6..4cce92b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,12 @@ -ОГРАНИЧЕННАЯ ПОЛЬЗОВАТЕЛЬСКАЯ ЛИЦЕНЗИЯ YOUTUBEDL -Версия 1.0, 8 октября 2026 года +ЛИЦЕНЗИЯ SOURCE AVAILABLE — YouTubeDL +Версия 1.1, 11 октября 2026 года Copyright (c) 2026 DepressedCat. Все права сохранены. Правообладатель: DepressedCat (https://git.lonarpgproxy.space/DepressedCat). +Исходный код доступен для ознакомления (Source Available). Права на использование, +изменение и распространение определяются условиями этой лицензии. + 1. ОБЛАСТЬ ДЕЙСТВИЯ Эта лицензия относится к собственным файлам проекта YouTubeDL, предоставляемым @@ -22,7 +25,8 @@ Copyright (c) 2026 DepressedCat. Все права сохранены. выбирать дорожки и параметры, объединять, конвертировать и сохранять файлы при наличии права сохранять соответствующий контент. -Разрешены необходимые для этого установочные и резервные копии, сборка +Разрешены просмотр и изучение опубликованных исходников, их локальная копия +для ознакомления и сборки, необходимые установочные и резервные копии, сборка неизменённой официальной поставки для собственного использования, настройка через штатные средства, установка официальных обновлений и удаление Программы. Это не предоставляет права изменять собственные файлы кода и дизайна. @@ -46,11 +50,21 @@ Copyright (c) 2026 DepressedCat. Все права сохранены. 4. ИСКЛЮЧЕНИЯ -4.1. Python, Node.js, yt-dlp, EJS, FFmpeg, FFprobe и другие сторонние компоненты, -их исходники и лицензионные тексты регулируются лицензиями их правообладателей. -Эта лицензия не ограничивает предоставленные ими права, в том числе права -на изменение, форки и распространение по GPL, MIT и другим лицензиям. -Состав и сведения о лицензиях приведены в THIRD_PARTY.md и материалах релиза. +4.1. Python, Node.js, yt-dlp, EJS, FFmpeg, FFprobe и другие сторонние компоненты +регулируются условиями собственных лицензий, установленными их правообладателями. + +Настоящая лицензия не ограничивает права, предоставленные пользователям лицензиями +указанных сторонних компонентов, включая права на изменение, создание производных +работ и распространение, если такие действия разрешены соответствующими лицензиями. + +Указанные права относятся к соответствующим сторонним компонентам и не предоставляют +пользователю дополнительных прав на авторские части Программы. Их изменение, создание +производных версий, форков и распространение запрещены без разрешения правообладателя, +за исключением случаев, предусмотренных настоящей лицензией, обязательными нормами +законодательства или применимыми лицензиями сторонних компонентов. + +Перечень сторонних компонентов и сведения об их лицензиях приведены в файле +THIRD_PARTY.md и материалах релиза. 4.2. Права, которые применимое законодательство не позволяет ограничивать договором, сохраняются. Эта лицензия не предоставляет прав на чужой контент diff --git a/PRIVACY.md b/PRIVACY.md index 3989e01..f6f72e6 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,6 +1,6 @@ # Обработка данных -Актуально для расширения **1.4.5** и помощника **1.3.2**. Исходники доступны для просмотра, +Актуально для расширения **1.4.7** и помощника **1.3.3**. Исходники доступны для просмотра, условия использования собственного кода ограничены [лицензией](LICENSE). У проекта нет собственного сервера обработки видео, аккаунтов, телеметрии, рекламы или аналитики. Это не означает отсутствие обработки данных: необходимые сведения используются на вашем компьютере, diff --git a/README.md b/README.md index 6447492..e180763 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ **[Инструкция](INSTALL.md)** · **[Обработка данных](PRIVACY.md)** · **[Сообщить об ошибке](https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/issues)** -Windows 10/11 x64 · Microsoft Edge / Google Chrome 120+ · расширение **1.4.6** · помощник **1.3.2**. +Windows 10/11 x64 · Microsoft Edge / Google Chrome 120+ · расширение **1.4.7** · помощник **1.3.3**. Расширение устанавливается распакованным, через режим разработчика. Прямая поставка через Gitea выбрана из-за ограничений на скачивание контента в [правилах Chrome Web Store](https://developer.chrome.com/docs/webstore/program-policies/malicious-and-prohibited) и [условиях YouTube](https://www.youtube.com/static?template=terms). ## Что умеет @@ -38,7 +38,7 @@ DRM-контент и гарантированный обход ограниче ## Установка: два отдельных скачивания -1. Откройте [последний релиз](https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/latest), скачайте **YouTubeDL-1.4.6.zip** и распакуйте в постоянную папку. +1. Откройте [последний релиз](https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/latest), скачайте **YouTubeDL-1.4.7.zip** и распакуйте в постоянную папку. 2. В `edge://extensions` или `chrome://extensions` включите **Режим разработчика**, нажмите **Загрузить распакованное расширение** и выберите папку **extension**. 3. **Отдельно скачайте YouTubeDL-Helper-Setup.exe из того же релиза** и запустите его вручную. EXE не встроен в расширение: кнопка установки открывает страницу релизов. 4. Обновите YouTube, откройте **⚙** и пройдите мастер. Выберите папку и параметры; зелёный статус подтверждает связь с помощником. @@ -102,8 +102,8 @@ Python остаётся в ветке 3.13, Node.js — в 24 LTS. ## Лицензии и разработка -Новые поставки собственного кода — по **[ограниченной пользовательской лицензии](LICENSE)**: -разрешены установка и использование по назначению. Изменение, самостоятельные форки, +Новые поставки собственного кода — по **[лицензии Source Available](LICENSE)**: +разрешены просмотр исходников, сборка неизменённой версии, установка и использование по назначению. Изменение, самостоятельные форки, производные версии и распространение без письменного разрешения правообладателя запрещены, с учётом исключений лицензии. Это распространяется и на само расширение. diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md index 3056248..1448aea 100644 --- a/THIRD_PARTY.md +++ b/THIRD_PARTY.md @@ -5,8 +5,9 @@ Встроенные компоненты помощника имеют **свои** лицензии; ограничения YouTubeDL не заменяют и не ограничивают их, в том числе права по GPL и MIT. Помощник вызывает FFmpeg отдельным процессом. +Mutagen не входит в комплект 1.3.3 и не устанавливается при обновлении его компонентов. -Автономный комплект помощника 1.3.2: +Автономный комплект помощника 1.3.3: | Компонент | Версия | Проект / лицензия | | --- | --- | --- | @@ -17,7 +18,7 @@ | FFmpeg / FFprobe | N-127236-g17ec998942-20261007, shared win64 GPL | [FFmpeg](https://github.com/FFmpeg/FFmpeg/tree/17ec998942), GPL v3 or later для этой сборки; [сценарии FFmpeg-Builds](https://github.com/yt-dlp/FFmpeg-Builds) | Python-пакеты комплекта: brotli 1.2.0, certifi 2026.7.22, charset-normalizer 3.5.2, -idna 3.20, mutagen 1.48.1, pycryptodomex 3.24.0, requests 2.34.2, urllib3 2.8.0, +idna 3.20, pycryptodomex 3.24.0, requests 2.34.2, urllib3 2.8.0, websockets 17.2. Их `METADATA` и лицензии находятся рядом с библиотеками в `runtime/python/Lib/site-packages/*.dist-info` установленного помощника. Тексты Python/Node.js/FFmpeg также входят в установленный комплект. diff --git a/docs/RELEASE-1.4.7.md b/docs/RELEASE-1.4.7.md new file mode 100644 index 0000000..d91d65c --- /dev/null +++ b/docs/RELEASE-1.4.7.md @@ -0,0 +1,16 @@ +# YouTubeDL 1.4.7 + +Расширение **1.4.7** для Edge и Chrome 120+. Локальный помощник — **1.3.3**. + +- Собственный код, документация и оформление распространяются по лицензии **Source Available**: разрешены просмотр исходников, сборка неизменённой версии и использование по назначению. Изменения, форки и распространение требуют письменного согласия DepressedCat с учётом исключений лицензии. +- Неиспользуемый Mutagen исключён из автономного помощника и списка обновляемых Python-пакетов. Объединение дорожек, конвертация, аудио и аппаратное ускорение продолжают выполняться через FFmpeg. +- Проверка состава Python-пакетов выполняется при сборке, проверке комплекта и обновлении. Пакеты, требующие отдельной проверки совместимости лицензии, отклоняются до активации; рабочий комплект сохраняется. +- Лицензии и исходные материалы сторонних компонентов доступны отдельно в служебном разделе загрузок. + +## Установка и обновление + +1. Скачайте **YouTubeDL-1.4.7.zip**, распакуйте в постоянную папку и загрузите папку `extension` через режим разработчика в `edge://extensions` или `chrome://extensions`. +2. Скачайте **YouTubeDL-Helper-Setup.exe** из этого релиза и обновите помощник до **1.3.3**. Обычное обновление компонентов не заменяет код самого помощника. +3. Перезагрузите расширение и открытые страницы YouTube, затем проверьте подключение помощника. + +Если расширение уже установлено, замените файлы прежней папки `extension` и нажмите «Перезагрузить» на странице расширений браузера. diff --git a/extension/manifest.json b/extension/manifest.json index f11733f..73c826f 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "YouTubeDL", "description": "Скачать видео и аудио с YouTube: качество, формат, папка, NVIDIA NVENC и AMD AMF.", - "version": "1.4.6", + "version": "1.4.7", "minimum_chrome_version": "120", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6bCUFVUmC9OqpL0+/vrQDKEesuiylybHxRWJXfMV7VfctMbDjAQzActxImzMGIviC8PJrCHT86v/erppUhIgqFrSftHVPPWmYbVJ/g6KBu1bRNIG5J1WDUnMO2E4hGNEJQe+6ezqGuGdk659qQOqP5khGbnog9wUs+1nPpolaXbXsUudn5leaxJ0SrdFT9oWjq59bAc7raEKjR1chU9pB9g9alEA8lqpcKUU8j7f/KJC9YvkOlTNEsLXhuLLzcC7Aw/eI3lgBqBLFWIcrEYrBPVFeIlNaVLdl6b8YqAD+eBaT1xdpT6VbGh/d3lf/Vq/TBzTzCplcMO2vIFGDKTNhQIDAQAB", "permissions": [ diff --git a/extension/shared.js b/extension/shared.js index d0e53c6..9279db4 100644 --- a/extension/shared.js +++ b/extension/shared.js @@ -12,7 +12,7 @@ export function stabilizeDownloadSpeed(job, previous, now = Date.now()) { && now >= previous.speedUpdatedAt && now - previous.speedUpdatedAt < 1500; return { ...job, speed: recent ? previous.speed : null, speedUpdatedAt: recent ? previous.speedUpdatedAt : null }; } -export const SUPPORTED_HELPER_VERSION = "1.3.2"; +export const SUPPORTED_HELPER_VERSION = "1.3.3"; const MINIMUM_EXTENSION_VERSION = "1.4.5"; function compareVersions(left, right) { if (![left, right].every(value => typeof value === "string" && /^\d+(?:\.\d+){1,3}$/.test(value))) return null; diff --git a/host/bootstrap.py b/host/bootstrap.py index 9353982..8c2f08d 100644 --- a/host/bootstrap.py +++ b/host/bootstrap.py @@ -7,6 +7,7 @@ from pathlib import Path from host.bundle import ROOT, executable, manifest from host.common import EXTENSION_ID, HOST_NAME from host.dependencies import status +from host.python_packages import validate_installed def verify(): @@ -14,6 +15,7 @@ def verify(): raise ValueError("Autonomous bundle metadata is missing.") if Path(sys.executable).resolve() != Path(executable("python")): raise ValueError("The helper must use its embedded Python.") + validate_installed(ROOT / "runtime/python/Lib/site-packages") measured = status() if any(item["status"] != "installed" for item in measured): raise ValueError("Bundled components failed verification: " + json.dumps(measured)) diff --git a/host/common.py b/host/common.py index 9631978..99ccd56 100644 --- a/host/common.py +++ b/host/common.py @@ -6,7 +6,7 @@ from pathlib import Path from urllib.parse import parse_qs, urlsplit HOST_NAME = "com.youtubedl.native" -VERSION = "1.3.2" +VERSION = "1.3.3" EXTENSION_ID = "ndnoeiaecgmjnjneekaeapeecmmjheli" VIDEO_CONTAINERS = {"mp4", "mkv", "webm"} AUDIO_CONTAINERS = {"m4a", "mp3", "opus", "flac", "wav"} diff --git a/host/dependencies.py b/host/dependencies.py index f7d0ee0..da322a2 100644 --- a/host/dependencies.py +++ b/host/dependencies.py @@ -64,6 +64,7 @@ def install(key, progress, workspace=None): raise ValueError("Нажмите «Обновить компоненты» для установки новых версий из интернета или «Восстановить» для переустановки встроенного комплекта.") if key not in {"node", "ffmpeg", "yt_dlp", "ejs"}: raise ValueError("Неизвестный компонент.") + from host.python_packages import requirements runtime = ROOT / ".runtime" runtime.mkdir(exist_ok=True) cfg = config() @@ -73,8 +74,8 @@ def install(key, progress, workspace=None): workspace(str(scratch)) if key in {"yt_dlp", "ejs"}: progress(None, "Устанавливаю Python-пакет") - package = "yt-dlp[default]>=2026.8.19" if key == "yt_dlp" else "yt-dlp-ejs" - measured = subprocess.run([sys.executable, "-m", "pip", "install", "--disable-pip-version-check", "--upgrade", package], + packages = requirements() if key == "yt_dlp" else ["yt-dlp-ejs"] + measured = subprocess.run([sys.executable, "-m", "pip", "install", "--disable-pip-version-check", "--upgrade", *packages], stdin=subprocess.DEVNULL, capture_output=True, text=True, encoding="utf-8", errors="replace", creationflags=NO_WINDOW, timeout=600) if measured.returncode: raise ValueError("Не удалось установить компонент. " + measured.stderr[-1200:]) diff --git a/host/python_packages.py b/host/python_packages.py new file mode 100644 index 0000000..e7e5b06 --- /dev/null +++ b/host/python_packages.py @@ -0,0 +1,41 @@ +"""Python dependencies used by the helper; optional cover tagging is excluded.""" +from __future__ import annotations + +import importlib.metadata +import re +from pathlib import Path + + +def requirements(yt_dlp="yt-dlp>=2026.8.19", ejs="yt-dlp-ejs"): + return [yt_dlp, ejs, "brotli", "certifi", "pycryptodomex", + "requests>=2.32.2,<3", "urllib3>=2,<3", "websockets>=13"] + + +def validate_metadata(records): + """Reject strong copyleft Python libraries before activating a bundle. + + FFmpeg is a separately executed program, so this policy concerns Python + libraries loaded into the helper process, not the FFmpeg distribution. + """ + for metadata in records: + name = re.sub(r"[-_.]+", "-", metadata.get("name", "").lower()) + licenses = [metadata.get("license_expression") or "", metadata.get("license") or "", + *metadata.get("classifier", [])] + incompatible = name == "mutagen" or any( + re.search(r"(?=2026.8.19 +yt-dlp>=2026.8.19 +yt-dlp-ejs +brotli +certifi +pycryptodomex +requests>=2.32.2,<3 +urllib3>=2,<3 +websockets>=13 diff --git a/site/index.html b/site/index.html index 3981e04..e0b9bd0 100644 --- a/site/index.html +++ b/site/index.html @@ -40,7 +40,7 @@
Установить расширение Посмотреть возможности

Бесплатно · Для Windows · Обработка на вашем компьютере

-
ВИДЕО. ЗВУК. ВАШ ВЫБОР.Выпуск 1.4.6
+
ВИДЕО. ЗВУК. ВАШ ВЫБОР.Выпуск 1.4.7

Один ролик.
Ваш формат.

MP4MKVWebMMP3M4AOpusFLACWAV
@@ -70,8 +70,8 @@

04 / НАЧНЁМ

Два скачивания.
Один комплект.

Windows 10/11 x64
Edge или Chrome 120+
Без прав администратора

-
1БРАУЗЕРv1.4.6

Расширение

Кнопки на YouTube, настройки и уведомления. Устанавливается распакованным.

Скачать расширение ZIP · Небольшой архив
-
2КОМПЬЮТЕРv1.3.2

Локальный помощник

Скачивает, соединяет и конвертирует. Все необходимые компоненты уже внутри.

Скачать помощник EXE · 124,3 МиБ · устанавливается отдельно
+
1БРАУЗЕРv1.4.7

Расширение

Кнопки на YouTube, настройки и уведомления. Устанавливается распакованным.

Скачать расширение ZIP · Небольшой архив
+
2КОМПЬЮТЕРv1.3.3

Локальный помощник

Скачивает, соединяет и конвертирует. Все необходимые компоненты уже внутри.

Скачать помощник EXE · 124,1 МиБ · устанавливается отдельно

Оба файла размещены в релизах Gitea ↗. Помощник не встроен в ZIP. Проверка SHA256 и подробности.

    @@ -93,11 +93,11 @@
    Как удалить всё?

    Удалите помощник кнопкой в меню или через приложения Windows, затем расширение из браузера. Скачанные файлы и история помощника сохраняются. Для удаления истории после удаления помощника удалите %LOCALAPPDATA%\YouTubeDL\Helper\data. Настройки расширения удаляются вместе с расширением.

    Почему расширения нет в Chrome Web Store?

    YouTubeDL распространяется через релизы Gitea из-за ограничений на скачивание контента. Правила Chrome Web Store запрещают инструменты для неразрешённого скачивания защищённого контента, а условия YouTube ограничивают скачивание разрешёнными сервисом способами или письменным разрешением YouTube и соответствующих правообладателей. Поэтому для YouTubeDL выбрана ручная установка распакованного расширения из официального релиза.

    Какие ОС поддерживаются?

    Сейчас опубликована сборка для Windows 10/11 x64 и Edge/Chrome 120+. Firefox, macOS, Linux и Windows ARM не входят в неё. Плейлисты, скачивание каналов и DRM-контент не заявляются.

    -
    Можно изменять расширение и публиковать свои версии?

    Лицензия разрешает использование по назначению и запрещает изменения, самостоятельные форки и распространение собственного кода без письменного разрешения, с учётом исключений в её тексте. Сторонние компоненты сохраняют свои лицензии.

    +
    Можно изменять расширение и публиковать свои версии?

    Лицензия Source Available разрешает просмотр исходников, сборку неизменённой версии и использование по назначению и запрещает изменения, самостоятельные форки и распространение собственного кода без письменного разрешения, с учётом исключений в её тексте. Сторонние компоненты сохраняют свои лицензии.

Видео. Звук.
Ваш выбор.

Скачать YouTubeDL
- + diff --git a/tests/extension.test.mjs b/tests/extension.test.mjs index fd8edf6..45d7f3b 100644 --- a/tests/extension.test.mjs +++ b/tests/extension.test.mjs @@ -55,19 +55,19 @@ test("brief missing speed samples do not flicker, expire and reset between track assert.equal(stabilizeDownloadSpeed({ ...sample, stage: "merging", speed: "8x" }, previous, 1200).speedUpdatedAt, null); }); test("helper compatibility detects outdated, newer and unidentifiable versions in both directions", () => { - assert.equal(checkHelperCompatibility({ version: "1.3.2" }, "1.4.5").compatible, true); - assert.equal(checkHelperCompatibility({ bundle: { version: "1.3.2" } }, "1.4.5").compatible, true); - assert.equal(checkHelperCompatibility({ version: "1.3.2.0" }, "1.4.5").compatible, true); + assert.equal(checkHelperCompatibility({ version: "1.3.3" }, "1.4.5").compatible, true); + assert.equal(checkHelperCompatibility({ bundle: { version: "1.3.3" } }, "1.4.5").compatible, true); + assert.equal(checkHelperCompatibility({ version: "1.3.3.0" }, "1.4.5").compatible, true); for (const version of ["1.3.0", "1.2.10", undefined, "unknown"]) { const result = checkHelperCompatibility({ version }, "1.4.5"); assert.equal(result.compatible, false); assert.equal(result.update, "helper"); } - for (const version of ["1.3.3", "1.3.20", "2.0.0"]) { + for (const version of ["1.3.4", "1.3.30", "2.0.0"]) { const result = checkHelperCompatibility({ version }, "1.4.5"); assert.equal(result.compatible, false); assert.equal(result.update, "extension"); } - assert.equal(checkHelperCompatibility({ version: "1.3.2" }, "1.4.2").update, "extension"); - assert.equal(checkHelperCompatibility({ version: "1.3.2", minimumExtensionVersion: "1.4.6" }, "1.4.5").update, "extension"); + assert.equal(checkHelperCompatibility({ version: "1.3.3" }, "1.4.2").update, "extension"); + assert.equal(checkHelperCompatibility({ version: "1.3.3", minimumExtensionVersion: "1.4.7" }, "1.4.5").update, "extension"); }); diff --git a/tests/history.test.mjs b/tests/history.test.mjs index 69fe44c..0ed4995 100644 --- a/tests/history.test.mjs +++ b/tests/history.test.mjs @@ -16,7 +16,7 @@ test("history connects without a helper, opens a tab, and retains failures befor return { onMessage: { addListener: listener => { receive = listener; } }, onDisconnect: noEvent, disconnect() {}, postMessage(message) { nativeRequests.push(message); - queueMicrotask(() => receive({ id: message.id, ok: message.action !== "inspect", result: message.action === "hello" ? { version: "1.3.2" } : { alive: true }, error: "Видео недоступно" })); + queueMicrotask(() => receive({ id: message.id, ok: message.action !== "inspect", result: message.action === "hello" ? { version: "1.3.3" } : { alive: true }, error: "Видео недоступно" })); } }; } }; globalThis.chrome = { runtime, storage: { local: { get: async () => structuredClone(storage), set: async data => Object.assign(storage, structuredClone(data)) } }, diff --git a/tests/native-connection.test.mjs b/tests/native-connection.test.mjs index a7a40ab..fc25111 100644 --- a/tests/native-connection.test.mjs +++ b/tests/native-connection.test.mjs @@ -4,7 +4,7 @@ import assert from "node:assert/strict"; test("native lifecycle recovers stale ports and publishes install/remove/update state to all menus", async () => { const ports = [], listeners = {}, storage = {}, pending = new Map(), updates = []; let installed = false; - let helperVersion = "1.3.2"; + let helperVersion = "1.3.3"; let componentsReady = true; const noEvent = { addListener() {} }; const runtime = { id: "test-id", getManifest: () => ({ version: "1.4.5" }), getURL: name => `chrome-extension://test-id/${name}`, @@ -94,7 +94,7 @@ test("native lifecycle recovers stale ports and publishes install/remove/update await new Promise(resolve => setTimeout(resolve, 20)); assert.equal((await call("state")).result.helper.hardware.ready, true, "Restoring a component refreshes readiness without reload"); assert.ok(fresh.sent.some(message => message.action === "helper_uninstall")); - for (const [version, update] of [["1.3.0", "helper"], ["1.3.3", "extension"]]) { + for (const [version, update] of [["1.3.0", "helper"], ["1.3.4", "extension"]]) { helperVersion = version; ports.at(-1).disconnect(); await new Promise(resolve => setImmediate(resolve)); @@ -107,6 +107,6 @@ test("native lifecycle recovers stale ports and publishes install/remove/update assert.equal((await call("download", { url: "https://youtu.be/jNQXAC9IVRw" })).ok, false, 'Incompatible versions cannot launch a download'); assert.ok(!port.sent.some(message => message.action === "download")); } - helperVersion = "1.3.2"; + helperVersion = "1.3.3"; assert.equal((await call("helper_status")).result.status, "running", 'Version repair is detected by the Check button without reloading'); }); diff --git a/tests/session.test.mjs b/tests/session.test.mjs index 381d5da..e0ee7d9 100644 --- a/tests/session.test.mjs +++ b/tests/session.test.mjs @@ -38,7 +38,7 @@ test("background passes fresh YouTube cookies to both worker actions and never p onMessage: { addListener(listener) { nativeListener = listener; } }, onDisconnect: noEvent, disconnect() {}, postMessage(message) { requests.push(message); - const result = message.action === "hello" ? { version: "1.3.2", ready: true, supportsBrowserCookies: true } : message.action === "ping" ? { alive: true } + const result = message.action === "hello" ? { version: "1.3.3", ready: true, supportsBrowserCookies: true } : message.action === "ping" ? { alive: true } : message.action === "download" ? { jobId: "auth-test-job" } : { title: "Synthetic video", videos: [], audios: [] }; queueMicrotask(() => nativeListener({ id: message.id, ok: true, result })); }, diff --git a/tests/site.test.mjs b/tests/site.test.mjs index 94a4a7e..6151a0e 100644 --- a/tests/site.test.mjs +++ b/tests/site.test.mjs @@ -35,7 +35,7 @@ try { assert.equal(new URL(page.url()).hash,"#install"); assert.ok(await page.locator(".download-grid").isVisible()); assert.equal(await page.locator('[data-download="extension"]').getAttribute("href"),`https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/download/v${version}/YouTubeDL-${version}.zip`); - assert.equal(await page.locator('[data-download="helper"]').getAttribute("href"),"https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/download/v1.4.6/YouTubeDL-Helper-Setup.exe"); + assert.equal(await page.locator('[data-download="helper"]').getAttribute("href"),"https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/download/v1.4.7/YouTubeDL-Helper-Setup.exe"); await page.locator('[data-screenshot="light"]').click(); await page.locator("#menu-screenshot").evaluate(image => image.decode()); assert.match(await page.locator("#menu-screenshot").getAttribute("src"), /menu-light\.png$/); diff --git a/tests/test_python_packages.py b/tests/test_python_packages.py new file mode 100644 index 0000000..f797a3b --- /dev/null +++ b/tests/test_python_packages.py @@ -0,0 +1,53 @@ +import json +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + +from host.python_packages import validate_metadata +from host.updates import resolve_packages + + +class PythonPackageLicenseTests(unittest.TestCase): + def test_permissive_and_weak_copyleft_metadata_is_accepted(self): + validate_metadata([{"name": "yt-dlp", "license_expression": "Unlicense"}, + {"name": "certifi", "license_expression": "MPL-2.0"}, + {"name": "library", "license_expression": "MIT"}, + {"name": "library", "license_expression": "LGPL-2.1-or-later"}]) + + def test_strong_copyleft_is_rejected_using_expression_or_classifiers(self): + for metadata in [{"name": "mutagen"}, + {"name": "dependency", "license_expression": "GPL-3.0-or-later"}, + {"name": "dependency", "license_expression": "AGPL-3.0-only"}, + {"name": "dependency", "classifier": ["License :: OSI Approved :: GNU General Public License v2 (GPLv2)"]}]: + with self.subTest(metadata=metadata), self.assertRaises(ValueError): + validate_metadata([metadata]) + + def test_incompatible_resolution_does_not_delete_the_existing_packages(self): + with tempfile.TemporaryDirectory() as temporary: + stage = Path(temporary) / "stage" + packages = stage / "runtime/python/Lib/site-packages" + packages.mkdir(parents=True) + sentinel = packages / "existing.py" + sentinel.write_text("original package", encoding="utf8") + work = Path(temporary) / "work" + work.mkdir() + + def resolve(command, **kwargs): + report = Path(command[command.index("--report") + 1]) + report.write_text(json.dumps({"install": [{"metadata": { + "name": "mutagen", "license_expression": "GPL-2.0-or-later"}}]}), encoding="utf8") + self.assertNotIn("yt-dlp[default]", command) + return SimpleNamespace(returncode=0) + + response = {"urls": [{"filename": "pip-1-py3-none-any.whl", "url": "https://files.pythonhosted.org/pip.whl", "digests": {"sha256": "a" * 64}}]} + with patch("host.updates.get", return_value=SimpleNamespace(json=lambda: response)), \ + patch("host.updates.fetch"), patch("host.updates.subprocess.run", side_effect=resolve): + with self.assertRaisesRegex(ValueError, "mutagen"): + resolve_packages(stage, work, lambda *args: None) + self.assertEqual(sentinel.read_text(), "original package") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/ui-fixture.js b/tests/ui-fixture.js index adfe1f8..0c5ae5e 100644 --- a/tests/ui-fixture.js +++ b/tests/ui-fixture.js @@ -18,7 +18,7 @@ window.__contentMessage = message => contentListeners.forEach(listener => listener(message)); let autoUpdate = true; window.__updateRequests = []; - const hardware = () => ({ version: window.__helperVersion || "1.3.2", bundle: { mode: "autonomous", version: window.__helperVersion || "1.3.2", updateTogether: true }, supportsComponentUpdates: true, supportsWorkFolder: true, updateSettings: { enabled: autoUpdate }, + const hardware = () => ({ version: window.__helperVersion || "1.3.3", bundle: { mode: "autonomous", version: window.__helperVersion || "1.3.3", updateTogether: true }, supportsComponentUpdates: true, supportsWorkFolder: true, updateSettings: { enabled: autoUpdate }, gpu: window.__amdOnly ? "AMD Radeon RX 7900 XTX" : "NVIDIA GeForce RTX 5080", nvenc: window.__dependencyMissing || window.__amdOnly ? [] : ["h264", "hevc", "av1"], amf: window.__amdOnly ? ["h264", "hevc", "av1"] : [], amf10bit: window.__amdOnly ? ["hevc", "av1"] : [], defaultFolder: "D:\\Videos", ready: !window.__dependencyMissing, dependencies: ["python", "yt_dlp", "ejs", "node", "ffmpeg"].map(id => ({ id, status: window.__dependencyMissing && id === "ffmpeg" ? "missing" : "installed", version: "test-version" })) }); @@ -31,7 +31,7 @@ ], audios: [{ id: "251", codec: "opus", extension: "webm", bitrate: 160, language: "en", note: "English", size: 5000000 }] }; window.chrome = { runtime: { - getManifest: () => ({ version: window.__extensionVersion || "1.4.6" }), + getManifest: () => ({ version: window.__extensionVersion || "1.4.7" }), getURL: file => `https://ui.ytdl.test/${file}`, sendMessage: async message => { if (message.type === "QUICK_DOWNLOAD") { window.__quickRequests.push(message.url); return { ok: true, jobId: "fixture-job" }; } diff --git a/tests/version-compatibility.test.mjs b/tests/version-compatibility.test.mjs index 92014a6..23bcd76 100644 --- a/tests/version-compatibility.test.mjs +++ b/tests/version-compatibility.test.mjs @@ -20,8 +20,8 @@ try { page.on("pageerror", error => errors.push(error.message)); await page.goto("https://ui.ytdl.test/downloader.html?theme=dark&url=https://www.youtube.com/watch?v=aircAruvnKk"); await page.locator('#helper-panel[data-state="incompatible"]').waitFor(); - assert.match(await page.locator('#helper-message').textContent(), /1\.3\.0.*1\.4\.6/); - assert.match(await page.locator('#helper-versions').textContent(), /Расширение 1\.4\.6.*Помощник 1\.3\.0/); + assert.match(await page.locator('#helper-message').textContent(), /1\.3\.0.*1\.4\.7/); + assert.match(await page.locator('#helper-versions').textContent(), /Расширение 1\.4\.7.*Помощник 1\.3\.0/); assert.equal(await page.locator('#helper-install').textContent(), 'Обновить помощник'); assert.equal(await page.locator('#download').isDisabled(), true); assert.equal(await page.locator('#download-form').isVisible(), false); @@ -30,13 +30,13 @@ try { await page.locator('#setup-helper[data-state="incompatible"]').waitFor(); assert.equal(await page.locator('#setup-next').isDisabled(), true); assert.equal(await page.locator('#setup-install').textContent(), 'Обновить помощник'); - assert.match(await page.locator('#setup-versions').textContent(), /1\.4\.6.*1\.3\.0/); - await page.evaluate(() => { window.__helperVersion = '1.3.2'; }); + assert.match(await page.locator('#setup-versions').textContent(), /1\.4\.7.*1\.3\.0/); + await page.evaluate(() => { window.__helperVersion = '1.3.3'; }); await page.locator('#setup-check').click(); await page.locator('#setup-helper[data-state="running"]').waitFor(); assert.match(await page.locator('#setup-versions').textContent(), /Версии совместимы/); await page.locator('#setup-next:not([disabled])').waitFor(); - await page.evaluate(() => { window.__helperVersion = '1.3.3'; }); + await page.evaluate(() => { window.__helperVersion = '1.3.4'; }); await page.locator('#setup-check').click(); await page.locator('#setup-helper[data-state="incompatible"]').waitFor(); assert.equal(await page.locator('#setup-next').isDisabled(), true); @@ -44,9 +44,9 @@ try { assert.equal(await page.locator('#setup-install-steps').isVisible(), false); await page.locator('#setup-close').click(); assert.equal(await page.locator('#helper-install').textContent(), 'Обновить расширение'); - assert.match(await page.locator('#helper-message').textContent(), /Помощник 1\.3\.3.*расширением 1\.4\.6/); + assert.match(await page.locator('#helper-message').textContent(), /Помощник 1\.3\.4.*расширением 1\.4\.7/); assert.equal(await page.locator('#download').isDisabled(), true); - await page.evaluate(() => { window.__helperVersion = '1.3.2'; }); + await page.evaluate(() => { window.__helperVersion = '1.3.3'; }); await page.locator('#helper-start').click(); await page.locator('#helper-panel[data-state="running"]').waitFor(); await page.locator('#download:not([disabled])').waitFor(); diff --git a/tools/build_bundle.py b/tools/build_bundle.py index 7df36f3..fb056cf 100644 --- a/tools/build_bundle.py +++ b/tools/build_bundle.py @@ -26,6 +26,8 @@ PYTHON_SHA256 = "90b4e5b9898b72d744650524bff92377c367f44bd5fbd09e3148656c080ad90 NODE = "v24.21.0" FFMPEG_ARCHIVE = "ffmpeg-master-latest-win64-gpl-shared.zip" FFMPEG_SHA256 = "eb7afd4ecced72ab7d5d6fdd36dbc359173aa2daf6081817f7b678345898838b" +sys.path.insert(0, str(ROOT)) +from host.python_packages import validate_installed def digest(path): @@ -107,10 +109,11 @@ def prepare(): wheels = CACHE / "wheels"; wheels.mkdir(exist_ok=True) options = ["--platform", "win_amd64", "--python-version", "3.13", "--implementation", "cp", "--abi", "cp313", "--abi", "abi3", "--abi", "none", "--only-binary", ":all:"] - requirement = "yt-dlp[default]==2026.8.19" - subprocess.run([sys.executable, "-m", "pip", "download", "--disable-pip-version-check", "-d", str(wheels), *options, requirement], check=True) + requirements = ["yt-dlp==2026.8.19", "yt-dlp-ejs==0.8.0", "brotli==1.2.0", "certifi==2026.7.22", "pycryptodomex==3.24.0", "requests==2.34.2", "urllib3==2.8.0", "websockets==17.2", "charset-normalizer==3.5.2", "idna==3.20"] + subprocess.run([sys.executable, "-m", "pip", "download", "--disable-pip-version-check", "-d", str(wheels), *options, *requirements], check=True) subprocess.run([sys.executable, "-m", "pip", "install", "--disable-pip-version-check", "--no-index", "--find-links", str(wheels), - "--target", str(python / "Lib" / "site-packages"), "--no-compile", "--ignore-requires-python", *options, requirement], check=True) + "--target", str(python / "Lib" / "site-packages"), "--no-compile", "--ignore-requires-python", *options, *requirements], check=True) + validate_installed(python / "Lib/site-packages") (licenses / "THIRD-PARTY.txt").write_text( "Python: https://www.python.org/ ; license: runtime/python/LICENSE.txt\n" "Node.js: https://nodejs.org/ ; license: Node-LICENSE.txt\n" @@ -130,6 +133,8 @@ def build(): # pip's generated EXE launchers refer to the build interpreter. The helper # imports these libraries directly, so no console launchers are shipped. shutil.rmtree(scripts) + validate_installed(KIT / "runtime/python/Lib/site-packages") + shutil.copy2(ROOT / "LICENSE", KIT / "LICENSE") shutil.copytree(ROOT / "host", KIT / "host", dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) sys.path.insert(0, str(ROOT)) from host.common import VERSION