tgbot: показывать запись матча метабазы (провайдер+id+ссылка)
В карточке подтверждения и уведомлении о готовности бот теперь показывает
запись матча метабазы — провайдер, id и кликабельную ссылку на страницу
записи, — как веб-страница /download/{id} и экран ревью. Так ошибочную
привязку видно и из Telegram.
Билдер URL записи (providerURL/matchURL) вынесен из internal/httpapi в ядро
internal/worker (worker.ProviderURL + метод (*ReviewData).MatchURL()), чтобы
оба транспорта строили ссылку одинаково; httpapi делегирует туда. baseLine
переведён на эффективные provider/id (с учётом ручных правок), URL в href
экранируется escHref (сверх esc закрывает кавычку — иначе изготовленный id
разорвал бы атрибут и Telegram отклонил бы сообщение). При отсутствии матча
карточка ревью показывает «нет матча», уведомление о готовности строку
опускает.
Capability notifications: ADDED «Показ записи матча метабазы» + MODIFIED
требования об экранировании (id матча и URL, контекст href).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.vakhrushev.me/av/jellybit/internal/recognize"
|
||||
"git.vakhrushev.me/av/jellybit/internal/store"
|
||||
)
|
||||
|
||||
func TestProviderURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
provider string
|
||||
id string
|
||||
mtype string
|
||||
want string
|
||||
}{
|
||||
{"tmdb movie", "tmdb", "693134", "movie", "https://www.themoviedb.org/movie/693134"},
|
||||
{"tmdb series", "tmdb", "60622", "series", "https://www.themoviedb.org/tv/60622"},
|
||||
{"tvdb series", "tvdb", "269613", "series", "https://www.thetvdb.com/dereferrer/series/269613"},
|
||||
{"tvdb movie", "tvdb", "12345", "movie", "https://www.thetvdb.com/dereferrer/movie/12345"},
|
||||
{"imdb", "imdb", "tt0111161", "movie", "https://www.imdb.com/title/tt0111161"},
|
||||
{"unknown provider → пусто", "kinopoisk", "42", "movie", ""},
|
||||
{"пустой id → пусто", "tmdb", "", "movie", ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if got := ProviderURL(c.provider, c.id, c.mtype); got != c.want {
|
||||
t.Errorf("ProviderURL(%q,%q,%q) = %q, want %q", c.provider, c.id, c.mtype, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMatchURLNoLinkWhenUnbuildable — эффективный провайдер, для которого URL не
|
||||
// строится и совпадающего кандидата нет, даёт пустую ссылку (транспорт покажет
|
||||
// матч текстом — сценарий «URL записи неизвестен»).
|
||||
func TestMatchURLNoLinkWhenUnbuildable(t *testing.T) {
|
||||
rd := &ReviewData{Provider: "kinopoisk", ProviderID: "42"}
|
||||
if got := rd.MatchURL(); got != "" {
|
||||
t.Errorf("MatchURL = %q, want пусто (URL не строится, кандидата нет)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMatchURLCanonicalFromEffective — без кандидатов ссылка строится по
|
||||
// эффективным provider/id и типу медиа плана.
|
||||
func TestMatchURLCanonicalFromEffective(t *testing.T) {
|
||||
rd := &ReviewData{
|
||||
Provider: "tmdb", ProviderID: "60622",
|
||||
Plan: recognize.Plan{Type: recognize.MediaSeries},
|
||||
}
|
||||
if got := rd.MatchURL(); got != "https://www.themoviedb.org/tv/60622" {
|
||||
t.Errorf("MatchURL = %q, want канонический URL по типу плана", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMatchURLPrefersMatchingCandidate — URL берётся у выбранного кандидата,
|
||||
// когда его provider+id совпадают с эффективными.
|
||||
func TestMatchURLPrefersMatchingCandidate(t *testing.T) {
|
||||
rd := &ReviewData{
|
||||
Provider: "tmdb", ProviderID: "693134",
|
||||
Candidates: []store.MetadataCandidate{{
|
||||
Provider: "tmdb", ProviderID: "693134", Chosen: true,
|
||||
URL: store.NullString("https://custom.example/x"),
|
||||
}},
|
||||
}
|
||||
if got := rd.MatchURL(); got != "https://custom.example/x" {
|
||||
t.Errorf("MatchURL = %q, want URL выбранного кандидата", got)
|
||||
}
|
||||
}
|
||||
@@ -957,6 +957,49 @@ type ReviewData struct {
|
||||
Overrides map[string]string
|
||||
}
|
||||
|
||||
// MatchURL — ссылка на подтверждённую запись метабазы для этой загрузки. Общий
|
||||
// билдер для всех транспортов (веб и Telegram строят ссылку одинаково). Приоритет
|
||||
// — URL выбранного кандидата, но только если его provider+id совпадают с
|
||||
// эффективными (человек мог выбрать кандидата, затем вручную переопределить id —
|
||||
// тогда кандидат указывает на другую запись). Иначе строим канонический URL по
|
||||
// эффективным provider/id и типу медиа плана; если не удаётся — пусто (транспорт
|
||||
// покажет матч текстом).
|
||||
func (rd *ReviewData) MatchURL() string {
|
||||
for _, c := range rd.Candidates {
|
||||
if c.Chosen && c.Provider == rd.Provider && c.ProviderID == rd.ProviderID &&
|
||||
c.URL.Valid && c.URL.String != "" {
|
||||
return c.URL.String
|
||||
}
|
||||
}
|
||||
return ProviderURL(rd.Provider, rd.ProviderID, string(rd.Plan.Type))
|
||||
}
|
||||
|
||||
// ProviderURL строит канонический URL записи метабазы с учётом типа медиа.
|
||||
// Пустой id или неизвестный провайдер → пусто.
|
||||
func ProviderURL(provider, id, mediaType string) string {
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
switch provider {
|
||||
case "tmdb":
|
||||
kind := "movie"
|
||||
if mediaType == "series" {
|
||||
kind = "tv"
|
||||
}
|
||||
return "https://www.themoviedb.org/" + kind + "/" + id
|
||||
case "tvdb":
|
||||
kind := "series"
|
||||
if mediaType == "movie" {
|
||||
kind = "movie"
|
||||
}
|
||||
return "https://www.thetvdb.com/dereferrer/" + kind + "/" + id
|
||||
case "imdb":
|
||||
return "https://www.imdb.com/title/" + id
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// SourceKind — вид источника в едином списке ревью.
|
||||
type SourceKind string
|
||||
|
||||
|
||||
Reference in New Issue
Block a user