mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-12 00:06:37 +08:00
split subtitle service helpers
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
"github.com/ShukeBta/MediaStationGo/internal/service/cloud"
|
||||
)
|
||||
|
||||
func discoverCloudSubtitles(ctx context.Context, s *SubtitleService, m model.Media) []SubtitleTrack {
|
||||
if s == nil || s.storage == nil {
|
||||
return []SubtitleTrack{}
|
||||
}
|
||||
typ, mediaRef, ok := cloudSubtitleMediaRef(m)
|
||||
if !ok {
|
||||
return []SubtitleTrack{}
|
||||
}
|
||||
dirRef, mediaName := splitCloudRef(mediaRef)
|
||||
if mediaName == "" {
|
||||
return []SubtitleTrack{}
|
||||
}
|
||||
base := strings.TrimSuffix(mediaName, filepath.Ext(mediaName))
|
||||
entries, err := s.storage.CloudList(ctx, typ, dirRef)
|
||||
if err != nil {
|
||||
if s.log != nil {
|
||||
s.log.Debug("list cloud subtitles failed", zap.String("provider", typ), zap.String("dir", dirRef), zap.Error(err))
|
||||
}
|
||||
return []SubtitleTrack{}
|
||||
}
|
||||
tracks := cloudSubtitleTracks(typ, entries, base, false)
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir || !isSubtitleDirectory(entry.Name) || strings.TrimSpace(entry.ID) == "" {
|
||||
continue
|
||||
}
|
||||
subEntries, err := s.storage.CloudList(ctx, typ, entry.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
tracks = append(tracks, cloudSubtitleTracks(typ, subEntries, base, true)...)
|
||||
}
|
||||
return tracks
|
||||
}
|
||||
|
||||
func cloudSubtitleTracks(typ string, entries []cloud.FileEntry, base string, subdir bool) []SubtitleTrack {
|
||||
tracks := make([]SubtitleTrack, 0)
|
||||
baseLower := strings.ToLower(base)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir {
|
||||
continue
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(entry.Name))
|
||||
codec, ok := extToCodec[ext]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fullName := strings.TrimSuffix(entry.Name, ext)
|
||||
if !subdir && !strings.HasPrefix(strings.ToLower(fullName), baseLower) {
|
||||
continue
|
||||
}
|
||||
ref := cloudEntryRef(typ, entry.ID, entry.PickCode)
|
||||
if ref == "" {
|
||||
continue
|
||||
}
|
||||
lang := detectLang(fullName, base)
|
||||
tracks = append(tracks, SubtitleTrack{
|
||||
Lang: lang,
|
||||
Label: lang,
|
||||
Path: buildCloudSubtitlePath(typ, ref, entry.Name),
|
||||
Codec: codec,
|
||||
})
|
||||
}
|
||||
return tracks
|
||||
}
|
||||
|
||||
func cloudSubtitleMediaRef(m model.Media) (typ, ref string, ok bool) {
|
||||
if info, parsed := ParseCloudLibraryMount(m.Path); parsed && strings.TrimSpace(info.DisplayDir) != "" {
|
||||
return info.Provider, info.DisplayDir, true
|
||||
}
|
||||
if typ, ref, parsed := parseCloudMediaPlaybackURL(m.STRMURL); parsed {
|
||||
return typ, ref, true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func splitCloudRef(ref string) (dir, name string) {
|
||||
ref = strings.Trim(strings.ReplaceAll(strings.TrimSpace(ref), "\\", "/"), "/")
|
||||
if ref == "" {
|
||||
return "", ""
|
||||
}
|
||||
idx := strings.LastIndex(ref, "/")
|
||||
if idx < 0 {
|
||||
return "", ref
|
||||
}
|
||||
return ref[:idx], ref[idx+1:]
|
||||
}
|
||||
|
||||
func isSubtitleDirectory(name string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(name)) {
|
||||
case "subs", "sub", ".sub", "subtitles", "subtitle":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func buildCloudSubtitlePath(typ, ref, name string) string {
|
||||
u := url.URL{
|
||||
Scheme: "cloud",
|
||||
Host: strings.TrimSpace(typ),
|
||||
Path: "/" + strings.TrimLeft(strings.TrimSpace(ref), "/"),
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("name", strings.TrimSpace(name))
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func parseCloudSubtitlePath(raw string) (typ, ref, name string, ok bool) {
|
||||
u, err := url.Parse(strings.TrimSpace(raw))
|
||||
if err != nil || strings.ToLower(u.Scheme) != "cloud" || strings.TrimSpace(u.Host) == "" {
|
||||
return "", "", "", false
|
||||
}
|
||||
ref = strings.TrimLeft(u.EscapedPath(), "/")
|
||||
if decoded, err := url.PathUnescape(ref); err == nil {
|
||||
ref = decoded
|
||||
}
|
||||
return strings.TrimSpace(u.Host), strings.TrimSpace(ref), strings.TrimSpace(u.Query().Get("name")), ref != ""
|
||||
}
|
||||
|
||||
func serveCloudSubtitle(ctx context.Context, s *SubtitleService, m model.Media, typ, ref, name string, w io.Writer) error {
|
||||
if s == nil || s.storage == nil {
|
||||
return errors.New("cloud storage service unavailable")
|
||||
}
|
||||
mediaTyp, _, ok := cloudSubtitleMediaRef(m)
|
||||
if !ok || mediaTyp != typ {
|
||||
return ErrCloudPlaybackUnavailable
|
||||
}
|
||||
allowed := false
|
||||
for _, track := range discoverCloudSubtitles(ctx, s, m) {
|
||||
if track.Path == buildCloudSubtitlePath(typ, ref, name) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return fmt.Errorf("path escape")
|
||||
}
|
||||
body, err := s.storage.CloudReadText(ctx, typ, ref, 8<<20)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(firstNonEmpty(name, ref)))
|
||||
switch ext {
|
||||
case ".vtt":
|
||||
_, err = io.WriteString(w, body)
|
||||
case ".srt":
|
||||
_, err = io.WriteString(w, srtToVTT(body))
|
||||
case ".ass", ".ssa":
|
||||
_, err = io.WriteString(w, assToVTT(body))
|
||||
default:
|
||||
return errors.New("unsupported subtitle format")
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user