Ingest: контекст распознавания из полей magnet-ссылки

Синтезируем контекст из полей самой magnet-ссылки (dn, xl, tr/xs, kt) без
сети и дополняем им текст от транспорта: пользовательский текст первым, при
пустом — синтез единственный. Обогащённый контекст идёт только в
download.Context (его читают recognition и веб-UI); вход namer и отображаемое
имя не меняются — строки-факты (Размер:/Трекер:) в display_name не текут.

- magnet.Info: поля ExactLength/Sources/Keywords + Info.Context() (синтез,
  отсев dn-заглушек *-topic-<id>, домен трекера, человекочитаемый размер)
- Parse устойчив к ссылке в процент-кодировке (разовый QueryUnescape)
- ingest: mergeContext → download.Context, namer на сыром req.Context
- Влита дельта capability ingest в openspec/specs, change заархивирован

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-07 20:38:01 +03:00
co-authored by Claude Opus 4.8
parent 6d801ed03b
commit e9ec26d09a
10 changed files with 837 additions and 7 deletions
+153 -1
View File
@@ -1,6 +1,10 @@
package magnet
import "testing"
import (
"net/url"
"strings"
"testing"
)
func TestParse(t *testing.T) {
tests := []struct {
@@ -79,6 +83,154 @@ func TestParseErrors(t *testing.T) {
}
}
func TestParseExtraFields(t *testing.T) {
raw := "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6" +
"&xl=2200000000&xs=http%3A%2F%2Fmirror.example%2Ft.torrent&kt=dune+scifi"
got, err := Parse(raw)
if err != nil {
t.Fatalf("Parse: %v", err)
}
if got.ExactLength != 2200000000 {
t.Errorf("xl = %d, want 2200000000", got.ExactLength)
}
if len(got.Sources) != 1 || got.Sources[0] != "http://mirror.example/t.torrent" {
t.Errorf("xs = %v", got.Sources)
}
if len(got.Keywords) != 1 || got.Keywords[0] != "dune scifi" {
t.Errorf("kt = %v", got.Keywords)
}
}
func TestParseInvalidXLIgnored(t *testing.T) {
for _, raw := range []string{
"magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6&xl=notanumber",
"magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6&xl=-5",
"magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6",
} {
got, err := Parse(raw)
if err != nil {
t.Fatalf("Parse(%q): %v", raw, err)
}
if got.ExactLength != 0 {
t.Errorf("Parse(%q): xl = %d, want 0", raw, got.ExactLength)
}
}
}
func TestInfoContext(t *testing.T) {
tests := []struct {
name string
info Info
want []string // подстроки, которые ДОЛЖНЫ быть
notWant []string // подстроки, которых быть НЕ должно
}{
{
name: "содержательный dn",
info: Info{DisplayName: "Dune.Part.Two.2024.2160p"},
want: []string{"Dune.Part.Two.2024.2160p"},
notWant: []string{"Размер:", "Трекер:"},
},
{
name: "заглушка-dn не берётся как имя",
info: Info{DisplayName: "rutracker-topic-6514485", Trackers: []string{"http://bt.t-ru.org/ann?magnet"}},
want: []string{"Трекер: t-ru.org"},
notWant: []string{"rutracker-topic"},
},
{
name: "только размер",
info: Info{ExactLength: 2200000000},
want: []string{"Размер: ≈ 2.0 GiB"},
},
{
name: "домен из xs, служебный поддомен убран",
info: Info{Sources: []string{"udp://tracker.example.org:80/announce"}},
want: []string{"Трекер: example.org"},
notWant: []string{"tracker.example.org"},
},
{
name: "все поля вместе",
info: Info{
DisplayName: "Dune",
ExactLength: 1500,
Trackers: []string{"http://bt.t-ru.org/ann"},
Keywords: []string{"scifi dune"}, // как отдаёт url.Query() из kt=scifi+dune
},
want: []string{"Dune", "Размер:", "Трекер: t-ru.org", "Ключевые слова: scifi, dune"},
},
{
name: "пустой Info → пустой контекст",
info: Info{},
want: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := tc.info.Context()
if len(tc.want) == 0 && got != "" {
t.Errorf("Context() = %q, want пусто", got)
}
for _, w := range tc.want {
if !strings.Contains(got, w) {
t.Errorf("Context() = %q, want содержит %q", got, w)
}
}
for _, nw := range tc.notWant {
if strings.Contains(got, nw) {
t.Errorf("Context() = %q, НЕ должно содержать %q", got, nw)
}
}
})
}
}
// Реальная ссылка с рутрекера: dn несёт полный релиз-заголовок в
// процент-кодировке (кириллица + латиница, сезон/серии/год/жанры/качество),
// tr — announce-хост bt4.t-ru.org. Проверяем полный путь до Context().
func TestParseRealRutrackerLink(t *testing.T) {
const raw = "magnet:?xt=urn:btih:BACA24E18C7382A9E9A44132C8D7DB86C4D319C2" +
"&tr=http%3A%2F%2Fbt4.t-ru.org%2Fann%3Fmagnet" +
"&dn=%D0%91%D1%83%D1%85%D1%82%D0%B0%20%D0%B2%D0%B4%D0%BE%D0%B2%20%2F%20Widow's%20Bay%20%2F%20%D0%A1%D0%B5%D0%B7%D0%BE%D0%BD%3A%201%20%2F%20%D0%A1%D0%B5%D1%80%D0%B8%D0%B8%3A%201-10%20%D0%B8%D0%B7%2010%20(%D0%A5%D0%B8%D1%80%D0%BE%20%D0%9C%D1%83%D1%80%D0%B0%D0%B9)%20%5B2026%2C%20%D0%A1%D0%A8%D0%90%2C%20%D0%A3%D0%B6%D0%B0%D1%81%D1%8B%2C%20%D0%B4%D1%80%D0%B0%D0%BC%D0%B0%2C%20%D0%BA%D0%BE%D0%BC%D0%B5%D0%B4%D0%B8%D1%8F%2C%20WEB-DL%201080p%5D%207%20x%20MVO"
got, err := Parse(raw)
if err != nil {
t.Fatalf("Parse: %v", err)
}
if got.Infohash != "baca24e18c7382a9e9a44132c8d7db86c4d319c2" {
t.Errorf("infohash = %q", got.Infohash)
}
// dn раскодирован url.Query() — без ручного urldecode.
const wantName = "Бухта вдов / Widow's Bay / Сезон: 1 / Серии: 1-10 из 10 " +
"(Хиро Мурай) [2026, США, Ужасы, драма, комедия, WEB-DL 1080p] 7 x MVO"
if got.DisplayName != wantName {
t.Errorf("dn = %q", got.DisplayName)
}
ctx := got.Context()
if !strings.Contains(ctx, "Widow's Bay") || !strings.Contains(ctx, "2026") {
t.Errorf("Context() без релиз-данных: %q", ctx)
}
if !strings.Contains(ctx, "Трекер: t-ru.org") {
t.Errorf("Context() без домена трекера (bt4. должен срезаться): %q", ctx)
}
if strings.Contains(ctx, "bt4.") {
t.Errorf("служебный поддомен не срезан: %q", ctx)
}
}
// Ссылку могли скопировать целиком в процент-кодировке (например вытащили из
// другого URL) — Parse должен раскодировать её один раз и разобрать.
func TestParseDoubleEncoded(t *testing.T) {
const inner = "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6&dn=Dune"
got, err := Parse(url.QueryEscape(inner))
if err != nil {
t.Fatalf("Parse(double-encoded): %v", err)
}
if got.Infohash != "541adcff3b6dd5dba7088ea83317d9d6fac331d6" {
t.Errorf("infohash = %q", got.Infohash)
}
if got.DisplayName != "Dune" {
t.Errorf("dn = %q", got.DisplayName)
}
}
func TestParseNormalisesCase(t *testing.T) {
lower := "magnet:?xt=urn:btih:541adcff3b6dd5dba7088ea83317d9d6fac331d6"
upper := "magnet:?xt=urn:btih:541ADCFF3B6DD5DBA7088EA83317D9D6FAC331D6"