Files
MeBox/internal/service/cloud_metadata.go
T

470 lines
13 KiB
Go

package service
import (
"context"
"encoding/xml"
"path/filepath"
"strconv"
"strings"
"github.com/ShukeBta/MediaStationGo/internal/service/cloud"
)
type cloudSidecarSet struct {
nfoByName map[string]string
nfoByBase map[string]string
jsonByName map[string]string
jsonByBase map[string]string
imageByName map[string]string
imageByBase map[string]string
}
func newCloudSidecarSet(typ string, entries []cloud.FileEntry) cloudSidecarSet {
set := cloudSidecarSet{
nfoByName: make(map[string]string),
nfoByBase: make(map[string]string),
jsonByName: make(map[string]string),
jsonByBase: make(map[string]string),
imageByName: make(map[string]string),
imageByBase: make(map[string]string),
}
for _, entry := range entries {
if entry.IsDir {
continue
}
ref := cloudEntryRef(typ, entry.ID, entry.PickCode)
if ref == "" {
continue
}
name := strings.TrimSpace(entry.Name)
ext := strings.ToLower(filepath.Ext(name))
base := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(name, ext)))
if name == "" || base == "" {
continue
}
switch ext {
case ".nfo":
set.nfoByName[strings.ToLower(name)] = ref
set.nfoByBase[base] = ref
case ".json":
set.jsonByName[strings.ToLower(name)] = ref
set.jsonByBase[base] = ref
case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tbn":
set.imageByName[strings.ToLower(name)] = ref
set.imageByBase[base] = ref
}
}
return set
}
func (s *ScannerService) cloudDirectoryMetadata(ctx context.Context, typ, displayDir string, sidecars cloudSidecarSet, inherited *LocalMetadata) *LocalMetadata {
meta := cloneLocalMetadata(inherited)
if hinted, _ := pathHintMetadata(displayDir, true); hinted != nil {
meta = mergeCloudMetadata(meta, hinted)
}
for _, name := range cloudShowNFOCandidates(displayDir) {
ref := sidecars.nfoByName[strings.ToLower(name)]
if ref == "" {
ref = sidecars.nfoByBase[strings.ToLower(strings.TrimSuffix(name, filepath.Ext(name)))]
}
if ref == "" {
continue
}
if local, doc, err := s.readCloudNFO(ctx, typ, ref, true); err == nil && local != nil {
local = applyCloudNFOArtwork(typ, sidecars, local, doc)
meta = mergeCloudMetadata(meta, local)
break
}
}
for _, name := range cloudDirectoryJSONCandidates(displayDir) {
ref := cloudJSONRefByName(sidecars, name)
if ref == "" {
continue
}
if local, err := s.readCloudJSONMetadata(ctx, typ, ref, sidecars); err == nil && local != nil {
meta = mergeCloudMetadata(meta, local)
break
}
}
meta = applyCloudDirectoryArtwork(typ, displayDir, sidecars, meta)
if !cloudMetadataUseful(meta) {
return nil
}
return meta
}
func (s *ScannerService) cloudFileMetadata(ctx context.Context, typ, displayPath, fileName string, sidecars cloudSidecarSet, inherited *LocalMetadata, seriesLike bool) *LocalMetadata {
season, episode := ParseEpisode(displayPath)
seriesLike = seriesLike || season > 0 || episode > 0
meta := cloneLocalMetadata(inherited)
if hinted, _ := pathHintMetadata(displayPath, seriesLike); hinted != nil {
meta = mergeCloudPathHintMetadata(meta, hinted)
}
base := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(fileName, filepath.Ext(fileName))))
if ref := sidecars.nfoByBase[base]; ref != "" {
if local, doc, err := s.readCloudNFO(ctx, typ, ref, seriesLike); err == nil && local != nil {
local = applyCloudNFOArtwork(typ, sidecars, local, doc)
if seriesLike && doc != nil {
if meta == nil {
meta = &LocalMetadata{}
}
mergeEpisodeMetadata(meta, local, doc)
meta.HasNFO = true
} else {
meta = mergeCloudMetadata(meta, local)
}
}
}
for _, name := range cloudFileJSONCandidates(fileName, base) {
ref := cloudJSONRefByName(sidecars, name)
if ref == "" {
continue
}
if local, err := s.readCloudJSONMetadata(ctx, typ, ref, sidecars); err == nil && local != nil {
meta = mergeCloudMetadata(meta, local)
break
}
}
meta = applyCloudFileArtwork(typ, sidecars, displayPath, fileName, base, meta)
if !cloudMetadataUseful(meta) {
return nil
}
return meta
}
func (s *ScannerService) readCloudJSONMetadata(ctx context.Context, typ, ref string, sidecars cloudSidecarSet) (*LocalMetadata, error) {
if s.storage == nil {
return nil, nil
}
body, err := s.storage.CloudReadText(ctx, typ, ref, 512<<10)
if err != nil {
return nil, err
}
meta, artwork := metadataFromCloudJSON([]byte(body))
if meta == nil {
return nil, nil
}
meta = applyCloudJSONArtwork(typ, sidecars, meta, artwork)
return meta, nil
}
func (s *ScannerService) readCloudNFO(ctx context.Context, typ, ref string, seriesLike bool) (*LocalMetadata, *nfoDocument, error) {
if s.storage == nil {
return nil, nil, nil
}
body, err := s.storage.CloudReadText(ctx, typ, ref, 512<<10)
if err != nil {
return nil, nil, err
}
var doc nfoDocument
if err := xml.Unmarshal([]byte(body), &doc); err != nil {
return nil, nil, err
}
meta := metadataFromDoc(&doc, "", seriesLike)
return meta, &doc, nil
}
func applyCloudDirectoryArtwork(typ, displayDir string, sidecars cloudSidecarSet, meta *LocalMetadata) *LocalMetadata {
if meta == nil {
meta = &LocalMetadata{}
}
if meta.PosterURL == "" {
if ref := firstCloudImageRef(sidecars, cloudPosterNameCandidates(cloudDirectoryArtworkBases(displayDir), "poster", "folder", "cover", "show", "tvshow")...); ref != "" {
meta.PosterURL = cloudPlaybackURL(typ, ref)
meta.HasArtwork = true
}
}
if meta.BackdropURL == "" {
if ref := firstCloudImageRef(sidecars, cloudBackdropNameCandidates(cloudDirectoryArtworkBases(displayDir), "fanart", "backdrop", "background", "landscape")...); ref != "" {
meta.BackdropURL = cloudPlaybackURL(typ, ref)
meta.HasArtwork = true
}
}
return meta
}
func applyCloudFileArtwork(typ string, sidecars cloudSidecarSet, displayPath, fileName, base string, meta *LocalMetadata) *LocalMetadata {
if meta == nil {
meta = &LocalMetadata{}
}
bases := cloudFileArtworkBases(displayPath, fileName, base)
if meta.PosterURL == "" {
if ref := firstCloudImageRef(sidecars, cloudPosterNameCandidates(bases, "poster", "folder", "cover", "movie", "show", "thumb")...); ref != "" {
meta.PosterURL = cloudPlaybackURL(typ, ref)
meta.HasArtwork = true
}
}
if meta.BackdropURL == "" {
if ref := firstCloudImageRef(sidecars, cloudBackdropNameCandidates(bases, "fanart", "backdrop", "background", "landscape")...); ref != "" {
meta.BackdropURL = cloudPlaybackURL(typ, ref)
meta.HasArtwork = true
}
}
return meta
}
func cloudShowNFOCandidates(displayDir string) []string {
names := []string{"tvshow.nfo", "series.nfo", "show.nfo", "movie.nfo"}
base := strings.TrimSpace(pathBaseSlash(displayDir))
if base != "" {
names = append(names, base+".nfo")
}
return names
}
func cloudDirectoryJSONCandidates(displayDir string) []string {
names := []string{"movie.json", "metadata.json", "tvshow.json", "series.json", "show.json"}
base := strings.TrimSpace(pathBaseSlash(displayDir))
if base != "" {
names = append(names, base+".json", base+"-metadata.json", base+".metadata.json", base+"-mediainfo.json", base+".mediainfo.json")
}
return names
}
func cloudFileJSONCandidates(fileName, base string) []string {
if base == "" {
base = strings.ToLower(strings.TrimSpace(strings.TrimSuffix(fileName, filepath.Ext(fileName))))
}
cleanBases := cloudCleanArtworkBases(fileName)
bases := uniqueCloudArtworkNames(append([]string{base}, cleanBases...)...)
out := make([]string, 0, len(bases)*5+2)
for _, value := range bases {
out = append(out, value+".json", value+"-metadata.json", value+".metadata.json", value+"-mediainfo.json", value+".mediainfo.json")
}
return append(out, "movie.json", "metadata.json")
}
func cloudJSONRefByName(sidecars cloudSidecarSet, name string) string {
name = normalizeCloudArtworkName(name)
if name == "" || isHTTPURL(name) {
return ""
}
if ref := sidecars.jsonByName[strings.ToLower(name)]; ref != "" {
return ref
}
base := strings.TrimSuffix(name, filepath.Ext(name))
return sidecars.jsonByBase[strings.ToLower(base)]
}
func cloudFileArtworkBases(displayPath, fileName, base string) []string {
return uniqueCloudArtworkNames(append(
[]string{base},
append(cloudCleanArtworkBases(fileName), cloudDirectoryArtworkBases(pathDirSlash(displayPath))...)...,
)...)
}
func cloudDirectoryArtworkBases(displayDir string) []string {
base := pathBaseSlash(displayDir)
return uniqueCloudArtworkNames(append([]string{base}, cloudCleanArtworkBases(base)...)...)
}
func cloudCleanArtworkBases(value string) []string {
title, year := CleanQuery(value)
title = strings.TrimSpace(title)
if title == "" {
return nil
}
out := []string{title}
if year > 0 {
yearText := strconv.Itoa(year)
out = append(out,
title+" ("+yearText+")",
title+"."+yearText,
title+" "+yearText,
)
}
return out
}
func cloudPosterNameCandidates(bases []string, fallback ...string) []string {
out := make([]string, 0, len(bases)*7+len(fallback))
for _, base := range bases {
base = strings.TrimSpace(base)
if base == "" {
continue
}
out = append(out, base, base+"-poster", base+".poster", base+"-cover", base+".cover", base+"-thumb", base+".thumb")
}
return append(out, fallback...)
}
func cloudBackdropNameCandidates(bases []string, fallback ...string) []string {
out := make([]string, 0, len(bases)*6+len(fallback))
for _, base := range bases {
base = strings.TrimSpace(base)
if base == "" {
continue
}
out = append(out, base+"-fanart", base+".fanart", base+"-backdrop", base+".backdrop", base+"-background", base+".background")
}
return append(out, fallback...)
}
func uniqueCloudArtworkNames(values ...string) []string {
out := make([]string, 0, len(values))
seen := map[string]struct{}{}
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
key := strings.ToLower(value)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, value)
}
return out
}
func mergeCloudMetadata(dst, src *LocalMetadata) *LocalMetadata {
if src == nil {
return dst
}
if dst == nil {
return cloneLocalMetadata(src)
}
if src.Title != "" {
dst.Title = src.Title
}
if src.OriginalName != "" {
dst.OriginalName = src.OriginalName
}
if src.EpisodeTitle != "" {
dst.EpisodeTitle = src.EpisodeTitle
}
if src.AdultCode != "" {
dst.AdultCode = src.AdultCode
}
if src.Year > 0 {
dst.Year = src.Year
}
if src.Overview != "" {
dst.Overview = src.Overview
}
if src.Rating > 0 {
dst.Rating = src.Rating
}
if src.PosterURL != "" {
dst.PosterURL = src.PosterURL
}
if src.BackdropURL != "" {
dst.BackdropURL = src.BackdropURL
}
if src.TMDbID > 0 {
dst.TMDbID = src.TMDbID
}
if src.BangumiID > 0 {
dst.BangumiID = src.BangumiID
}
if src.DoubanID != "" {
dst.DoubanID = src.DoubanID
}
if src.TheTVDBID != "" {
dst.TheTVDBID = src.TheTVDBID
}
if src.SeasonNum > 0 || src.EpisodeNum > 0 {
dst.SeasonNum = src.SeasonNum
}
if src.EpisodeNum > 0 {
dst.EpisodeNum = src.EpisodeNum
}
if src.Genres != "" {
dst.Genres = src.Genres
}
if src.Countries != "" {
dst.Countries = src.Countries
}
if src.Languages != "" {
dst.Languages = src.Languages
}
dst.NSFW = dst.NSFW || src.NSFW
dst.HasNFO = dst.HasNFO || src.HasNFO
dst.HasArtwork = dst.HasArtwork || src.HasArtwork
dst.PathHint = dst.PathHint || src.PathHint
return dst
}
func mergeCloudPathHintMetadata(dst, hint *LocalMetadata) *LocalMetadata {
if hint == nil {
return dst
}
if dst == nil || !dst.HasNFO {
return mergeCloudMetadata(dst, hint)
}
if dst.Title == "" {
dst.Title = hint.Title
}
if dst.OriginalName == "" {
dst.OriginalName = hint.OriginalName
}
if dst.Year == 0 {
dst.Year = hint.Year
}
if dst.TMDbID == 0 {
dst.TMDbID = hint.TMDbID
}
if dst.BangumiID == 0 {
dst.BangumiID = hint.BangumiID
}
if dst.DoubanID == "" {
dst.DoubanID = hint.DoubanID
}
if dst.TheTVDBID == "" {
dst.TheTVDBID = hint.TheTVDBID
}
dst.PathHint = dst.PathHint || hint.PathHint
return dst
}
func cloneLocalMetadata(src *LocalMetadata) *LocalMetadata {
if src == nil {
return nil
}
cp := *src
return &cp
}
func cloudMetadataUseful(meta *LocalMetadata) bool {
return meta != nil && (meta.HasNFO || meta.HasArtwork || localHasDescriptiveMetadata(meta))
}
func cloudPlaybackURL(typ, ref string) string {
return CloudArtworkURL(typ, ref)
}
func joinCloudDisplayPath(parent, child string) string {
parent = strings.Trim(strings.ReplaceAll(strings.TrimSpace(parent), "\\", "/"), "/")
child = strings.Trim(strings.ReplaceAll(strings.TrimSpace(child), "\\", "/"), "/")
switch {
case parent == "":
return child
case child == "":
return parent
default:
return parent + "/" + child
}
}
func pathBaseSlash(value string) string {
value = strings.Trim(strings.ReplaceAll(strings.TrimSpace(value), "\\", "/"), "/")
if value == "" {
return ""
}
parts := strings.Split(value, "/")
return parts[len(parts)-1]
}
func pathDirSlash(value string) string {
value = strings.Trim(strings.ReplaceAll(strings.TrimSpace(value), "\\", "/"), "/")
if value == "" {
return ""
}
idx := strings.LastIndex(value, "/")
if idx < 0 {
return ""
}
return value[:idx]
}