mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
185 lines
5.3 KiB
Go
185 lines
5.3 KiB
Go
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 !subscriptionAllowsWash(sub) && local.LocalMediaCount > 0 {
|
|
if stats != nil {
|
|
stats.LocalAlreadySatisfied = true
|
|
}
|
|
return recordPreparedSelection(nil, stats)
|
|
}
|
|
return recordPreparedSelection(candidates[:1], stats)
|
|
}
|
|
|
|
if localSeriesPackSatisfiesSubscription(local) {
|
|
if stats != nil {
|
|
stats.LocalSeriesPackPresent = true
|
|
}
|
|
return recordPreparedSelection(nil, stats)
|
|
}
|
|
if local.LocalMediaCount > 0 {
|
|
trustedTotal := trustedAvailabilityTotal(local)
|
|
if trustedTotal > 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 candidateEpisodesAllExist(local.ExistingEpisodeKeys, season, candidate) {
|
|
if stats != nil {
|
|
stats.ExistingEpisodeSkipped++
|
|
}
|
|
continue
|
|
}
|
|
if trustedTotal > 0 && !candidateCoversMissingEpisode(candidate, missingSet) {
|
|
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 candidateEpisodesAllExist(existing map[string]struct{}, season int, candidate siteSearchCandidate) bool {
|
|
episodes := candidateEpisodeNumbers(candidate)
|
|
if len(episodes) == 0 {
|
|
return false
|
|
}
|
|
for _, episode := range episodes {
|
|
if _, exists := existing[episodeKey(season, episode)]; !exists {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func candidateCoversMissingEpisode(candidate siteSearchCandidate, missingSet map[int]struct{}) bool {
|
|
episodes := candidateEpisodeNumbers(candidate)
|
|
if len(episodes) == 0 {
|
|
return false
|
|
}
|
|
for _, episode := range episodes {
|
|
if _, missing := missingSet[episode]; missing {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func candidateEpisodeNumbers(candidate siteSearchCandidate) []int {
|
|
if len(candidate.Episodes) > 0 {
|
|
return candidate.Episodes
|
|
}
|
|
if candidate.Episode > 0 {
|
|
return []int{candidate.Episode}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func recordPreparedSelection(candidates []siteSearchCandidate, stats *siteSearchSelectionStats) []siteSearchCandidate {
|
|
if stats != nil {
|
|
stats.Selected = len(candidates)
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
func localSeriesPackSatisfiesSubscription(local LocalAvailability) bool {
|
|
if !local.HasSeriesPack {
|
|
return false
|
|
}
|
|
total := trustedAvailabilityTotal(local)
|
|
if total <= 0 {
|
|
return len(local.ExistingEpisodeKeys) == 0
|
|
}
|
|
if len(local.MissingEpisodes) > 0 {
|
|
return false
|
|
}
|
|
return len(local.ExistingEpisodeKeys) >= total
|
|
}
|
|
|
|
func trustedAvailabilityTotal(local LocalAvailability) int {
|
|
total := local.TotalEpisodes
|
|
if total <= 0 {
|
|
return 0
|
|
}
|
|
if maxEpisode := maxAvailabilityEpisode(local.ExistingEpisodeKeys); maxEpisode > total {
|
|
return 0
|
|
}
|
|
return total
|
|
}
|