282 lines
9.2 KiB
Markdown
282 lines
9.2 KiB
Markdown
# yt-shorts-downloader
|
||
|
||
Библиотека и CLI для скачивания YouTube Shorts в формате MP4 через Netscape cookie session file.
|
||
|
||
Основной контракт проекта:
|
||
|
||
- вход: ссылка 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.
|
||
|
||
## Установка
|
||
|
||
Для использования библиотеки:
|
||
|
||
```bash
|
||
uv pip install .
|
||
```
|
||
|
||
Для разработки:
|
||
|
||
```bash
|
||
uv sync --group dev
|
||
```
|
||
|
||
## Быстрый старт
|
||
|
||
### Python API
|
||
|
||
```python
|
||
from yt_shorts_downloader import download
|
||
|
||
video = download(
|
||
'https://www.youtube.com/shorts/VIDEO_ID',
|
||
'cookies.txt',
|
||
)
|
||
|
||
assert video.media_type == 'video/mp4'
|
||
assert video.filename.endswith('.mp4')
|
||
|
||
binary_mp4 = video.content
|
||
```
|
||
|
||
Если нужен файл на диске:
|
||
|
||
```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',
|
||
)
|
||
```
|
||
|
||
### CLI
|
||
|
||
```bash
|
||
uv run yt-shorts-downloader \
|
||
'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/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-режиме
|
||
- Ruff отвечает за lint и format
|
||
|
||
Основные команды:
|
||
|
||
```bash
|
||
make format
|
||
make lint
|
||
make typecheck
|
||
make test
|
||
make ci-check
|
||
make build
|
||
make package-version
|
||
```
|
||
|
||
## Пакетирование и публикация
|
||
|
||
Метаданные пакета и версия берутся из `[project]` в `pyproject.toml`.
|
||
|
||
Текущий publish flow:
|
||
|
||
- Gitea Actions запускает quality checks и tests на pull request и push в `main`
|
||
- после успешного CI на `main` workflow проверяет, существует ли текущая версия в Gitea Packages
|
||
- если версии ещё нет, пакет собирается и публикуется
|
||
- если версия уже существует, публикация пропускается без падения workflow
|
||
- старые версии пакета сохраняются в registry
|
||
|
||
Необходимые secrets для CI:
|
||
|
||
- `PYPI_REPOSITORY_URL`: endpoint вида `https://gitea.example.com/api/packages/<owner>/pypi`
|
||
- `PACKAGE_USERNAME`: пользователь Gitea
|
||
- `PACKAGE_TOKEN`: personal access token с правом package write
|
||
|
||
Локальная публикация:
|
||
|
||
```bash
|
||
export PYPI_REPOSITORY_URL='https://gitea.example.com/api/packages/<owner>/pypi'
|
||
export PACKAGE_USERNAME='<username>'
|
||
export PACKAGE_TOKEN='<token>'
|
||
|
||
make package-version
|
||
make publish-gitea
|
||
```
|
||
|
||
Для обратной совместимости `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`.
|