Files
MeBox/internal/service/strm_tree_normalize.go
T
2026-06-30 20:18:02 +08:00

285 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"net/url"
"path"
"regexp"
"strings"
)
var strmTreeListPrefixRE = regexp.MustCompile(`^\s*(?:[-*+•·]\s+|\d{1,4}[\.)、]\s+)`)
var strmTreeURLPathQueryKeys = []string{"path", "ref", "file", "filepath", "filename", "name"}
func normalizeSTRMTreeProvider(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "115", "115pan", "pan115", "cloud115":
return "cloud115"
case "alist", "openlist", "webdav", "clouddrive2", "s3":
return strings.ToLower(strings.TrimSpace(raw))
default:
return strings.ToLower(strings.TrimSpace(raw))
}
}
func normalizeSTRMTreeSource(raw string) string {
value := strings.TrimSpace(strings.Trim(raw, `"'`))
if value == "" {
return ""
}
if strings.HasPrefix(strings.ToLower(value), "cloud://") {
parts := strmLibraryPathParts(value)
if len(parts) == 0 {
return ""
}
return "/" + path.Join(parts...)
}
if parsed, err := url.Parse(value); err == nil && parsed.Scheme != "" && parsed.Scheme != "file" {
if ref := strmTreeSourceFromURL(parsed); ref != "" {
value = ref
}
}
value = trimSTRMTreeMediaMetadata(value)
value = strings.ReplaceAll(value, "\\", "/")
value = pathAfterWindowsDrivePrefix(value)
value = cleanSTRMTreePathDecorations(value)
value = strings.TrimSpace(value)
if value == "" {
return ""
}
if strmTreeHasUnsafeSegment(value) {
return strings.Trim(value, "/")
}
if strings.HasPrefix(value, "/") {
return path.Clean(value)
}
return strings.TrimPrefix(path.Clean("/"+value), "/")
}
func normalizeSTRMTreeSourceWithProvider(raw, fallbackProvider string) strmTreeSource {
provider := normalizeSTRMTreeProvider(fallbackProvider)
value := strings.TrimSpace(strings.Trim(raw, `"'`))
if value == "" {
return strmTreeSource{}
}
if info, ok := ParseCloudLibraryMount(value); ok {
source := normalizeSTRMTreeSource(info.DisplayDir)
if source == "" {
return strmTreeSource{}
}
ref := normalizeSTRMTreeSource(info.ScanDir)
if ref == "" {
ref = source
}
return strmTreeSource{Provider: normalizeSTRMTreeProvider(info.Provider), Path: source, RefPath: ref}
}
if parsed, err := url.Parse(value); err == nil {
if cloudProvider, ref := strmTreeCloudPlaySourceFromURL(parsed); cloudProvider != "" && ref != "" {
source := normalizeSTRMTreeSource(ref)
if source == "" {
return strmTreeSource{}
}
return strmTreeSource{Provider: normalizeSTRMTreeProvider(cloudProvider), Path: source}
}
}
source := normalizeSTRMTreeSource(value)
if source == "" {
return strmTreeSource{}
}
return strmTreeSource{Provider: provider, Path: source}
}
func strmTreeCloudPlaySourceFromURL(parsed *url.URL) (string, string) {
if parsed == nil {
return "", ""
}
parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
if len(parts) < 4 || !strings.EqualFold(parts[0], "api") || !strings.EqualFold(parts[1], "cloud") || !strings.EqualFold(parts[2], "play") {
return "", ""
}
provider := strings.TrimSpace(parts[3])
if decoded, err := url.PathUnescape(provider); err == nil {
provider = decoded
}
// /api/cloud/play is generated by this app with url.Values, where '+'
// represents a space. Decode the standard query view first so re-importing
// existing STRM links does not turn "My Movie.mkv" into "My+Movie.mkv".
for _, value := range parsed.Query()["ref"] {
if ref := strmTreeURLQueryMediaSource(value); ref != "" {
return provider, ref
}
}
for _, value := range strmTreeRawQueryValues(parsed.RawQuery, "ref") {
if ref := strmTreeURLQueryMediaSource(value); ref != "" {
return provider, ref
}
}
return "", ""
}
func strmTreeSourceFromURL(parsed *url.URL) string {
if parsed == nil {
return ""
}
for _, key := range strmTreeURLPathQueryKeys {
for _, value := range strmTreeRawQueryValues(parsed.RawQuery, key) {
if ref := strmTreeURLQueryMediaSource(value); ref != "" {
return ref
}
}
for _, value := range parsed.Query()[key] {
if ref := strmTreeURLQueryMediaSource(value); ref != "" {
return ref
}
}
}
if parsed.Path != "" && strmTreeURLQueryMediaSource(parsed.Path) != "" {
return parsed.Path
}
return ""
}
func strmTreeRawQueryValues(rawQuery, key string) []string {
key = strings.ToLower(strings.TrimSpace(key))
if rawQuery == "" || key == "" {
return nil
}
values := make([]string, 0, 1)
for _, pair := range strings.Split(rawQuery, "&") {
if pair == "" {
continue
}
rawKey, rawValue, _ := strings.Cut(pair, "=")
decodedKey, err := url.QueryUnescape(rawKey)
if err != nil {
decodedKey = rawKey
}
if strings.ToLower(strings.TrimSpace(decodedKey)) != key {
continue
}
decodedValue, err := url.PathUnescape(rawValue)
if err != nil {
decodedValue = rawValue
}
values = append(values, decodedValue)
}
return values
}
func strmTreeURLQueryMediaSource(raw string) string {
value := strings.TrimSpace(strings.Trim(raw, `"'`))
if value == "" {
return ""
}
if parsed, err := url.Parse(value); err == nil && parsed.Scheme != "" && parsed.Scheme != "file" {
value = parsed.Path
}
value = trimSTRMTreeMediaMetadata(value)
if strmTreeSourceIsVideo(value) {
return value
}
return ""
}
func strmTreeHasUnsafeSegment(value string) bool {
for _, part := range strings.Split(strings.ReplaceAll(value, "\\", "/"), "/") {
if strings.TrimSpace(part) == ".." {
return true
}
}
return false
}
func strmTreeSourceIsVideo(source string) bool {
ext := strings.ToLower(path.Ext(trimSTRMTreeMediaMetadata(cleanSTRMTreeItemName(source))))
if ext == ".strm" {
return false
}
_, ok := videoExtensions[ext]
return ok
}
func cleanSTRMTreePathDecorations(value string) string {
if value == "" {
return ""
}
leadingSlash := strings.HasPrefix(value, "/")
parts := strings.Split(value, "/")
for i, part := range parts {
if part == "" {
continue
}
parts[i] = cleanSTRMTreeItemName(part)
}
cleaned := strings.Join(parts, "/")
if leadingSlash && !strings.HasPrefix(cleaned, "/") {
cleaned = "/" + cleaned
}
return cleaned
}
func cleanSTRMTreeItemName(value string) string {
value = strings.TrimSpace(strings.Trim(value, `"'`))
for {
before := value
value = strings.TrimSpace(strmTreeListPrefixRE.ReplaceAllString(value, ""))
value = strings.TrimSpace(strings.TrimLeft(value, "\ufeff📁📂📄📃🎬🎞🎥📺🗂️"))
lower := strings.ToLower(value)
for _, marker := range []string{
"[file]", "[f]", "[文件]", "[檔案]", "[档案]",
"[dir]", "[d]", "[folder]", "[目录]", "[目錄]", "[文件夹]", "[資料夾]",
"(file)", "(f)", "(文件)", "(檔案)", "(档案)",
"(dir)", "(d)", "(folder)", "(目录)", "(目錄)", "(文件夹)", "(資料夾)",
} {
if strings.HasPrefix(lower, marker) {
value = strings.TrimLeft(strings.TrimSpace(value[len(marker):]), "-_:: ")
break
}
}
if value == before {
return value
}
}
}
func trimSTRMTreeMediaMetadata(value string) string {
value = strings.TrimSpace(value)
lower := strings.ToLower(value)
bestEnd := -1
for ext := range videoExtensions {
from := 0
for {
idx := strings.Index(lower[from:], ext)
if idx < 0 {
break
}
idx += from
end := idx + len(ext)
if strmTreeMediaExtBoundary(value, end) && end > bestEnd {
bestEnd = end
}
from = idx + 1
if from >= len(lower) {
break
}
}
}
if bestEnd > 0 && bestEnd < len(value) {
return strings.TrimSpace(value[:bestEnd])
}
return value
}
func strmTreeMediaExtBoundary(value string, end int) bool {
if end >= len(value) {
return true
}
for _, r := range value[end:] {
if r == '(' || r == '[' || r == '{' || r == '<' || r == '(' || r == '【' || r == ',' || r == ',' {
return true
}
return r == ' ' || r == '\t' || r == '\r' || r == '\n'
}
return true
}