Ссылка lostfilm.tv: сразу MVO LostFilm без загрузки страницы
docker / build (push) Successful in 1m26s
docker / build (push) Successful in 1m26s
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
111e854789
commit
5bd3b94b46
5 files changed
+40
-6
No files matched your search
@@ -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
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user