mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
b0fe40142a
* Rebrand MMTL to MeBox across codebase and assets Rename the project display name, Go module path, environment variable prefix (MEBOX_*), Docker image references, and UI branding from MMTL/mmtl to MeBox/mebox. Replace logo assets with the new MeBox icon and keep legacy SQLite migration support for existing mmtl.db deployments. Co-authored-by: truewhile <truewhile@users.noreply.github.com> * Fix logo icons: use cube-only crop without truncated text Previous icon generation cropped too much of the source image, including partial MeBox wordmark text that was cut off in square icon containers. Regenerate logo-64/192/512, favicon, and SVG from cube-only region. Co-authored-by: truewhile <truewhile@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: truewhile <truewhile@users.noreply.github.com>
141 lines
4.1 KiB
Go
141 lines
4.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/truewhile/MeBox/internal/service"
|
|
)
|
|
|
|
var metadataNoiseRE = regexp.MustCompile(`(?i)(自动订阅|订阅|全集|合集|complete|batch|season\s*\d+|s\d{1,2}|s\d{1,2}e\d{1,3}|第\s*\d+\s*季|第\s*\d+\s*[集话話期]|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)`)
|
|
var metadataYearRE = regexp.MustCompile(`(?:19|20)\d{2}`)
|
|
|
|
var displayMetadataCache sync.Map
|
|
|
|
type cachedDisplayMetadata struct {
|
|
value displayMetadata
|
|
expiresAt time.Time
|
|
}
|
|
|
|
type displayMetadata struct {
|
|
Source string
|
|
Title string
|
|
OriginalName string
|
|
OriginalLanguage string
|
|
PosterURL string
|
|
BackdropURL string
|
|
Overview string
|
|
Year int
|
|
Rating float32
|
|
Genres string
|
|
}
|
|
|
|
func lookupDisplayMetadata(ctx context.Context, svc *service.Container, title, fallback, mediaType string) displayMetadata {
|
|
if svc == nil {
|
|
return displayMetadata{}
|
|
}
|
|
query, year := metadataSearchQuery(title, fallback)
|
|
if query == "" {
|
|
return displayMetadata{}
|
|
}
|
|
searchType := normalizeMetadataMediaType(mediaType)
|
|
cacheKey := searchType + ":" + strconv.Itoa(year) + ":" + strings.ToLower(query)
|
|
if cached, ok := displayMetadataCache.Load(cacheKey); ok {
|
|
entry := cached.(cachedDisplayMetadata)
|
|
if time.Now().Before(entry.expiresAt) {
|
|
return entry.value
|
|
}
|
|
displayMetadataCache.Delete(cacheKey)
|
|
}
|
|
searchCtx, cancel := context.WithTimeout(ctx, 12*time.Second)
|
|
defer cancel()
|
|
results := service.SearchExternalMedia(searchCtx, query, year, searchType, svc.TMDb, svc.Douban, svc.Bangumi)
|
|
if len(results) == 0 {
|
|
displayMetadataCache.Store(cacheKey, cachedDisplayMetadata{expiresAt: time.Now().Add(30 * time.Minute)})
|
|
return displayMetadata{}
|
|
}
|
|
best := results[0]
|
|
for _, item := range results {
|
|
if searchType != "" && item.MediaType != "" && item.MediaType != searchType && !(searchType == "tv" && item.MediaType == "anime") {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(item.PosterURL) != "" {
|
|
best = item
|
|
break
|
|
}
|
|
}
|
|
originalLanguage := ""
|
|
if len(best.Languages) > 0 {
|
|
originalLanguage = strings.TrimSpace(best.Languages[0])
|
|
}
|
|
meta := displayMetadata{
|
|
Source: best.Source,
|
|
Title: best.Title,
|
|
OriginalName: best.OriginalName,
|
|
OriginalLanguage: originalLanguage,
|
|
PosterURL: best.PosterURL,
|
|
BackdropURL: best.BackdropURL,
|
|
Overview: best.Overview,
|
|
Year: best.Year,
|
|
Rating: best.Rating,
|
|
Genres: strings.Join(best.Genres, ","),
|
|
}
|
|
displayMetadataCache.Store(cacheKey, cachedDisplayMetadata{value: meta, expiresAt: time.Now().Add(12 * time.Hour)})
|
|
return meta
|
|
}
|
|
|
|
func metadataSearchQuery(values ...string) (string, int) {
|
|
for _, value := range values {
|
|
query := strings.TrimSpace(value)
|
|
if query == "" || strings.Contains(query, "://") {
|
|
continue
|
|
}
|
|
year := 0
|
|
if yearLoc := metadataYearRE.FindStringIndex(query); len(yearLoc) == 2 {
|
|
yearText := query[yearLoc[0]:yearLoc[1]]
|
|
year, _ = strconv.Atoi(yearText)
|
|
query = query[:yearLoc[1]]
|
|
}
|
|
query = stripBracketNoise(query)
|
|
query = metadataNoiseRE.ReplaceAllString(query, " ")
|
|
query = strings.NewReplacer("_", " ", ".", " ", "-", " ", "|", " ", "/", " ").Replace(query)
|
|
query = strings.Join(strings.Fields(query), " ")
|
|
query = strings.TrimSpace(query)
|
|
if query != "" {
|
|
return query, year
|
|
}
|
|
}
|
|
return "", 0
|
|
}
|
|
|
|
func stripBracketNoise(value string) string {
|
|
replacer := strings.NewReplacer("[", " ", "]", " ", "【", " ", "】", " ", "(", " ", ")", " ")
|
|
return replacer.Replace(value)
|
|
}
|
|
|
|
func normalizeMetadataMediaType(mediaType string) string {
|
|
switch strings.ToLower(strings.TrimSpace(mediaType)) {
|
|
case "movie":
|
|
return "movie"
|
|
case "anime":
|
|
return "anime"
|
|
case "tv", "series", "show", "variety", "综艺":
|
|
return "tv"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func firstNonEmptyString(values ...string) string {
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) != "" {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
}
|
|
return ""
|
|
}
|