Обновить документацию: уточнить формат скачиваемых видео, добавить примеры использования и требования к session file
This commit is contained in:
@@ -1,11 +1,33 @@
|
|||||||
# yt-shorts-downloader
|
# yt-shorts-downloader
|
||||||
|
|
||||||
Библиотека и CLI для скачивания YouTube Shorts через Netscape cookie session.
|
Библиотека и CLI для скачивания YouTube Shorts в формате MP4 через Netscape cookie session file.
|
||||||
|
|
||||||
Целевой контракт публичного API:
|
Основной контракт проекта:
|
||||||
|
|
||||||
- вход: ссылка YouTube и путь к session file
|
- вход: ссылка YouTube и путь к cookie-файлу
|
||||||
- выход: бинарный MP4 в памяти для интеграции в API
|
- выход для 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
|
uv sync --group dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Использование библиотеки
|
## Быстрый старт
|
||||||
|
|
||||||
|
### Python API
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from yt_shorts_downloader import download
|
from yt_shorts_downloader import download
|
||||||
|
|
||||||
video = download(
|
video = download(
|
||||||
"https://www.youtube.com/shorts/VIDEO_ID",
|
'https://www.youtube.com/shorts/VIDEO_ID',
|
||||||
"cookies.txt",
|
'cookies.txt',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert video.media_type == "video/mp4"
|
assert video.media_type == 'video/mp4'
|
||||||
|
assert video.filename.endswith('.mp4')
|
||||||
|
|
||||||
binary_mp4 = video.content
|
binary_mp4 = video.content
|
||||||
filename = video.filename
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Если нужен файл на диске, используйте вспомогательную функцию:
|
Если нужен файл на диске:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from yt_shorts_downloader import download_to_path
|
from yt_shorts_downloader import download_to_path
|
||||||
|
|
||||||
file_path = download_to_path(
|
file_path = download_to_path(
|
||||||
"https://www.youtube.com/shorts/VIDEO_ID",
|
'https://www.youtube.com/shorts/VIDEO_ID',
|
||||||
"cookies.txt",
|
'cookies.txt',
|
||||||
output_dir="downloads",
|
output_dir='downloads',
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI
|
### CLI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run yt-shorts-downloader \
|
uv run yt-shorts-downloader \
|
||||||
"https://www.youtube.com/shorts/VIDEO_ID" \
|
'https://www.youtube.com/shorts/VIDEO_ID' \
|
||||||
cookies.txt \
|
cookies.txt \
|
||||||
--output-dir downloads
|
--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-режиме
|
- mypy работает в strict-режиме
|
||||||
- runtime-зависимости лежат в секции project.dependencies
|
- Ruff отвечает за lint и format
|
||||||
- инструменты разработки лежат в секции dependency-groups.dev
|
|
||||||
|
|
||||||
Основные команды:
|
Основные команды:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make format
|
make format
|
||||||
make ci-check
|
make lint
|
||||||
|
make typecheck
|
||||||
make test
|
make test
|
||||||
|
make ci-check
|
||||||
make build
|
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.
|
- Gitea Actions запускает quality checks и tests на pull request и push в `main`
|
||||||
Версия пакета берётся из секции [project] -> version в pyproject.toml.
|
- после успешного CI на `main` workflow проверяет, существует ли текущая версия в Gitea Packages
|
||||||
Gitea не поддерживает повторную публикацию той же версии, поэтому CI сначала проверяет registry и пропускает upload, если эта версия уже существует.
|
- если версии ещё нет, пакет собирается и публикуется
|
||||||
|
- если версия уже существует, публикация пропускается без падения workflow
|
||||||
|
- старые версии пакета сохраняются в registry
|
||||||
|
|
||||||
Нужные secrets для CI workflow:
|
Необходимые secrets для CI:
|
||||||
|
|
||||||
- PYPI_REPOSITORY_URL: полный endpoint вида https://gitea.example.com/api/packages/<owner>/pypi
|
- `PYPI_REPOSITORY_URL`: endpoint вида `https://gitea.example.com/api/packages/<owner>/pypi`
|
||||||
- PACKAGE_USERNAME: пользователь Gitea
|
- `PACKAGE_USERNAME`: пользователь Gitea
|
||||||
- PACKAGE_TOKEN: personal access token с правом package write
|
- `PACKAGE_TOKEN`: personal access token с правом package write
|
||||||
|
|
||||||
Публикация идёт автоматически на push в main после успешных lint/typecheck/test jobs.
|
Локальная публикация:
|
||||||
Если версия пакета уже существует в registry, upload будет пропущен до вызова twine upload.
|
|
||||||
Если версия в pyproject.toml увеличена, в Gitea Packages появится новая версия пакета, а старые версии останутся доступными.
|
|
||||||
|
|
||||||
Локально тот же сценарий можно выполнить так:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export PYPI_REPOSITORY_URL="https://gitea.example.com/api/packages/<owner>/pypi"
|
export PYPI_REPOSITORY_URL='https://gitea.example.com/api/packages/<owner>/pypi'
|
||||||
export PACKAGE_USERNAME="<username>"
|
export PACKAGE_USERNAME='<username>'
|
||||||
export PACKAGE_TOKEN="<token>"
|
export PACKAGE_TOKEN='<token>'
|
||||||
|
|
||||||
make package-version
|
make package-version
|
||||||
make publish-gitea
|
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`.
|
||||||
|
|||||||
Reference in New Issue
Block a user