Files
MeBox/internal/service/download_identity.go
T
2026-06-26 15:20:48 +08:00

208 lines
5.3 KiB
Go

package service
import (
"fmt"
"net/url"
"path"
"regexp"
"strings"
"unicode"
)
var torrentEpisodeToken = regexp.MustCompile(`(?i)e\d{1,3}`)
var downloadPackTitleToken = regexp.MustCompile(`(?i)(?:^|[\s._-])(?:complete|batch|pack|合集|全集|整季|全季)(?:[\s._-]|$)`)
func localAvailabilityTitleCandidates(title string) []string {
seen := map[string]struct{}{}
out := make([]string, 0, 6)
add := func(value string) {
value = strings.TrimSpace(value)
if value == "" {
return
}
if _, ok := seen[value]; ok {
return
}
seen[value] = struct{}{}
out = append(out, value)
}
add(availabilityQuery(title, ""))
if cleaned, _ := CleanQuery(title); cleaned != "" {
for _, candidate := range titleCandidates(cleaned) {
add(candidate)
fields := strings.Fields(candidate)
for i := len(fields) - 1; i >= 1; i-- {
prefix := strings.Join(fields[:i], " ")
if containsCJK(prefix) {
add(prefix)
}
}
}
}
return out
}
func downloadTaskBlocksDuplicate(status string) bool {
switch strings.ToLower(strings.TrimSpace(status)) {
case "failed", "error", "removed", "cancelled", "canceled":
return false
default:
return true
}
}
func downloadTaskBlocksReadd(status string) bool {
switch strings.ToLower(strings.TrimSpace(status)) {
case "failed", "error", "deleted", "removed", "cancelled", "canceled":
return false
default:
return true
}
}
func downloadTaskIdentityKey(name string) string {
if key := downloadMediaIdentityKey(name); key != "" {
return key
}
return normalizedDownloadTitleKey(name)
}
type downloadMediaIdentity struct {
TitleKey string
Year int
Episodes []episodeRef
Pack bool
}
func parseDownloadMediaIdentity(name string) downloadMediaIdentity {
title, year := CleanQuery(name)
if isSeriesPackTitle(name) {
title = downloadPackTitleToken.ReplaceAllString(title, " ")
}
titleKey := normalizeAvailabilityComparable(title)
if titleKey == "" {
titleKey = normalizeAvailabilityComparable(availabilityQuery(name, ""))
}
return downloadMediaIdentity{
TitleKey: titleKey,
Year: year,
Episodes: episodeRefsFromTitle(name),
Pack: isSeriesPackTitle(name),
}
}
func downloadTitleCoversRequest(existing, requested string) bool {
current := parseDownloadMediaIdentity(existing)
want := parseDownloadMediaIdentity(requested)
if current.TitleKey == "" || want.TitleKey == "" {
currentKey := normalizedDownloadTitleKey(existing)
wantKey := normalizedDownloadTitleKey(requested)
return currentKey != "" && wantKey != "" && (currentKey == wantKey || strings.Contains(currentKey, wantKey) || strings.Contains(wantKey, currentKey))
}
if current.TitleKey != want.TitleKey {
return false
}
if current.Year > 0 && want.Year > 0 && current.Year != want.Year {
return false
}
if downloadIdentityCoversWholeSeason(existing, current) {
return true
}
if len(current.Episodes) == 0 || len(want.Episodes) == 0 {
return len(current.Episodes) == len(want.Episodes)
}
currentEpisodes := map[string]struct{}{}
for _, ref := range current.Episodes {
currentEpisodes[episodeKey(ref.Season, ref.Episode)] = struct{}{}
}
for _, ref := range want.Episodes {
if _, ok := currentEpisodes[episodeKey(ref.Season, ref.Episode)]; !ok {
return false
}
}
return true
}
func downloadIdentityCoversWholeSeason(title string, identity downloadMediaIdentity) bool {
if !identity.Pack || len(identity.Episodes) > 0 {
return false
}
return seriesPackRE.MatchString(title)
}
func downloadMediaIdentityKey(name string) string {
name = strings.ToLower(strings.TrimSpace(name))
if name == "" {
return ""
}
identity := parseDownloadMediaIdentity(name)
titleKey := identity.TitleKey
if titleKey == "" {
return ""
}
parts := []string{titleKey}
if identity.Year > 0 {
parts = append(parts, fmt.Sprintf("y%d", identity.Year))
}
if len(identity.Episodes) > 0 {
first := identity.Episodes[0]
last := identity.Episodes[len(identity.Episodes)-1]
parts = append(parts, fmt.Sprintf("s%02de%03d", first.Season, first.Episode))
if len(identity.Episodes) > 1 {
parts = append(parts, fmt.Sprintf("to%03d", last.Episode))
}
}
return strings.Join(parts, "|")
}
func normalizedDownloadTitleKey(name string) string {
name = strings.ToLower(strings.TrimSpace(name))
var b strings.Builder
for _, r := range name {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
b.WriteRune(r)
}
}
return b.String()
}
func publicDownloadTitle(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return "下载任务"
}
if u, err := url.Parse(raw); err == nil {
if dn := strings.TrimSpace(u.Query().Get("dn")); dn != "" {
if decoded, err := url.QueryUnescape(dn); err == nil && strings.TrimSpace(decoded) != "" {
return strings.TrimSpace(decoded)
}
return dn
}
if u.Host != "" {
base := path.Base(u.Path)
if base != "." && base != "/" && base != "" {
base = strings.TrimSuffix(base, path.Ext(base))
if base != "" {
return base
}
}
return u.Host
}
}
if strings.HasPrefix(strings.ToLower(raw), "magnet:") {
return "磁力下载"
}
return "下载任务"
}
func normalizeTorrentName(name string) string {
name = torrentEpisodeToken.ReplaceAllString(strings.ToLower(name), "")
var b strings.Builder
for _, r := range name {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
b.WriteRune(r)
}
}
return b.String()
}