diff --git a/internal/service/cloud_metadata.go b/internal/service/cloud_metadata.go index 0f8caeb..589e7ca 100644 --- a/internal/service/cloud_metadata.go +++ b/internal/service/cloud_metadata.go @@ -4,7 +4,6 @@ import ( "context" "encoding/xml" "path/filepath" - "strconv" "strings" "github.com/ShukeBta/MediaStationGo/internal/service/cloud" @@ -93,86 +92,6 @@ func (s *ScannerService) cloudDirectoryMetadata(ctx context.Context, typ, displa return meta } -func cloudFileJSONIsEpisodeMetadata(seriesLike bool, parsedSeason, parsedEpisode int, meta *LocalMetadata) bool { - if !seriesLike || meta == nil { - return false - } - return parsedSeason > 0 || - parsedEpisode > 0 || - meta.SeasonNum > 0 || - meta.EpisodeNum > 0 || - strings.TrimSpace(meta.EpisodeTitle) != "" -} - -func mergeCloudEpisodeMetadata(dst, episode *LocalMetadata) *LocalMetadata { - if episode == nil { - return dst - } - if dst == nil { - dst = &LocalMetadata{} - } - mergeCloudEpisodeIdentity(dst, episode) - mergeCloudEpisodeDisplay(dst, episode) - mergeCloudEpisodeNumbersAndTaxonomy(dst, episode) - dst.NSFW = dst.NSFW || episode.NSFW - dst.HasNFO = dst.HasNFO || episode.HasNFO - dst.HasArtwork = dst.HasArtwork || episode.HasArtwork - return dst -} - -func mergeCloudEpisodeIdentity(dst, episode *LocalMetadata) { - showTitle := "" - if episode.EpisodeTitle != "" && episode.Title != "" && !strings.EqualFold(episode.Title, episode.EpisodeTitle) { - showTitle = episode.Title - } - if showTitle != "" { - dst.Title = showTitle - } - episodeTitle := strings.TrimSpace(episode.EpisodeTitle) - if episodeTitle == "" && (episode.SeasonNum > 0 || episode.EpisodeNum > 0) { - episodeTitle = strings.TrimSpace(episode.Title) - } - if episodeTitle != "" && !strings.EqualFold(episodeTitle, strings.TrimSpace(dst.Title)) { - dst.EpisodeTitle = episodeTitle - } -} - -func mergeCloudEpisodeDisplay(dst, episode *LocalMetadata) { - if dst.Year == 0 && episode.Year > 0 { - dst.Year = episode.Year - } - if episode.Overview != "" { - dst.Overview = episode.Overview - } - if episode.Rating > 0 { - dst.Rating = episode.Rating - } - if episode.PosterURL != "" { - dst.PosterURL = episode.PosterURL - } - if episode.BackdropURL != "" { - dst.BackdropURL = episode.BackdropURL - } -} - -func mergeCloudEpisodeNumbersAndTaxonomy(dst, episode *LocalMetadata) { - if episode.SeasonNum > 0 { - dst.SeasonNum = episode.SeasonNum - } - if episode.EpisodeNum > 0 { - dst.EpisodeNum = episode.EpisodeNum - } - if dst.Genres == "" && episode.Genres != "" { - dst.Genres = episode.Genres - } - if dst.Countries == "" && episode.Countries != "" { - dst.Countries = episode.Countries - } - if dst.Languages == "" && episode.Languages != "" { - dst.Languages = episode.Languages - } -} - 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 @@ -247,307 +166,3 @@ func (s *ScannerService) readCloudNFO(ctx context.Context, typ, ref string, seri 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] -} diff --git a/internal/service/cloud_metadata_artwork.go b/internal/service/cloud_metadata_artwork.go index 87a477b..65201da 100644 --- a/internal/service/cloud_metadata_artwork.go +++ b/internal/service/cloud_metadata_artwork.go @@ -24,6 +24,45 @@ func applyCloudNFOArtwork(typ string, sidecars cloudSidecarSet, meta *LocalMetad return meta } +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 firstCloudImageRef(sidecars cloudSidecarSet, names ...string) string { for _, name := range names { if ref := cloudImageRefByName(sidecars, name); ref != "" { diff --git a/internal/service/cloud_metadata_candidates.go b/internal/service/cloud_metadata_candidates.go new file mode 100644 index 0000000..10d125f --- /dev/null +++ b/internal/service/cloud_metadata_candidates.go @@ -0,0 +1,122 @@ +package service + +import ( + "path/filepath" + "strconv" + "strings" +) + +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 +} diff --git a/internal/service/cloud_metadata_episode.go b/internal/service/cloud_metadata_episode.go new file mode 100644 index 0000000..8627577 --- /dev/null +++ b/internal/service/cloud_metadata_episode.go @@ -0,0 +1,83 @@ +package service + +import "strings" + +func cloudFileJSONIsEpisodeMetadata(seriesLike bool, parsedSeason, parsedEpisode int, meta *LocalMetadata) bool { + if !seriesLike || meta == nil { + return false + } + return parsedSeason > 0 || + parsedEpisode > 0 || + meta.SeasonNum > 0 || + meta.EpisodeNum > 0 || + strings.TrimSpace(meta.EpisodeTitle) != "" +} + +func mergeCloudEpisodeMetadata(dst, episode *LocalMetadata) *LocalMetadata { + if episode == nil { + return dst + } + if dst == nil { + dst = &LocalMetadata{} + } + mergeCloudEpisodeIdentity(dst, episode) + mergeCloudEpisodeDisplay(dst, episode) + mergeCloudEpisodeNumbersAndTaxonomy(dst, episode) + dst.NSFW = dst.NSFW || episode.NSFW + dst.HasNFO = dst.HasNFO || episode.HasNFO + dst.HasArtwork = dst.HasArtwork || episode.HasArtwork + return dst +} + +func mergeCloudEpisodeIdentity(dst, episode *LocalMetadata) { + showTitle := "" + if episode.EpisodeTitle != "" && episode.Title != "" && !strings.EqualFold(episode.Title, episode.EpisodeTitle) { + showTitle = episode.Title + } + if showTitle != "" { + dst.Title = showTitle + } + episodeTitle := strings.TrimSpace(episode.EpisodeTitle) + if episodeTitle == "" && (episode.SeasonNum > 0 || episode.EpisodeNum > 0) { + episodeTitle = strings.TrimSpace(episode.Title) + } + if episodeTitle != "" && !strings.EqualFold(episodeTitle, strings.TrimSpace(dst.Title)) { + dst.EpisodeTitle = episodeTitle + } +} + +func mergeCloudEpisodeDisplay(dst, episode *LocalMetadata) { + if dst.Year == 0 && episode.Year > 0 { + dst.Year = episode.Year + } + if episode.Overview != "" { + dst.Overview = episode.Overview + } + if episode.Rating > 0 { + dst.Rating = episode.Rating + } + if episode.PosterURL != "" { + dst.PosterURL = episode.PosterURL + } + if episode.BackdropURL != "" { + dst.BackdropURL = episode.BackdropURL + } +} + +func mergeCloudEpisodeNumbersAndTaxonomy(dst, episode *LocalMetadata) { + if episode.SeasonNum > 0 { + dst.SeasonNum = episode.SeasonNum + } + if episode.EpisodeNum > 0 { + dst.EpisodeNum = episode.EpisodeNum + } + if dst.Genres == "" && episode.Genres != "" { + dst.Genres = episode.Genres + } + if dst.Countries == "" && episode.Countries != "" { + dst.Countries = episode.Countries + } + if dst.Languages == "" && episode.Languages != "" { + dst.Languages = episode.Languages + } +} diff --git a/internal/service/cloud_metadata_merge.go b/internal/service/cloud_metadata_merge.go new file mode 100644 index 0000000..8300dbb --- /dev/null +++ b/internal/service/cloud_metadata_merge.go @@ -0,0 +1,153 @@ +package service + +import "strings" + +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] +}