Docker-сервис, выбор основы и сравнение моделей, сборщик датасета
docker / build (push) Failing after 39s
docker / build (push) Failing after 39s
- Docker: Dockerfile (CPU), compose с FlareSolverr, настройка через DUB_* переменные, токен доступа к /predict, tools/pack_models.py, workflow Gitea Actions, docs/docker.md - Модель грузится офлайн (config.json рядом с весами); общий код вынесен в src/spans.py, сервису не нужны sklearn/matplotlib/pandas - Обучение: профили tiny2/base/large/modern, запуски в models/runs, реестр и честное сравнение на одной тестовой выборке (src/runs.py), пункт меню «Модели» - Датасет: язык и аудио-параметры дорожек, 18+ по строкам «Аудио», ручные метки по ссылкам, переиндексация сохранённого HTML, параллельная сборка, сбалансированная выборка - Тексты страниц (data/pages) больше не хранятся в git Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
d155b8584b
commit
efc9a4f56a
411 files changed
+87495
-27363
No files matched your search
@@ -0,0 +1,172 @@
|
||||
"""Автопоиск разделов трекеров с зарубежными фильмами и сериалами.
|
||||
|
||||
* rutracker, nnm: берём список разделов со страницы индекса, оставляем те, чьё
|
||||
название подходит под INCLUDE и не попадает в EXCLUDE, и проверяем каждый
|
||||
открытием первой страницы (пустые и «родительские» разделы отбрасываются).
|
||||
Разные форматы (DVD / HD / UHD / 3D) и годы покрываются самими разделами
|
||||
(«Зарубежное кино (UHD Video)», «Фильмы 2016-2020», «Зарубежная Классика»…).
|
||||
* kinozal: разделов нет, есть фильтры browse.php (категория, формат, год).
|
||||
Строится сетка «год × формат», категории чередуются, невозможные сочетания
|
||||
(4K до 2012 и т. п.) пропускаются.
|
||||
|
||||
Результат кладётся в кэш (data/raw/_sections.json) и используется при
|
||||
первом запуске сбора; пересоздать можно командой `discover --force`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
import harvest_core as hv
|
||||
|
||||
# меняется при правке правил отбора: кэш разделов со старой версией ищется заново
|
||||
RULES_VERSION = 3
|
||||
SECTIONS_TTL_DAYS = 30 # разделы меняются редко: раньше срока заново не ищем (кроме --force)
|
||||
|
||||
INDEX_URL = {
|
||||
"rutracker": "https://rutracker.org/forum/index.php?c=7",
|
||||
"nnm": "https://nnmclub.to/forum/index.php",
|
||||
}
|
||||
|
||||
INCLUDE = {
|
||||
"rutracker": re.compile(
|
||||
r"(?i)зарубежн\w*\s+(?:кино|фильм\w*|сериал\w*|мульт\w*)|новинки и сериалы в стадии показа"
|
||||
r"|^фильмы\s+20\d\d|классика мирового кинематографа|мультфильм\w*|мультсериал\w*"
|
||||
r"|аниме|онгоинги"),
|
||||
"nnm": re.compile(
|
||||
r"(?i)зарубежн\w*\s+(?:кино|фильм\w*|сериал\w*|мульт\w*|новинк\w*|классик\w*)"
|
||||
r"|старые зарубежные|зарубежная классика сериалов|мультфильм\w*|мультсериал\w*|аниме"),
|
||||
}
|
||||
|
||||
# родительские разделы rutracker без собственных тем: подразделы берём со страницы раздела
|
||||
PARENT = re.compile(r"(?i)^(?:мультфильмы|мультсериалы|аниме)$")
|
||||
CARTOON = re.compile(r"(?i)мульт|аниме|онгоинг")
|
||||
|
||||
EXCLUDE = re.compile(
|
||||
r"(?i)^зарубежное кино$|наш[еи]|русск|отечеств|ссср|ближн|азиат|индий|турец|латин|apple|iphone|ipad"
|
||||
r"|олимп|предложен|помощь|книг|музык|видеоурок|игр\w*|коллекц|в оригинале|на др\. языках"
|
||||
r"|без (?:русского|озвученного)|реклам|бренд|концерт|аудио|шансон|rap|rock"
|
||||
r"|\bost\b|саундтрек|soundtrack|lossless|\bamv\b|клип|music|караоке|\bmp3\b|\bflac\b")
|
||||
|
||||
MIN_INDEX_LINKS = 100 # на индексе rutracker/nnm сотни разделов; меньше — страница не дозагрузилась
|
||||
MIN_TOPICS = 5 # меньше тем на первой странице — раздел пустой или служебный
|
||||
|
||||
# kinozal: категории (c) чередуются; формат (v) и год (d) — сетка
|
||||
KZ_BROWSE = "https://kinozal.me/browse.php?s=&g=0&c={c}&v={v}&d={d}&w=0&t=0&f=0"
|
||||
# общие списки (все года и форматы) и категории для сетки год × формат
|
||||
KZ_BASE_CATS = [("1002", "все фильмы"), ("46", "сериалы зарубежные"),
|
||||
("21", "мульты зарубежные"), ("20", "аниме")]
|
||||
KZ_GRID_CATS = KZ_BASE_CATS[:3]
|
||||
KZ_FORMATS = [("3", "рипы HD", 2000), ("4", "Blu-Ray/Remux", 2006), ("7", "4K", 2012),
|
||||
("1", "рипы DVD/BD", 1990), ("2", "DVD-5/9", 1990)]
|
||||
KZ_YEARS = [2026, 2023, 2020, 2016, 2012, 2008, 2003, 1998]
|
||||
|
||||
_FORUM_LINKS_JS = r"""JSON.stringify([...document.querySelectorAll('a[href*="viewforum.php?f="]')]
|
||||
.map(a => ({id: (a.getAttribute('href').match(/f=(\d+)/) || [])[1],
|
||||
name: a.innerText.replace(/\s+/g, ' ').trim()})).filter(x => x.id && x.name))"""
|
||||
|
||||
|
||||
def candidate_forums(cdp, site: str) -> list[dict]:
|
||||
"""Разделы с индекса, подходящие по названию (без проверки наполнения)."""
|
||||
nav = hv.goto(cdp, INDEX_URL[site], min_len=500, timeout=60)
|
||||
if not nav["ok"]:
|
||||
raise RuntimeError(f"{site}: индекс не открылся ({nav['title']})")
|
||||
seen: dict[str, str] = {}
|
||||
for _ in range(6): # индекс может дорисоваться уже после load
|
||||
seen = {}
|
||||
for it in json.loads(cdp.eval(_FORUM_LINKS_JS) or "[]"):
|
||||
seen.setdefault(it["id"], it["name"])
|
||||
if len(seen) >= MIN_INDEX_LINKS:
|
||||
break
|
||||
time.sleep(3)
|
||||
else:
|
||||
raise RuntimeError(f"{site}: индекс отдал только {len(seen)} разделов "
|
||||
f"(нужно ≥ {MIN_INDEX_LINKS}) — войдите в аккаунт/пройдите Cloudflare")
|
||||
return [{"ref": i, "name": n} for i, n in seen.items()
|
||||
if INCLUDE[site].search(n) and not EXCLUDE.search(n)]
|
||||
|
||||
|
||||
def subforums(cdp, site: str, ref: str) -> list[dict]:
|
||||
"""Подразделы мультфильмов/аниме со страницы родительского раздела."""
|
||||
base = "https://rutracker.org/forum/" if site == "rutracker" else "https://nnmclub.to/forum/"
|
||||
nav = hv.goto(cdp, f"{base}viewforum.php?f={ref}", min_len=500, timeout=60)
|
||||
if not nav["ok"]:
|
||||
return []
|
||||
out = {}
|
||||
for it in json.loads(cdp.eval(_FORUM_LINKS_JS) or "[]"):
|
||||
if it["id"] != ref and CARTOON.search(it["name"]) and not EXCLUDE.search(it["name"]):
|
||||
out.setdefault(it["id"], it["name"])
|
||||
return [{"ref": i, "name": n} for i, n in out.items()]
|
||||
|
||||
|
||||
def probe(cdp, site: str, ref: str) -> int:
|
||||
"""Число тем на первой странице раздела (0 — не открылся или пуст)."""
|
||||
fn = hv.rt_forum_page if site == "rutracker" else hv.nnm_forum_page
|
||||
ok, items = fn(cdp, ref, 0)
|
||||
return len(items) if ok else 0
|
||||
|
||||
|
||||
def discover_forums(cdp, site: str, on_stage=None, on_step=None, probes: dict | None = None,
|
||||
on_probe=None) -> list[dict]:
|
||||
"""Разделы-кандидаты с проверкой наполнения.
|
||||
|
||||
on_stage(text) — этап; on_step(done, total, name) — прогресс проверки;
|
||||
probes {ref: число тем} — результаты прошлых проверок (повторно не открываются);
|
||||
on_probe(ref, n) — вызывается после каждой новой проверки (для сохранения по ходу).
|
||||
Возвращает все кандидаты с полями topics, keep, status ('ok' | '?' | 'мало').
|
||||
"""
|
||||
stage = on_stage or (lambda _t: None)
|
||||
step = on_step or (lambda *_a: None)
|
||||
probes = probes if probes is not None else {}
|
||||
stage("читаю индекс разделов")
|
||||
cands = candidate_forums(cdp, site)
|
||||
known = {c["ref"] for c in cands}
|
||||
parents = [c for c in cands if PARENT.match(c["name"])]
|
||||
for i, parent in enumerate(parents):
|
||||
stage(f"подразделы «{parent['name']}»")
|
||||
for sub in subforums(cdp, site, parent["ref"]):
|
||||
if sub["ref"] not in known:
|
||||
known.add(sub["ref"])
|
||||
cands.append(sub)
|
||||
out = []
|
||||
for i, c in enumerate(cands):
|
||||
step(i, len(cands), c["name"])
|
||||
n = probes.get(c["ref"]) or 0
|
||||
if not n: # в кэше нет, либо прошлая проверка не удалась
|
||||
n = probe(cdp, site, c["ref"])
|
||||
time.sleep(1.5) # реже ловим лимиты Cloudflare
|
||||
if n == 0:
|
||||
n = probe(cdp, site, c["ref"])
|
||||
if on_probe:
|
||||
on_probe(c["ref"], n)
|
||||
# 0 = не удалось проверить (оставляем), 1..MIN_TOPICS-1 = раздел почти пуст
|
||||
keep = n == 0 or n >= MIN_TOPICS
|
||||
out.append({**c, "topics": n, "keep": keep,
|
||||
"status": "ok" if n >= MIN_TOPICS else ("?" if keep else "мало")})
|
||||
step(len(cands), len(cands), "")
|
||||
return out
|
||||
|
||||
|
||||
def kinozal_grid() -> list[dict]:
|
||||
"""Списки kinozal: сначала общие (все года и форматы), затем год × формат × категория."""
|
||||
plan = [{"ref": KZ_BROWSE.format(c=c, v=0, d=0), "name": f"{cname}: все года и форматы"}
|
||||
for c, cname in KZ_BASE_CATS]
|
||||
for year in KZ_YEARS:
|
||||
for v, vname, since in KZ_FORMATS:
|
||||
if year < since:
|
||||
continue
|
||||
for c, cname in KZ_GRID_CATS:
|
||||
plan.append({"ref": KZ_BROWSE.format(c=c, v=v, d=year),
|
||||
"name": f"{year}, {vname}, {cname}"})
|
||||
return plan
|
||||
|
||||
|
||||
def kinozal_plan(cdp, log=print) -> list[dict]:
|
||||
"""Сетка списков kinozal; первый адрес проверяется на живость."""
|
||||
plan = kinozal_grid()
|
||||
ok, ids = hv.kz_browse_ids(cdp, plan[0]["ref"])
|
||||
log(f"kinozal: сетка из {len(plan)} списков (год × формат × категория); "
|
||||
f"проверка первого: {'ok' if ok and ids else 'пусто/не открылся'} ({len(ids)} раздач)")
|
||||
return plan if ok and ids else []
|
||||
Reference in new issue
Block a user