Files
truewhile b0fe40142a Rebrand MMTL to MeBox (name, logo, Docker image) (#17)
* 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>
2026-09-02 16:26:28 +08:00

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 ""
}