194 lines
5.6 KiB
Go
194 lines
5.6 KiB
Go
package historyimport
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type matcherRepository interface {
|
|
MatchMediaByExternalID(ctx context.Context, kind, column, value string) ([]mediaLookupRow, error)
|
|
MatchMediaByTitleYear(ctx context.Context, kind, title string, year int) ([]mediaLookupRow, error)
|
|
MatchEpisodeByExternalID(ctx context.Context, column, value string) ([]mediaLookupRow, error)
|
|
MatchEpisodeBySeries(ctx context.Context, seriesID string, seasonNumber, episodeNumber int) (*Match, error)
|
|
}
|
|
|
|
type Matcher struct {
|
|
repo matcherRepository
|
|
}
|
|
|
|
func NewMatcher(repo matcherRepository) *Matcher {
|
|
return &Matcher{repo: repo}
|
|
}
|
|
|
|
func (m *Matcher) Match(ctx context.Context, record Record) (*Match, string, error) {
|
|
switch record.Kind {
|
|
case KindMovie:
|
|
return m.matchMovie(ctx, record)
|
|
case KindEpisode:
|
|
return m.matchEpisode(ctx, record)
|
|
case KindSeries:
|
|
return m.matchSeries(ctx, record)
|
|
default:
|
|
return nil, "unsupported item kind", nil
|
|
}
|
|
}
|
|
|
|
func (m *Matcher) matchMovie(ctx context.Context, record Record) (*Match, string, error) {
|
|
match, reason, err := m.matchMedia(ctx, KindMovie, record.TMDBID, record.IMDbID, "", record.Title, record.Year)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return match, reason, nil
|
|
}
|
|
|
|
func (m *Matcher) matchSeries(ctx context.Context, record Record) (*Match, string, error) {
|
|
match, reason, err := m.matchMedia(ctx, KindSeries, record.TMDBID, record.IMDbID, record.TVDBID, record.Title, record.Year)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return match, reason, nil
|
|
}
|
|
|
|
func (m *Matcher) matchEpisode(ctx context.Context, record Record) (*Match, string, error) {
|
|
attempts := make([]string, 0, 4)
|
|
for _, candidate := range []struct {
|
|
column string
|
|
value string
|
|
label string
|
|
}{
|
|
{column: "tvdb_id", value: record.TVDBID, label: "tvdb_id"},
|
|
{column: "tmdb_id", value: record.TMDBID, label: "tmdb_id"},
|
|
{column: "imdb_id", value: record.IMDbID, label: "imdb_id"},
|
|
} {
|
|
if candidate.value == "" {
|
|
continue
|
|
}
|
|
rows, err := m.repo.MatchEpisodeByExternalID(ctx, candidate.column, candidate.value)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if len(rows) == 1 {
|
|
return &Match{
|
|
MediaItemID: rows[0].ContentID,
|
|
Kind: KindEpisode,
|
|
Title: rows[0].Title,
|
|
Year: rows[0].Year,
|
|
}, "", nil
|
|
}
|
|
if len(rows) > 1 {
|
|
return nil, fmt.Sprintf("ambiguous episode %s match for %q (%d rows)", candidate.label, candidate.value, len(rows)), nil
|
|
}
|
|
attempts = append(attempts, fmt.Sprintf("no episode %s match for %q", candidate.label, candidate.value))
|
|
}
|
|
|
|
if record.EpisodeNumber <= 0 {
|
|
attempts = append(attempts, "missing season or episode number")
|
|
return nil, strings.Join(attempts, "; "), nil
|
|
}
|
|
|
|
seriesRecord := Record{
|
|
Kind: KindSeries,
|
|
Title: record.SeriesTitle,
|
|
Year: record.SeriesYear,
|
|
TMDBID: record.SeriesTMDBID,
|
|
IMDbID: record.SeriesIMDbID,
|
|
TVDBID: record.SeriesTVDBID,
|
|
}
|
|
seriesMatch, reason, err := m.matchSeries(ctx, seriesRecord)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if seriesMatch == nil {
|
|
attempts = append(attempts, "series match failed: "+reason)
|
|
return nil, strings.Join(attempts, "; "), nil
|
|
}
|
|
|
|
match, err := m.repo.MatchEpisodeBySeries(ctx, seriesMatch.MediaItemID, record.SeasonNumber, record.EpisodeNumber)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if match == nil {
|
|
attempts = append(attempts, fmt.Sprintf("no matching episode for S%02dE%02d", record.SeasonNumber, record.EpisodeNumber))
|
|
return nil, strings.Join(attempts, "; "), nil
|
|
}
|
|
return match, "", nil
|
|
}
|
|
|
|
func (m *Matcher) matchMedia(ctx context.Context, kind, tmdbID, imdbID, tvdbID, title string, year int) (*Match, string, error) {
|
|
attempts := make([]string, 0, 4)
|
|
candidates := []struct {
|
|
column string
|
|
value string
|
|
label string
|
|
}{
|
|
{column: "tmdb_id", value: tmdbID, label: "tmdb_id"},
|
|
{column: "imdb_id", value: imdbID, label: "imdb_id"},
|
|
}
|
|
if kind == KindSeries {
|
|
candidates = []struct {
|
|
column string
|
|
value string
|
|
label string
|
|
}{
|
|
{column: "tvdb_id", value: tvdbID, label: "tvdb_id"},
|
|
{column: "tmdb_id", value: tmdbID, label: "tmdb_id"},
|
|
{column: "imdb_id", value: imdbID, label: "imdb_id"},
|
|
}
|
|
}
|
|
|
|
for _, candidate := range candidates {
|
|
if candidate.value == "" {
|
|
continue
|
|
}
|
|
rows, err := m.repo.MatchMediaByExternalID(ctx, kind, candidate.column, candidate.value)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if len(rows) == 1 {
|
|
return &Match{
|
|
MediaItemID: rows[0].ContentID,
|
|
Kind: kind,
|
|
Title: rows[0].Title,
|
|
Year: rows[0].Year,
|
|
}, "", nil
|
|
}
|
|
if len(rows) > 1 {
|
|
return nil, fmt.Sprintf("ambiguous %s match for %q (%d rows)", candidate.label, candidate.value, len(rows)), nil
|
|
}
|
|
attempts = append(attempts, fmt.Sprintf("no %s match for %q", candidate.label, candidate.value))
|
|
}
|
|
|
|
if title == "" {
|
|
if len(attempts) == 0 {
|
|
return nil, "missing identifiers and title", nil
|
|
}
|
|
return nil, strings.Join(attempts, "; "), nil
|
|
}
|
|
|
|
rows, err := m.repo.MatchMediaByTitleYear(ctx, kind, title, year)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if len(rows) == 1 {
|
|
return &Match{
|
|
MediaItemID: rows[0].ContentID,
|
|
Kind: kind,
|
|
Title: rows[0].Title,
|
|
Year: rows[0].Year,
|
|
}, "", nil
|
|
}
|
|
if len(rows) > 1 {
|
|
return nil, fmt.Sprintf("ambiguous exact title/year match for %s (%d rows)", describeTitleYear(title, year), len(rows)), nil
|
|
}
|
|
attempts = append(attempts, fmt.Sprintf("no exact title/year match for %s", describeTitleYear(title, year)))
|
|
return nil, strings.Join(attempts, "; "), nil
|
|
}
|
|
|
|
func describeTitleYear(title string, year int) string {
|
|
if year > 0 {
|
|
return fmt.Sprintf("%q (%d)", title, year)
|
|
}
|
|
return fmt.Sprintf("%q", title)
|
|
}
|