mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-12 16:26:36 +08:00
147 lines
4.2 KiB
Go
147 lines
4.2 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"regexp"
|
||
"strings"
|
||
|
||
"github.com/truewhile/MeBox/internal/model"
|
||
"github.com/truewhile/MeBox/internal/repository"
|
||
)
|
||
|
||
var availabilityNoiseRE = regexp.MustCompile(`(?i)(自动订阅|订阅|全集|合集|complete|batch|s\d{1,2}e\d{1,3}|season\s*\d+|s\d{1,2}|第\s*\d+\s*季|第\s*\d+\s*[集话話期]|\(\d{4}\)|\b\d{4}\b|2160p|1080p|720p|4k|uhd|bluray|blu-ray|web-?dl|hdtv|remux|x26[45]|h\.?26[45]|hevc|avc|hdr10?\+?|dovi|dv|atmos|aac|ddp?5\.1|truehd|flac)`)
|
||
|
||
type LocalAvailability struct {
|
||
DownloadedEpisodes int
|
||
TotalEpisodes int
|
||
LocalMediaCount int
|
||
MissingEpisodes []int
|
||
InLibrary bool
|
||
HasSeriesPack bool
|
||
ExistingEpisodeKeys map[string]struct{}
|
||
MissingEpisodeKeys map[string]struct{}
|
||
}
|
||
|
||
func EnrichExternalMediaAvailability(ctx context.Context, repo *repository.Container, items []ExternalMediaResult) {
|
||
for i := range items {
|
||
availability := LookupLocalAvailability(ctx, repo, items[i].Title, items[i].SubscribeKeyword, items[i].MediaType, items[i].TotalEpisodes)
|
||
items[i].DownloadedEpisodes = availability.DownloadedEpisodes
|
||
items[i].LocalMediaCount = availability.LocalMediaCount
|
||
items[i].MissingEpisodes = availability.MissingEpisodes
|
||
items[i].InLibrary = availability.InLibrary
|
||
if items[i].TotalEpisodes == 0 {
|
||
items[i].TotalEpisodes = availability.TotalEpisodes
|
||
}
|
||
}
|
||
}
|
||
|
||
func LookupLocalAvailability(ctx context.Context, repo *repository.Container, title, keyword, mediaType string, expectedTotal int) LocalAvailability {
|
||
out := LocalAvailability{
|
||
TotalEpisodes: expectedTotal,
|
||
ExistingEpisodeKeys: map[string]struct{}{},
|
||
MissingEpisodeKeys: map[string]struct{}{},
|
||
}
|
||
if repo == nil || repo.DB == nil {
|
||
return out
|
||
}
|
||
query := availabilityQuery(title, keyword)
|
||
if query == "" {
|
||
return out
|
||
}
|
||
like := "%" + query + "%"
|
||
// 方言相关的 ILIKE/LIKE:PostgreSQL 下用 LIKE 会因标题大小写与查询不一致而
|
||
// 漏判「本地已有该剧」,进而反复提示订阅缺失。
|
||
likeOp := repository.LikeOperator(repo.DB)
|
||
var rows []model.Media
|
||
if err := repo.DB.WithContext(ctx).
|
||
Where("title "+likeOp+" ? OR original_name "+likeOp+" ? OR path "+likeOp+" ?", like, like, like).
|
||
Order("season_num asc, episode_num asc, episode_fraction asc, created_at desc").
|
||
Limit(2000).
|
||
Find(&rows).Error; err != nil {
|
||
return out
|
||
}
|
||
out.LocalMediaCount = len(rows)
|
||
out.InLibrary = len(rows) > 0
|
||
if len(rows) == 0 {
|
||
return out
|
||
}
|
||
|
||
seriesLike := isSubscriptionSeriesType(mediaType)
|
||
for _, row := range rows {
|
||
if row.EpisodeNum <= 0 {
|
||
if seriesLike {
|
||
out.HasSeriesPack = true
|
||
}
|
||
continue
|
||
}
|
||
season := row.SeasonNum
|
||
if season <= 0 {
|
||
season = 1
|
||
}
|
||
key := episodeKey(season, row.EpisodeNum)
|
||
out.ExistingEpisodeKeys[key] = struct{}{}
|
||
}
|
||
if seriesLike || len(out.ExistingEpisodeKeys) > 0 {
|
||
out.DownloadedEpisodes = len(out.ExistingEpisodeKeys)
|
||
out.MissingEpisodes = missingEpisodes(out.ExistingEpisodeKeys, out.TotalEpisodes)
|
||
for _, episode := range out.MissingEpisodes {
|
||
out.MissingEpisodeKeys[episodeKey(1, episode)] = struct{}{}
|
||
}
|
||
return out
|
||
}
|
||
out.DownloadedEpisodes = 1
|
||
if out.TotalEpisodes == 0 {
|
||
out.TotalEpisodes = 1
|
||
}
|
||
return out
|
||
}
|
||
|
||
func isSubscriptionSeriesType(mediaType string) bool {
|
||
switch normalizeMediaType(mediaType, "", "") {
|
||
case "tv", "anime", "variety":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func missingEpisodes(existing map[string]struct{}, total int) []int {
|
||
if total <= 0 {
|
||
return nil
|
||
}
|
||
missing := make([]int, 0)
|
||
for episode := 1; episode <= total; episode++ {
|
||
if _, ok := existing[episodeKey(1, episode)]; ok {
|
||
continue
|
||
}
|
||
missing = append(missing, episode)
|
||
}
|
||
return missing
|
||
}
|
||
|
||
func availabilityQuery(title, keyword string) string {
|
||
for _, candidate := range []string{keyword, title} {
|
||
cleaned := cleanAvailabilityTitle(candidate)
|
||
if cleaned != "" {
|
||
return cleaned
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func cleanAvailabilityTitle(value string) string {
|
||
value = availabilityNoiseRE.ReplaceAllString(value, " ")
|
||
value = strings.TrimSpace(strings.Join(strings.Fields(value), " "))
|
||
value = strings.TrimSuffix(value, "-")
|
||
value = strings.TrimSpace(value)
|
||
return value
|
||
}
|
||
|
||
func episodeKey(season, episode int) string {
|
||
if season <= 0 {
|
||
season = 1
|
||
}
|
||
return fmt.Sprintf("%02dE%03d", season, episode)
|
||
}
|