Files
MeBox/internal/service/strm_tree_url_test.go
T

313 lines
13 KiB
Go

package service
import (
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"go.uber.org/zap"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/repository"
)
func TestGenerateSTRMFromTreeUsesMediaPathFromURLQuery(t *testing.T) {
outDir := filepath.Join(t.TempDir(), "strm")
svc := NewSTRMService(zap.NewNop(), nil, nil)
res, err := svc.GenerateFromTree(t.Context(), GenerateSTRMTreeOptions{
Provider: "openlist",
Paths: []string{
"https://openlist.example.com/api/fs/get?path=%2FMovies%2FDune.Part.Two.2024.mkv",
"https://openlist.example.com/api/fs/get?path=%2FMovies%2FA%2BB.2026.mkv",
"https://openlist.example.com/api/fs/get?path=/Movies/A+B.Raw.2026.mkv",
"https://openlist.example.com/api/raw?ref=/Shows/Some.Show/S01E01.mp4",
"https://cdn.example.com/media/Movies/Direct.Movie.2026.mkv?token=secret",
"https://cdn.example.com/media/Movies/A+B.Direct.2026.mkv?token=secret",
"https://openlist.example.com/api/fs/get?id=12345",
},
OutputDir: outDir,
})
if err != nil {
t.Fatal(err)
}
if res.Generated != 6 || len(res.Errors) != 0 {
t.Fatalf("result = %#v, want six generated videos and non-media API URL ignored", res)
}
movie := readSTRM(t, filepath.Join(outDir, "Movies", "Dune.Part.Two.2024.strm"))
if !strings.Contains(movie, "ref=%2FMovies%2FDune.Part.Two.2024.mkv") || strings.Contains(movie, "api%2Ffs%2Fget") {
t.Fatalf("movie strm url = %q, want query media path as cloud ref", movie)
}
plusMovie := readSTRM(t, filepath.Join(outDir, "Movies", "A+B.2026.strm"))
if !strings.Contains(plusMovie, "ref=%2FMovies%2FA%2BB.2026.mkv") || strings.Contains(plusMovie, "A+B.2026.mkv") {
t.Fatalf("plus movie strm url = %q, want literal plus preserved and encoded in ref", plusMovie)
}
rawPlusMovie := readSTRM(t, filepath.Join(outDir, "Movies", "A+B.Raw.2026.strm"))
if !strings.Contains(rawPlusMovie, "ref=%2FMovies%2FA%2BB.Raw.2026.mkv") || strings.Contains(rawPlusMovie, "A+B.Raw.2026.mkv") {
t.Fatalf("raw plus movie strm url = %q, want raw query plus preserved and encoded in ref", rawPlusMovie)
}
show := readSTRM(t, filepath.Join(outDir, "Shows", "Some.Show", "S01E01.strm"))
if !strings.Contains(show, "ref=%2FShows%2FSome.Show%2FS01E01.mp4") {
t.Fatalf("show strm url = %q, want ref query media path", show)
}
direct := readSTRM(t, filepath.Join(outDir, "media", "Movies", "Direct.Movie.2026.strm"))
if !strings.Contains(direct, "ref=%2Fmedia%2FMovies%2FDirect.Movie.2026.mkv") {
t.Fatalf("direct url strm = %q, want normal URL path media ref", direct)
}
plusDirect := readSTRM(t, filepath.Join(outDir, "media", "Movies", "A+B.Direct.2026.strm"))
if !strings.Contains(plusDirect, "ref=%2Fmedia%2FMovies%2FA%2BB.Direct.2026.mkv") {
t.Fatalf("plus direct url strm = %q, want URL path plus preserved", plusDirect)
}
if _, err := os.Stat(filepath.Join(outDir, "api", "fs", "get.strm")); !os.IsNotExist(err) {
t.Fatalf("non-media API URL should not generate STRM, stat err=%v", err)
}
}
func TestGenerateSTRMFromTreePreservesSourceProvider(t *testing.T) {
outDir := filepath.Join(t.TempDir(), "strm")
svc := NewSTRMService(zap.NewNop(), nil, nil)
res, err := svc.GenerateFromTree(t.Context(), GenerateSTRMTreeOptions{
Provider: "115",
Paths: []string{
"cloud://openlist/Movies/OpenList.Movie.2026.mkv",
"/api/cloud/play/cloud115?ref=%2FShows%2FCloud115.Show.S01E01.mkv",
"/api/cloud/play/openlist?ref=%2FMovies%2FMy+Space.Movie.2026.mkv",
"https://media.example.com/api/cloud/play/openlist?ref=%2FMovies%2FRemote.OpenList.Movie.2026.mkv",
"/Movies/Fallback.115.Movie.2026.mkv",
},
OutputDir: outDir,
})
if err != nil {
t.Fatal(err)
}
if res.Generated != 5 || len(res.Errors) != 0 {
t.Fatalf("result = %#v, want five generated videos", res)
}
openlist := readSTRM(t, filepath.Join(outDir, "Movies", "OpenList.Movie.2026.strm"))
if !strings.Contains(openlist, "/api/cloud/play/openlist?") || !strings.Contains(openlist, "ref=%2FMovies%2FOpenList.Movie.2026.mkv") {
t.Fatalf("cloud:// source url = %q, want openlist provider and original ref", openlist)
}
cloud115 := readSTRM(t, filepath.Join(outDir, "Shows", "Cloud115.Show.S01E01.strm"))
if !strings.Contains(cloud115, "/api/cloud/play/cloud115?") || !strings.Contains(cloud115, "ref=%2FShows%2FCloud115.Show.S01E01.mkv") {
t.Fatalf("cloud play source url = %q, want cloud115 provider preserved", cloud115)
}
spaceMoviePath := filepath.Join(outDir, "Movies", "My Space.Movie.2026.strm")
spaceMovie := readSTRM(t, spaceMoviePath)
if _, err := os.Stat(filepath.Join(outDir, "Movies", "My+Space.Movie.2026.strm")); !os.IsNotExist(err) {
t.Fatalf("cloud play source should not create literal-plus local path, stat err=%v", err)
}
spaceURL, err := url.Parse(spaceMovie)
if err != nil {
t.Fatalf("parse cloud play source url %q: %v", spaceMovie, err)
}
if got := spaceURL.Query().Get("ref"); got != "/Movies/My Space.Movie.2026.mkv" {
t.Fatalf("cloud play source ref = %q, want decoded space path", got)
}
remoteOpenlist := readSTRM(t, filepath.Join(outDir, "Movies", "Remote.OpenList.Movie.2026.strm"))
if !strings.Contains(remoteOpenlist, "/api/cloud/play/openlist?") || !strings.Contains(remoteOpenlist, "ref=%2FMovies%2FRemote.OpenList.Movie.2026.mkv") {
t.Fatalf("absolute cloud play source url = %q, want openlist provider preserved", remoteOpenlist)
}
fallback := readSTRM(t, filepath.Join(outDir, "Movies", "Fallback.115.Movie.2026.strm"))
if !strings.Contains(fallback, "/api/cloud/play/cloud115?") || !strings.Contains(fallback, "ref=%2FMovies%2FFallback.115.Movie.2026.mkv") {
t.Fatalf("plain source url = %q, want fallback cloud115 provider", fallback)
}
}
func TestGenerateSTRMFromTreeCloudMountUsesScanDirForRef(t *testing.T) {
outDir := filepath.Join(t.TempDir(), "strm")
svc := NewSTRMService(zap.NewNop(), nil, nil)
res, err := svc.GenerateFromTree(t.Context(), GenerateSTRMTreeOptions{
Provider: "115",
Paths: []string{
"cloud://openlist/%E7%94%B5%E5%BD%B1/%E5%88%AB%E5%90%8D%E7%9B%AE%E5%BD%95/Alias.Movie.2026.mkv?dir=%2Factual%2Fcloud%2Fmovies%2FAlias.Movie.2026.mkv",
},
OutputDir: outDir,
})
if err != nil {
t.Fatal(err)
}
if res.Generated != 1 || len(res.Errors) != 0 {
t.Fatalf("result = %#v, want one generated video", res)
}
got := readSTRM(t, filepath.Join(outDir, "电影", "别名目录", "Alias.Movie.2026.strm"))
if !strings.Contains(got, "/api/cloud/play/openlist?") {
t.Fatalf("strm url = %q, want provider from cloud mount", got)
}
if !strings.Contains(got, "ref=%2Factual%2Fcloud%2Fmovies%2FAlias.Movie.2026.mkv") {
t.Fatalf("strm url = %q, want dir scan path as playable ref", got)
}
if strings.Contains(got, "%E5%88%AB%E5%90%8D%E7%9B%AE%E5%BD%95") {
t.Fatalf("strm url = %q, display path leaked into playable ref", got)
}
}
func TestGenerateSTRMFromTreeDoesNotDedupeDifferentProviders(t *testing.T) {
outDir := filepath.Join(t.TempDir(), "strm")
svc := NewSTRMService(zap.NewNop(), nil, nil)
res, err := svc.GenerateFromTree(t.Context(), GenerateSTRMTreeOptions{
Provider: "115",
Paths: []string{"cloud://openlist/Movies/Same.Movie.mkv", "cloud://cloud115/Movies/Same.Movie.mkv"},
OutputDir: outDir,
Overwrite: true,
})
if err != nil {
t.Fatal(err)
}
if res.Generated != 1 || res.Updated != 1 || len(res.Errors) != 0 {
t.Fatalf("result = %#v, want both provider-specific sources processed", res)
}
got := readSTRM(t, filepath.Join(outDir, "Movies", "Same.Movie.strm"))
if !strings.Contains(got, "/api/cloud/play/cloud115?") || !strings.Contains(got, "ref=%2FMovies%2FSame.Movie.mkv") {
t.Fatalf("final strm url = %q, want second provider write to prove it was not deduped", got)
}
}
func TestGenerateSTRMFromTreeMissingOnlySkipsMediaAlreadyInLibrary(t *testing.T) {
db := newServiceTestDB(t, &model.Media{})
repos := repository.New(db)
rows := []model.Media{
{
Base: model.Base{ID: "existing-strm"},
Title: "Existing STRM",
Path: "cloud://openlist/Movies/Existing.Path.Copy.mkv",
STRMURL: "/api/cloud/play/openlist?ref=%2FMovies%2FExisting.Movie.mkv",
},
{
Base: model.Base{ID: "existing-cloud-path"},
Title: "Existing Cloud Path",
Path: "cloud://cloud115/Shows/Existing.Show.S01E01.mkv",
},
{
Base: model.Base{ID: "different-provider"},
Title: "Different Provider",
Path: "cloud://cloud115/Shows/Same.Provider.mkv",
},
}
for i := range rows {
if err := repos.DB.Create(&rows[i]).Error; err != nil {
t.Fatal(err)
}
}
outDir := filepath.Join(t.TempDir(), "strm")
svc := NewSTRMService(zap.NewNop(), repos, nil)
res, err := svc.GenerateFromTree(t.Context(), GenerateSTRMTreeOptions{
Provider: "openlist",
Paths: []string{
"cloud://openlist/Movies/Existing.Movie.mkv",
"cloud://openlist/Movies/New.Movie.mkv",
"cloud://cloud115/Shows/Existing.Show.S01E01.mkv",
"cloud://openlist/Shows/Same.Provider.mkv",
},
OutputDir: outDir,
MissingOnly: true,
})
if err != nil {
t.Fatal(err)
}
if res.Generated != 2 || res.Skipped != 2 || len(res.Errors) != 0 {
t.Fatalf("result = %#v, want generated=2 skipped=2", res)
}
if _, err := os.Stat(filepath.Join(outDir, "Movies", "Existing.Movie.strm")); !os.IsNotExist(err) {
t.Fatalf("existing media should not generate STRM, stat err=%v", err)
}
assertFileContains(t, filepath.Join(outDir, "Movies", "New.Movie.strm"), "/api/cloud/play/openlist?ref=%2FMovies%2FNew.Movie.mkv")
assertFileContains(t, filepath.Join(outDir, "Shows", "Same.Provider.strm"), "/api/cloud/play/openlist?ref=%2FShows%2FSame.Provider.mkv")
for _, item := range res.Items {
if item.Title == "Existing.Movie" && item.Reason != "already in media library" {
t.Fatalf("existing media skip reason = %q", item.Reason)
}
}
}
func TestGenerateSTRMFromTreeStripsPathListPrefixes(t *testing.T) {
outDir := filepath.Join(t.TempDir(), "strm")
svc := NewSTRMService(zap.NewNop(), nil, nil)
tree := strings.Join([]string{
"- /Movies/Dune.Part.Two.2024.mkv",
"* https://openlist.example.com/api/fs/get?path=%2FShows%2FSome.Show%2FS01E01.mp4",
"1. /Anime/Frieren/Frieren.S01E01.mp4",
"2) /Anime/Frieren/Frieren.S01E02.mp4",
"• /Documentaries/Earth.2026.mkv",
}, "\n")
res, err := svc.GenerateFromTree(t.Context(), GenerateSTRMTreeOptions{
Provider: "openlist",
TreeText: tree,
OutputDir: outDir,
})
if err != nil {
t.Fatal(err)
}
if res.Generated != 5 || len(res.Errors) != 0 {
t.Fatalf("result = %#v, want five generated videos with list prefixes stripped", res)
}
moviePath := filepath.Join(outDir, "Movies", "Dune.Part.Two.2024.strm")
movie := readSTRM(t, moviePath)
if strings.Contains(moviePath, "- ") || !strings.Contains(movie, "ref=%2FMovies%2FDune.Part.Two.2024.mkv") {
t.Fatalf("markdown bullet prefix leaked into movie path/ref: path=%q url=%q", moviePath, movie)
}
show := readSTRM(t, filepath.Join(outDir, "Shows", "Some.Show", "S01E01.strm"))
if strings.Contains(show, "api%2Ffs%2Fget") || !strings.Contains(show, "ref=%2FShows%2FSome.Show%2FS01E01.mp4") {
t.Fatalf("bullet URL query source was not cleaned: %q", show)
}
if _, err := os.Stat(filepath.Join(outDir, "1. ", "Anime", "Frieren", "Frieren.S01E01.strm")); !os.IsNotExist(err) {
t.Fatalf("numbered prefix should not create a literal prefix directory, stat err=%v", err)
}
if got := readSTRM(t, filepath.Join(outDir, "Documentaries", "Earth.2026.strm")); !strings.Contains(got, "ref=%2FDocumentaries%2FEarth.2026.mkv") {
t.Fatalf("round bullet source ref = %q", got)
}
}
func TestGenerateSTRMFromTreeOutputPrefixOnlyAffectsLocalPath(t *testing.T) {
outDir := filepath.Join(t.TempDir(), "strm")
svc := NewSTRMService(zap.NewNop(), nil, nil)
res, err := svc.GenerateFromTree(t.Context(), GenerateSTRMTreeOptions{
Provider: "openlist",
Paths: []string{"/cloud/Dune.Part.Two.2024.mkv"},
SourceRoot: "/cloud",
OutputPrefix: "电影/欧美电影",
OutputDir: outDir,
})
if err != nil {
t.Fatal(err)
}
if res.Generated != 1 || len(res.Errors) != 0 {
t.Fatalf("result = %#v, want one generated video", res)
}
got := readSTRM(t, filepath.Join(outDir, "电影", "欧美电影", "Dune.Part.Two.2024.strm"))
if strings.Contains(got, "%E7%94%B5%E5%BD%B1") || strings.Contains(got, "%E6%AC%A7%E7%BE%8E%E7%94%B5%E5%BD%B1") {
t.Fatalf("strm url = %q, output prefix should not be injected into cloud ref", got)
}
if !strings.Contains(got, "ref=%2Fcloud%2FDune.Part.Two.2024.mkv") {
t.Fatalf("strm url = %q, want original cloud ref", got)
}
}
func TestGenerateSTRMFromTreeRejectsUnsafeOutputPrefix(t *testing.T) {
outDir := filepath.Join(t.TempDir(), "strm")
svc := NewSTRMService(zap.NewNop(), nil, nil)
res, err := svc.GenerateFromTree(t.Context(), GenerateSTRMTreeOptions{
Provider: "openlist",
Paths: []string{"Movies/Movie.mkv"},
OutputPrefix: "../escape",
OutputDir: outDir,
})
if err != nil {
t.Fatal(err)
}
if res.Generated != 0 || len(res.Errors) != 1 {
t.Fatalf("result = %#v, want unsafe prefix rejected", res)
}
if _, err := os.Stat(filepath.Join(outDir, "..", "escape", "Movies", "Movie.strm")); !os.IsNotExist(err) {
t.Fatalf("unsafe prefixed strm should not be written, stat err=%v", err)
}
}