Publish Source Available licenses and remove Mutagen in YouTubeDL 1.4.7 / helper 1.3.3

This commit is contained in:
DepressedCat committed 2026-10-11 23:51:30 +03:00
1 parent 71a808b298
commit 50457bc35b
27 files changed
+201 -58

No files matched your search

+1 -1
View File
@@ -24,7 +24,7 @@ npm run check
python tools/build.py python tools/build.py
``` ```
`tools/build.py` собирает небольшой `dist/YouTubeDL-1.4.6.zip` без EXE. `tools/build.py` собирает небольшой `dist/YouTubeDL-1.4.7.zip` без EXE.
Для сборки расширения не требуется предварительно собирать помощник. Для сборки расширения не требуется предварительно собирать помощник.
## Браузерные проверки ## Браузерные проверки
+3 -3
View File
@@ -6,7 +6,7 @@ Firefox, macOS, Linux и Windows ARM не входят в эту сборку.
Скачивания доступны в [релизах проекта](https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/latest). Скачивания доступны в [релизах проекта](https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/latest).
Помощник распространяется **отдельно**: внутри архива расширения EXE нет. Помощник распространяется **отдельно**: внутри архива расширения EXE нет.
1. Скачайте **YouTubeDL-1.4.6.zip** и распакуйте его в постоянную папку. Не удаляйте и не перемещайте её после установки. 1. Скачайте **YouTubeDL-1.4.7.zip** и распакуйте его в постоянную папку. Не удаляйте и не перемещайте её после установки.
2. Откройте `edge://extensions` или `chrome://extensions`. Включите **Режим разработчика**, нажмите **Загрузить распакованное расширение** и выберите папку **extension** внутри распакованного архива. 2. Откройте `edge://extensions` или `chrome://extensions`. Включите **Режим разработчика**, нажмите **Загрузить распакованное расширение** и выберите папку **extension** внутри распакованного архива.
3. На странице релиза отдельно скачайте **YouTubeDL-Helper-Setup.exe**. Это автономный комплект: Python, yt-dlp, EJS, Node.js, FFmpeg и FFprobe уже внутри. 3. На странице релиза отдельно скачайте **YouTubeDL-Helper-Setup.exe**. Это автономный комплект: Python, yt-dlp, EJS, Node.js, FFmpeg и FFprobe уже внутри.
4. Запустите EXE вручную и дождитесь окончания установки. Администратор и интернет для установки комплекта не требуются. Программы устанавливаются для текущего пользователя в `%LOCALAPPDATA%\YouTubeDL\Helper`. 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. Обновление компонентов обновляет инструменты внутри комплекта и не заменяет само расширение или код помощника. После установки помощника нажмите «Проверить»: статус изменится без перезагрузки страницы. После замены файлов расширения нажмите «Обновить» на странице расширений браузера и перезагрузите вкладку YouTube. Обновление компонентов обновляет инструменты внутри комплекта и не заменяет само расширение или код помощника.
+22 -8
View File
@@ -1,9 +1,12 @@
ОГРАНИЧЕННАЯ ПОЛЬЗОВАТЕЛЬСКАЯ ЛИЦЕНЗИЯ YOUTUBEDL ЛИЦЕНЗИЯ SOURCE AVAILABLE — YouTubeDL
Версия 1.0, 8 октября 2026 года Версия 1.1, 11 октября 2026 года
Copyright (c) 2026 DepressedCat. Все права сохранены. Copyright (c) 2026 DepressedCat. Все права сохранены.
Правообладатель: DepressedCat (https://git.lonarpgproxy.space/DepressedCat). Правообладатель: DepressedCat (https://git.lonarpgproxy.space/DepressedCat).
Исходный код доступен для ознакомления (Source Available). Права на использование,
изменение и распространение определяются условиями этой лицензии.
1. ОБЛАСТЬ ДЕЙСТВИЯ 1. ОБЛАСТЬ ДЕЙСТВИЯ
Эта лицензия относится к собственным файлам проекта YouTubeDL, предоставляемым Эта лицензия относится к собственным файлам проекта YouTubeDL, предоставляемым
@@ -22,7 +25,8 @@ Copyright (c) 2026 DepressedCat. Все права сохранены.
выбирать дорожки и параметры, объединять, конвертировать и сохранять файлы выбирать дорожки и параметры, объединять, конвертировать и сохранять файлы
при наличии права сохранять соответствующий контент. при наличии права сохранять соответствующий контент.
Разрешены необходимые для этого установочные и резервные копии, сборка Разрешены просмотр и изучение опубликованных исходников, их локальная копия
для ознакомления и сборки, необходимые установочные и резервные копии, сборка
неизменённой официальной поставки для собственного использования, настройка неизменённой официальной поставки для собственного использования, настройка
через штатные средства, установка официальных обновлений и удаление Программы. через штатные средства, установка официальных обновлений и удаление Программы.
Это не предоставляет права изменять собственные файлы кода и дизайна. Это не предоставляет права изменять собственные файлы кода и дизайна.
@@ -46,11 +50,21 @@ Copyright (c) 2026 DepressedCat. Все права сохранены.
4. ИСКЛЮЧЕНИЯ 4. ИСКЛЮЧЕНИЯ
4.1. Python, Node.js, yt-dlp, EJS, FFmpeg, FFprobe и другие сторонние компоненты, 4.1. Python, Node.js, yt-dlp, EJS, FFmpeg, FFprobe и другие сторонние компоненты
их исходники и лицензионные тексты регулируются лицензиями их правообладателей. регулируются условиями собственных лицензий, установленными их правообладателями.
Эта лицензия не ограничивает предоставленные ими права, в том числе права
на изменение, форки и распространение по GPL, MIT и другим лицензиям. Настоящая лицензия не ограничивает права, предоставленные пользователям лицензиями
Состав и сведения о лицензиях приведены в THIRD_PARTY.md и материалах релиза. указанных сторонних компонентов, включая права на изменение, создание производных
работ и распространение, если такие действия разрешены соответствующими лицензиями.
Указанные права относятся к соответствующим сторонним компонентам и не предоставляют
пользователю дополнительных прав на авторские части Программы. Их изменение, создание
производных версий, форков и распространение запрещены без разрешения правообладателя,
за исключением случаев, предусмотренных настоящей лицензией, обязательными нормами
законодательства или применимыми лицензиями сторонних компонентов.
Перечень сторонних компонентов и сведения об их лицензиях приведены в файле
THIRD_PARTY.md и материалах релиза.
4.2. Права, которые применимое законодательство не позволяет ограничивать 4.2. Права, которые применимое законодательство не позволяет ограничивать
договором, сохраняются. Эта лицензия не предоставляет прав на чужой контент договором, сохраняются. Эта лицензия не предоставляет прав на чужой контент
+1 -1
View File
@@ -1,6 +1,6 @@
# Обработка данных # Обработка данных
Актуально для расширения **1.4.5** и помощника **1.3.2**. Исходники доступны для просмотра, Актуально для расширения **1.4.7** и помощника **1.3.3**. Исходники доступны для просмотра,
условия использования собственного кода ограничены [лицензией](LICENSE). условия использования собственного кода ограничены [лицензией](LICENSE).
У проекта нет собственного сервера обработки видео, аккаунтов, телеметрии, рекламы или аналитики. У проекта нет собственного сервера обработки видео, аккаунтов, телеметрии, рекламы или аналитики.
Это не означает отсутствие обработки данных: необходимые сведения используются на вашем компьютере, Это не означает отсутствие обработки данных: необходимые сведения используются на вашем компьютере,
+4 -4
View File
@@ -11,7 +11,7 @@
**[Инструкция](INSTALL.md)** · **[Обработка данных](PRIVACY.md)** · **[Инструкция](INSTALL.md)** · **[Обработка данных](PRIVACY.md)** ·
**[Сообщить об ошибке](https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/issues)** **[Сообщить об ошибке](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). Расширение устанавливается распакованным, через режим разработчика. Прямая поставка через 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**. 2. В `edge://extensions` или `chrome://extensions` включите **Режим разработчика**, нажмите **Загрузить распакованное расширение** и выберите папку **extension**.
3. **Отдельно скачайте YouTubeDL-Helper-Setup.exe из того же релиза** и запустите его вручную. EXE не встроен в расширение: кнопка установки открывает страницу релизов. 3. **Отдельно скачайте YouTubeDL-Helper-Setup.exe из того же релиза** и запустите его вручную. EXE не встроен в расширение: кнопка установки открывает страницу релизов.
4. Обновите YouTube, откройте **⚙** и пройдите мастер. Выберите папку и параметры; зелёный статус подтверждает связь с помощником. 4. Обновите YouTube, откройте **⚙** и пройдите мастер. Выберите папку и параметры; зелёный статус подтверждает связь с помощником.
@@ -102,8 +102,8 @@ Python остаётся в ветке 3.13, Node.js — в 24 LTS.
## Лицензии и разработка ## Лицензии и разработка
Новые поставки собственного кода — по **[ограниченной пользовательской лицензии](LICENSE)**: Новые поставки собственного кода — по **[лицензии Source Available](LICENSE)**:
разрешены установка и использование по назначению. Изменение, самостоятельные форки, разрешены просмотр исходников, сборка неизменённой версии, установка и использование по назначению. Изменение, самостоятельные форки,
производные версии и распространение без письменного разрешения правообладателя запрещены, производные версии и распространение без письменного разрешения правообладателя запрещены,
с учётом исключений лицензии. Это распространяется и на само расширение. с учётом исключений лицензии. Это распространяется и на само расширение.
+3 -2
View File
@@ -5,8 +5,9 @@
Встроенные компоненты помощника имеют **свои** лицензии; ограничения YouTubeDL Встроенные компоненты помощника имеют **свои** лицензии; ограничения YouTubeDL
не заменяют и не ограничивают их, в том числе права по GPL и MIT. не заменяют и не ограничивают их, в том числе права по GPL и MIT.
Помощник вызывает FFmpeg отдельным процессом. Помощник вызывает 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) | | 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, 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` и лицензии находятся рядом с библиотеками в websockets 17.2. Их `METADATA` и лицензии находятся рядом с библиотеками в
`runtime/python/Lib/site-packages/*.dist-info` установленного помощника. `runtime/python/Lib/site-packages/*.dist-info` установленного помощника.
Тексты Python/Node.js/FFmpeg также входят в установленный комплект. Тексты Python/Node.js/FFmpeg также входят в установленный комплект.
+16
View File
@@ -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` и нажмите «Перезагрузить» на странице расширений браузера.
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "YouTubeDL", "name": "YouTubeDL",
"description": "Скачать видео и аудио с YouTube: качество, формат, папка, NVIDIA NVENC и AMD AMF.", "description": "Скачать видео и аудио с YouTube: качество, формат, папка, NVIDIA NVENC и AMD AMF.",
"version": "1.4.6", "version": "1.4.7",
"minimum_chrome_version": "120", "minimum_chrome_version": "120",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6bCUFVUmC9OqpL0+/vrQDKEesuiylybHxRWJXfMV7VfctMbDjAQzActxImzMGIviC8PJrCHT86v/erppUhIgqFrSftHVPPWmYbVJ/g6KBu1bRNIG5J1WDUnMO2E4hGNEJQe+6ezqGuGdk659qQOqP5khGbnog9wUs+1nPpolaXbXsUudn5leaxJ0SrdFT9oWjq59bAc7raEKjR1chU9pB9g9alEA8lqpcKUU8j7f/KJC9YvkOlTNEsLXhuLLzcC7Aw/eI3lgBqBLFWIcrEYrBPVFeIlNaVLdl6b8YqAD+eBaT1xdpT6VbGh/d3lf/Vq/TBzTzCplcMO2vIFGDKTNhQIDAQAB", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6bCUFVUmC9OqpL0+/vrQDKEesuiylybHxRWJXfMV7VfctMbDjAQzActxImzMGIviC8PJrCHT86v/erppUhIgqFrSftHVPPWmYbVJ/g6KBu1bRNIG5J1WDUnMO2E4hGNEJQe+6ezqGuGdk659qQOqP5khGbnog9wUs+1nPpolaXbXsUudn5leaxJ0SrdFT9oWjq59bAc7raEKjR1chU9pB9g9alEA8lqpcKUU8j7f/KJC9YvkOlTNEsLXhuLLzcC7Aw/eI3lgBqBLFWIcrEYrBPVFeIlNaVLdl6b8YqAD+eBaT1xdpT6VbGh/d3lf/Vq/TBzTzCplcMO2vIFGDKTNhQIDAQAB",
"permissions": [ "permissions": [
+1 -1
View File
@@ -12,7 +12,7 @@ export function stabilizeDownloadSpeed(job, previous, now = Date.now()) {
&& now >= previous.speedUpdatedAt && now - previous.speedUpdatedAt < 1500; && now >= previous.speedUpdatedAt && now - previous.speedUpdatedAt < 1500;
return { ...job, speed: recent ? previous.speed : null, speedUpdatedAt: recent ? previous.speedUpdatedAt : null }; 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"; const MINIMUM_EXTENSION_VERSION = "1.4.5";
function compareVersions(left, right) { function compareVersions(left, right) {
if (![left, right].every(value => typeof value === "string" && /^\d+(?:\.\d+){1,3}$/.test(value))) return null; if (![left, right].every(value => typeof value === "string" && /^\d+(?:\.\d+){1,3}$/.test(value))) return null;
+2
View File
@@ -7,6 +7,7 @@ from pathlib import Path
from host.bundle import ROOT, executable, manifest from host.bundle import ROOT, executable, manifest
from host.common import EXTENSION_ID, HOST_NAME from host.common import EXTENSION_ID, HOST_NAME
from host.dependencies import status from host.dependencies import status
from host.python_packages import validate_installed
def verify(): def verify():
@@ -14,6 +15,7 @@ def verify():
raise ValueError("Autonomous bundle metadata is missing.") raise ValueError("Autonomous bundle metadata is missing.")
if Path(sys.executable).resolve() != Path(executable("python")): if Path(sys.executable).resolve() != Path(executable("python")):
raise ValueError("The helper must use its embedded Python.") raise ValueError("The helper must use its embedded Python.")
validate_installed(ROOT / "runtime/python/Lib/site-packages")
measured = status() measured = status()
if any(item["status"] != "installed" for item in measured): if any(item["status"] != "installed" for item in measured):
raise ValueError("Bundled components failed verification: " + json.dumps(measured)) raise ValueError("Bundled components failed verification: " + json.dumps(measured))
+1 -1
View File
@@ -6,7 +6,7 @@ from pathlib import Path
from urllib.parse import parse_qs, urlsplit from urllib.parse import parse_qs, urlsplit
HOST_NAME = "com.youtubedl.native" HOST_NAME = "com.youtubedl.native"
VERSION = "1.3.2" VERSION = "1.3.3"
EXTENSION_ID = "ndnoeiaecgmjnjneekaeapeecmmjheli" EXTENSION_ID = "ndnoeiaecgmjnjneekaeapeecmmjheli"
VIDEO_CONTAINERS = {"mp4", "mkv", "webm"} VIDEO_CONTAINERS = {"mp4", "mkv", "webm"}
AUDIO_CONTAINERS = {"m4a", "mp3", "opus", "flac", "wav"} AUDIO_CONTAINERS = {"m4a", "mp3", "opus", "flac", "wav"}
+3 -2
View File
@@ -64,6 +64,7 @@ def install(key, progress, workspace=None):
raise ValueError("Нажмите «Обновить компоненты» для установки новых версий из интернета или «Восстановить» для переустановки встроенного комплекта.") raise ValueError("Нажмите «Обновить компоненты» для установки новых версий из интернета или «Восстановить» для переустановки встроенного комплекта.")
if key not in {"node", "ffmpeg", "yt_dlp", "ejs"}: if key not in {"node", "ffmpeg", "yt_dlp", "ejs"}:
raise ValueError("Неизвестный компонент.") raise ValueError("Неизвестный компонент.")
from host.python_packages import requirements
runtime = ROOT / ".runtime" runtime = ROOT / ".runtime"
runtime.mkdir(exist_ok=True) runtime.mkdir(exist_ok=True)
cfg = config() cfg = config()
@@ -73,8 +74,8 @@ def install(key, progress, workspace=None):
workspace(str(scratch)) workspace(str(scratch))
if key in {"yt_dlp", "ejs"}: if key in {"yt_dlp", "ejs"}:
progress(None, "Устанавливаю Python-пакет") progress(None, "Устанавливаю Python-пакет")
package = "yt-dlp[default]>=2026.8.19" if key == "yt_dlp" else "yt-dlp-ejs" packages = requirements() if key == "yt_dlp" else ["yt-dlp-ejs"]
measured = subprocess.run([sys.executable, "-m", "pip", "install", "--disable-pip-version-check", "--upgrade", package], 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) stdin=subprocess.DEVNULL, capture_output=True, text=True, encoding="utf-8", errors="replace", creationflags=NO_WINDOW, timeout=600)
if measured.returncode: if measured.returncode:
raise ValueError("Не удалось установить компонент. " + measured.stderr[-1200:]) raise ValueError("Не удалось установить компонент. " + measured.stderr[-1200:])
+41
View File
@@ -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"(?<![A-Z])(?:A?GPL)(?:[-\s\dv]|$)|GNU (?:Affero )?General Public License",
license, re.IGNORECASE) for license in licenses)
if incompatible:
raise ValueError(f"Python-пакет {name} требует отдельной проверки совместимости лицензии; комплект не изменён.")
def validate_installed(directory: Path):
records = []
for distribution in importlib.metadata.distributions(path=[str(directory)]):
metadata = distribution.metadata
records.append({"name": metadata.get("Name", ""),
"license_expression": metadata.get("License-Expression", ""),
"license": metadata.get("License", ""),
"classifier": metadata.get_all("Classifier") or []})
validate_metadata(records)
return {distribution.metadata["Name"]: distribution.version
for distribution in importlib.metadata.distributions(path=[str(directory)])}
+4 -1
View File
@@ -19,6 +19,7 @@ from urllib.parse import urlsplit
import requests import requests
from host.bundle import ROOT, installation_root, manifest from host.bundle import ROOT, installation_root, manifest
from host.media import NO_WINDOW, run_small from host.media import NO_WINDOW, run_small
from host.python_packages import requirements, validate_metadata, validate_installed
ALLOWED_HOSTS = {"www.python.org", "nodejs.org", "api.github.com", "github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com", "pypi.org", "files.pythonhosted.org"} ALLOWED_HOSTS = {"www.python.org", "nodejs.org", "api.github.com", "github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com", "pypi.org", "files.pythonhosted.org"}
FFMPEG_ASSET = "ffmpeg-master-latest-win64-gpl-shared.zip" FFMPEG_ASSET = "ffmpeg-master-latest-win64-gpl-shared.zip"
@@ -183,11 +184,12 @@ def resolve_packages(stage, work, progress):
report = work / "python-packages.json" report = work / "python-packages.json"
script = "import sys;sys.path.insert(0,sys.argv.pop(1));from pip._internal.cli.main import main;raise SystemExit(main(sys.argv[1:]))" script = "import sys;sys.path.insert(0,sys.argv.pop(1));from pip._internal.cli.main import main;raise SystemExit(main(sys.argv[1:]))"
command = [str(stage / "runtime/python/python.exe"), "-I", "-X", "utf8", "-B", "-c", script, str(pip), "--isolated", "--disable-pip-version-check", "install", command = [str(stage / "runtime/python/python.exe"), "-I", "-X", "utf8", "-B", "-c", script, str(pip), "--isolated", "--disable-pip-version-check", "install",
"--dry-run", "--ignore-installed", "--only-binary=:all:", "--no-cache-dir", "--quiet", "--report", str(report), "--index-url", "https://pypi.org/simple", "yt-dlp[default]", "yt-dlp-ejs"] "--dry-run", "--ignore-installed", "--only-binary=:all:", "--no-cache-dir", "--quiet", "--report", str(report), "--index-url", "https://pypi.org/simple", *requirements()]
result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=600, creationflags=NO_WINDOW, result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=600, creationflags=NO_WINDOW,
env={**os.environ, "TEMP": str(work), "TMP": str(work)}, stdin=subprocess.DEVNULL) env={**os.environ, "TEMP": str(work), "TMP": str(work)}, stdin=subprocess.DEVNULL)
if result.returncode: raise ValueError("Не удалось подобрать совместимые Python-пакеты. " + result.stderr[-1200:]) if result.returncode: raise ValueError("Не удалось подобрать совместимые Python-пакеты. " + result.stderr[-1200:])
records = json.loads(report.read_text(encoding="utf-8"))["install"] records = json.loads(report.read_text(encoding="utf-8"))["install"]
validate_metadata([record["metadata"] for record in records])
packages = stage / "runtime/python/Lib/site-packages" packages = stage / "runtime/python/Lib/site-packages"
if packages.exists(): shutil.rmtree(packages) if packages.exists(): shutil.rmtree(packages)
packages.mkdir(parents=True) packages.mkdir(parents=True)
@@ -206,6 +208,7 @@ def resolve_packages(stage, work, progress):
if packages.resolve() not in destination.parents: raise ValueError("Некорректный путь Python-пакета.") if packages.resolve() not in destination.parents: raise ValueError("Некорректный путь Python-пакета.")
destination.parent.mkdir(parents=True, exist_ok=True) destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(wheel.read(member)) destination.write_bytes(wheel.read(member))
validate_installed(packages)
return {item["metadata"]["name"]: item["metadata"]["version"] for item in records} return {item["metadata"]["name"]: item["metadata"]["version"] for item in records}
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "youtubedl", "name": "youtubedl",
"version": "1.4.6", "version": "1.4.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "youtubedl", "name": "youtubedl",
"version": "1.4.6", "version": "1.4.7",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"devDependencies": { "devDependencies": {
"playwright": "^1.64.0" "playwright": "^1.64.0"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "youtubedl", "name": "youtubedl",
"version": "1.4.6", "version": "1.4.7",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"private": true, "private": true,
"type": "module", "type": "module",
+8 -1
View File
@@ -1 +1,8 @@
yt-dlp[default]>=2026.8.19 yt-dlp>=2026.8.19
yt-dlp-ejs
brotli
certifi
pycryptodomex
requests>=2.32.2,<3
urllib3>=2,<3
websockets>=13
+5 -5
View File
@@ -40,7 +40,7 @@
<div class="hero-actions"><a class="button primary" href="#install">Установить расширение <span aria-hidden="true">↓</span></a><a class="hero-link" href="#features">Посмотреть возможности <span aria-hidden="true">↗</span></a></div> <div class="hero-actions"><a class="button primary" href="#install">Установить расширение <span aria-hidden="true">↓</span></a><a class="hero-link" href="#features">Посмотреть возможности <span aria-hidden="true">↗</span></a></div>
<p class="hero-note">Бесплатно · Для Windows · Обработка на вашем компьютере</p> <p class="hero-note">Бесплатно · Для Windows · Обработка на вашем компьютере</p>
</div> </div>
<div class="hero-bottom"><span>ВИДЕО. ЗВУК. ВАШ ВЫБОР.</span><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/tag/v1.4.6">Выпуск 1.4.6 <span aria-hidden="true">↗</span></a></div> <div class="hero-bottom"><span>ВИДЕО. ЗВУК. ВАШ ВЫБОР.</span><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/tag/v1.4.7">Выпуск 1.4.7 <span aria-hidden="true">↗</span></a></div>
</div> </div>
</section> </section>
<div class="format-strip"><div class="wrap"><p>Один ролик.<br><strong>Ваш формат.</strong></p><div class="format-list" aria-label="Форматы"><span>MP4</span><span>MKV</span><span>WebM</span><i aria-hidden="true"></i><span>MP3</span><span>M4A</span><span>Opus</span><span>FLAC</span><span>WAV</span></div></div></div> <div class="format-strip"><div class="wrap"><p>Один ролик.<br><strong>Ваш формат.</strong></p><div class="format-list" aria-label="Форматы"><span>MP4</span><span>MKV</span><span>WebM</span><i aria-hidden="true"></i><span>MP3</span><span>M4A</span><span>Opus</span><span>FLAC</span><span>WAV</span></div></div></div>
@@ -70,8 +70,8 @@
<section class="install-section" id="install" aria-labelledby="install-title"><div class="wrap"> <section class="install-section" id="install" aria-labelledby="install-title"><div class="wrap">
<div class="install-heading reveal"><div><p class="overline">04 / НАЧНЁМ</p><h2 id="install-title">Два скачивания.<br><em>Один комплект.</em></h2></div><p>Windows 10/11 x64<br>Edge или Chrome 120+<br><span>Без прав администратора</span></p></div> <div class="install-heading reveal"><div><p class="overline">04 / НАЧНЁМ</p><h2 id="install-title">Два скачивания.<br><em>Один комплект.</em></h2></div><p>Windows 10/11 x64<br>Edge или Chrome 120+<br><span>Без прав администратора</span></p></div>
<div class="download-grid reveal"> <div class="download-grid reveal">
<article class="download-card"><div class="download-card-top"><span class="package-number">1</span><span class="package-tag">БРАУЗЕР</span><span class="version">v1.4.6</span></div><h3>Расширение</h3><p>Кнопки на YouTube, настройки и уведомления. Устанавливается распакованным.</p><a class="button primary package-link" data-download="extension" href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/download/v1.4.6/YouTubeDL-1.4.6.zip">Скачать расширение <span aria-hidden="true">↓</span></a><small>ZIP · <span id="extension-size">Небольшой архив</span></small></article> <article class="download-card"><div class="download-card-top"><span class="package-number">1</span><span class="package-tag">БРАУЗЕР</span><span class="version">v1.4.7</span></div><h3>Расширение</h3><p>Кнопки на YouTube, настройки и уведомления. Устанавливается распакованным.</p><a class="button primary package-link" data-download="extension" href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/download/v1.4.7/YouTubeDL-1.4.7.zip">Скачать расширение <span aria-hidden="true">↓</span></a><small>ZIP · <span id="extension-size">Небольшой архив</span></small></article>
<article class="download-card helper-card"><div class="download-card-top"><span class="package-number">2</span><span class="package-tag">КОМПЬЮТЕР</span><span class="version">v1.3.2</span></div><h3>Локальный помощник</h3><p>Скачивает, соединяет и конвертирует. Все необходимые компоненты уже внутри.</p><a class="button secondary package-link" data-download="helper" href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/download/v1.4.6/YouTubeDL-Helper-Setup.exe">Скачать помощник <span aria-hidden="true">↓</span></a><small>EXE · 124,3 МиБ · устанавливается отдельно</small></article> <article class="download-card helper-card"><div class="download-card-top"><span class="package-number">2</span><span class="package-tag">КОМПЬЮТЕР</span><span class="version">v1.3.3</span></div><h3>Локальный помощник</h3><p>Скачивает, соединяет и конвертирует. Все необходимые компоненты уже внутри.</p><a class="button secondary package-link" data-download="helper" href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/download/v1.4.7/YouTubeDL-Helper-Setup.exe">Скачать помощник <span aria-hidden="true">↓</span></a><small>EXE · 124,1 МиБ · устанавливается отдельно</small></article>
</div> </div>
<p class="download-note">Оба файла размещены в <a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/latest">релизах Gitea ↗</a>. Помощник не встроен в ZIP. <a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/src/branch/main/INSTALL.md#проверка-скачанных-файлов">Проверка SHA256 и подробности</a>.</p> <p class="download-note">Оба файла размещены в <a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/releases/latest">релизах Gitea ↗</a>. Помощник не встроен в ZIP. <a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/src/branch/main/INSTALL.md#проверка-скачанных-файлов">Проверка SHA256 и подробности</a>.</p>
<ol class="install-steps reveal"> <ol class="install-steps reveal">
@@ -93,11 +93,11 @@
<details><summary>Как удалить всё?<span aria-hidden="true">+</span></summary><p>Удалите помощник кнопкой в меню или через приложения Windows, затем расширение из браузера. Скачанные файлы и история помощника сохраняются. Для удаления истории после удаления помощника удалите <code>%LOCALAPPDATA%\YouTubeDL\Helper\data</code>. Настройки расширения удаляются вместе с расширением.</p></details> <details><summary>Как удалить всё?<span aria-hidden="true">+</span></summary><p>Удалите помощник кнопкой в меню или через приложения Windows, затем расширение из браузера. Скачанные файлы и история помощника сохраняются. Для удаления истории после удаления помощника удалите <code>%LOCALAPPDATA%\YouTubeDL\Helper\data</code>. Настройки расширения удаляются вместе с расширением.</p></details>
<details><summary>Почему расширения нет в Chrome Web Store?<span aria-hidden="true">+</span></summary><p>YouTubeDL распространяется через релизы Gitea из-за ограничений на скачивание контента. <a href="https://developer.chrome.com/docs/webstore/program-policies/malicious-and-prohibited" target="_blank" rel="noopener noreferrer">Правила Chrome Web Store</a> запрещают инструменты для неразрешённого скачивания защищённого контента, а <a href="https://www.youtube.com/static?template=terms" target="_blank" rel="noopener noreferrer">условия YouTube</a> ограничивают скачивание разрешёнными сервисом способами или письменным разрешением YouTube и соответствующих правообладателей. Поэтому для YouTubeDL выбрана ручная установка распакованного расширения из официального релиза.</p></details> <details><summary>Почему расширения нет в Chrome Web Store?<span aria-hidden="true">+</span></summary><p>YouTubeDL распространяется через релизы Gitea из-за ограничений на скачивание контента. <a href="https://developer.chrome.com/docs/webstore/program-policies/malicious-and-prohibited" target="_blank" rel="noopener noreferrer">Правила Chrome Web Store</a> запрещают инструменты для неразрешённого скачивания защищённого контента, а <a href="https://www.youtube.com/static?template=terms" target="_blank" rel="noopener noreferrer">условия YouTube</a> ограничивают скачивание разрешёнными сервисом способами или письменным разрешением YouTube и соответствующих правообладателей. Поэтому для YouTubeDL выбрана ручная установка распакованного расширения из официального релиза.</p></details>
<details><summary>Какие ОС поддерживаются?<span aria-hidden="true">+</span></summary><p>Сейчас опубликована сборка для Windows 10/11 x64 и Edge/Chrome 120+. Firefox, macOS, Linux и Windows ARM не входят в неё. Плейлисты, скачивание каналов и DRM-контент не заявляются.</p></details> <details><summary>Какие ОС поддерживаются?<span aria-hidden="true">+</span></summary><p>Сейчас опубликована сборка для Windows 10/11 x64 и Edge/Chrome 120+. Firefox, macOS, Linux и Windows ARM не входят в неё. Плейлисты, скачивание каналов и DRM-контент не заявляются.</p></details>
<details><summary>Можно изменять расширение и публиковать свои версии?<span aria-hidden="true">+</span></summary><p><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/src/branch/main/LICENSE">Лицензия</a> разрешает использование по назначению и запрещает изменения, самостоятельные форки и распространение собственного кода без письменного разрешения, с учётом исключений в её тексте. Сторонние компоненты сохраняют свои лицензии.</p></details> <details><summary>Можно изменять расширение и публиковать свои версии?<span aria-hidden="true">+</span></summary><p><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/src/branch/main/LICENSE">Лицензия Source Available</a> разрешает просмотр исходников, сборку неизменённой версии и использование по назначению и запрещает изменения, самостоятельные форки и распространение собственного кода без письменного разрешения, с учётом исключений в её тексте. Сторонние компоненты сохраняют свои лицензии.</p></details>
</div></div></section> </div></div></section>
<section class="closing"><div class="wrap"><img src="assets/mark.svg" alt="" width="48" height="48"><h2>Видео. Звук.<br><span>Ваш выбор.</span></h2><a class="button primary" href="#install">Скачать YouTubeDL <span aria-hidden="true">↓</span></a></div></section> <section class="closing"><div class="wrap"><img src="assets/mark.svg" alt="" width="48" height="48"><h2>Видео. Звук.<br><span>Ваш выбор.</span></h2><a class="button primary" href="#install">Скачать YouTubeDL <span aria-hidden="true">↓</span></a></div></section>
</main> </main>
<footer class="site-footer"><div class="wrap footer-top"><a class="brand" href="#"><img src="assets/mark.svg" alt="" width="28" height="28"><span>YouTubeDL</span></a><div><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL">Исходники ↗</a><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/src/branch/main/PRIVACY.md">Данные</a><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/src/branch/main/LICENSE">Лицензия</a><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/src/branch/main/THIRD_PARTY.md">Компоненты</a><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/issues">Поддержка</a></div></div><div class="wrap footer-bottom"><p>© 2026 DepressedCat. YouTubeDL распространяется по ограниченной пользовательской лицензии.</p><p>Независимый проект. Не связан с YouTube/Google, NVIDIA или AMD. Сохраняйте контент, который вы вправе скачивать.</p><p>На сайте нет счётчиков и собственных cookies. Хостинг — собственный сервер проекта.</p></div></footer> <footer class="site-footer"><div class="wrap footer-top"><a class="brand" href="#"><img src="assets/mark.svg" alt="" width="28" height="28"><span>YouTubeDL</span></a><div><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL">Исходники ↗</a><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/src/branch/main/PRIVACY.md">Данные</a><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/src/branch/main/LICENSE">Лицензия</a><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/src/branch/main/THIRD_PARTY.md">Компоненты</a><a href="https://git.lonarpgproxy.space/DepressedCat/YouTubeDL/issues">Поддержка</a></div></div><div class="wrap footer-bottom"><p>© 2026 DepressedCat. YouTubeDL распространяется по лицензии Source Available.</p><p>Независимый проект. Не связан с YouTube/Google, NVIDIA или AMD. Сохраняйте контент, который вы вправе скачивать.</p><p>На сайте нет счётчиков и собственных cookies. Хостинг — собственный сервер проекта.</p></div></footer>
<div id="copy-status" class="copy-status" role="status" aria-live="polite" hidden></div> <div id="copy-status" class="copy-status" role="status" aria-live="polite" hidden></div>
</body> </body>
</html> </html>
+6 -6
View File
@@ -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); assert.equal(stabilizeDownloadSpeed({ ...sample, stage: "merging", speed: "8x" }, previous, 1200).speedUpdatedAt, null);
}); });
test("helper compatibility detects outdated, newer and unidentifiable versions in both directions", () => { 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({ version: "1.3.3" }, "1.4.5").compatible, true);
assert.equal(checkHelperCompatibility({ bundle: { version: "1.3.2" } }, "1.4.5").compatible, true); assert.equal(checkHelperCompatibility({ bundle: { version: "1.3.3" } }, "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.0" }, "1.4.5").compatible, true);
for (const version of ["1.3.0", "1.2.10", undefined, "unknown"]) { for (const version of ["1.3.0", "1.2.10", undefined, "unknown"]) {
const result = checkHelperCompatibility({ version }, "1.4.5"); const result = checkHelperCompatibility({ version }, "1.4.5");
assert.equal(result.compatible, false); assert.equal(result.compatible, false);
assert.equal(result.update, "helper"); 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"); const result = checkHelperCompatibility({ version }, "1.4.5");
assert.equal(result.compatible, false); assert.equal(result.compatible, false);
assert.equal(result.update, "extension"); assert.equal(result.update, "extension");
} }
assert.equal(checkHelperCompatibility({ version: "1.3.2" }, "1.4.2").update, "extension"); assert.equal(checkHelperCompatibility({ version: "1.3.3" }, "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", minimumExtensionVersion: "1.4.7" }, "1.4.5").update, "extension");
}); });
+1 -1
View File
@@ -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() {}, return { onMessage: { addListener: listener => { receive = listener; } }, onDisconnect: noEvent, disconnect() {},
postMessage(message) { postMessage(message) {
nativeRequests.push(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)) } }, globalThis.chrome = { runtime, storage: { local: { get: async () => structuredClone(storage), set: async data => Object.assign(storage, structuredClone(data)) } },
+3 -3
View File
@@ -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 () => { test("native lifecycle recovers stale ports and publishes install/remove/update state to all menus", async () => {
const ports = [], listeners = {}, storage = {}, pending = new Map(), updates = []; const ports = [], listeners = {}, storage = {}, pending = new Map(), updates = [];
let installed = false; let installed = false;
let helperVersion = "1.3.2"; let helperVersion = "1.3.3";
let componentsReady = true; let componentsReady = true;
const noEvent = { addListener() {} }; const noEvent = { addListener() {} };
const runtime = { id: "test-id", getManifest: () => ({ version: "1.4.5" }), getURL: name => `chrome-extension://test-id/${name}`, 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)); 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.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")); 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; helperVersion = version;
ports.at(-1).disconnect(); ports.at(-1).disconnect();
await new Promise(resolve => setImmediate(resolve)); 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.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")); 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'); assert.equal((await call("helper_status")).result.status, "running", 'Version repair is detected by the Check button without reloading');
}); });
+1 -1
View File
@@ -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() {}, onMessage: { addListener(listener) { nativeListener = listener; } }, onDisconnect: noEvent, disconnect() {},
postMessage(message) { postMessage(message) {
requests.push(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: [] }; : message.action === "download" ? { jobId: "auth-test-job" } : { title: "Synthetic video", videos: [], audios: [] };
queueMicrotask(() => nativeListener({ id: message.id, ok: true, result })); queueMicrotask(() => nativeListener({ id: message.id, ok: true, result }));
}, },
+1 -1
View File
@@ -35,7 +35,7 @@ try {
assert.equal(new URL(page.url()).hash,"#install"); assert.equal(new URL(page.url()).hash,"#install");
assert.ok(await page.locator(".download-grid").isVisible()); 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="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('[data-screenshot="light"]').click();
await page.locator("#menu-screenshot").evaluate(image => image.decode()); await page.locator("#menu-screenshot").evaluate(image => image.decode());
assert.match(await page.locator("#menu-screenshot").getAttribute("src"), /menu-light\.png$/); assert.match(await page.locator("#menu-screenshot").getAttribute("src"), /menu-light\.png$/);
+53
View File
@@ -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()
+2 -2
View File
@@ -18,7 +18,7 @@
window.__contentMessage = message => contentListeners.forEach(listener => listener(message)); window.__contentMessage = message => contentListeners.forEach(listener => listener(message));
let autoUpdate = true; let autoUpdate = true;
window.__updateRequests = []; 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"], 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, 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" })) }); 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 }] }; ], audios: [{ id: "251", codec: "opus", extension: "webm", bitrate: 160, language: "en", note: "English", size: 5000000 }] };
window.chrome = { window.chrome = {
runtime: { runtime: {
getManifest: () => ({ version: window.__extensionVersion || "1.4.6" }), getManifest: () => ({ version: window.__extensionVersion || "1.4.7" }),
getURL: file => `https://ui.ytdl.test/${file}`, getURL: file => `https://ui.ytdl.test/${file}`,
sendMessage: async message => { sendMessage: async message => {
if (message.type === "QUICK_DOWNLOAD") { window.__quickRequests.push(message.url); return { ok: true, jobId: "fixture-job" }; } if (message.type === "QUICK_DOWNLOAD") { window.__quickRequests.push(message.url); return { ok: true, jobId: "fixture-job" }; }
+7 -7
View File
@@ -20,8 +20,8 @@ try {
page.on("pageerror", error => errors.push(error.message)); 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.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(); 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-message').textContent(), /1\.3\.0.*1\.4\.7/);
assert.match(await page.locator('#helper-versions').textContent(), /Расширение 1\.4\.6.*Помощник 1\.3\.0/); 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('#helper-install').textContent(), 'Обновить помощник');
assert.equal(await page.locator('#download').isDisabled(), true); assert.equal(await page.locator('#download').isDisabled(), true);
assert.equal(await page.locator('#download-form').isVisible(), false); assert.equal(await page.locator('#download-form').isVisible(), false);
@@ -30,13 +30,13 @@ try {
await page.locator('#setup-helper[data-state="incompatible"]').waitFor(); await page.locator('#setup-helper[data-state="incompatible"]').waitFor();
assert.equal(await page.locator('#setup-next').isDisabled(), true); assert.equal(await page.locator('#setup-next').isDisabled(), true);
assert.equal(await page.locator('#setup-install').textContent(), 'Обновить помощник'); assert.equal(await page.locator('#setup-install').textContent(), 'Обновить помощник');
assert.match(await page.locator('#setup-versions').textContent(), /1\.4\.6.*1\.3\.0/); assert.match(await page.locator('#setup-versions').textContent(), /1\.4\.7.*1\.3\.0/);
await page.evaluate(() => { window.__helperVersion = '1.3.2'; }); await page.evaluate(() => { window.__helperVersion = '1.3.3'; });
await page.locator('#setup-check').click(); await page.locator('#setup-check').click();
await page.locator('#setup-helper[data-state="running"]').waitFor(); await page.locator('#setup-helper[data-state="running"]').waitFor();
assert.match(await page.locator('#setup-versions').textContent(), /Версии совместимы/); assert.match(await page.locator('#setup-versions').textContent(), /Версии совместимы/);
await page.locator('#setup-next:not([disabled])').waitFor(); 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-check').click();
await page.locator('#setup-helper[data-state="incompatible"]').waitFor(); await page.locator('#setup-helper[data-state="incompatible"]').waitFor();
assert.equal(await page.locator('#setup-next').isDisabled(), true); assert.equal(await page.locator('#setup-next').isDisabled(), true);
@@ -44,9 +44,9 @@ try {
assert.equal(await page.locator('#setup-install-steps').isVisible(), false); assert.equal(await page.locator('#setup-install-steps').isVisible(), false);
await page.locator('#setup-close').click(); await page.locator('#setup-close').click();
assert.equal(await page.locator('#helper-install').textContent(), 'Обновить расширение'); 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); 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-start').click();
await page.locator('#helper-panel[data-state="running"]').waitFor(); await page.locator('#helper-panel[data-state="running"]').waitFor();
await page.locator('#download:not([disabled])').waitFor(); await page.locator('#download:not([disabled])').waitFor();
+8 -3
View File
@@ -26,6 +26,8 @@ PYTHON_SHA256 = "90b4e5b9898b72d744650524bff92377c367f44bd5fbd09e3148656c080ad90
NODE = "v24.21.0" NODE = "v24.21.0"
FFMPEG_ARCHIVE = "ffmpeg-master-latest-win64-gpl-shared.zip" FFMPEG_ARCHIVE = "ffmpeg-master-latest-win64-gpl-shared.zip"
FFMPEG_SHA256 = "eb7afd4ecced72ab7d5d6fdd36dbc359173aa2daf6081817f7b678345898838b" FFMPEG_SHA256 = "eb7afd4ecced72ab7d5d6fdd36dbc359173aa2daf6081817f7b678345898838b"
sys.path.insert(0, str(ROOT))
from host.python_packages import validate_installed
def digest(path): def digest(path):
@@ -107,10 +109,11 @@ def prepare():
wheels = CACHE / "wheels"; wheels.mkdir(exist_ok=True) wheels = CACHE / "wheels"; wheels.mkdir(exist_ok=True)
options = ["--platform", "win_amd64", "--python-version", "3.13", "--implementation", "cp", options = ["--platform", "win_amd64", "--python-version", "3.13", "--implementation", "cp",
"--abi", "cp313", "--abi", "abi3", "--abi", "none", "--only-binary", ":all:"] "--abi", "cp313", "--abi", "abi3", "--abi", "none", "--only-binary", ":all:"]
requirement = "yt-dlp[default]==2026.8.19" 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, requirement], check=True) 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), 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( (licenses / "THIRD-PARTY.txt").write_text(
"Python: https://www.python.org/ ; license: runtime/python/LICENSE.txt\n" "Python: https://www.python.org/ ; license: runtime/python/LICENSE.txt\n"
"Node.js: https://nodejs.org/ ; license: Node-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 # pip's generated EXE launchers refer to the build interpreter. The helper
# imports these libraries directly, so no console launchers are shipped. # imports these libraries directly, so no console launchers are shipped.
shutil.rmtree(scripts) 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")) shutil.copytree(ROOT / "host", KIT / "host", dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from host.common import VERSION from host.common import VERSION