Skip to content

Commit

Permalink
Implement the getAlbumInfo and getAlbumInfo endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
ironsmile committed May 19, 2024
1 parent bfd38af commit 6df6236
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 16 deletions.
14 changes: 10 additions & 4 deletions src/library/local_library.go
Original file line number Diff line number Diff line change
Expand Up @@ -512,12 +512,15 @@ func (lib *LocalLibrary) GetArtist(
fav sql.NullInt64
rating sql.NullInt16
)
if err := row.Scan(
err := row.Scan(
&res.Name,
&res.AlbumCount,
&fav,
&rating,
); err != nil {
)
if errors.Is(err, sql.ErrNoRows) {
return ErrArtistNotFound
} else if err != nil {
return fmt.Errorf("sql query for artist info failed: %w", err)
}
res.ID = artistID
Expand Down Expand Up @@ -577,7 +580,7 @@ func (lib *LocalLibrary) GetAlbum(
plays sql.NullInt64
lastPlayed sql.NullInt64
)
if err := row.Scan(
err := row.Scan(
&res.Name,
&res.Artist,
&res.SongCount,
Expand All @@ -586,7 +589,10 @@ func (lib *LocalLibrary) GetAlbum(
&lastPlayed,
&fav,
&rating,
); err != nil {
)
if errors.Is(err, sql.ErrNoRows) {
return ErrAlbumNotFound
} else if err != nil {
return fmt.Errorf("sql query for artist info failed: %w", err)
}
res.ID = albumID
Expand Down
7 changes: 7 additions & 0 deletions src/webserver/subsonic/get_album_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package subsonic

import "net/http"

func (s *subsonic) getAlbumInfo(w http.ResponseWriter, req *http.Request) {
s.getAlbumInfo2(w, req)
}
83 changes: 83 additions & 0 deletions src/webserver/subsonic/get_album_info_2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package subsonic

import (
"errors"
"net/http"
"net/url"
"strconv"

"github.com/ironsmile/euterpe/src/library"
)

func (s *subsonic) getAlbumInfo2(w http.ResponseWriter, req *http.Request) {
idString := req.Form.Get("id")
subsonicID, err := strconv.ParseInt(idString, 10, 64)
if idString == "" || err != nil || !isAlbumID(subsonicID) {
resp := responseError(errCodeNotFound, "album not found")
encodeResponse(w, req, resp)
return
}

albumID := toAlbumDBID(subsonicID)

album, err := s.lib.GetAlbum(req.Context(), albumID)
if err != nil && errors.Is(err, library.ErrAlbumNotFound) {
resp := responseError(errCodeNotFound, "album not found")
encodeResponse(w, req, resp)
return
} else if err != nil {
resp := responseError(errCodeGeneric, err.Error())
encodeResponse(w, req, resp)
return
}

artURL, query := s.getAlbumImageURL(req, album.ID)

resp := albumInfoResponse{
baseResponse: responseOk(),
AlbumInfo: xsdAlbumInfo{
LastfmURL: "https://last.fm/music/" + url.PathEscape(album.Artist) + "/" +
url.PathEscape(album.Name),
},
}

query.Set("size", "150")
artURL.RawQuery = query.Encode()
resp.AlbumInfo.SmallImageURL = artURL.String()

query.Set("size", "300")
artURL.RawQuery = query.Encode()
resp.AlbumInfo.MediumImageURL = artURL.String()

query.Set("size", "600")
artURL.RawQuery = query.Encode()
resp.AlbumInfo.LargeImageURL = artURL.String()

encodeResponse(w, req, resp)
}

// getAlbumImageURL returns a URL for album image with query parameters
// for access set from the request.
// albumID is an ID from the database.
func (s *subsonic) getAlbumImageURL(
req *http.Request,
albumID int64,
) (url.URL, url.Values) {
query := make(url.Values)
query.Set("id", albumConverArtID(albumID))
setQueryFromReq(query, req)
artURL := url.URL{
Scheme: getProtoFromRequest(req),
Host: getHostFromRequest(req),
Path: s.prefix + "/getCoverArt",
RawQuery: query.Encode(),
}

return artURL, query
}

type albumInfoResponse struct {
baseResponse

AlbumInfo xsdAlbumInfo `xml:"albumInfo"`
}
13 changes: 10 additions & 3 deletions src/webserver/subsonic/get_artist_info.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package subsonic

import (
"errors"
"net/http"
"strconv"

"github.com/ironsmile/euterpe/src/library"
)

func (s *subsonic) getArtistInfo(w http.ResponseWriter, req *http.Request) {
Expand All @@ -16,14 +19,18 @@ func (s *subsonic) getArtistInfo(w http.ResponseWriter, req *http.Request) {

artistID := toArtistDBID(subsonicID)

albums := s.lib.GetArtistAlbums(req.Context(), artistID)
if len(albums) == 0 {
artist, err := s.lib.GetArtist(req.Context(), artistID)
if errors.Is(err, library.ErrArtistNotFound) {
resp := responseError(errCodeNotFound, "artist not found")
encodeResponse(w, req, resp)
return
} else if err != nil {
resp := responseError(errCodeGeneric, err.Error())
encodeResponse(w, req, resp)
return
}

baseArtistInfo := s.getArtistInfoBase(req, artistID)
baseArtistInfo := s.getArtistInfoBase(req, artist)
resp := artistInfoResponse{
baseResponse: baseArtistInfo.baseResponse,
ArtistInfo: baseArtistInfo.ArtistInfo2,
Expand Down
21 changes: 15 additions & 6 deletions src/webserver/subsonic/get_artist_info_2.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package subsonic

import (
"errors"
"net/http"
"net/url"
"strconv"

"github.com/ironsmile/euterpe/src/library"
)

func (s *subsonic) getArtistInfo2(w http.ResponseWriter, req *http.Request) {
Expand All @@ -17,26 +20,32 @@ func (s *subsonic) getArtistInfo2(w http.ResponseWriter, req *http.Request) {

artistID := toArtistDBID(subsonicID)

albums := s.lib.GetArtistAlbums(req.Context(), artistID)
if len(albums) == 0 {
artist, err := s.lib.GetArtist(req.Context(), artistID)
if err != nil && errors.Is(err, library.ErrArtistNotFound) {
resp := responseError(errCodeNotFound, "artist not found")
encodeResponse(w, req, resp)
return
} else if err != nil {
resp := responseError(errCodeGeneric, err.Error())
encodeResponse(w, req, resp)
return
}

resp := s.getArtistInfoBase(req, artistID)
resp := s.getArtistInfoBase(req, artist)
encodeResponse(w, req, resp)
}

func (s *subsonic) getArtistInfoBase(
req *http.Request,
artistID int64,
artist library.Artist,
) artistInfo2Response {
artURL, query := s.getAristImageURL(req, artistID)
artURL, query := s.getAristImageURL(req, artist.ID)

resp := artistInfo2Response{
baseResponse: responseOk(),
ArtistInfo2: xsdArtistInfoBase{},
ArtistInfo2: xsdArtistInfoBase{
LastfmURL: "https://last.fm/music/" + url.PathEscape(artist.Name),
},
}

query.Set("size", "150")
Expand Down
2 changes: 2 additions & 0 deletions src/webserver/subsonic/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func (s *subsonic) initRouter() {
setUpHandler("/getStarred", s.getStarred)
setUpHandler("/getStarred2", s.getStarred2)
setUpHandler("/getTopSongs", s.getTopSongs)
setUpHandler("/getAlbumInfo", s.getAlbumInfo)
setUpHandler("/getAlbumInfo2", s.getAlbumInfo2)

s.mux = s.authHandler(router)
}
Expand Down
4 changes: 2 additions & 2 deletions src/webserver/subsonic/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
- [x] getVideoInfo - always returns "not found"
- [x] getArtistInfo
- [x] getArtistInfo2
- [ ] getAlbumInfo
- [ ] getAlbumInfo2
- [x] getAlbumInfo
- [x] getAlbumInfo2
- [ ] getSimilarSongs
- [ ] getSimilarSongs2
- [x] getTopSongs
Expand Down
69 changes: 68 additions & 1 deletion src/webserver/subsonic/xml_xsd_vaidate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,28 @@ func TestSubsonicXMLResponses(t *testing.T) {
},
}
},
GetAlbumStub: func(ctx context.Context, i int64) (library.Album, error) {
return library.Album{
ID: 44,
Name: "First Albumov",
Artist: "Albumov Artist",
SongCount: 5,
Duration: 23283737,
Plays: 932,
Favourite: 1714856348,
LastPlayed: 1714856348,
Rating: 3,
}, nil
},
GetArtistStub: func(ctx context.Context, i int64) (library.Artist, error) {
return library.Artist{
ID: 11,
Name: "Artistov Epicuerus",
AlbumCount: 4,
Favourite: 1714856348,
Rating: 5,
}, nil
},
}
browser := &libraryfakes.FakeBrowser{
BrowseArtistsStub: func(ba library.BrowseArgs) ([]library.Artist, int) {
Expand Down Expand Up @@ -432,6 +454,14 @@ func TestSubsonicXMLResponses(t *testing.T) {
desc: "getTopSongs",
url: testURL("/getTopSongs?artist=First+Artist&count=3"),
},
{
desc: "getAlbumInfo",
url: testURL("/getAlbumInfo?id=55"),
},
{
desc: "getAlbumInfo2",
url: testURL("/getAlbumInfo2?id=55"),
},
}

for _, test := range tests {
Expand Down Expand Up @@ -504,7 +534,14 @@ func TestSubsonicXMLResponses(t *testing.T) {
// TestSubsonicXMLErrors checks that errors returned from the Subsonic API have the
// correct error code and also have a valid XML.
func TestSubsonicXMLErrors(t *testing.T) {
lib := &libraryfakes.FakeLibrary{}
lib := &libraryfakes.FakeLibrary{
GetAlbumStub: func(ctx context.Context, i int64) (library.Album, error) {
return library.Album{}, library.ErrAlbumNotFound
},
GetArtistStub: func(ctx context.Context, i int64) (library.Artist, error) {
return library.Artist{}, library.ErrArtistNotFound
},
}
browser := &libraryfakes.FakeBrowser{}

err := xsdvalidate.Init()
Expand Down Expand Up @@ -694,6 +731,36 @@ func TestSubsonicXMLErrors(t *testing.T) {
url: testURL("/getTopSongs?artist=Not+Found"),
errorCode: 70,
},
{
desc: "getAlbumInfo strange arguments",
url: testURL("/getAlbumInfo?id=Not+Found"),
errorCode: 70,
},
{
desc: "getAlbumInfo album ID not correct range",
url: testURL("/getAlbumInfo?id=%d", int64(2e9)+55),
errorCode: 70,
},
{
desc: "getAlbumInfo album not found",
url: testURL("/getAlbumInfo?id=99"),
errorCode: 70,
},
{
desc: "getAlbumInfo2 strange arguments",
url: testURL("/getAlbumInfo2?id=Not+Found"),
errorCode: 70,
},
{
desc: "getAlbumInfo2 album ID not correct range",
url: testURL("/getAlbumInfo2?id=%d", int64(2e9)+55),
errorCode: 70,
},
{
desc: "getAlbumInfo2 album not found",
url: testURL("/getAlbumInfo2?id=99"),
errorCode: 70,
},
}

for _, test := range tests {
Expand Down
10 changes: 10 additions & 0 deletions src/webserver/subsonic/xsd_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,16 @@ type xsdAlbumWithSongsID3 struct {
}

type xsdArtistInfoBase struct {
Notes string `xml:"notes,omitempty" json:"notes,omitempty"`
LastfmURL string `xml:"lastFmUrl,omitempty" json:"lastFmUrl,omitempty"`
SmallImageURL string `xml:"smallImageUrl" json:"smallImageUrl"`
MediumImageURL string `xml:"mediumImageUrl" json:"mediumImageUrl"`
LargeImageURL string `xml:"largeImageUrl" json:"largeImageUrl"`
}

type xsdAlbumInfo struct {
Notes string `xml:"notes,omitempty" json:"notes,omitempty"`
LastfmURL string `xml:"lastFmUrl,omitempty" json:"lastFmUrl,omitempty"`
SmallImageURL string `xml:"smallImageUrl" json:"smallImageUrl"`
MediumImageURL string `xml:"mediumImageUrl" json:"mediumImageUrl"`
LargeImageURL string `xml:"largeImageUrl" json:"largeImageUrl"`
Expand Down

0 comments on commit 6df6236

Please sign in to comment.