Канонизация. Один ролик теперь даёт одну запись: youtu.be/X, youtube.com/watch?v=X и та же ссылка с тайм-кодом или списком воспроизведения раньше считались тремя разными роликами, и счётчик популярности размазывался. Заодно закрыта приватная проблема: из адреса вырезаются метки перехода и всё похожее на секреты (token, api_key, signature и прочее) — лента публичная, и публиковать чужой токен нельзя. Уже накопленные записи приводятся к новому виду при старте, счётчики схлопнувшихся строк складываются. /readyz. Прежний /healthz отвечал «жив» даже при полном диске, мёртвом уборщике, неработающей БД и отсутствующем ffmpeg. Новый эндпоинт проверяет всё это плюс глубину очереди и возраст последнего прохода уборщика, и при деградации отдаёт 503 — чтобы поломка была видна мониторингу, а не только пользователям. Тесты. 187 проверок, сеть не используется. Покрыты: селекторы форматов (дословные эталоны плюс компиляция всех сочетаний самим yt-dlp), валидация ссылок и SSRF, path traversal и символические ссылки, имена файлов с кириллицей, отсев форматов с неизвестными кодеками, счётчики и лента, HTTP-слой. В conftest пути принудительно уводятся во временный каталог: импорт app создаёт менеджер, который подметает каталог загрузок, и без этой предосторожности прогон тестов удалял файлы работающего сервиса — так уже случалось. Тесты сразу нашли две несогласованности, обе исправлены: * отмена несуществующей задачи отвечала 409, хотя соседние ручки на ту же задачу отвечают 404; * метки времени в ленте писались с точностью до секунды, поэтому порядок «недавних» среди записей одной секунды был произвольным. Также объединены карты сообщений об ошибках: они дублировались в двух ручках и успели разойтись — на одну и ту же ссылку /api/info отвечал «Этот адрес недоступен», а /api/downloads глотал код и говорил «Плохая ссылка». Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwY2Cg54RK7WZdvMaLV5Di
102 lines
3.9 KiB
Python
102 lines
3.9 KiB
Python
"""Разбор метаданных: какие форматы попадают в выдачу, а какие отсеиваются.
|
||
|
||
Здесь закреплена дорогая ошибка: у yt-dlp строка "none" означает, что
|
||
дорожки точно нет, а None — что кодек просто неизвестен. Код писал
|
||
`f.get("vcodec") or "none"` и превращал неизвестный в отсутствующий, из-за
|
||
чего у всех сайтов, не сообщающих кодеки (то есть почти у всех, кроме
|
||
YouTube), выбрасывались ВСЕ форматы.
|
||
"""
|
||
import pytest
|
||
|
||
import downloader as dl
|
||
|
||
|
||
class FakeYDL:
|
||
"""Подменяет YoutubeDL: отдаёт заранее заданные метаданные без сети."""
|
||
|
||
def __init__(self, info):
|
||
self._info = info
|
||
|
||
def __call__(self, opts):
|
||
return self
|
||
|
||
def __enter__(self):
|
||
return self
|
||
|
||
def __exit__(self, *a):
|
||
return False
|
||
|
||
def extract_info(self, url, download=False):
|
||
return self._info
|
||
|
||
def sanitize_info(self, info):
|
||
return info
|
||
|
||
|
||
@pytest.fixture
|
||
def probe_with(monkeypatch):
|
||
def run(info):
|
||
monkeypatch.setattr(dl.yt_dlp, "YoutubeDL", FakeYDL(info))
|
||
return dl.probe("https://example.com/v")
|
||
return run
|
||
|
||
|
||
def test_formats_with_unknown_codecs_are_kept(probe_with, fake_info):
|
||
out = probe_with(fake_info)
|
||
ids = [f["format_id"] for f in out["formats"]]
|
||
assert "hls-480" in ids, "формат с vcodec=None выброшен — это регрессия"
|
||
assert "mp4-low" in ids
|
||
|
||
|
||
def test_format_with_explicit_none_both_is_dropped(probe_with, fake_info):
|
||
"""Раскадровка не содержит ни видео, ни звука — её показывать незачем."""
|
||
out = probe_with(fake_info)
|
||
assert "sb0" not in [f["format_id"] for f in out["formats"]]
|
||
|
||
|
||
def test_kind_classification(probe_with, fake_info):
|
||
out = probe_with(fake_info)
|
||
kinds = {f["format_id"]: f["kind"] for f in out["formats"]}
|
||
assert kinds["137"] == "video" # есть видео, звука нет
|
||
assert kinds["140"] == "audio" # есть звук, видео нет
|
||
assert kinds["hls-480"] == "both" # неизвестно -> считаем полноценным
|
||
|
||
|
||
def test_unknown_codec_reported_as_none_not_crash(probe_with, fake_info):
|
||
"""Раньше здесь падал v.split('.') у None, и запрос отвечал общей
|
||
ошибкой вместо выдачи форматов."""
|
||
out = probe_with(fake_info)
|
||
f = next(f for f in out["formats"] if f["format_id"] == "hls-480")
|
||
assert f["vcodec"] is None and f["acodec"] is None
|
||
|
||
|
||
def test_codec_is_shortened(probe_with, fake_info):
|
||
out = probe_with(fake_info)
|
||
f = next(f for f in out["formats"] if f["format_id"] == "137")
|
||
assert f["vcodec"] == "avc1" # из 'avc1.640028'
|
||
|
||
|
||
def test_heights_include_unknown_codec_formats(probe_with, fake_info):
|
||
"""Список качеств строился по тому же принципу и терял те же форматы."""
|
||
out = probe_with(fake_info)
|
||
assert "480" in out["heights"]
|
||
assert "1080" in out["heights"]
|
||
|
||
|
||
def test_duration_limit(probe_with, fake_info, monkeypatch):
|
||
import config
|
||
monkeypatch.setattr(config, "MAX_DURATION_SEC", 60)
|
||
with pytest.raises(ValueError) as e:
|
||
probe_with(fake_info) # ролик на 120 секунд
|
||
assert str(e.value) == "too_long"
|
||
|
||
|
||
def test_empty_formats_does_not_crash(probe_with):
|
||
out = probe_with({"id": "x", "title": "Пусто", "formats": []})
|
||
assert out["formats"] == []
|
||
assert out["heights"] == []
|
||
|
||
|
||
def test_missing_title_gets_placeholder(probe_with):
|
||
out = probe_with({"id": "x", "formats": []})
|
||
assert out["title"] == "Без названия"
|