mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-08 14:26:37 +08:00
refactor media service helpers
This commit is contained in:
@@ -3,12 +3,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -33,11 +28,6 @@ type MediaVisibility struct {
|
|||||||
HiddenLibraryIDs []string
|
HiddenLibraryIDs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type MediaItem struct {
|
|
||||||
model.Media
|
|
||||||
Versions []model.Media `json:"versions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxMediaSearchLimit = 50000
|
const maxMediaSearchLimit = 50000
|
||||||
const maxMediaSearchPageSize = 2000
|
const maxMediaSearchPageSize = 2000
|
||||||
|
|
||||||
@@ -180,42 +170,6 @@ func (s *MediaService) ListMediaVisibleGrouped(ctx context.Context, libraryID st
|
|||||||
return paginateMediaItems(grouped, page, pageSize), int64(len(grouped)), nil
|
return paginateMediaItems(grouped, page, pageSize), int64(len(grouped)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mediaListCacheValue struct {
|
|
||||||
Items []model.Media `json:"items"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MediaService) mediaListCacheKey(libraryID string, libraryIDs []string, page, pageSize int, filter repository.MediaQueryFilter) string {
|
|
||||||
allowed := append([]string(nil), filter.AllowedLibraryIDs...)
|
|
||||||
hidden := append([]string(nil), filter.HiddenLibraryIDs...)
|
|
||||||
libs := append([]string(nil), libraryIDs...)
|
|
||||||
sort.Strings(allowed)
|
|
||||||
sort.Strings(hidden)
|
|
||||||
sort.Strings(libs)
|
|
||||||
sum := sha1.Sum([]byte(strings.Join([]string{
|
|
||||||
libraryID,
|
|
||||||
strings.Join(libs, ","),
|
|
||||||
fmt.Sprintf("%d:%d:%t", page, pageSize, filter.IncludeNSFW),
|
|
||||||
strings.Join(allowed, ","),
|
|
||||||
strings.Join(hidden, ","),
|
|
||||||
}, "|")))
|
|
||||||
return "media:list:" + hex.EncodeToString(sum[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MediaService) mediaCacheTTLSeconds() int {
|
|
||||||
if s == nil || s.cfg == nil || s.cfg.Cache.MediaTTLSeconds < 1 {
|
|
||||||
return 15
|
|
||||||
}
|
|
||||||
return s.cfg.Cache.MediaTTLSeconds
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MediaService) invalidateMediaCache(ctx context.Context) {
|
|
||||||
if s != nil && s.cache != nil {
|
|
||||||
s.cache.DeletePrefix(ctx, "media:")
|
|
||||||
s.cache.DeletePrefix(ctx, "stats:")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MediaService) listMediaVisibleForGrouping(ctx context.Context, libraryID string, visibility MediaVisibility) ([]model.Media, error) {
|
func (s *MediaService) listMediaVisibleForGrouping(ctx context.Context, libraryID string, visibility MediaVisibility) ([]model.Media, error) {
|
||||||
visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility)
|
visibility = ExpandMediaVisibilityForMergedCloudLibraries(ctx, s.repo, visibility)
|
||||||
libraryIDs, err := MergedLibraryIDsForLibrary(ctx, s.repo, libraryID)
|
libraryIDs, err := MergedLibraryIDsForLibrary(ctx, s.repo, libraryID)
|
||||||
@@ -250,209 +204,6 @@ func (s *MediaService) listMediaVisibleForGrouping(ctx context.Context, libraryI
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeGroupedMediaPage(page, pageSize int) (int, int) {
|
|
||||||
if page < 1 {
|
|
||||||
page = 1
|
|
||||||
}
|
|
||||||
if pageSize <= 0 {
|
|
||||||
pageSize = 50
|
|
||||||
}
|
|
||||||
if pageSize > maxMediaSearchPageSize {
|
|
||||||
pageSize = maxMediaSearchPageSize
|
|
||||||
}
|
|
||||||
return page, pageSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func paginateMediaItems(items []MediaItem, page, pageSize int) []MediaItem {
|
|
||||||
page, pageSize = normalizeGroupedMediaPage(page, pageSize)
|
|
||||||
if len(items) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
start := (page - 1) * pageSize
|
|
||||||
if start >= len(items) {
|
|
||||||
return []MediaItem{}
|
|
||||||
}
|
|
||||||
end := start + pageSize
|
|
||||||
if end > len(items) {
|
|
||||||
end = len(items)
|
|
||||||
}
|
|
||||||
return items[start:end]
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstMediaItems(items []MediaItem, limit int) []MediaItem {
|
|
||||||
if len(items) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = 50
|
|
||||||
}
|
|
||||||
if limit > maxMediaSearchLimit {
|
|
||||||
limit = maxMediaSearchLimit
|
|
||||||
}
|
|
||||||
if limit > len(items) {
|
|
||||||
limit = len(items)
|
|
||||||
}
|
|
||||||
return items[:limit]
|
|
||||||
}
|
|
||||||
|
|
||||||
func groupMediaVersions(items []model.Media) []MediaItem {
|
|
||||||
if len(items) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
type group struct {
|
|
||||||
key string
|
|
||||||
primary model.Media
|
|
||||||
rows []model.Media
|
|
||||||
}
|
|
||||||
groups := make([]group, 0, len(items))
|
|
||||||
byKey := make(map[string]int, len(items))
|
|
||||||
for _, item := range items {
|
|
||||||
key := mediaVersionGroupKey(item)
|
|
||||||
if key == "" {
|
|
||||||
groups = append(groups, group{primary: item, rows: []model.Media{item}})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if idx, ok := byKey[key]; ok {
|
|
||||||
groups[idx].rows = append(groups[idx].rows, item)
|
|
||||||
if betterMediaVersion(item, groups[idx].primary) {
|
|
||||||
groups[idx].primary = item
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
byKey[key] = len(groups)
|
|
||||||
groups = append(groups, group{key: key, primary: item, rows: []model.Media{item}})
|
|
||||||
}
|
|
||||||
out := make([]MediaItem, 0, len(groups))
|
|
||||||
for _, g := range groups {
|
|
||||||
sort.SliceStable(g.rows, func(i, j int) bool {
|
|
||||||
return betterMediaVersion(g.rows[i], g.rows[j])
|
|
||||||
})
|
|
||||||
item := MediaItem{Media: g.primary}
|
|
||||||
if len(g.rows) > 1 {
|
|
||||||
item.Versions = g.rows
|
|
||||||
}
|
|
||||||
out = append(out, item)
|
|
||||||
}
|
|
||||||
sort.SliceStable(out, func(i, j int) bool {
|
|
||||||
return out[i].CreatedAt.After(out[j].CreatedAt)
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func mediaVersionGroupKey(m model.Media) string {
|
|
||||||
if m.SeasonNum > 0 || m.EpisodeNum > 0 {
|
|
||||||
switch {
|
|
||||||
case m.TMDbID > 0:
|
|
||||||
return fmt.Sprintf("episode:tmdb:%d:%d:%d", m.TMDbID, m.SeasonNum, m.EpisodeNum)
|
|
||||||
case m.BangumiID > 0:
|
|
||||||
return fmt.Sprintf("episode:bangumi:%d:%d:%d", m.BangumiID, m.SeasonNum, m.EpisodeNum)
|
|
||||||
case strings.TrimSpace(m.DoubanID) != "":
|
|
||||||
return fmt.Sprintf("episode:douban:%s:%d:%d", strings.ToLower(strings.TrimSpace(m.DoubanID)), m.SeasonNum, m.EpisodeNum)
|
|
||||||
case strings.TrimSpace(m.TheTVDBID) != "":
|
|
||||||
return fmt.Sprintf("episode:thetvdb:%s:%d:%d", strings.ToLower(strings.TrimSpace(m.TheTVDBID)), m.SeasonNum, m.EpisodeNum)
|
|
||||||
}
|
|
||||||
title := firstNonEmpty(m.OriginalName, m.Title)
|
|
||||||
if title == "" {
|
|
||||||
title, _ = CleanQuery(m.Path)
|
|
||||||
}
|
|
||||||
title, _ = mediaVersionTitleKey(title)
|
|
||||||
if title == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.Join([]string{
|
|
||||||
"episode",
|
|
||||||
strings.ToLower(strings.TrimSpace(m.LibraryID)),
|
|
||||||
title,
|
|
||||||
fmt.Sprintf("%d:%d", m.SeasonNum, m.EpisodeNum),
|
|
||||||
}, "|")
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case m.TMDbID > 0:
|
|
||||||
return fmt.Sprintf("tmdb:%d", m.TMDbID)
|
|
||||||
case m.BangumiID > 0:
|
|
||||||
return fmt.Sprintf("bangumi:%d", m.BangumiID)
|
|
||||||
case strings.TrimSpace(m.DoubanID) != "":
|
|
||||||
return "douban:" + strings.ToLower(strings.TrimSpace(m.DoubanID))
|
|
||||||
case strings.TrimSpace(m.TheTVDBID) != "":
|
|
||||||
return "thetvdb:" + strings.ToLower(strings.TrimSpace(m.TheTVDBID))
|
|
||||||
}
|
|
||||||
title := firstNonEmpty(m.OriginalName, m.Title)
|
|
||||||
titleYear := 0
|
|
||||||
if title == "" {
|
|
||||||
title, _ = CleanQuery(m.Path)
|
|
||||||
} else {
|
|
||||||
title, titleYear = mediaVersionTitleKey(title)
|
|
||||||
}
|
|
||||||
if title == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
year := m.Year
|
|
||||||
if year <= 0 {
|
|
||||||
year = titleYear
|
|
||||||
}
|
|
||||||
if year <= 0 {
|
|
||||||
_, year = CleanQuery(m.Path)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("movie:%s:%d", title, year)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mediaVersionTitleKey(value string) (string, int) {
|
|
||||||
cleaned, year := CleanQuery(value)
|
|
||||||
if strings.TrimSpace(cleaned) == "" {
|
|
||||||
cleaned = value
|
|
||||||
}
|
|
||||||
return normalizeMediaVersionText(cleaned), year
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeMediaVersionText(value string) string {
|
|
||||||
value = strings.ToLower(strings.TrimSpace(value))
|
|
||||||
if value == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
fields := strings.FieldsFunc(value, func(r rune) bool {
|
|
||||||
switch r {
|
|
||||||
case '.', '_', '-', ' ', '\t', '/', '\\', '[', ']', '(', ')', '(', ')', '【', '】':
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
out := fields[:0]
|
|
||||||
for _, field := range fields {
|
|
||||||
field = strings.TrimSpace(field)
|
|
||||||
if field == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, noise := noiseTokenSet[field]; noise {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, field)
|
|
||||||
}
|
|
||||||
return strings.Join(out, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func betterMediaVersion(candidate, current model.Media) bool {
|
|
||||||
candidateCloud := isCloudMediaVersion(candidate)
|
|
||||||
currentCloud := isCloudMediaVersion(current)
|
|
||||||
if candidateCloud != currentCloud {
|
|
||||||
return !candidateCloud
|
|
||||||
}
|
|
||||||
candidatePixels := candidate.Width * candidate.Height
|
|
||||||
currentPixels := current.Width * current.Height
|
|
||||||
if candidatePixels != currentPixels {
|
|
||||||
return candidatePixels > currentPixels
|
|
||||||
}
|
|
||||||
if candidate.SizeBytes != current.SizeBytes {
|
|
||||||
return candidate.SizeBytes > current.SizeBytes
|
|
||||||
}
|
|
||||||
return candidate.CreatedAt.After(current.CreatedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isCloudMediaVersion(media model.Media) bool {
|
|
||||||
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(media.Path)), "cloud://") ||
|
|
||||||
strings.Contains(strings.ToLower(strings.TrimSpace(media.STRMURL)), "/api/cloud/play/")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchMedia performs a simple LIKE search across titles.
|
// SearchMedia performs a simple LIKE search across titles.
|
||||||
func (s *MediaService) SearchMedia(ctx context.Context, query string, limit int) ([]model.Media, error) {
|
func (s *MediaService) SearchMedia(ctx context.Context, query string, limit int) ([]model.Media, error) {
|
||||||
return s.SearchMediaVisible(ctx, query, limit, MediaVisibility{IncludeNSFW: true})
|
return s.SearchMediaVisible(ctx, query, limit, MediaVisibility{IncludeNSFW: true})
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||||
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mediaListCacheValue struct {
|
||||||
|
Items []model.Media `json:"items"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MediaService) mediaListCacheKey(libraryID string, libraryIDs []string, page, pageSize int, filter repository.MediaQueryFilter) string {
|
||||||
|
allowed := append([]string(nil), filter.AllowedLibraryIDs...)
|
||||||
|
hidden := append([]string(nil), filter.HiddenLibraryIDs...)
|
||||||
|
libs := append([]string(nil), libraryIDs...)
|
||||||
|
sort.Strings(allowed)
|
||||||
|
sort.Strings(hidden)
|
||||||
|
sort.Strings(libs)
|
||||||
|
sum := sha1.Sum([]byte(strings.Join([]string{
|
||||||
|
libraryID,
|
||||||
|
strings.Join(libs, ","),
|
||||||
|
fmt.Sprintf("%d:%d:%t", page, pageSize, filter.IncludeNSFW),
|
||||||
|
strings.Join(allowed, ","),
|
||||||
|
strings.Join(hidden, ","),
|
||||||
|
}, "|")))
|
||||||
|
return "media:list:" + hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MediaService) mediaCacheTTLSeconds() int {
|
||||||
|
if s == nil || s.cfg == nil || s.cfg.Cache.MediaTTLSeconds < 1 {
|
||||||
|
return 15
|
||||||
|
}
|
||||||
|
return s.cfg.Cache.MediaTTLSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MediaService) invalidateMediaCache(ctx context.Context) {
|
||||||
|
if s != nil && s.cache != nil {
|
||||||
|
s.cache.DeletePrefix(ctx, "media:")
|
||||||
|
s.cache.DeletePrefix(ctx, "stats:")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MediaItem struct {
|
||||||
|
model.Media
|
||||||
|
Versions []model.Media `json:"versions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeGroupedMediaPage(page, pageSize int) (int, int) {
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 50
|
||||||
|
}
|
||||||
|
if pageSize > maxMediaSearchPageSize {
|
||||||
|
pageSize = maxMediaSearchPageSize
|
||||||
|
}
|
||||||
|
return page, pageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func paginateMediaItems(items []MediaItem, page, pageSize int) []MediaItem {
|
||||||
|
page, pageSize = normalizeGroupedMediaPage(page, pageSize)
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
start := (page - 1) * pageSize
|
||||||
|
if start >= len(items) {
|
||||||
|
return []MediaItem{}
|
||||||
|
}
|
||||||
|
end := start + pageSize
|
||||||
|
if end > len(items) {
|
||||||
|
end = len(items)
|
||||||
|
}
|
||||||
|
return items[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstMediaItems(items []MediaItem, limit int) []MediaItem {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
if limit > maxMediaSearchLimit {
|
||||||
|
limit = maxMediaSearchLimit
|
||||||
|
}
|
||||||
|
if limit > len(items) {
|
||||||
|
limit = len(items)
|
||||||
|
}
|
||||||
|
return items[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupMediaVersions(items []model.Media) []MediaItem {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
type group struct {
|
||||||
|
key string
|
||||||
|
primary model.Media
|
||||||
|
rows []model.Media
|
||||||
|
}
|
||||||
|
groups := make([]group, 0, len(items))
|
||||||
|
byKey := make(map[string]int, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
key := mediaVersionGroupKey(item)
|
||||||
|
if key == "" {
|
||||||
|
groups = append(groups, group{primary: item, rows: []model.Media{item}})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if idx, ok := byKey[key]; ok {
|
||||||
|
groups[idx].rows = append(groups[idx].rows, item)
|
||||||
|
if betterMediaVersion(item, groups[idx].primary) {
|
||||||
|
groups[idx].primary = item
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byKey[key] = len(groups)
|
||||||
|
groups = append(groups, group{key: key, primary: item, rows: []model.Media{item}})
|
||||||
|
}
|
||||||
|
out := make([]MediaItem, 0, len(groups))
|
||||||
|
for _, g := range groups {
|
||||||
|
sort.SliceStable(g.rows, func(i, j int) bool {
|
||||||
|
return betterMediaVersion(g.rows[i], g.rows[j])
|
||||||
|
})
|
||||||
|
item := MediaItem{Media: g.primary}
|
||||||
|
if len(g.rows) > 1 {
|
||||||
|
item.Versions = g.rows
|
||||||
|
}
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
sort.SliceStable(out, func(i, j int) bool {
|
||||||
|
return out[i].CreatedAt.After(out[j].CreatedAt)
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func mediaVersionGroupKey(m model.Media) string {
|
||||||
|
if m.SeasonNum > 0 || m.EpisodeNum > 0 {
|
||||||
|
switch {
|
||||||
|
case m.TMDbID > 0:
|
||||||
|
return fmt.Sprintf("episode:tmdb:%d:%d:%d", m.TMDbID, m.SeasonNum, m.EpisodeNum)
|
||||||
|
case m.BangumiID > 0:
|
||||||
|
return fmt.Sprintf("episode:bangumi:%d:%d:%d", m.BangumiID, m.SeasonNum, m.EpisodeNum)
|
||||||
|
case strings.TrimSpace(m.DoubanID) != "":
|
||||||
|
return fmt.Sprintf("episode:douban:%s:%d:%d", strings.ToLower(strings.TrimSpace(m.DoubanID)), m.SeasonNum, m.EpisodeNum)
|
||||||
|
case strings.TrimSpace(m.TheTVDBID) != "":
|
||||||
|
return fmt.Sprintf("episode:thetvdb:%s:%d:%d", strings.ToLower(strings.TrimSpace(m.TheTVDBID)), m.SeasonNum, m.EpisodeNum)
|
||||||
|
}
|
||||||
|
title := firstNonEmpty(m.OriginalName, m.Title)
|
||||||
|
if title == "" {
|
||||||
|
title, _ = CleanQuery(m.Path)
|
||||||
|
}
|
||||||
|
title, _ = mediaVersionTitleKey(title)
|
||||||
|
if title == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join([]string{
|
||||||
|
"episode",
|
||||||
|
strings.ToLower(strings.TrimSpace(m.LibraryID)),
|
||||||
|
title,
|
||||||
|
fmt.Sprintf("%d:%d", m.SeasonNum, m.EpisodeNum),
|
||||||
|
}, "|")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case m.TMDbID > 0:
|
||||||
|
return fmt.Sprintf("tmdb:%d", m.TMDbID)
|
||||||
|
case m.BangumiID > 0:
|
||||||
|
return fmt.Sprintf("bangumi:%d", m.BangumiID)
|
||||||
|
case strings.TrimSpace(m.DoubanID) != "":
|
||||||
|
return "douban:" + strings.ToLower(strings.TrimSpace(m.DoubanID))
|
||||||
|
case strings.TrimSpace(m.TheTVDBID) != "":
|
||||||
|
return "thetvdb:" + strings.ToLower(strings.TrimSpace(m.TheTVDBID))
|
||||||
|
}
|
||||||
|
title := firstNonEmpty(m.OriginalName, m.Title)
|
||||||
|
titleYear := 0
|
||||||
|
if title == "" {
|
||||||
|
title, _ = CleanQuery(m.Path)
|
||||||
|
} else {
|
||||||
|
title, titleYear = mediaVersionTitleKey(title)
|
||||||
|
}
|
||||||
|
if title == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
year := m.Year
|
||||||
|
if year <= 0 {
|
||||||
|
year = titleYear
|
||||||
|
}
|
||||||
|
if year <= 0 {
|
||||||
|
_, year = CleanQuery(m.Path)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("movie:%s:%d", title, year)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mediaVersionTitleKey(value string) (string, int) {
|
||||||
|
cleaned, year := CleanQuery(value)
|
||||||
|
if strings.TrimSpace(cleaned) == "" {
|
||||||
|
cleaned = value
|
||||||
|
}
|
||||||
|
return normalizeMediaVersionText(cleaned), year
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeMediaVersionText(value string) string {
|
||||||
|
value = strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
fields := strings.FieldsFunc(value, func(r rune) bool {
|
||||||
|
switch r {
|
||||||
|
case '.', '_', '-', ' ', '\t', '/', '\\', '[', ']', '(', ')', '(', ')', '【', '】':
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
out := fields[:0]
|
||||||
|
for _, field := range fields {
|
||||||
|
field = strings.TrimSpace(field)
|
||||||
|
if field == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, noise := noiseTokenSet[field]; noise {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, field)
|
||||||
|
}
|
||||||
|
return strings.Join(out, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func betterMediaVersion(candidate, current model.Media) bool {
|
||||||
|
candidateCloud := isCloudMediaVersion(candidate)
|
||||||
|
currentCloud := isCloudMediaVersion(current)
|
||||||
|
if candidateCloud != currentCloud {
|
||||||
|
return !candidateCloud
|
||||||
|
}
|
||||||
|
candidatePixels := candidate.Width * candidate.Height
|
||||||
|
currentPixels := current.Width * current.Height
|
||||||
|
if candidatePixels != currentPixels {
|
||||||
|
return candidatePixels > currentPixels
|
||||||
|
}
|
||||||
|
if candidate.SizeBytes != current.SizeBytes {
|
||||||
|
return candidate.SizeBytes > current.SizeBytes
|
||||||
|
}
|
||||||
|
return candidate.CreatedAt.After(current.CreatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCloudMediaVersion(media model.Media) bool {
|
||||||
|
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(media.Path)), "cloud://") ||
|
||||||
|
strings.Contains(strings.ToLower(strings.TrimSpace(media.STRMURL)), "/api/cloud/play/")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user