package service import ( "sort" "github.com/ShukeBta/MediaStationGo/internal/model" ) func selectPreparedSubscriptionCandidates(candidates []siteSearchCandidate, sub *model.Subscription, local LocalAvailability) []siteSearchCandidate { return selectPreparedSubscriptionCandidatesWithStats(candidates, sub, local, nil) } func selectPreparedSubscriptionCandidatesWithStats(candidates []siteSearchCandidate, sub *model.Subscription, local LocalAvailability, stats *siteSearchSelectionStats) []siteSearchCandidate { if len(candidates) > 1 { sort.SliceStable(candidates, func(i, j int) bool { if candidates[i].Score != candidates[j].Score { return candidates[i].Score > candidates[j].Score } if candidates[i].Item.Seeders != candidates[j].Item.Seeders { return candidates[i].Item.Seeders > candidates[j].Item.Seeders } return candidates[i].Item.Size > candidates[j].Item.Size }) } if len(candidates) == 0 { return recordPreparedSelection(nil, stats) } mediaType := normalizeMediaType(sub.MediaType, sub.Name+" "+sub.Filter, "") if !isSubscriptionSeriesType(mediaType) { // 非洗版订阅成功下载一次即满足,媒体库/下载中已存在则不再重复下载。 if (sub == nil || !sub.WashEnabled) && local.LocalMediaCount > 0 { if stats != nil { stats.LocalAlreadySatisfied = true } return recordPreparedSelection(nil, stats) } return recordPreparedSelection(candidates[:1], stats) } if local.HasSeriesPack { if stats != nil { stats.LocalSeriesPackPresent = true } return recordPreparedSelection(nil, stats) } if local.LocalMediaCount > 0 { if local.TotalEpisodes > 0 && len(local.MissingEpisodes) == 0 { if stats != nil { stats.SeriesComplete = true } return recordPreparedSelection(nil, stats) } missingSet := missingEpisodeSet(local) onlyMissing := make([]siteSearchCandidate, 0, len(candidates)) var packFallback *siteSearchCandidate for i := range candidates { candidate := candidates[i] if candidate.Episode <= 0 { // 整季/全集包(无单集号)。剧集完结后站点常只挂全集包, // 这里记下来作兜底:当单集候选不足以补齐缺失集时启用, // 否则"补全缺失集"在站点只有全集包时永远匹配为空。 if stats != nil { stats.NoEpisodeSkipped++ } if candidate.Pack && packFallback == nil { packFallback = &candidates[i] if stats != nil { stats.PackFallbackAvailable = true } } continue } season := candidate.Season if season <= 0 { season = 1 } if _, exists := local.ExistingEpisodeKeys[episodeKey(season, candidate.Episode)]; exists { if stats != nil { stats.ExistingEpisodeSkipped++ } continue } if local.TotalEpisodes > 0 { if _, missing := missingSet[candidate.Episode]; !missing { if stats != nil { stats.NotMissingEpisodeSkipped++ } continue } } onlyMissing = append(onlyMissing, candidate) } selected := sortedEpisodeCandidates(onlyMissing) if len(selected) == 0 && packFallback != nil { // 没有可用的单集候选,但站点有整季/全集包 → 用包兜底补缺集。 // 代价是会重下已有集,但用户主动触发补全时这是可接受的。 if stats != nil { stats.PackFallbackUsed = true } return recordPreparedSelection([]siteSearchCandidate{*packFallback}, stats) } return recordPreparedSelection(selected, stats) } for _, candidate := range candidates { if candidate.Pack { return recordPreparedSelection([]siteSearchCandidate{candidate}, stats) } } selected := sortedEpisodeCandidates(candidates) if len(selected) == 0 { return recordPreparedSelection(candidates[:1], stats) } return recordPreparedSelection(selected, stats) } func recordPreparedSelection(candidates []siteSearchCandidate, stats *siteSearchSelectionStats) []siteSearchCandidate { if stats != nil { stats.Selected = len(candidates) } return candidates }