Files
MeBox/internal/service/local_metadata_artwork.go
T
2026-09-11 20:45:54 +08:00

315 lines
8.4 KiB
Go

package service
import (
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"net/url"
"os"
"path/filepath"
"strings"
)
func metadataFromArtwork(mediaPath, showBaseDir string) *LocalMetadata {
meta := &LocalMetadata{}
mergeArtworkMetadata(meta, mediaPath, showBaseDir)
if meta.PosterURL == "" && meta.BackdropURL == "" {
return nil
}
return meta
}
func mergeArtworkMetadata(meta *LocalMetadata, mediaPath, showBaseDir string) {
if meta == nil {
return
}
mediaDir := filepath.Dir(mediaPath)
if localPoster := firstLocalPoster(mediaPath, showBaseDir); localPoster != "" {
meta.PosterURL = localPoster
} else if meta.PosterURL == "" {
meta.PosterURL = firstAdultLooseImage(mediaDir, "poster")
}
dirs := []string{mediaDir, showBaseDir}
for _, dir := range dirs {
if dir == "" {
continue
}
if img := firstExistingImage(dir, localBackdropCandidates(mediaPath)...); img != "" {
meta.BackdropURL = img
break
}
if meta.BackdropURL == "" {
meta.BackdropURL = firstAdultLooseImage(dir, "backdrop")
}
}
if !isLocalPath(meta.PosterURL) {
if dmmPoster := adultDMMPosterFromSampleURL(meta.BackdropURL); dmmPoster != "" {
meta.PosterURL = dmmPoster
}
}
if meta.PosterURL != "" || meta.BackdropURL != "" {
meta.HasArtwork = true
}
}
func localPosterCandidates(mediaPath string) []string {
names := make([]string, 0, 24)
seen := map[string]struct{}{}
add := func(name string) {
name = strings.TrimSpace(name)
if name == "" {
return
}
key := strings.ToLower(name)
if _, ok := seen[key]; ok {
return
}
seen[key] = struct{}{}
names = append(names, name)
}
for _, base := range mediaSidecarBaseVariants(mediaPath) {
add(base + "-poster")
add(base + ".poster")
add(base + "-cover")
add(base + ".cover")
add(base)
}
for _, name := range []string{"poster", "folder", "cover", "movie", "show"} {
add(name)
}
for _, base := range mediaSidecarBaseVariants(mediaPath) {
add(base + "-thumb")
add(base + ".thumb")
}
add("thumb")
return append(adultArtworkNameCandidates(mediaPath, "poster"), names...)
}
func localBackdropCandidates(mediaPath string) []string {
names := make([]string, 0, 24)
seen := map[string]struct{}{}
add := func(name string) {
name = strings.TrimSpace(name)
if name == "" {
return
}
key := strings.ToLower(name)
if _, ok := seen[key]; ok {
return
}
seen[key] = struct{}{}
names = append(names, name)
}
for _, base := range mediaSidecarBaseVariants(mediaPath) {
add(base + "-fanart")
add(base + ".fanart")
add(base + "-backdrop")
add(base + ".backdrop")
add(base + "-background")
}
for _, name := range []string{"fanart", "backdrop", "background", "landscape", "banner", "clearart"} {
add(name)
}
return append(adultArtworkNameCandidates(mediaPath, "backdrop"), names...)
}
func adultArtworkNameCandidates(mediaPath, kind string) []string {
code := AdultCodeFromMediaPath(mediaPath)
if code == "" {
return nil
}
compact := strings.ReplaceAll(code, "-", "")
bases := []string{code, compact}
bases = append(bases, adultDMMNameCandidates(code)...)
out := make([]string, 0, len(bases)*6)
for _, base := range bases {
if base == "" {
continue
}
if kind == "poster" {
out = append(out, base, base+"-poster", base+".poster", base+"-cover", base+".cover", base+"-thumb", base+".thumb", base+"pl", base+"-pl")
} else {
out = append(out, base+"-fanart", base+".fanart", base+"-backdrop", base+".backdrop", base+"-background", base+"-landscape", base+"jp", base+"jp-1")
}
}
return out
}
func adultDMMNameCandidates(code string) []string {
parts := adultStandardPattern.FindStringSubmatch(code)
if len(parts) < 3 {
return nil
}
prefix := strings.ToLower(parts[1])
num := strings.TrimLeft(parts[2], "0")
if num == "" {
num = "0"
}
padded := num
for len(padded) < 5 {
padded = "0" + padded
}
return []string{prefix + padded}
}
func firstExistingImage(dir string, names ...string) string {
if dir == "" {
return ""
}
for _, name := range names {
for _, ext := range []string{".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tbn", ".img"} {
path := filepath.Join(dir, name+ext)
if fileExists(path) {
return filepath.Clean(path)
}
}
}
return ""
}
func nfoPosterValues(doc *nfoDocument) []string {
if doc == nil {
return nil
}
values := []string{doc.Poster, doc.Art.Poster}
for _, thumb := range doc.Thumbs {
aspect := strings.ToLower(strings.TrimSpace(thumb.Aspect))
if aspect == "" || aspect == "poster" || aspect == "cover" || aspect == "default" {
values = append(values, thumb.Value)
}
}
values = append(values, doc.Art.Thumb)
return values
}
func nfoBackdropValues(doc *nfoDocument) []string {
if doc == nil {
return nil
}
values := []string{doc.Fanart.Value, doc.Art.Fanart, doc.Art.Backdrop, doc.Art.Background, doc.Art.Landscape, doc.Art.Banner}
for _, thumb := range doc.Thumbs {
aspect := strings.ToLower(strings.TrimSpace(thumb.Aspect))
if aspect == "fanart" || aspect == "backdrop" || aspect == "background" || aspect == "landscape" {
values = append(values, thumb.Value)
}
}
values = append(values, doc.Fanart.Thumbs...)
return values
}
func firstLocalPoster(mediaPath, showBaseDir string) string {
mediaDir := filepath.Dir(mediaPath)
dirs := []string{}
if showBaseDir != "" && !samePath(showBaseDir, mediaDir) {
dirs = append(dirs, showBaseDir)
}
dirs = append(dirs, mediaDir)
for _, dir := range dirs {
if localPoster := firstExistingPosterImage(dir, localPosterCandidates(mediaPath)...); localPoster != "" {
return localPoster
}
}
return ""
}
func firstExistingPosterImage(dir string, names ...string) string {
if dir == "" {
return ""
}
for _, name := range names {
if isRejectedPosterName(name) {
continue
}
for _, ext := range []string{".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tbn", ".img"} {
path := filepath.Join(dir, name+ext)
if fileExists(path) && likelyPosterImage(path) {
return filepath.Clean(path)
}
}
}
return ""
}
func firstAdultLooseImage(dir, kind string) string {
if dir == "" {
return ""
}
matches, _ := filepath.Glob(filepath.Join(dir, "*"))
preferred := []string{}
fallback := []string{}
for _, path := range matches {
ext := strings.ToLower(filepath.Ext(path))
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".webp" && ext != ".gif" && ext != ".bmp" && ext != ".tbn" && ext != ".img" {
continue
}
name := strings.ToLower(strings.TrimSuffix(filepath.Base(path), ext))
if kind == "poster" {
if isRejectedPosterName(name) {
continue
}
if strings.Contains(name, "poster") || strings.Contains(name, "cover") || strings.Contains(name, "folder") || strings.Contains(name, "movie") || strings.HasSuffix(name, "pl") {
preferred = append(preferred, path)
}
} else if strings.Contains(name, "fanart") || strings.Contains(name, "backdrop") || strings.Contains(name, "background") || strings.Contains(name, "landscape") || strings.Contains(name, "jp") {
preferred = append(preferred, path)
}
if kind != "poster" || likelyPosterImage(path) {
fallback = append(fallback, path)
}
}
if len(preferred) > 0 {
return filepath.Clean(preferred[0])
}
if kind == "poster" && len(fallback) == 1 {
return filepath.Clean(fallback[0])
}
return ""
}
func isRejectedPosterName(name string) bool {
name = strings.ToLower(name)
rejected := []string{
"actor", "actors", "actress", "cast", "avatar", "portrait", "person",
"sample", "screenshot", "screen", "still", "scene", "extrafanart", "extrathumb",
"fanart", "backdrop", "background", "landscape", "banner", "clearlogo", "clearart", "logo", "disc",
}
for _, token := range rejected {
if strings.Contains(name, token) {
return true
}
}
return false
}
func likelyPosterImage(path string) bool {
file, err := os.Open(path) // #nosec G304 -- path is a discovered artwork sidecar under the configured library root.
if err != nil {
return false
}
defer file.Close()
cfg, _, err := image.DecodeConfig(file)
if err != nil || cfg.Width <= 0 || cfg.Height <= 0 {
return true
}
return cfg.Height >= cfg.Width
}
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func isHTTPURL(raw string) bool {
u, err := url.Parse(raw)
if err != nil {
return false
}
return (u.Scheme == "http" || u.Scheme == "https") && u.Host != ""
}
func isLocalPath(raw string) bool {
raw = strings.TrimSpace(raw)
return raw != "" && !isHTTPURL(raw)
}