From 0a3cf972008c6c6b58c9245edc66e00507cd45fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D1=8F=D1=82=D0=BA=D0=B8=D0=BD=D0=90=D1=80=D1=82?= =?UTF-8?q?=D1=91=D0=BC?= Date: Wed, 27 May 2026 17:59:21 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8E:=20=D1=83=D1=82=D0=BE=D1=87=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=20=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=D1=87=D0=B8=D0=B2=D0=B0=D0=B5=D0=BC=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B2=D0=B8=D0=B4=D0=B5=D0=BE,=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80?= =?UTF-8?q?=D1=8B=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D1=82=D1=80=D0=B5=D0=B1?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BA=20session=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 258 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 213 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 79d264e..703101f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,33 @@ # yt-shorts-downloader -Библиотека и CLI для скачивания YouTube Shorts через Netscape cookie session. +Библиотека и CLI для скачивания YouTube Shorts в формате MP4 через Netscape cookie session file. -Целевой контракт публичного API: +Основной контракт проекта: -- вход: ссылка YouTube и путь к session file -- выход: бинарный MP4 в памяти для интеграции в API +- вход: ссылка YouTube и путь к cookie-файлу +- выход для API: MP4 в памяти +- выход для CLI: путь к сохранённому MP4-файлу + +## Возможности + +- типизированный Python API +- CLI для сохранения файла на диск +- предварительная валидация YouTube URL +- предварительная валидация session file +- интеграция с yt-dlp +- strict mypy и локальные stubs для нетипизированных зависимостей +- автоматическая публикация пакета в Gitea Packages после успешного CI + +## Требования + +- Python 3.12+ +- валидный Netscape cookie file из авторизованной YouTube-сессии +- один поддерживаемый JavaScript runtime: + - Deno + - Node.js 22+ +- опционально, но желательно: ffmpeg + +Если `ffmpeg` отсутствует, проект использует более простой MP4 format selector. ## Установка @@ -21,93 +43,239 @@ uv pip install . uv sync --group dev ``` -## Использование библиотеки +## Быстрый старт + +### Python API ```python from yt_shorts_downloader import download video = download( - "https://www.youtube.com/shorts/VIDEO_ID", - "cookies.txt", + 'https://www.youtube.com/shorts/VIDEO_ID', + 'cookies.txt', ) -assert video.media_type == "video/mp4" +assert video.media_type == 'video/mp4' +assert video.filename.endswith('.mp4') + binary_mp4 = video.content -filename = video.filename ``` -Если нужен файл на диске, используйте вспомогательную функцию: +Если нужен файл на диске: ```python from yt_shorts_downloader import download_to_path file_path = download_to_path( - "https://www.youtube.com/shorts/VIDEO_ID", - "cookies.txt", - output_dir="downloads", + 'https://www.youtube.com/shorts/VIDEO_ID', + 'cookies.txt', + output_dir='downloads', ) ``` -## CLI +### CLI ```bash uv run yt-shorts-downloader \ - "https://www.youtube.com/shorts/VIDEO_ID" \ - cookies.txt \ - --output-dir downloads + 'https://www.youtube.com/shorts/VIDEO_ID' \ + cookies.txt \ + --output-dir downloads ``` +CLI печатает в stdout итоговый путь к скачанному файлу. + +## Пример интеграции с FastAPI + +```python +from fastapi import FastAPI, HTTPException +from fastapi.responses import Response + +from yt_shorts_downloader import download +from yt_shorts_downloader.exceptions import YtShortsDownloaderError + +app = FastAPI() + + +@app.get('/download') +def download_short(url: str) -> Response: + try: + video = download(url, 'cookies.txt') + except YtShortsDownloaderError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + return Response( + content=video.content, + media_type=video.media_type, + headers={ + 'Content-Disposition': f'attachment; filename="{video.filename}"', + }, + ) +``` + +## Публичный API + +### `download(url, session_path) -> DownloadedVideo` + +Скачивает видео во временную директорию, проверяет, что итоговый артефакт имеет формат MP4, и возвращает объект `DownloadedVideo`. + +Поля `DownloadedVideo`: + +- `filename`: имя файла +- `content`: MP4-байты +- `media_type`: всегда `video/mp4` + +### `download_short(url, session_path) -> DownloadedVideo` + +Алиас для `download`. + +### `download_to_path(url, session_path, output_dir=None) -> Path` + +Скачивает итоговый MP4 на диск и возвращает `Path` к файлу. + +### `validate_session_file(path) -> SessionValidation` + +Проверяет session file до попытки скачивания. Поля результата: + +- `exists` +- `structurally_valid` +- `fresh` +- `is_usable` +- `message` + +## Требования к session file + +Проект ожидает Netscape cookie file, например экспортированный из браузера. + +Проверяется: + +- существование файла +- структура Netscape cookie format с 7 tab-separated columns +- наличие cookie для YouTube или Google +- наличие достаточного количества актуальных auth-cookie + +Ключевые cookie-имена: + +- `SID` +- `HSID` +- `SSID` +- `APISID` +- `SAPISID` +- `__Secure-1PSID` +- `__Secure-3PSID` +- `LOGIN_INFO` + +Если файл структурно валиден, но auth-cookie устарели или неполные, библиотека выбрасывает `InvalidSessionError` до запуска yt-dlp. + +## Ошибки + +Пакет экспортирует следующие исключения: + +- `YtShortsDownloaderError`: базовое исключение библиотеки +- `InvalidUrlError`: неподдерживаемая или некорректная ссылка +- `InvalidSessionError`: невалидный или устаревший session file +- `JsRuntimeUnavailableError`: не найден Deno или Node.js 22+ +- `VideoDownloadError`: yt-dlp не смог получить валидный MP4 + +## Диагностика и troubleshooting + +Частые причины проблем: + +- `InvalidUrlError`: ссылка не относится к YouTube или имеет неверную схему +- `InvalidSessionError`: cookie-файл не найден, повреждён или содержит устаревшие auth-cookie +- `JsRuntimeUnavailableError`: в системе нет Deno и нет Node.js 22+ +- `VideoDownloadError`: yt-dlp не смог скачать видео или итоговый файл не оказался MP4 +- публикация в Gitea Packages пропускается: версия из `pyproject.toml` уже существует в registry + +Практические рекомендации: + +- переэкспортируйте cookies после переавторизации в браузере +- установите `ffmpeg`, если хотите лучший вариант объединения аудио и видео +- увеличивайте `project.version` в `pyproject.toml` перед новой публикацией пакета +- не коммитьте cookie-файлы в репозиторий + ## Архитектура -- src/yt_shorts_downloader/api.py: публичный API библиотеки -- src/yt_shorts_downloader/models: доменные модели -- src/yt_shorts_downloader/services: валидация и разбор session file -- src/yt_shorts_downloader/core: интеграция с yt-dlp и инфраструктурные функции -- stubs: локальные mypy stubs для внешних зависимостей без полной типизации +Структура проекта разделена по зонам ответственности: -## Качество +- `src/yt_shorts_downloader/api.py`: публичный API библиотеки +- `src/yt_shorts_downloader/cli.py`: CLI entrypoint +- `src/yt_shorts_downloader/models`: доменные модели и типизированные возвращаемые объекты +- `src/yt_shorts_downloader/services`: логика валидации session file +- `src/yt_shorts_downloader/core`: интеграция с yt-dlp, поиск runtime и URL validation +- `stubs`: локальные typing stubs для внешних пакетов без полной типизации +Это позволяет держать публичный API компактным, а инфраструктурный код изолировать в `core`. + +## Разработка + +Инструменты настраиваются в `pyproject.toml`. + +- runtime-зависимости находятся в `[project.dependencies]` +- инструменты разработки находятся в `[dependency-groups.dev]` - mypy работает в strict-режиме -- runtime-зависимости лежат в секции project.dependencies -- инструменты разработки лежат в секции dependency-groups.dev +- Ruff отвечает за lint и format Основные команды: ```bash make format -make ci-check +make lint +make typecheck make test +make ci-check make build +make package-version ``` -## CI/CD +## Пакетирование и публикация -В репозитории добавлены Gitea Actions workflow: +Метаданные пакета и версия берутся из `[project]` в `pyproject.toml`. -- .gitea/workflows/ci.yml: lint, mypy, deptry, pytest и автоматическая публикация в Gitea Packages после успешного CI на push в main +Текущий publish flow: -Публикация настроена в Gitea PyPI registry по документации Gitea Packages. -Версия пакета берётся из секции [project] -> version в pyproject.toml. -Gitea не поддерживает повторную публикацию той же версии, поэтому CI сначала проверяет registry и пропускает upload, если эта версия уже существует. +- Gitea Actions запускает quality checks и tests на pull request и push в `main` +- после успешного CI на `main` workflow проверяет, существует ли текущая версия в Gitea Packages +- если версии ещё нет, пакет собирается и публикуется +- если версия уже существует, публикация пропускается без падения workflow +- старые версии пакета сохраняются в registry -Нужные secrets для CI workflow: +Необходимые secrets для CI: -- PYPI_REPOSITORY_URL: полный endpoint вида https://gitea.example.com/api/packages//pypi -- PACKAGE_USERNAME: пользователь Gitea -- PACKAGE_TOKEN: personal access token с правом package write +- `PYPI_REPOSITORY_URL`: endpoint вида `https://gitea.example.com/api/packages//pypi` +- `PACKAGE_USERNAME`: пользователь Gitea +- `PACKAGE_TOKEN`: personal access token с правом package write -Публикация идёт автоматически на push в main после успешных lint/typecheck/test jobs. -Если версия пакета уже существует в registry, upload будет пропущен до вызова twine upload. -Если версия в pyproject.toml увеличена, в Gitea Packages появится новая версия пакета, а старые версии останутся доступными. - -Локально тот же сценарий можно выполнить так: +Локальная публикация: ```bash -export PYPI_REPOSITORY_URL="https://gitea.example.com/api/packages//pypi" -export PACKAGE_USERNAME="" -export PACKAGE_TOKEN="" +export PYPI_REPOSITORY_URL='https://gitea.example.com/api/packages//pypi' +export PACKAGE_USERNAME='' +export PACKAGE_TOKEN='' + make package-version make publish-gitea ``` -Для обратной совместимости локальная команда publish-gitea также принимает переменные GITEA_PYPI_REPOSITORY_URL, GITEA_PACKAGE_USERNAME и GITEA_PACKAGE_TOKEN. +Для обратной совместимости `publish-gitea` также принимает: + +- `GITEA_PYPI_REPOSITORY_URL` +- `GITEA_PACKAGE_USERNAME` +- `GITEA_PACKAGE_TOKEN` + +## CI/CD + +Основной workflow: + +- `.gitea/workflows/ci.yml` + +Он выполняет: + +- Ruff linting +- strict mypy type checking +- deptry dependency checks +- pytest test suite +- условную публикацию в Gitea Packages после успешного push CI на `main` + +## Лицензия + +См. `LICENSE`.