mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-04 12:36:37 +08:00
fix media organize and subscription workflows
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user