From 4536b6199c3e4a04b14c6d34cea89dd2f712deed Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Fri, 26 Jun 2026 19:17:29 +0800 Subject: [PATCH] refactor manual scrape service by responsibility --- internal/service/manual_scrape.go | 422 -------------------- internal/service/manual_scrape_helpers.go | 90 +++++ internal/service/manual_scrape_providers.go | 197 +++++++++ internal/service/manual_scrape_search.go | 157 ++++++++ 4 files changed, 444 insertions(+), 422 deletions(-) create mode 100644 internal/service/manual_scrape_helpers.go create mode 100644 internal/service/manual_scrape_providers.go create mode 100644 internal/service/manual_scrape_search.go diff --git a/internal/service/manual_scrape.go b/internal/service/manual_scrape.go index 02cf603..c4779a6 100644 --- a/internal/service/manual_scrape.go +++ b/internal/service/manual_scrape.go @@ -3,8 +3,6 @@ package service import ( "context" "errors" - "fmt" - "strconv" "strings" "github.com/ShukeBta/MediaStationGo/internal/model" @@ -39,154 +37,6 @@ func (r ManualScrapeRequest) EpisodeArtworkOption() *bool { return r.EpisodeArtwork } -func (s *ScraperService) ManualSearch(ctx context.Context, media *model.Media, query, provider, mediaType string) ([]ExternalMediaResult, error) { - if s == nil || media == nil { - return nil, errors.New("media required") - } - lib, _ := s.repo.Library.FindByID(ctx, media.LibraryID) - queries := manualSearchQueries(media, lib, query) - if len(queries) == 0 { - return nil, errors.New("search query required") - } - if mediaType == "" { - if mediaIsEpisodic(media, lib) { - mediaType = "tv" - } else if lib != nil { - mediaType = lib.Type - } - } - mediaType = normalizeMediaType(mediaType, queries[0], "") - providers := manualSearchProviderSet(provider) - year := mediaYearHint(media) - if year <= 0 { - _, year = CleanQuery(queries[0]) - } - - out := make([]ExternalMediaResult, 0, 6) - add := func(source, typ string, match *Match) { - if match == nil || strings.TrimSpace(match.Title) == "" { - return - } - out = append(out, ExternalMediaResult{ - Source: source, - MediaType: typ, - Title: match.Title, - OriginalName: match.OriginalName, - Overview: match.Overview, - PosterURL: match.PosterURL, - BackdropURL: match.BackdropURL, - Year: match.Year, - Rating: match.Rating, - TMDbID: match.TMDbID, - BangumiID: match.BangumiID, - DoubanID: match.DoubanID, - TheTVDBID: match.TheTVDBID, - SubscribeKeyword: buildSubscribeKeyword(match.Title, match.Year), - SubscribeAliases: buildSubscribeAliases(match.Title, match.OriginalName, match.Year), - Languages: match.Languages, - Countries: match.Countries, - Genres: match.Genres, - NSFW: match.NSFW, - }) - } - - if providers.want("adult") { - for _, candidateQuery := range queries { - for _, match := range s.manualAdultMatches(ctx, media, candidateQuery) { - add("adult", "adult", match) - } - } - } - if providers.want("tmdb") { - for _, candidateQuery := range queries { - for _, candidate := range s.manualTMDbCandidates(ctx, candidateQuery, year, mediaType) { - add("tmdb", candidate.MediaType, candidate.Match) - } - } - } - if providers.want("douban") { - for _, candidateQuery := range queries { - if match := s.manualDoubanMatch(ctx, candidateQuery); match != nil { - add("douban", normalizeMediaType(mediaType, candidateQuery, ""), match) - } - } - } - if providers.want("bangumi") { - for _, candidateQuery := range queries { - if match := s.manualBangumiMatch(ctx, candidateQuery); match != nil { - add("bangumi", "anime", match) - } - } - } - if providers.want("thetvdb") { - for _, candidateQuery := range queries { - if match := s.manualTheTVDBMatch(ctx, candidateQuery); match != nil { - add("thetvdb", "tv", match) - } - } - } - return dedupeExternalMedia(out), nil -} - -type manualSearchProviders map[string]struct{} - -func manualSearchProviderSet(provider string) manualSearchProviders { - out := manualSearchProviders{} - for _, field := range strings.FieldsFunc(provider, func(r rune) bool { - return r == ',' || r == ';' || r == '|' || r == ' ' - }) { - field = strings.ToLower(strings.TrimSpace(field)) - if field == "" || field == "all" { - return nil - } - out[field] = struct{}{} - } - if len(out) == 0 { - return nil - } - return out -} - -func (p manualSearchProviders) want(provider string) bool { - if len(p) == 0 { - return true - } - _, ok := p[provider] - return ok -} - -func manualSearchQueries(media *model.Media, lib *model.Library, query string) []string { - seen := map[string]struct{}{} - out := make([]string, 0, 4) - add := func(value string) { - value = strings.Join(strings.Fields(strings.TrimSpace(value)), " ") - if value == "" { - return - } - key := strings.ToLower(value) - if _, ok := seen[key]; ok { - return - } - seen[key] = struct{}{} - out = append(out, value) - } - - add(query) - if strings.TrimSpace(query) == "" && media != nil { - add(firstText(media.Title, media.OriginalName)) - } - if media != nil { - for _, candidate := range scrapeQueryCandidates(media, lib) { - add(candidate) - } - if len(out) == 0 { - title, _ := CleanQuery(media.Path) - add(title) - } - } - return out -} - func (s *ScraperService) ApplyManualMatch(ctx context.Context, mediaID string, req ManualScrapeRequest) (*model.Media, error) { return s.ApplyManualMatchWithOptions(ctx, mediaID, req, ScrapeOptions{EpisodeArtwork: req.EpisodeArtworkOption()}) } @@ -253,275 +103,3 @@ func (s *ScraperService) manualRequestMatch(ctx context.Context, req ManualScrap } return fallback() } - -type manualTMDbCandidate struct { - MediaType string - Match *Match -} - -func (s *ScraperService) manualTMDbCandidates(ctx context.Context, query string, year int, mediaType string) []manualTMDbCandidate { - if s.tmdb == nil || !s.tmdb.Enabled() { - return nil - } - if id, ok := parsePositiveInt(query); ok { - out := make([]manualTMDbCandidate, 0, 2) - for _, typ := range manualTMDbIDSearchTypes(mediaType) { - if match := s.manualTMDbMatchByIDForType(ctx, id, typ); match != nil { - out = append(out, manualTMDbCandidate{MediaType: typ, Match: match}) - } - } - return out - } - out := make([]manualTMDbCandidate, 0, 4) - for _, typ := range manualTMDbSearchTypes(mediaType) { - switch typ { - case "movie": - if matches, err := s.tmdb.SearchMovieCandidates(ctx, query, year); err == nil { - for _, match := range matches { - out = append(out, manualTMDbCandidate{MediaType: "movie", Match: match}) - } - } - case "tv": - if matches, err := s.tmdb.SearchTVCandidates(ctx, query, year); err == nil { - for _, match := range matches { - out = append(out, manualTMDbCandidate{MediaType: "tv", Match: match}) - } - } - } - } - return out -} - -func (s *ScraperService) manualTMDbMatches(ctx context.Context, query string, year int, mediaType string) []*Match { - candidates := s.manualTMDbCandidates(ctx, query, year, mediaType) - out := make([]*Match, 0, len(candidates)) - for _, candidate := range candidates { - out = append(out, candidate.Match) - } - return out -} - -func manualTMDbIDSearchTypes(mediaType string) []string { - switch normalizeMediaType(mediaType, "", "") { - case "tv", "anime", "variety": - return []string{"tv", "movie"} - case "movie", "adult": - return []string{"movie", "tv"} - default: - return []string{"movie", "tv"} - } -} - -func manualTMDbSearchTypes(mediaType string) []string { - if strings.TrimSpace(mediaType) == "" { - return []string{"movie", "tv"} - } - switch normalizeMediaType(mediaType, "", "") { - case "tv", "anime", "variety": - return []string{"tv", "movie"} - case "movie", "adult": - return []string{"movie"} - default: - return []string{"movie", "tv"} - } -} - -func (s *ScraperService) manualAdultMatches(ctx context.Context, media *model.Media, query string) []*Match { - candidates := []string{query} - if media != nil { - candidates = append(candidates, media.Path, media.OriginalName, media.Title) - } - out := make([]*Match, 0, 1) - seen := map[string]struct{}{} - for _, candidate := range candidates { - code := normalizeAdultCode(candidate) - if code == "" { - continue - } - if _, ok := seen[code]; ok { - continue - } - seen[code] = struct{}{} - if match := s.manualAdultMatch(ctx, code); match != nil { - out = append(out, match) - } - } - return out -} - -func (s *ScraperService) manualAdultMatch(ctx context.Context, code string) *Match { - if s.adult == nil || !s.adult.Enabled() { - return nil - } - match, err := s.adult.Search(ctx, code) - if err != nil || match == nil { - return nil - } - return match -} - -func (s *ScraperService) manualTMDbMatchByID(ctx context.Context, id int, mediaType string) *Match { - if s.tmdb == nil || !s.tmdb.Enabled() || id <= 0 { - return nil - } - if mediaType == "tv" || mediaType == "anime" || mediaType == "variety" { - if match, err := s.tmdb.GetTVMatch(ctx, id); err == nil && match != nil { - return match - } - } - if match, err := s.tmdb.GetMovieMatch(ctx, id); err == nil && match != nil { - return match - } - if match, err := s.tmdb.GetTVMatch(ctx, id); err == nil && match != nil { - return match - } - return nil -} - -func (s *ScraperService) manualTMDbMatchByIDForType(ctx context.Context, id int, mediaType string) *Match { - if s.tmdb == nil || !s.tmdb.Enabled() || id <= 0 { - return nil - } - switch normalizeMediaType(mediaType, "", "") { - case "tv", "anime", "variety": - if match, err := s.tmdb.GetTVMatch(ctx, id); err == nil && match != nil { - return match - } - case "movie", "adult": - if match, err := s.tmdb.GetMovieMatch(ctx, id); err == nil && match != nil { - return match - } - } - return nil -} - -func (s *ScraperService) manualDoubanMatch(ctx context.Context, query string) *Match { - if s.douban == nil || !s.douban.Enabled() { - return nil - } - if id, ok := parsePositiveIDString(query); ok { - if match, err := s.douban.GetMatchByID(ctx, id); err == nil && match != nil { - return match - } - } - match, err := s.douban.SearchMatch(ctx, query) - if err != nil { - return nil - } - return match -} - -func (s *ScraperService) manualBangumiMatch(ctx context.Context, query string) *Match { - if s.bangumi == nil || !s.bangumi.Enabled() { - return nil - } - if id, ok := parsePositiveInt(query); ok { - if match, err := s.bangumi.GetSubject(ctx, id); err == nil && match != nil { - return match - } - } - match, err := s.bangumi.Search(ctx, query) - if err != nil { - return nil - } - return match -} - -func (s *ScraperService) manualTheTVDBMatch(ctx context.Context, query string) *Match { - if s.thetvdb == nil || !s.thetvdb.Enabled() { - return nil - } - if id, ok := parsePositiveIDString(normalizeTheTVDBSeriesID(query)); ok { - if match, err := s.thetvdb.GetSeriesMatchByID(ctx, id); err == nil && match != nil { - return match - } - } - match, err := s.thetvdb.SearchSeries(ctx, query) - if err != nil { - return nil - } - return match -} - -func mergeManualRequestIntoMatch(match *Match, req ManualScrapeRequest) *Match { - if match == nil { - match = &Match{} - } - if req.Title != "" { - match.Title = req.Title - } - if mediaType := normalizeOrganizeMediaType(req.MediaType); mediaType != "" { - match.MediaType = mediaType - } - if req.OriginalName != "" { - match.OriginalName = req.OriginalName - } - if req.Overview != "" { - match.Overview = req.Overview - } - if req.PosterURL != "" { - match.PosterURL = req.PosterURL - } - if req.BackdropURL != "" { - match.BackdropURL = req.BackdropURL - } - if req.Year > 0 { - match.Year = req.Year - } - if req.Rating > 0 { - match.Rating = req.Rating - } - if req.TMDbID > 0 { - match.TMDbID = req.TMDbID - } - if req.BangumiID > 0 { - match.BangumiID = req.BangumiID - } - if req.DoubanID != "" { - match.DoubanID = req.DoubanID - } - if req.TheTVDBID != "" { - match.TheTVDBID = req.TheTVDBID - } - if len(req.Genres) > 0 { - match.Genres = req.Genres - } - if len(req.Countries) > 0 { - match.Countries = req.Countries - } - if len(req.Languages) > 0 { - match.Languages = req.Languages - } - if req.NSFW { - match.NSFW = true - } - return match -} - -func isTVLikeTMDbMatch(match *Match, mediaType string) bool { - return mediaType == "tv" || mediaType == "anime" || mediaType == "variety" -} - -func parsePositiveInt(value string) (int, bool) { - value = strings.TrimSpace(value) - if strings.Contains(value, ":") { - value = value[strings.LastIndex(value, ":")+1:] - } - id, err := strconv.Atoi(strings.TrimSpace(value)) - return id, err == nil && id > 0 -} - -func parsePositiveIDString(value string) (string, bool) { - id, ok := parsePositiveInt(value) - if !ok { - return "", false - } - return strconv.Itoa(id), true -} - -func manualScrapeBatchName(ids []string) string { - if len(ids) == 1 { - return ids[0] - } - return fmt.Sprintf("%d 个媒体", len(ids)) -} diff --git a/internal/service/manual_scrape_helpers.go b/internal/service/manual_scrape_helpers.go new file mode 100644 index 0000000..91b6762 --- /dev/null +++ b/internal/service/manual_scrape_helpers.go @@ -0,0 +1,90 @@ +package service + +import ( + "fmt" + "strconv" + "strings" +) + +func mergeManualRequestIntoMatch(match *Match, req ManualScrapeRequest) *Match { + if match == nil { + match = &Match{} + } + if req.Title != "" { + match.Title = req.Title + } + if mediaType := normalizeOrganizeMediaType(req.MediaType); mediaType != "" { + match.MediaType = mediaType + } + if req.OriginalName != "" { + match.OriginalName = req.OriginalName + } + if req.Overview != "" { + match.Overview = req.Overview + } + if req.PosterURL != "" { + match.PosterURL = req.PosterURL + } + if req.BackdropURL != "" { + match.BackdropURL = req.BackdropURL + } + if req.Year > 0 { + match.Year = req.Year + } + if req.Rating > 0 { + match.Rating = req.Rating + } + if req.TMDbID > 0 { + match.TMDbID = req.TMDbID + } + if req.BangumiID > 0 { + match.BangumiID = req.BangumiID + } + if req.DoubanID != "" { + match.DoubanID = req.DoubanID + } + if req.TheTVDBID != "" { + match.TheTVDBID = req.TheTVDBID + } + if len(req.Genres) > 0 { + match.Genres = req.Genres + } + if len(req.Countries) > 0 { + match.Countries = req.Countries + } + if len(req.Languages) > 0 { + match.Languages = req.Languages + } + if req.NSFW { + match.NSFW = true + } + return match +} + +func isTVLikeTMDbMatch(match *Match, mediaType string) bool { + return mediaType == "tv" || mediaType == "anime" || mediaType == "variety" +} + +func parsePositiveInt(value string) (int, bool) { + value = strings.TrimSpace(value) + if strings.Contains(value, ":") { + value = value[strings.LastIndex(value, ":")+1:] + } + id, err := strconv.Atoi(strings.TrimSpace(value)) + return id, err == nil && id > 0 +} + +func parsePositiveIDString(value string) (string, bool) { + id, ok := parsePositiveInt(value) + if !ok { + return "", false + } + return strconv.Itoa(id), true +} + +func manualScrapeBatchName(ids []string) string { + if len(ids) == 1 { + return ids[0] + } + return fmt.Sprintf("%d 个媒体", len(ids)) +} diff --git a/internal/service/manual_scrape_providers.go b/internal/service/manual_scrape_providers.go new file mode 100644 index 0000000..51f3d7c --- /dev/null +++ b/internal/service/manual_scrape_providers.go @@ -0,0 +1,197 @@ +package service + +import ( + "context" + "strings" + + "github.com/ShukeBta/MediaStationGo/internal/model" +) + +type manualTMDbCandidate struct { + MediaType string + Match *Match +} + +func (s *ScraperService) manualTMDbCandidates(ctx context.Context, query string, year int, mediaType string) []manualTMDbCandidate { + if s.tmdb == nil || !s.tmdb.Enabled() { + return nil + } + if id, ok := parsePositiveInt(query); ok { + out := make([]manualTMDbCandidate, 0, 2) + for _, typ := range manualTMDbIDSearchTypes(mediaType) { + if match := s.manualTMDbMatchByIDForType(ctx, id, typ); match != nil { + out = append(out, manualTMDbCandidate{MediaType: typ, Match: match}) + } + } + return out + } + out := make([]manualTMDbCandidate, 0, 4) + for _, typ := range manualTMDbSearchTypes(mediaType) { + switch typ { + case "movie": + if matches, err := s.tmdb.SearchMovieCandidates(ctx, query, year); err == nil { + for _, match := range matches { + out = append(out, manualTMDbCandidate{MediaType: "movie", Match: match}) + } + } + case "tv": + if matches, err := s.tmdb.SearchTVCandidates(ctx, query, year); err == nil { + for _, match := range matches { + out = append(out, manualTMDbCandidate{MediaType: "tv", Match: match}) + } + } + } + } + return out +} + +func (s *ScraperService) manualTMDbMatches(ctx context.Context, query string, year int, mediaType string) []*Match { + candidates := s.manualTMDbCandidates(ctx, query, year, mediaType) + out := make([]*Match, 0, len(candidates)) + for _, candidate := range candidates { + out = append(out, candidate.Match) + } + return out +} + +func manualTMDbIDSearchTypes(mediaType string) []string { + switch normalizeMediaType(mediaType, "", "") { + case "tv", "anime", "variety": + return []string{"tv", "movie"} + case "movie", "adult": + return []string{"movie", "tv"} + default: + return []string{"movie", "tv"} + } +} + +func manualTMDbSearchTypes(mediaType string) []string { + if strings.TrimSpace(mediaType) == "" { + return []string{"movie", "tv"} + } + switch normalizeMediaType(mediaType, "", "") { + case "tv", "anime", "variety": + return []string{"tv", "movie"} + case "movie", "adult": + return []string{"movie"} + default: + return []string{"movie", "tv"} + } +} + +func (s *ScraperService) manualAdultMatches(ctx context.Context, media *model.Media, query string) []*Match { + candidates := []string{query} + if media != nil { + candidates = append(candidates, media.Path, media.OriginalName, media.Title) + } + out := make([]*Match, 0, 1) + seen := map[string]struct{}{} + for _, candidate := range candidates { + code := normalizeAdultCode(candidate) + if code == "" { + continue + } + if _, ok := seen[code]; ok { + continue + } + seen[code] = struct{}{} + if match := s.manualAdultMatch(ctx, code); match != nil { + out = append(out, match) + } + } + return out +} + +func (s *ScraperService) manualAdultMatch(ctx context.Context, code string) *Match { + if s.adult == nil || !s.adult.Enabled() { + return nil + } + match, err := s.adult.Search(ctx, code) + if err != nil || match == nil { + return nil + } + return match +} + +func (s *ScraperService) manualTMDbMatchByID(ctx context.Context, id int, mediaType string) *Match { + if s.tmdb == nil || !s.tmdb.Enabled() || id <= 0 { + return nil + } + if mediaType == "tv" || mediaType == "anime" || mediaType == "variety" { + if match, err := s.tmdb.GetTVMatch(ctx, id); err == nil && match != nil { + return match + } + } + if match, err := s.tmdb.GetMovieMatch(ctx, id); err == nil && match != nil { + return match + } + if match, err := s.tmdb.GetTVMatch(ctx, id); err == nil && match != nil { + return match + } + return nil +} + +func (s *ScraperService) manualTMDbMatchByIDForType(ctx context.Context, id int, mediaType string) *Match { + if s.tmdb == nil || !s.tmdb.Enabled() || id <= 0 { + return nil + } + switch normalizeMediaType(mediaType, "", "") { + case "tv", "anime", "variety": + if match, err := s.tmdb.GetTVMatch(ctx, id); err == nil && match != nil { + return match + } + case "movie", "adult": + if match, err := s.tmdb.GetMovieMatch(ctx, id); err == nil && match != nil { + return match + } + } + return nil +} + +func (s *ScraperService) manualDoubanMatch(ctx context.Context, query string) *Match { + if s.douban == nil || !s.douban.Enabled() { + return nil + } + if id, ok := parsePositiveIDString(query); ok { + if match, err := s.douban.GetMatchByID(ctx, id); err == nil && match != nil { + return match + } + } + match, err := s.douban.SearchMatch(ctx, query) + if err != nil { + return nil + } + return match +} + +func (s *ScraperService) manualBangumiMatch(ctx context.Context, query string) *Match { + if s.bangumi == nil || !s.bangumi.Enabled() { + return nil + } + if id, ok := parsePositiveInt(query); ok { + if match, err := s.bangumi.GetSubject(ctx, id); err == nil && match != nil { + return match + } + } + match, err := s.bangumi.Search(ctx, query) + if err != nil { + return nil + } + return match +} + +func (s *ScraperService) manualTheTVDBMatch(ctx context.Context, query string) *Match { + if s.thetvdb == nil || !s.thetvdb.Enabled() { + return nil + } + if id, ok := parsePositiveIDString(normalizeTheTVDBSeriesID(query)); ok { + if match, err := s.thetvdb.GetSeriesMatchByID(ctx, id); err == nil && match != nil { + return match + } + } + match, err := s.thetvdb.SearchSeries(ctx, query) + if err != nil { + return nil + } + return match +} diff --git a/internal/service/manual_scrape_search.go b/internal/service/manual_scrape_search.go new file mode 100644 index 0000000..fe97855 --- /dev/null +++ b/internal/service/manual_scrape_search.go @@ -0,0 +1,157 @@ +package service + +import ( + "context" + "errors" + "strings" + + "github.com/ShukeBta/MediaStationGo/internal/model" +) + +func (s *ScraperService) ManualSearch(ctx context.Context, media *model.Media, query, provider, mediaType string) ([]ExternalMediaResult, error) { + if s == nil || media == nil { + return nil, errors.New("media required") + } + lib, _ := s.repo.Library.FindByID(ctx, media.LibraryID) + queries := manualSearchQueries(media, lib, query) + if len(queries) == 0 { + return nil, errors.New("search query required") + } + if mediaType == "" { + if mediaIsEpisodic(media, lib) { + mediaType = "tv" + } else if lib != nil { + mediaType = lib.Type + } + } + mediaType = normalizeMediaType(mediaType, queries[0], "") + providers := manualSearchProviderSet(provider) + year := mediaYearHint(media) + if year <= 0 { + _, year = CleanQuery(queries[0]) + } + + out := make([]ExternalMediaResult, 0, 6) + add := func(source, typ string, match *Match) { + if match == nil || strings.TrimSpace(match.Title) == "" { + return + } + out = append(out, ExternalMediaResult{ + Source: source, + MediaType: typ, + Title: match.Title, + OriginalName: match.OriginalName, + Overview: match.Overview, + PosterURL: match.PosterURL, + BackdropURL: match.BackdropURL, + Year: match.Year, + Rating: match.Rating, + TMDbID: match.TMDbID, + BangumiID: match.BangumiID, + DoubanID: match.DoubanID, + TheTVDBID: match.TheTVDBID, + SubscribeKeyword: buildSubscribeKeyword(match.Title, match.Year), + SubscribeAliases: buildSubscribeAliases(match.Title, match.OriginalName, match.Year), + Languages: match.Languages, + Countries: match.Countries, + Genres: match.Genres, + NSFW: match.NSFW, + }) + } + + if providers.want("adult") { + for _, candidateQuery := range queries { + for _, match := range s.manualAdultMatches(ctx, media, candidateQuery) { + add("adult", "adult", match) + } + } + } + if providers.want("tmdb") { + for _, candidateQuery := range queries { + for _, candidate := range s.manualTMDbCandidates(ctx, candidateQuery, year, mediaType) { + add("tmdb", candidate.MediaType, candidate.Match) + } + } + } + if providers.want("douban") { + for _, candidateQuery := range queries { + if match := s.manualDoubanMatch(ctx, candidateQuery); match != nil { + add("douban", normalizeMediaType(mediaType, candidateQuery, ""), match) + } + } + } + if providers.want("bangumi") { + for _, candidateQuery := range queries { + if match := s.manualBangumiMatch(ctx, candidateQuery); match != nil { + add("bangumi", "anime", match) + } + } + } + if providers.want("thetvdb") { + for _, candidateQuery := range queries { + if match := s.manualTheTVDBMatch(ctx, candidateQuery); match != nil { + add("thetvdb", "tv", match) + } + } + } + return dedupeExternalMedia(out), nil +} + +type manualSearchProviders map[string]struct{} + +func manualSearchProviderSet(provider string) manualSearchProviders { + out := manualSearchProviders{} + for _, field := range strings.FieldsFunc(provider, func(r rune) bool { + return r == ',' || r == ';' || r == '|' || r == ' ' + }) { + field = strings.ToLower(strings.TrimSpace(field)) + if field == "" || field == "all" { + return nil + } + out[field] = struct{}{} + } + if len(out) == 0 { + return nil + } + return out +} + +func (p manualSearchProviders) want(provider string) bool { + if len(p) == 0 { + return true + } + _, ok := p[provider] + return ok +} + +func manualSearchQueries(media *model.Media, lib *model.Library, query string) []string { + seen := map[string]struct{}{} + out := make([]string, 0, 4) + add := func(value string) { + value = strings.Join(strings.Fields(strings.TrimSpace(value)), " ") + if value == "" { + return + } + key := strings.ToLower(value) + if _, ok := seen[key]; ok { + return + } + seen[key] = struct{}{} + out = append(out, value) + } + + add(query) + if strings.TrimSpace(query) == "" && media != nil { + add(firstText(media.Title, media.OriginalName)) + } + if media != nil { + for _, candidate := range scrapeQueryCandidates(media, lib) { + add(candidate) + } + if len(out) == 0 { + title, _ := CleanQuery(media.Path) + add(title) + } + } + return out +}