Добавил поиск метаданных по каталогам
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package recognize
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"git.vakhrushev.me/av/jellybit/internal/metadata"
|
||||
)
|
||||
|
||||
// matchMetadata сверяет план с включёнными базами: ищет по названию+году и,
|
||||
// если ровно один кандидат уверенно совпадает (название и год), возвращает
|
||||
// матч с официальным id и каноническим именем. Несколько кандидатов или их
|
||||
// отсутствие → nil (тогда авто-раскладки не будет). Ошибки провайдера не
|
||||
// валят распознавание — просто нет матча.
|
||||
func (r *Recognizer) matchMetadata(ctx context.Context, plan Plan) *Match {
|
||||
if len(r.providers) == 0 {
|
||||
return nil
|
||||
}
|
||||
mt := metadata.Movie
|
||||
if plan.Type == MediaSeries {
|
||||
mt = metadata.Series
|
||||
}
|
||||
|
||||
searchTitle := plan.ProviderHint
|
||||
if strings.TrimSpace(searchTitle) == "" {
|
||||
searchTitle = plan.Title
|
||||
}
|
||||
matchTitles := normSet(plan.Title, plan.OriginalTitle)
|
||||
|
||||
for _, p := range r.providers {
|
||||
cands, err := p.Search(ctx, metadata.Query{Type: mt, Title: searchTitle, Year: plan.Year})
|
||||
if err != nil {
|
||||
r.log.Warn("recognize: metadata search failed", "provider", p.Name(), "err", err)
|
||||
continue
|
||||
}
|
||||
strong := strongMatches(cands, plan.Year, matchTitles)
|
||||
if len(strong) != 1 {
|
||||
continue
|
||||
}
|
||||
c := strong[0]
|
||||
match := &Match{Provider: c.Provider, ProviderID: c.ID, Title: c.Title, Year: c.Year}
|
||||
if mt == metadata.Series {
|
||||
if counts, cerr := p.SeasonEpisodeCounts(ctx, c.ID); cerr == nil {
|
||||
match.SeasonEpisodeCounts = counts
|
||||
} else {
|
||||
r.log.Warn("recognize: episode counts failed",
|
||||
"provider", p.Name(), "id", c.ID, "err", cerr)
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// strongMatches оставляет кандидатов, чьё название совпадает с одним из
|
||||
// названий плана (после нормализации) и год бьётся (±1 год), дедуплицируя
|
||||
// по id.
|
||||
func strongMatches(cands []metadata.Candidate, year int, titles map[string]bool) []metadata.Candidate {
|
||||
seen := map[string]bool{}
|
||||
var out []metadata.Candidate
|
||||
for _, c := range cands {
|
||||
if !yearMatches(year, c.Year) {
|
||||
continue
|
||||
}
|
||||
if !titles[normalize(c.Title)] && !titles[normalize(c.OriginalTitle)] {
|
||||
continue
|
||||
}
|
||||
if seen[c.ID] {
|
||||
continue
|
||||
}
|
||||
seen[c.ID] = true
|
||||
out = append(out, c)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// yearMatches: год известен у обоих и расходится не больше чем на 1 (разные
|
||||
// базы по-разному датируют релиз), либо где-то год неизвестен.
|
||||
func yearMatches(a, b int) bool {
|
||||
if a == 0 || b == 0 {
|
||||
return true
|
||||
}
|
||||
d := a - b
|
||||
if d < 0 {
|
||||
d = -d
|
||||
}
|
||||
return d <= 1
|
||||
}
|
||||
|
||||
// normSet — множество нормализованных непустых названий.
|
||||
func normSet(titles ...string) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
for _, t := range titles {
|
||||
if n := normalize(t); n != "" {
|
||||
out[n] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// normalize приводит название к сравнимому виду: нижний регистр, только
|
||||
// буквы/цифры (юникод), одиночные пробелы.
|
||||
func normalize(s string) string {
|
||||
var b strings.Builder
|
||||
prevSpace := false
|
||||
for _, r := range strings.ToLower(s) {
|
||||
switch {
|
||||
case unicode.IsLetter(r) || unicode.IsDigit(r):
|
||||
b.WriteRune(r)
|
||||
prevSpace = false
|
||||
case !prevSpace:
|
||||
b.WriteByte(' ')
|
||||
prevSpace = true
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
Reference in New Issue
Block a user