Ссылка 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": [одна дорожка]}` |
|
| `text` | string | одна дорожка (строка вида «Профессиональный (многоголосый закадровый) \| LostFilm») → ответ `{"tracks": [одна дорожка]}` |
|
||||||
| `html` | string | HTML (или текст) страницы раздачи → ответ-агрегат релиза |
|
| `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` берётся со страницы |
|
| `title` | string, необязательно | заголовок раздачи для `html` (во фрагменте вкладки его нет); для `url` берётся со страницы |
|
||||||
| `cookie` | string, необязательно | Cookie сессии (`uid=...; pass=...`) для `url`. **Для kinozal обычно не нужен**: описание многих раздач открывается и без входа (проверено на `details.php?id=2158906`). Передавайте, только если сервис вернул `400` про форму входа |
|
| `cookie` | string, необязательно | Cookie сессии (`uid=...; pass=...`) для `url`. **Для kinozal обычно не нужен**: описание многих раздач открывается и без входа (проверено на `details.php?id=2158906`). Передавайте, только если сервис вернул `400` про форму входа |
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
| `series` | object \| null | сезон, выпущенные серии, «какая студия какие серии» (для Sonarr); `null` — сведений нет |
|
| `series` | object \| null | сезон, выпущенные серии, «какая студия какие серии» (для Sonarr); `null` — сведений нет |
|
||||||
| `title` | string \| null | заголовок раздачи, если найден |
|
| `title` | string \| null | заголовок раздачи, если найден |
|
||||||
| `segmenter` | `"model"` \| `"rules"` | чем страница разбита на строки дорожек: обученным сегментатором или запасными правилами |
|
| `segmenter` | `"model"` \| `"rules"` | чем страница разбита на строки дорожек: обученным сегментатором или запасными правилами |
|
||||||
| `tracker` | string | только для `url`: `rutracker`, `nnm` или `kinozal` |
|
| `tracker` | string | только для `url`: `rutracker`, `nnm`, `kinozal` или `lostfilm` |
|
||||||
|
|
||||||
`series`: `season` — номер сезона строкой или `null`; `episodes` — выпущенные серии («1-9»);
|
`series`: `season` — номер сезона строкой или `null`; `episodes` — выпущенные серии («1-9»);
|
||||||
`total` — всего серий; `by_studio` — `{студия: серии}`; `full_release` — студии без указанных серий
|
`total` — всего серий; `by_studio` — `{студия: серии}`; `full_release` — студии без указанных серий
|
||||||
@@ -154,7 +154,7 @@ type Track = {
|
|||||||
age_confidence: number; // 0..1
|
age_confidence: number; // 0..1
|
||||||
lang: string | null; // 3-буквенный код или null
|
lang: string | null; // 3-буквенный код или null
|
||||||
lang_confidence: number | null; // не null только при lang_source == "model"
|
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
|
role: "перевод" | "оригинал" | "комментарии"; // никогда null
|
||||||
official: boolean;
|
official: boolean;
|
||||||
audio: { codec: string | null; channels: number | null; bitrate_kbps: number | null } | null;
|
audio: { codec: string | null; channels: number | null; bitrate_kbps: number | null } | null;
|
||||||
@@ -179,7 +179,7 @@ type Release = {
|
|||||||
} | null; // null — ни сезона, ни серий в раздаче нет
|
} | null; // null — ни сезона, ни серий в раздаче нет
|
||||||
title: string | null;
|
title: string | null;
|
||||||
segmenter: "model" | "rules";
|
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)
|
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:
|
def detect_tracker(url: str) -> str:
|
||||||
"""Определяет трекер по домену; неизвестный хост -> ValueError."""
|
"""Определяет трекер по домену; неизвестный хост -> ValueError."""
|
||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
|
|||||||
@@ -160,6 +160,20 @@ def original_info(lines: list[dict]) -> dict | None:
|
|||||||
return 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)")
|
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 fastapi.responses import JSONResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from .fetch import fetch_html
|
from .fetch import fetch_html, is_lostfilm
|
||||||
from .predict import DubPredictor
|
from .predict import DubPredictor
|
||||||
from .release import predict_release
|
from .release import lostfilm_release, predict_release
|
||||||
|
|
||||||
|
|
||||||
class Utf8JSONResponse(JSONResponse):
|
class Utf8JSONResponse(JSONResponse):
|
||||||
@@ -95,6 +95,8 @@ def create_app(predictor: DubPredictor, api_token: str | None = None,
|
|||||||
try:
|
try:
|
||||||
if req.text:
|
if req.text:
|
||||||
return {"tracks": [predictor.predict_text(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:
|
if req.url:
|
||||||
try:
|
try:
|
||||||
html, tracker = fetch_html(req.url, cookie=req.cookie, timeout=fetch_timeout)
|
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",
|
check("без DUB_API_TOKEN /predict открыт, /health показывает segmenter и auth",
|
||||||
TestClient(create_app(_Stub())).post("/predict", json={"text": "a"}).status_code == 200
|
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")
|
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
|
import src.service as _svc8 # noqa: E402
|
||||||
_fetch_calls: list = []
|
_fetch_calls: list = []
|
||||||
_orig_fetch = _svc8.fetch_html
|
_orig_fetch = _svc8.fetch_html
|
||||||
|
|||||||
Reference in new issue
Block a user