dd81a7ef set CanDownload=false to stop Wholphin's screensaver from
404ing on the nonexistent /Items/{id}/Download route — but the flag is
load-bearing for Infuse, which refuses Direct Play (Static=true
streaming) of items it believes it cannot download. With omitempty the
field vanished from the JSON entirely and Infuse playback broke, while
PlaybackInfo-negotiating clients were unaffected.
Resolve the underlying inconsistency instead of trading one client for
the other: implement GET/HEAD /Items/{id}/Download serving the original
file (range support, Content-Disposition, optional mediaSourceId for
multi-version items) under stream-group auth, and restore
CanDownload=true now that the route exists. Fixes Infuse playback and
keeps Wholphin's download callers working.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
141 lines
3.9 KiB
Go
141 lines
3.9 KiB
Go
package jellycompat
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type requestContextKey string
|
|
|
|
const originalPathKey requestContextKey = "jellycompat_original_path"
|
|
|
|
var compatPathSegments = map[string]string{
|
|
"system": "System",
|
|
"info": "Info",
|
|
"public": "Public",
|
|
"branding": "Branding",
|
|
"configuration": "Configuration",
|
|
"quickconnect": "QuickConnect",
|
|
"enabled": "Enabled",
|
|
"users": "Users",
|
|
"authenticatebyname": "AuthenticateByName",
|
|
"me": "Me",
|
|
"views": "Views",
|
|
"items": "Items",
|
|
"latest": "Latest",
|
|
"suggestions": "Suggestions",
|
|
"similar": "Similar",
|
|
"thememedia": "ThemeMedia",
|
|
"themesongs": "ThemeSongs",
|
|
"specialfeatures": "SpecialFeatures",
|
|
"intros": "Intros",
|
|
"download": "Download",
|
|
"images": "Images",
|
|
"primary": "Primary",
|
|
"backdrop": "Backdrop",
|
|
"logo": "Logo",
|
|
"genres": "Genres",
|
|
"shows": "Shows",
|
|
"seasons": "Seasons",
|
|
"episodes": "Episodes",
|
|
"nextup": "NextUp",
|
|
"useritems": "UserItems",
|
|
"resume": "Resume",
|
|
"userdata": "UserData",
|
|
"userviews": "UserViews",
|
|
"userfavoriteitems": "UserFavoriteItems",
|
|
"userplayeditems": "UserPlayedItems",
|
|
"favoriteitems": "FavoriteItems",
|
|
"playeditems": "PlayedItems",
|
|
"search": "Search",
|
|
"hints": "Hints",
|
|
"sessions": "Sessions",
|
|
"capabilities": "Capabilities",
|
|
"full": "Full",
|
|
"videos": "Videos",
|
|
"hls": "hls",
|
|
"subtitles": "Subtitles",
|
|
"logout": "Logout",
|
|
"playing": "Playing",
|
|
"progress": "Progress",
|
|
"stopped": "Stopped",
|
|
"ping": "Ping",
|
|
"filters": "Filters",
|
|
"mediasegments": "MediaSegments",
|
|
"episode": "Episode",
|
|
"timestamps": "Timestamps",
|
|
"introtimestamps": "IntroTimestamps",
|
|
"playback": "Playback",
|
|
"bitratetest": "BitrateTest",
|
|
"playbackinfo": "PlaybackInfo",
|
|
"displaypreferences": "DisplayPreferences",
|
|
"persons": "Persons",
|
|
"studios": "Studios",
|
|
"movies": "Movies",
|
|
"recommendations": "Recommendations",
|
|
"groupingoptions": "GroupingOptions",
|
|
"library": "Library",
|
|
"virtualfolders": "VirtualFolders",
|
|
"socket": "socket",
|
|
}
|
|
|
|
func normalizeCompatPathMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
normalized := canonicalizeCompatPath(r.URL.Path)
|
|
if normalized == r.URL.Path {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
ctx := context.WithValue(r.Context(), originalPathKey, r.URL.Path)
|
|
clone := r.Clone(ctx)
|
|
urlCopy := *clone.URL
|
|
urlCopy.Path = normalized
|
|
urlCopy.RawPath = normalized
|
|
clone.URL = &urlCopy
|
|
next.ServeHTTP(w, clone)
|
|
})
|
|
}
|
|
|
|
func canonicalizeCompatPath(path string) string {
|
|
if path == "" || path == "/" {
|
|
return path
|
|
}
|
|
|
|
parts := strings.Split(path, "/")
|
|
if len(parts) > 1 {
|
|
switch strings.ToLower(parts[1]) {
|
|
case "emby", "jellyfin":
|
|
parts = append([]string{""}, parts[2:]...)
|
|
}
|
|
}
|
|
for i := 1; i < len(parts); i++ {
|
|
part := parts[i]
|
|
if part == "" {
|
|
continue
|
|
}
|
|
parts[i] = canonicalizeCompatSegment(part)
|
|
}
|
|
return strings.Join(parts, "/")
|
|
}
|
|
|
|
func canonicalizeCompatSegment(segment string) string {
|
|
lower := strings.ToLower(segment)
|
|
if mapped, ok := compatPathSegments[lower]; ok {
|
|
return mapped
|
|
}
|
|
if lower == "master.m3u8" {
|
|
return lower
|
|
}
|
|
if strings.HasPrefix(lower, "stream.") {
|
|
return lower
|
|
}
|
|
return segment
|
|
}
|
|
|
|
func originalPathFromContext(ctx context.Context) string {
|
|
path, _ := ctx.Value(originalPathKey).(string)
|
|
return path
|
|
}
|