stash/internal/api/resolver_query_scene.go
modal-error c61058c302 feat: Add signed URLs for scene streaming (AirPlay/Chromecast)
HMAC-signed URLs allow authenticated streaming to devices that cannot pass cookies (AirPlay, Chromecast). Signing is scoped to scene stream using a prefix-based approach so one signature covers all derivative segment URLs.

Credentialid hides username from public network.

When credentials are disabled, signing is bypassed entirely. API key takes precedence over signed params when both are present.
2026-03-08 15:11:35 -04:00

62 lines
1.5 KiB
Go

package api
import (
"context"
"fmt"
"strconv"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/session"
"github.com/stashapp/stash/pkg/signedurl"
)
func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*manager.SceneStreamEndpoint, error) {
sceneID, err := strconv.Atoi(*id)
if err != nil {
return nil, err
}
// find the scene
var scene *models.Scene
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
scene, err = r.repository.Scene.Find(ctx, sceneID)
if scene != nil {
err = scene.LoadPrimaryFile(ctx, r.repository.File)
}
return err
}); err != nil {
return nil, err
}
if scene == nil {
return nil, fmt.Errorf("scene with id %d not found", sceneID)
}
config := manager.GetInstance().Config
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewSceneURLBuilder(baseURL, scene)
streamURL := builder.GetStreamURL("")
if config.HasCredentials() {
userID := session.GetCurrentUserID(ctx)
if userID == nil {
return nil, fmt.Errorf("user ID not found")
}
streamURL.RawQuery = signedParams(config, *userID, signedurl.DerivePrefix(streamURL.Path)).Encode()
} else {
apiKey := config.GetAPIKey()
if apiKey != "" {
v := streamURL.Query()
v.Set("apikey", apiKey)
streamURL.RawQuery = v.Encode()
}
}
return manager.GetSceneStreamPaths(scene, streamURL, config.GetMaxStreamingTranscodeSize())
}