diff --git a/internal/handler/emby_playback_info_routes_test.go b/internal/handler/emby_playback_info_routes_test.go index 1cf4f41..5a7c29c 100644 --- a/internal/handler/emby_playback_info_routes_test.go +++ b/internal/handler/emby_playback_info_routes_test.go @@ -152,14 +152,14 @@ func TestEmbyPlaybackInfoDoesNotExposeTokenInCloudPath(t *testing.T) { } source := body["MediaSources"].([]any)[0].(map[string]any) pathURL, _ := source["Path"].(string) - if pathURL != "/api/stream/cloud-1" { - t.Fatalf("cloud Path should stay as non-tokenized display stream URL, got %#v", source) + if pathURL != "/Movies/Movie.mkv" { + t.Fatalf("cloud Path should expose the OpenList source path, got %#v", source) } if strings.Contains(pathURL, "api_key=") || strings.Contains(pathURL, "token=") { t.Fatalf("cloud Path must not expose auth key/token: %#v", source) } - if strings.Contains(pathURL, "/api/cloud/play/") { - t.Fatalf("cloud Path should not expose naked cloud play URL: %#v", source) + if strings.Contains(pathURL, "/api/cloud/play/") || strings.Contains(pathURL, "/api/stream/") { + t.Fatalf("cloud Path should not expose a playback URL: %#v", source) } directURL, _ := source["DirectStreamUrl"].(string) if !strings.HasPrefix(directURL, "/api/stream/cloud-1") || !strings.Contains(directURL, "api_key=") { @@ -236,8 +236,8 @@ func TestEmbyItemsDoNotExposeTokenInEmbeddedCloudPath(t *testing.T) { } source := items[0].(map[string]any)["MediaSources"].([]any)[0].(map[string]any) pathURL, _ := source["Path"].(string) - if pathURL != "/api/stream/cloud-1" { - t.Fatalf("embedded cloud Path should stay as non-tokenized display stream URL, got %#v", source) + if pathURL != "/Movies/Movie.mkv" { + t.Fatalf("embedded cloud Path should expose the OpenList source path, got %#v", source) } if strings.Contains(pathURL, "api_key=") || strings.Contains(pathURL, "token=") { t.Fatalf("embedded cloud Path must not expose auth key/token: %#v", source) diff --git a/internal/service/emby_playback.go b/internal/service/emby_playback.go index 97c5eea..ab1160a 100644 --- a/internal/service/emby_playback.go +++ b/internal/service/emby_playback.go @@ -204,7 +204,6 @@ func (e *EmbyService) mediaSource(ctx context.Context, m *model.Media, asEmbedde // follow the same STRM entry as generated .strm files; when disabled we // expose /Videos/{id}/stream so playback uses the Emby 302/proxy path. src["IsRemote"] = true - src["Path"] = playURL } return src } @@ -213,7 +212,7 @@ func (e *EmbyService) baseMediaSource(m *model.Media, container string, isCloud return map[string]any{ "Id": m.ID, "Name": m.Title, - "Path": m.Path, + "Path": embyMediaSourcePath(m), "Container": container, "Size": m.SizeBytes, "Protocol": "Http", @@ -231,6 +230,35 @@ func (e *EmbyService) baseMediaSource(m *model.Media, container string, isCloud } } +// embyMediaSourcePath keeps Emby's Path field as a source identity rather +// than a playback endpoint. OpenList and other path-based cloud providers +// carry the real provider path in the internal cloud-play ref query. For +// opaque provider refs, fall back to the display path stored in cloud://. +func embyMediaSourcePath(m *model.Media) string { + if m == nil { + return "" + } + if _, ref, ok := parseCloudMediaPlaybackURL(m.STRMURL); ok && strings.HasPrefix(ref, "/") { + return ref + } + raw := strings.TrimSpace(m.Path) + u, err := url.Parse(raw) + if err != nil || !strings.EqualFold(u.Scheme, "cloud") { + return m.Path + } + sourcePath := strings.TrimSpace(u.Path) + if decoded, decodeErr := url.PathUnescape(sourcePath); decodeErr == nil { + sourcePath = decoded + } + if sourcePath == "" { + return "/" + } + if !strings.HasPrefix(sourcePath, "/") { + sourcePath = "/" + sourcePath + } + return sourcePath +} + func embyMediaContainer(m *model.Media) string { container := strings.Trim(strings.ToLower(m.Container), ". ") if container == "" { diff --git a/internal/service/emby_playback_test.go b/internal/service/emby_playback_test.go index 87e77c9..bc52251 100644 --- a/internal/service/emby_playback_test.go +++ b/internal/service/emby_playback_test.go @@ -335,8 +335,8 @@ func TestEmbyPlaybackInfoKeepsSTRMBehindStreamEndpoint(t *testing.T) { if src["DirectStreamUrl"] != "/api/stream/cloud-1" { t.Fatalf("strm playback should prefer /api/stream when enabled: %#v", src) } - if src["Path"] != "/api/stream/cloud-1" { - t.Fatalf("path should prefer /api/stream when enabled: %#v", src) + if src["Path"] != "/Movies/f1.mkv" { + t.Fatalf("path should expose the OpenList source path: %#v", src) } streams := src["MediaStreams"].([]map[string]any) if len(streams) == 0 || streams[0]["Type"] != "Video" { @@ -373,8 +373,23 @@ func TestEmbyPlaybackInfoUsesVideoStreamWhenSTRMDisabled(t *testing.T) { if src["DirectStreamUrl"] != "/Videos/cloud-302/stream.mkv" { t.Fatalf("302/proxy mode should use Emby video stream URL: %#v", src) } - if src["Path"] != "/Videos/cloud-302/stream.mkv" { - t.Fatalf("302/proxy mode path should use Emby video stream URL: %#v", src) + if src["Path"] != "/Movies/Movie.mkv" { + t.Fatalf("302/proxy mode should preserve the OpenList source path: %#v", src) + } +} + +func TestEmbyMediaSourcePathFallsBackToCloudDisplayPath(t *testing.T) { + media := &model.Media{ + Path: `cloud://cloud115/Shows/My%20Show/S01E01.mkv`, + STRMURL: `/api/cloud/play/cloud115?ref=opaque-file-id`, + } + if got := embyMediaSourcePath(media); got != "/Shows/My Show/S01E01.mkv" { + t.Fatalf("source path = %q", got) + } + + local := &model.Media{Path: `D:\\Media\\Movie.mkv`, STRMURL: `https://example.test/movie.mkv`} + if got := embyMediaSourcePath(local); got != local.Path { + t.Fatalf("local path = %q, want %q", got, local.Path) } }