Files
MeBox/internal/service/cloud_mount.go
T
2026-06-10 18:46:30 +08:00

204 lines
6.2 KiB
Go

package service
import (
"net/url"
"strings"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/service/cloud"
)
// CloudMountInfo is the canonical identity of a mounted cloud library. ScanDir
// is the provider id/path used for listing. DisplayDir is a hierarchical path
// used to prevent mounting both a parent and its child as separate libraries.
type CloudMountInfo struct {
Provider string
DisplayDir string
ScanDir string
Path string
}
type CloudMountConflict struct {
Library model.Library `json:"library"`
Exact bool `json:"exact"`
Nested bool `json:"nested"`
ExistingIsAncestor bool `json:"existing_is_ancestor"`
}
func BuildCloudLibraryPath(provider, scanDir, displayDir string) string {
provider = strings.TrimSpace(provider)
scanDir = normalizeCloudMountDir(provider, scanDir)
displayDir = normalizeCloudMountDir(provider, firstNonEmpty(displayDir, scanDir))
if provider == "" {
return ""
}
base := "cloud://" + provider
if displayDir == "" {
if scanDir != "" {
return base + "?dir=" + url.QueryEscape(scanDir)
}
return base
}
path := base + "/" + url.PathEscape(displayDir)
if scanDir != "" && scanDir != displayDir {
path += "?dir=" + url.QueryEscape(scanDir)
}
return path
}
func ParseCloudLibraryMount(raw string) (CloudMountInfo, bool) {
raw = strings.TrimSpace(raw)
if !strings.HasPrefix(strings.ToLower(raw), "cloud://") {
return CloudMountInfo{}, false
}
u, err := url.Parse(raw)
if err != nil || strings.ToLower(u.Scheme) != "cloud" {
return CloudMountInfo{}, false
}
provider := strings.TrimSpace(u.Host)
if provider == "" {
return CloudMountInfo{}, false
}
displayDir := strings.Trim(strings.TrimSpace(u.Path), "/")
if decoded, err := url.PathUnescape(displayDir); err == nil {
displayDir = decoded
}
scanDir := displayDir
if qDir := strings.TrimSpace(u.Query().Get("dir")); qDir != "" {
if decoded, err := url.QueryUnescape(qDir); err == nil {
qDir = decoded
}
scanDir = qDir
}
displayDir = normalizeCloudMountDir(provider, displayDir)
scanDir = normalizeCloudMountDir(provider, scanDir)
return CloudMountInfo{
Provider: provider,
DisplayDir: displayDir,
ScanDir: scanDir,
Path: raw,
}, true
}
func FindCloudMountConflict(libs []model.Library, provider, scanDir, displayDir string) *CloudMountConflict {
candidate := CloudMountInfo{
Provider: strings.TrimSpace(provider),
DisplayDir: normalizeCloudMountDir(provider, firstNonEmpty(displayDir, scanDir)),
ScanDir: normalizeCloudMountDir(provider, scanDir),
}
for _, lib := range libs {
existing, ok := ParseCloudLibraryMount(lib.Path)
if !ok || existing.Provider != candidate.Provider {
continue
}
if existing.DisplayDir == candidate.DisplayDir {
return &CloudMountConflict{Library: lib, Exact: true}
}
if existing.ScanDir != "" && candidate.ScanDir != "" && existing.ScanDir == candidate.ScanDir {
return &CloudMountConflict{Library: lib, Exact: true}
}
if cloudMountAncestor(candidate.DisplayDir, existing.DisplayDir) {
return &CloudMountConflict{Library: lib, Nested: true}
}
}
return nil
}
func CloudLibraryShadowed(libs []model.Library, lib model.Library) *CloudMountConflict {
current, ok := ParseCloudLibraryMount(lib.Path)
if !ok {
return nil
}
for _, existing := range libs {
if existing.ID == lib.ID || !existing.Enabled {
continue
}
info, ok := ParseCloudLibraryMount(existing.Path)
if !ok || info.Provider != current.Provider {
continue
}
if info.DisplayDir == current.DisplayDir && existing.CreatedAt.Before(lib.CreatedAt) {
return &CloudMountConflict{Library: existing, Exact: true}
}
if cloudMountAncestor(current.DisplayDir, info.DisplayDir) {
return &CloudMountConflict{Library: existing, Nested: true}
}
}
return nil
}
func FilterShadowedCloudLibraries(libs []model.Library) []model.Library {
out := make([]model.Library, 0, len(libs))
for _, lib := range libs {
if CloudLibraryShadowed(libs, lib) == nil {
out = append(out, lib)
}
}
return out
}
func ShadowedCloudLibraryIDSet(libs []model.Library) map[string]bool {
out := make(map[string]bool)
for _, lib := range libs {
if CloudLibraryShadowed(libs, lib) != nil {
out[lib.ID] = true
}
}
return out
}
func InferCloudMountMediaType(dir, name string) string {
text := strings.ToLower(dir + " " + name)
switch {
case strings.Contains(text, "成人") || strings.Contains(text, "adult") || strings.Contains(text, "jav") || strings.Contains(text, "9kg"):
return "adult"
case containsAny(text, "动画电影", "华语电影", "外语电影", "欧美电影", "日韩电影", "韩国电影", "日本电影", "港台电影", "香港电影", "台湾电影", "大陆电影", "国产电影", "纪录片", "演唱会", "电影", "movie", "movies", "film", "films", "documentary", "concert"):
return "movie"
case containsAny(text, "综艺", "真人秀", "脱口秀", "晚会", "variety"):
return "variety"
case containsAny(text, "国漫", "日漫", "日番", "番剧", "动漫", "欧美动漫", "动画剧集", "anime"):
return "anime"
case containsAny(text, "国产剧", "大陆剧", "华语剧", "欧美剧", "日韩剧", "韩剧", "日剧", "港剧", "台剧", "泰剧", "英剧", "美剧", "短剧", "电视剧", "剧集", "连续剧", "series", "tv", "shows"):
return "tv"
default:
return "movie"
}
}
func containsAny(text string, values ...string) bool {
for _, value := range values {
if strings.Contains(text, value) {
return true
}
}
return false
}
func cloudMountAncestor(parent, child string) bool {
parent = strings.Trim(parent, "/")
child = strings.Trim(child, "/")
if parent == child {
return false
}
if parent == "" {
return child != ""
}
return strings.HasPrefix(child, parent+"/")
}
func normalizeCloudMountDir(provider, value string) string {
value = strings.TrimSpace(value)
if decoded, err := url.PathUnescape(value); err == nil {
value = decoded
}
if decoded, err := url.QueryUnescape(value); err == nil {
value = decoded
}
value = strings.ReplaceAll(value, "\\", "/")
value = strings.Trim(strings.TrimSpace(value), "/")
if value == "." || ((provider == cloud.Type115 || provider == cloud.TypeQuark) && value == "0") {
return ""
}
return value
}