package subsonic import ( "bytes" "encoding/json" "io" "net/http" "net/url" "testing" "git.noroutes.net/NoRoutes/vibetunes/internal/database" ) func postTranscodeDecision(t *testing.T, srv string, q map[string]string, client ClientInfo) *http.Response { t.Helper() body, err := json.Marshal(client) if err != nil { t.Fatal(err) } format := q["f"] if format == "" { format = "xml" } url := srv + "/rest/getTranscodeDecision.view?u=" + q["u"] + "&p=" + q["p"] + "&v=1.16.1&c=vibesonic-test&f=" + format + "&mediaId=" + q["mediaId"] + "&mediaType=" + q["mediaType"] req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } return resp } func TestTranscodeDecisionDirectPlay(t *testing.T) { s, srv := newTestRecorderServer(t) createTestUser(t, s, "alice", "password123", false) seedLibrary(t, s) song := firstSong(t, s) client := ClientInfo{ Name: "TestClient", Platform: "linux", DirectPlayProfiles: []DirectPlayProfile{ {Containers: []string{"mp3"}, AudioCodecs: []string{"mp3"}, Protocols: []string{"http"}}, }, } resp := postTranscodeDecision(t, srv.URL, map[string]string{"u": "alice", "p": "password123", "mediaId": song.ID, "mediaType": "song", "f": "json"}, client) j := decodeJSON(t, resp) d, ok := j["transcodeDecision"].(map[string]any) if !ok { t.Fatalf("transcodeDecision missing: %v", j) } if d["canDirectPlay"] != true { t.Fatalf("canDirectPlay = %v, want true (client declared mp3 direct play, fixture songs are mp3)", d) } if _, has := d["transcodeStream"]; has { t.Error("transcodeStream should be absent when direct play is possible") } } func TestTranscodeDecisionFallsBackToTranscode(t *testing.T) { s, srv := newTestRecorderServer(t) createTestUser(t, s, "alice", "password123", false) seedLibrary(t, s) song := firstSong(t, s) client := ClientInfo{ Name: "TestClient", Platform: "linux", DirectPlayProfiles: []DirectPlayProfile{ {Containers: []string{"flac"}, AudioCodecs: []string{"flac"}, Protocols: []string{"http"}}, }, TranscodingProfiles: []TranscodingProfile{ {Container: "ogg", AudioCodec: "opus", Protocol: "http", MaxAudioChannels: 2}, }, MaxTranscodingAudioBitrate: 96, } resp := postTranscodeDecision(t, srv.URL, map[string]string{"u": "alice", "p": "password123", "mediaId": song.ID, "mediaType": "song", "f": "json"}, client) j := decodeJSON(t, resp) d := j["transcodeDecision"].(map[string]any) if d["canDirectPlay"] != nil && d["canDirectPlay"] != false { t.Errorf("canDirectPlay = %v, want false (source is mp3, client only direct-plays flac)", d["canDirectPlay"]) } if d["canTranscode"] != true { t.Fatalf("canTranscode = %v, want true", d) } ts, ok := d["transcodeStream"].(map[string]any) if !ok { t.Fatalf("transcodeStream missing: %v", d) } if ts["codec"] != "opus" || ts["audioBitrate"] != float64(96) { t.Errorf("transcodeStream = %v, want opus @ 96kbps", ts) } params, ok := d["transcodeParams"].(string) if !ok || params == "" { t.Fatalf("transcodeParams missing: %v", d) } // getTranscodeStream must accept exactly the transcodeParams token // getTranscodeDecision returned and actually produce transcoded // audio bytes. streamResp, err := http.Get(srv.URL + "/rest/getTranscodeStream.view?u=alice&p=password123&v=1.16.1&c=vibesonic-test&mediaId=" + song.ID + "&mediaType=song&transcodeParams=" + url.QueryEscape(params)) if err != nil { t.Fatal(err) } defer streamResp.Body.Close() if streamResp.StatusCode != http.StatusOK { t.Fatalf("getTranscodeStream status = %d", streamResp.StatusCode) } data, _ := io.ReadAll(streamResp.Body) if len(data) == 0 { t.Fatal("getTranscodeStream returned no bytes") } if ct := streamResp.Header.Get("Content-Type"); ct != "audio/ogg" { t.Errorf("Content-Type = %q, want audio/ogg for an opus transcode", ct) } } func TestTranscodeDecisionNoCompatibleProfile(t *testing.T) { s, srv := newTestRecorderServer(t) createTestUser(t, s, "alice", "password123", false) seedLibrary(t, s) song := firstSong(t, s) client := ClientInfo{ Name: "PickyClient", Platform: "linux", DirectPlayProfiles: []DirectPlayProfile{ {Containers: []string{"flac"}, AudioCodecs: []string{"flac"}, Protocols: []string{"http"}}, }, TranscodingProfiles: []TranscodingProfile{ {Container: "wma", AudioCodec: "wmav2", Protocol: "http"}, }, } resp := postTranscodeDecision(t, srv.URL, map[string]string{"u": "alice", "p": "password123", "mediaId": song.ID, "mediaType": "song", "f": "json"}, client) j := decodeJSON(t, resp) d := j["transcodeDecision"].(map[string]any) if d["canTranscode"] != nil && d["canTranscode"] != false { t.Errorf("canTranscode = %v, want false (server has no wmav2 encoder)", d["canTranscode"]) } if d["errorReason"] == nil || d["errorReason"] == "" { t.Error("expected a non-empty errorReason when no profile is compatible") } } func TestTranscodeDecisionRequiresStreamPermission(t *testing.T) { s, db := newTestServer(t) u := createTestUser(t, s, "alice", "password123", false) perms := u.Permissions perms.Stream = false if err := database.SetUserPermissions(db, u.ID, perms); err != nil { t.Fatal(err) } srv := newSubsonicTestServer(t, s) seedLibrary(t, s) song := firstSong(t, s) resp := postTranscodeDecision(t, srv, map[string]string{"u": "alice", "p": "password123", "mediaId": song.ID, "mediaType": "song"}, ClientInfo{}) x := decodeXML(t, resp) if x.Status != "failed" || x.Error == nil || x.Error.Code != ErrUnauthorized { t.Fatalf("expected error 50, got %+v", x) } } func TestTranscodeDecisionUnknownMedia(t *testing.T) { s, srv := newTestRecorderServer(t) createTestUser(t, s, "alice", "password123", false) resp := postTranscodeDecision(t, srv.URL, map[string]string{"u": "alice", "p": "password123", "mediaId": "sg_doesnotexist", "mediaType": "song"}, ClientInfo{}) x := decodeXML(t, resp) if x.Status != "failed" || x.Error == nil || x.Error.Code != ErrNotFound { t.Fatalf("expected error 70, got %+v", x) } }