Ссылка lostfilm.tv: сразу MVO LostFilm без загрузки страницы
docker / build (push) Successful in 1m26s

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-11-01 21:32:35 +07:00
1 parent 111e854789
commit 5bd3b94b46
5 files changed
+40 -6

No files matched your search

+4 -4
View File
@@ -22,7 +22,7 @@
|---|---|---|
| `text` | string | одна дорожка (строка вида «Профессиональный (многоголосый закадровый) \| LostFilm») → ответ `{"tracks": [одна дорожка]}` |
| `html` | string | HTML (или текст) страницы раздачи → ответ-агрегат релиза |
| `url` | string | ссылка на раздачу rutracker / nnmclub / kinozal; страницу сервис скачивает сам (при 403/503 — через FlareSolverr) → ответ-агрегат + `tracker` |
| `url` | string | ссылка на раздачу rutracker / nnmclub / kinozal; страницу сервис скачивает сам (при 403/503 — через FlareSolverr) → ответ-агрегат + `tracker`. Ссылка на `lostfilm.tv` (и поддомены) страницу не загружает: сразу возвращается одна дорожка «проф. многоголосый, LostFilm, RUS» (`tracker: "lostfilm"`, `lang_source: "url"`, `source: "url"`) |
| `title` | string, необязательно | заголовок раздачи для `html` (во фрагменте вкладки его нет); для `url` берётся со страницы |
| `cookie` | string, необязательно | Cookie сессии (`uid=...; pass=...`) для `url`. **Для kinozal обычно не нужен**: описание многих раздач открывается и без входа (проверено на `details.php?id=2158906`). Передавайте, только если сервис вернул `400` про форму входа |
@@ -56,7 +56,7 @@
| `series` | object \| null | сезон, выпущенные серии, «какая студия какие серии» (для Sonarr); `null` — сведений нет |
| `title` | string \| null | заголовок раздачи, если найден |
| `segmenter` | `"model"` \| `"rules"` | чем страница разбита на строки дорожек: обученным сегментатором или запасными правилами |
| `tracker` | string | только для `url`: `rutracker`, `nnm` или `kinozal` |
| `tracker` | string | только для `url`: `rutracker`, `nnm`, `kinozal` или `lostfilm` |
`series`: `season` — номер сезона строкой или `null`; `episodes` — выпущенные серии («1-9»);
`total` — всего серий; `by_studio` — `{студия: серии}`; `full_release` — студии без указанных серий
@@ -154,7 +154,7 @@ type Track = {
age_confidence: number; // 0..1
lang: string | null; // 3-буквенный код или null
lang_confidence: number | null; // не null только при lang_source == "model"
lang_source: "model" | "audio" | "text" | null; // null вместе с lang
lang_source: "model" | "audio" | "text" | "url" | null; // "url" — ссылка lostfilm.tv; null вместе с lang
role: "перевод" | "оригинал" | "комментарии"; // никогда null
official: boolean;
audio: { codec: string | null; channels: number | null; bitrate_kbps: number | null } | null;
@@ -179,7 +179,7 @@ type Release = {
} | null; // null — ни сезона, ни серий в раздаче нет
title: string | null;
segmenter: "model" | "rules";
tracker?: "rutracker" | "nnm" | "kinozal"; // только для url
tracker?: "rutracker" | "nnm" | "kinozal" | "lostfilm"; // только для url
};
```
+6
View File
@@ -42,6 +42,12 @@ MAX_BYTES = 5_000_000
RETRY_CODES = (403, 429, 500, 502, 503, 520, 521, 522, 523, 524)
def is_lostfilm(url: str) -> bool:
"""Ссылка на сайт LostFilm (lostfilm.tv и зеркала): озвучка известна без загрузки."""
host = (urlparse(url).hostname or "").lower()
return "lostfilm" in host.split(".")[-2:-1] or host.startswith("lostfilm.") or ".lostfilm." in host
def detect_tracker(url: str) -> str:
"""Определяет трекер по домену; неизвестный хост -> ValueError."""
parsed = urlparse(url)
+14
View File
@@ -160,6 +160,20 @@ def original_info(lines: list[dict]) -> dict | None:
return None
def lostfilm_release(title: str | None = None) -> dict:
"""Ссылка на lostfilm.tv: озвучка известна заранее — MVO LostFilm, страница не нужна."""
track = {"segment": "Профессиональный (многоголосый закадровый) LostFilm",
"translation_type": "проф. многоголосый", "type_confidence": 1.0,
"studio_span": "LostFilm", "studio": "LostFilm", "studio_confidence": 1.0,
"is_18plus": False, "age_confidence": 1.0, "lang": "RUS", "lang_confidence": None,
"lang_source": "url", "role": "перевод", "official": False, "audio": None,
"source": "url", "episode_ranges": [], "episodes": None}
result = aggregate([track])
result.update(original=None, series=series_info(title or "", result["tracks"]),
title=title or None, segmenter="rules")
return result
RE_SEASON = re.compile(r"(?i)сезон[ыа]?\s*(\d+(?:\s*[-–]\s*\d+)?)|\bS(\d{1,2})\b(?!E)")
+4 -2
View File
@@ -31,9 +31,9 @@ from fastapi import Depends, FastAPI, Header, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from .fetch import fetch_html
from .fetch import fetch_html, is_lostfilm
from .predict import DubPredictor
from .release import predict_release
from .release import lostfilm_release, predict_release
class Utf8JSONResponse(JSONResponse):
@@ -95,6 +95,8 @@ def create_app(predictor: DubPredictor, api_token: str | None = None,
try:
if req.text:
return {"tracks": [predictor.predict_text(req.text)]}
if req.url and is_lostfilm(req.url):
return {**lostfilm_release(req.title), "tracker": "lostfilm"}
if req.url:
try:
html, tracker = fetch_html(req.url, cookie=req.cookie, timeout=fetch_timeout)
+12
View File
@@ -1105,6 +1105,18 @@ check("токен: Bearer и X-API-Key принимаются",
check("без DUB_API_TOKEN /predict открыт, /health показывает segmenter и auth",
TestClient(create_app(_Stub())).post("/predict", json={"text": "a"}).status_code == 200
and _c.get("/health").json()["auth"] is True and _c.get("/health").json()["segmenter"] == "rules")
_lf = _c.post("/predict", json={"url": "https://www.lostfilm.tv/series/X/season_1/episode_2/"},
headers={"X-API-Key": "tok"})
_lfj = _lf.json() if _lf.status_code == 200 else {}
check("ссылка lostfilm.tv: сразу MVO LostFilm без загрузки страницы",
_lfj.get("tracker") == "lostfilm" and _lfj["studios"][0]["studio"] == "LostFilm"
and _lfj["studios"][0]["translation_type"] == "проф. многоголосый" and _lfj["tracks"][0]["lang"] == "RUS",
str(_lf.status_code))
from src.fetch import is_lostfilm # noqa: E402
check("is_lostfilm: lostfilm.tv и поддомены да, rutracker и подделка в пути нет",
is_lostfilm("https://lostfilm.tv/x") and is_lostfilm("https://www.lostfilm.tv/x")
and not is_lostfilm("https://rutracker.org/forum/viewtopic.php?t=1&u=lostfilm.tv")
and not is_lostfilm("https://evil.com/lostfilm.tv"))
import src.service as _svc8 # noqa: E402
_fetch_calls: list = []
_orig_fetch = _svc8.fetch_html