diff --git a/.golangci.yml b/.golangci.yml index 1dcfc2b..882ed26 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,14 +8,11 @@ issues: exclude-rules: - path: _test\.go linters: - - gocyclo - errcheck - - dupl - - gosec - text: "weak cryptographic primitive" linters: - gosec - - path: model/model.go + - path: model/model\.go linters: - lll - path: server/handler/ diff --git a/model/model.go b/model/model.go index c8a3215..540e2e3 100644 --- a/model/model.go +++ b/model/model.go @@ -9,17 +9,17 @@ import ( type Artist struct { IDBase - Name string `gorm:"not null; unique_index"` - Albums []Album `gorm:"foreignkey:TagArtistID"` + Name string `gorm:"not null; unique_index"` + Albums []*Album `gorm:"foreignkey:TagArtistID"` } type Track struct { IDBase CrudBase - Album Album + Album *Album AlbumID int `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` Filename string `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null"` - Artist Artist + Artist *Artist ArtistID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` Duration int `gorm:"not null" sql:"default: null"` Size int `gorm:"not null" sql:"default: null"` @@ -63,9 +63,9 @@ type Setting struct { type Play struct { IDBase - User User + User *User UserID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - Album Album + Album *Album AlbumID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` Time time.Time `sql:"default: null"` Count int @@ -79,10 +79,10 @@ type Album struct { Parent *Album ParentID int `sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` Cover string `sql:"default: null"` - TagArtist Artist + TagArtist *Artist TagArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` TagTitle string `gorm:"index" sql:"default: null"` TagYear int `sql:"default: null"` - Tracks []Track + Tracks []*Track IsNew bool `gorm:"-"` } diff --git a/scanner/walk.go b/scanner/walk.go index 2deccdb..5f97632 100644 --- a/scanner/walk.go +++ b/scanner/walk.go @@ -58,7 +58,7 @@ func (s *Scanner) callbackPost(fullPath string, info *godirwalk.Dirent) error { if folder.IsNew { folder.ParentID = s.curFolderID() folder.Cover = s.curCover - s.tx.Save(&folder) + s.tx.Save(folder) } s.curCover = "" log.Printf("processed folder `%s`\n", fullPath) @@ -66,42 +66,45 @@ func (s *Scanner) callbackPost(fullPath string, info *godirwalk.Dirent) error { } func (s *Scanner) handleFolder(it *item) error { - var folder model.Album + folder := &model.Album{} + defer s.curFolders.Push(folder) err := s.tx. Where(model.Album{ LeftPath: it.directory, RightPath: it.filename, }). - First(&folder). + First(folder). Error if !gorm.IsRecordNotFoundError(err) && it.stat.ModTime().Before(folder.UpdatedAt) { // we found the record but it hasn't changed - s.curFolders.Push(&folder) return nil } folder.LeftPath = it.directory folder.RightPath = it.filename - s.tx.Save(&folder) + s.tx.Save(folder) folder.IsNew = true - s.curFolders.Push(&folder) return nil } func (s *Scanner) handleTrack(it *item) error { // // set track basics - var track model.Track + track := &model.Track{} + defer func() { + // id will will be found (the first early return) + // or created the tx.Save(track) + s.seenTracks[track.ID] = struct{}{} + }() err := s.tx. Where(model.Track{ AlbumID: s.curFolderID(), Filename: it.filename, }). - First(&track). + First(track). Error if !gorm.IsRecordNotFoundError(err) && it.stat.ModTime().Before(track.UpdatedAt) { - s.seenTracks[track.ID] = struct{}{} // we found the record but it hasn't changed return nil } @@ -125,18 +128,17 @@ func (s *Scanner) handleTrack(it *item) error { track.TagYear = tags.Year() // // set album artist basics - var artist model.Artist + artist := &model.Artist{} err = s.tx. Where("name = ?", tags.AlbumArtist()). - First(&artist). + First(artist). Error if gorm.IsRecordNotFoundError(err) { artist.Name = tags.AlbumArtist() - s.tx.Save(&artist) + s.tx.Save(artist) } track.ArtistID = artist.ID - s.tx.Save(&track) - s.seenTracks[track.ID] = struct{}{} + s.tx.Save(track) // // set album if this is the first track in the folder if !s.curFolder().IsNew { diff --git a/server/handler/construct_sub_by_tags.go b/server/handler/construct_sub_by_tags.go index db3ebd5..fda5408 100644 --- a/server/handler/construct_sub_by_tags.go +++ b/server/handler/construct_sub_by_tags.go @@ -8,14 +8,17 @@ import ( ) func makeAlbumFromAlbum(a *model.Album, artist *model.Artist) *subsonic.Album { - return &subsonic.Album{ - ID: a.ID, - Name: a.TagTitle, - Created: a.CreatedAt, - CoverID: a.ID, - Artist: artist.Name, - ArtistID: artist.ID, + ret := &subsonic.Album{ + ID: a.ID, + Name: a.TagTitle, + Created: a.CreatedAt, + CoverID: a.ID, } + if artist != nil { + ret.Artist = artist.Name + ret.ArtistID = artist.ID + } + return ret } func makeTrackFromTrack(t *model.Track, album *model.Album) *subsonic.Track { diff --git a/server/handler/handler.go b/server/handler/handler.go index bb6197c..3877293 100644 --- a/server/handler/handler.go +++ b/server/handler/handler.go @@ -24,10 +24,10 @@ type Controller struct { } func (c *Controller) GetSetting(key string) string { - var setting model.Setting + setting := &model.Setting{} c.DB. Where("key = ?", key). - First(&setting) + First(setting) return setting.Value } @@ -39,13 +39,13 @@ func (c *Controller) SetSetting(key, value string) { } func (c *Controller) GetUserFromName(name string) *model.User { - var user model.User + user := &model.User{} err := c.DB. Where("name = ?", name). - First(&user). + First(user). Error if gorm.IsRecordNotFoundError(err) { return nil } - return &user + return user } diff --git a/server/handler/handler_admin.go b/server/handler/handler_admin.go index d041c4d..2155b72 100644 --- a/server/handler/handler_admin.go +++ b/server/handler/handler_admin.go @@ -48,11 +48,11 @@ func (c *Controller) ServeLogout(w http.ResponseWriter, r *http.Request) { } func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) { - var data templateData - c.DB.Table("artists").Count(&data.ArtistCount) - c.DB.Table("albums").Count(&data.AlbumCount) - c.DB.Table("tracks").Count(&data.TrackCount) - c.DB.Find(&data.AllUsers) + data := &templateData{} + c.DB.Table("artists").Count(data.ArtistCount) + c.DB.Table("albums").Count(data.AlbumCount) + c.DB.Table("tracks").Count(data.TrackCount) + c.DB.Find(data.AllUsers) data.CurrentLastFMAPIKey = c.GetSetting("lastfm_api_key") scheme := firstExisting( "http", // fallback @@ -66,7 +66,7 @@ func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) { r.Host, ) data.RequestRoot = fmt.Sprintf("%s://%s", scheme, host) - renderTemplate(w, r, c.Templates["home"], &data) + renderTemplate(w, r, c.Templates["home"], data) } func (c *Controller) ServeChangeOwnPassword(w http.ResponseWriter, r *http.Request) { @@ -127,27 +127,27 @@ func (c *Controller) ServeChangePassword(w http.ResponseWriter, r *http.Request) http.Error(w, "please provide a username", 400) return } - var user model.User + user := &model.User{} err := c.DB. Where("name = ?", username). - First(&user). + First(user). Error if gorm.IsRecordNotFoundError(err) { http.Error(w, "couldn't find a user with that name", 400) return } - var data templateData - data.SelectedUser = &user - renderTemplate(w, r, c.Templates["change_password"], &data) + data := &templateData{} + data.SelectedUser = user + renderTemplate(w, r, c.Templates["change_password"], data) } func (c *Controller) ServeChangePasswordDo(w http.ResponseWriter, r *http.Request) { session := r.Context().Value(contextSessionKey).(*sessions.Session) username := r.URL.Query().Get("user") - var user model.User + user := &model.User{} c.DB. Where("name = ?", username). - First(&user) + First(user) passwordOne := r.FormValue("password_one") passwordTwo := r.FormValue("password_two") err := validatePasswords(passwordOne, passwordTwo) @@ -158,7 +158,7 @@ func (c *Controller) ServeChangePasswordDo(w http.ResponseWriter, r *http.Reques return } user.Password = passwordOne - c.DB.Save(&user) + c.DB.Save(user) http.Redirect(w, r, "/admin/home", http.StatusSeeOther) } @@ -168,27 +168,27 @@ func (c *Controller) ServeDeleteUser(w http.ResponseWriter, r *http.Request) { http.Error(w, "please provide a username", 400) return } - var user model.User + user := &model.User{} err := c.DB. Where("name = ?", username). - First(&user). + First(user). Error if gorm.IsRecordNotFoundError(err) { http.Error(w, "couldn't find a user with that name", 400) return } - var data templateData - data.SelectedUser = &user - renderTemplate(w, r, c.Templates["delete_user"], &data) + data := &templateData{} + data.SelectedUser = user + renderTemplate(w, r, c.Templates["delete_user"], data) } func (c *Controller) ServeDeleteUserDo(w http.ResponseWriter, r *http.Request) { username := r.URL.Query().Get("user") - var user model.User + user := &model.User{} c.DB. Where("name = ?", username). - First(&user) - c.DB.Delete(&user) + First(user) + c.DB.Delete(user) http.Redirect(w, r, "/admin/home", http.StatusSeeOther) } @@ -232,10 +232,10 @@ func (c *Controller) ServeCreateUserDo(w http.ResponseWriter, r *http.Request) { } func (c *Controller) ServeUpdateLastFMAPIKey(w http.ResponseWriter, r *http.Request) { - var data templateData + data := &templateData{} data.CurrentLastFMAPIKey = c.GetSetting("lastfm_api_key") data.CurrentLastFMAPISecret = c.GetSetting("lastfm_secret") - renderTemplate(w, r, c.Templates["update_lastfm_api_key"], &data) + renderTemplate(w, r, c.Templates["update_lastfm_api_key"], data) } func (c *Controller) ServeUpdateLastFMAPIKeyDo(w http.ResponseWriter, r *http.Request) { diff --git a/server/handler/handler_sub_by_folder.go b/server/handler/handler_sub_by_folder.go index c49bba4..26c798e 100644 --- a/server/handler/handler_sub_by_folder.go +++ b/server/handler/handler_sub_by_folder.go @@ -19,12 +19,12 @@ import ( // under the root directory func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) { - var folders []model.Album + var folders []*model.Album c.DB. Where("parent_id = 1"). Find(&folders) - var indexMap = make(map[rune]*subsonic.Index) - var indexes []*subsonic.Index + indexMap := make(map[rune]*subsonic.Index) + indexes := []*subsonic.Index{} for _, folder := range folders { i := indexOf(folder.RightPath) index, ok := indexMap[i] @@ -37,7 +37,7 @@ func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) { indexes = append(indexes, index) } index.Artists = append(index.Artists, - makeArtistFromFolder(&folder)) + makeArtistFromFolder(folder)) } sort.Slice(indexes, func(i, j int) bool { return indexes[i].Name < indexes[j].Name @@ -57,28 +57,28 @@ func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) { return } childrenObj := []*subsonic.Track{} - var folder model.Album - c.DB.First(&folder, id) + folder := &model.Album{} + c.DB.First(folder, id) // // start looking for child childFolders in the current dir - var childFolders []model.Album + var childFolders []*model.Album c.DB. Where("parent_id = ?", id). Find(&childFolders) for _, c := range childFolders { childrenObj = append(childrenObj, - makeChildFromFolder(&c, &folder)) + makeChildFromFolder(c, folder)) } // // start looking for child childTracks in the current dir - var childTracks []model.Track + var childTracks []*model.Track c.DB. Where("album_id = ?", id). Preload("Album"). Order("filename"). Find(&childTracks) for _, c := range childTracks { - toAppend := makeChildFromTrack(&c, &folder) + toAppend := makeChildFromTrack(c, folder) if getStrParam(r, "c") == "Jamstash" { // jamstash thinks it can't play flacs toAppend.ContentType = "audio/mpeg" @@ -89,7 +89,7 @@ func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) { // // respond section sub := subsonic.NewResponse() - sub.Directory = makeDirFromFolder(&folder, childrenObj) + sub.Directory = makeDirFromFolder(folder, childrenObj) respond(w, r, sub) } @@ -133,7 +133,7 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) { "unknown value `%s` for parameter 'type'", listType) return } - var folders []model.Album + var folders []*model.Album q. Where("albums.tag_artist_id IS NOT NULL"). Offset(getIntParamOr(r, "offset", 0)). @@ -144,7 +144,7 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) { sub.Albums = &subsonic.Albums{} for _, folder := range folders { sub.Albums.List = append(sub.Albums.List, - makeAlbumFromFolder(&folder)) + makeAlbumFromFolder(folder)) } respond(w, r, sub) } @@ -160,7 +160,7 @@ func (c *Controller) SearchTwo(w http.ResponseWriter, r *http.Request) { results := &subsonic.SearchResultTwo{} // // search "artists" - var artists []model.Album + var artists []*model.Album c.DB. Where("parent_id = 1 AND right_path LIKE ?", query). Offset(getIntParamOr(r, "artistOffset", 0)). @@ -168,11 +168,11 @@ func (c *Controller) SearchTwo(w http.ResponseWriter, r *http.Request) { Find(&artists) for _, a := range artists { results.Artists = append(results.Artists, - makeDirFromFolder(&a, nil)) + makeDirFromFolder(a, nil)) } // // search "albums" - var albums []model.Album + var albums []*model.Album c.DB. Preload("Parent"). Where("tag_artist_id IS NOT NULL AND right_path LIKE ?", query). @@ -181,11 +181,11 @@ func (c *Controller) SearchTwo(w http.ResponseWriter, r *http.Request) { Find(&albums) for _, a := range albums { results.Albums = append(results.Albums, - makeChildFromFolder(&a, a.Parent)) + makeChildFromFolder(a, a.Parent)) } // // search tracks - var tracks []model.Track + var tracks []*model.Track c.DB. Preload("Album"). Where("filename LIKE ?", query). @@ -194,7 +194,7 @@ func (c *Controller) SearchTwo(w http.ResponseWriter, r *http.Request) { Find(&tracks) for _, t := range tracks { results.Tracks = append(results.Tracks, - makeChildFromTrack(&t, &t.Album)) + makeChildFromTrack(t, t.Album)) } // sub := subsonic.NewResponse() diff --git a/server/handler/handler_sub_by_tags.go b/server/handler/handler_sub_by_tags.go index 35e3741..1801abf 100644 --- a/server/handler/handler_sub_by_tags.go +++ b/server/handler/handler_sub_by_tags.go @@ -13,10 +13,10 @@ import ( ) func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) { - var artists []model.Artist + var artists []*model.Artist c.DB.Find(&artists) - var indexMap = make(map[rune]*subsonic.Index) - var indexes subsonic.Artists + indexMap := make(map[rune]*subsonic.Index) + indexes := &subsonic.Artists{} for _, artist := range artists { i := indexOf(artist.Name) index, ok := indexMap[i] @@ -29,13 +29,13 @@ func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) { indexes.List = append(indexes.List, index) } index.Artists = append(index.Artists, - makeArtistFromArtist(&artist)) + makeArtistFromArtist(artist)) } sort.Slice(indexes.List, func(i, j int) bool { return indexes.List[i].Name < indexes.List[j].Name }) sub := subsonic.NewResponse() - sub.Artists = &indexes + sub.Artists = indexes respond(w, r, sub) } @@ -45,15 +45,15 @@ func (c *Controller) GetArtist(w http.ResponseWriter, r *http.Request) { respondError(w, r, 10, "please provide an `id` parameter") return } - var artist model.Artist + artist := &model.Artist{} c.DB. Preload("Albums"). - First(&artist, id) + First(artist, id) sub := subsonic.NewResponse() - sub.Artist = makeArtistFromArtist(&artist) + sub.Artist = makeArtistFromArtist(artist) for _, album := range artist.Albums { sub.Artist.Albums = append(sub.Artist.Albums, - makeAlbumFromAlbum(&album, &artist)) + makeAlbumFromAlbum(album, artist)) } respond(w, r, sub) } @@ -64,23 +64,23 @@ func (c *Controller) GetAlbum(w http.ResponseWriter, r *http.Request) { respondError(w, r, 10, "please provide an `id` parameter") return } - var album model.Album + album := &model.Album{} err = c.DB. Preload("TagArtist"). Preload("Tracks", func(db *gorm.DB) *gorm.DB { return db.Order("tracks.tag_track_number") }). - First(&album, id). + First(album, id). Error if gorm.IsRecordNotFoundError(err) { respondError(w, r, 10, "couldn't find an album with that id") return } sub := subsonic.NewResponse() - sub.Album = makeAlbumFromAlbum(&album, &album.TagArtist) + sub.Album = makeAlbumFromAlbum(album, album.TagArtist) for _, track := range album.Tracks { sub.Album.Tracks = append(sub.Album.Tracks, - makeTrackFromTrack(&track, &album)) + makeTrackFromTrack(track, album)) } respond(w, r, sub) } @@ -131,7 +131,7 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) { "unknown value `%s` for parameter 'type'", listType) return } - var albums []model.Album + var albums []*model.Album q. Where("albums.tag_artist_id IS NOT NULL"). Offset(getIntParamOr(r, "offset", 0)). @@ -142,7 +142,7 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) { sub.AlbumsTwo = &subsonic.Albums{} for _, album := range albums { sub.AlbumsTwo.List = append(sub.AlbumsTwo.List, - makeAlbumFromAlbum(&album, &album.TagArtist)) + makeAlbumFromAlbum(album, album.TagArtist)) } respond(w, r, sub) } @@ -158,7 +158,7 @@ func (c *Controller) SearchThree(w http.ResponseWriter, r *http.Request) { results := &subsonic.SearchResultThree{} // // search "artists" - var artists []model.Artist + var artists []*model.Artist c.DB. Where("name LIKE ?", query). Offset(getIntParamOr(r, "artistOffset", 0)). @@ -166,11 +166,11 @@ func (c *Controller) SearchThree(w http.ResponseWriter, r *http.Request) { Find(&artists) for _, a := range artists { results.Artists = append(results.Artists, - makeArtistFromArtist(&a)) + makeArtistFromArtist(a)) } // // search "albums" - var albums []model.Album + var albums []*model.Album c.DB. Preload("TagArtist"). Where("tag_title LIKE ?", query). @@ -179,11 +179,11 @@ func (c *Controller) SearchThree(w http.ResponseWriter, r *http.Request) { Find(&albums) for _, a := range albums { results.Albums = append(results.Albums, - makeAlbumFromAlbum(&a, &a.TagArtist)) + makeAlbumFromAlbum(a, a.TagArtist)) } // // search tracks - var tracks []model.Track + var tracks []*model.Track c.DB. Preload("Album"). Where("tag_title LIKE ?", query). @@ -192,7 +192,7 @@ func (c *Controller) SearchThree(w http.ResponseWriter, r *http.Request) { Find(&tracks) for _, t := range tracks { results.Tracks = append(results.Tracks, - makeTrackFromTrack(&t, &t.Album)) + makeTrackFromTrack(t, t.Album)) } sub := subsonic.NewResponse() sub.SearchResultThree = results diff --git a/server/handler/handler_sub_common.go b/server/handler/handler_sub_common.go index 1536f2d..c6b89c0 100644 --- a/server/handler/handler_sub_common.go +++ b/server/handler/handler_sub_common.go @@ -1,6 +1,7 @@ package handler import ( + "log" "net/http" "os" "path" @@ -32,10 +33,10 @@ func (c *Controller) Stream(w http.ResponseWriter, r *http.Request) { respondError(w, r, 10, "please provide an `id` parameter") return } - var track model.Track + track := &model.Track{} err = c.DB. Preload("Album"). - First(&track, id). + First(track, id). Error if gorm.IsRecordNotFoundError(err) { respondError(w, r, 70, "media with id `%d` was not found", id) @@ -75,10 +76,10 @@ func (c *Controller) GetCoverArt(w http.ResponseWriter, r *http.Request) { respondError(w, r, 10, "please provide an `id` parameter") return } - var folder model.Album + folder := &model.Album{} err = c.DB. Select("id, path, cover"). - First(&folder, id). + First(folder, id). Error if gorm.IsRecordNotFoundError(err) { respondError(w, r, 10, "could not find a cover with that id") @@ -123,17 +124,17 @@ func (c *Controller) Scrobble(w http.ResponseWriter, r *http.Request) { return } // fetch track for getting info to send to last.fm function - var track model.Track + track := &model.Track{} c.DB. Preload("Album"). Preload("Artist"). - First(&track, id) + First(track, id) // scrobble with above info err = lastfm.Scrobble( c.GetSetting("lastfm_api_key"), c.GetSetting("lastfm_secret"), user.LastFMSession, - &track, + track, // clients will provide time in miliseconds, so use that or // instead convert UnixNano to miliseconds getIntParamOr(r, "time", int(time.Now().UnixNano()/1e6)), @@ -158,14 +159,22 @@ func (c *Controller) GetMusicFolders(w http.ResponseWriter, r *http.Request) { } func (c *Controller) StartScan(w http.ResponseWriter, r *http.Request) { - scanC := scanner.New(c.DB, c.MusicPath) - go scanC.Start() + go func() { + err := scanner. + New(c.DB, c.MusicPath). + Start() + if err != nil { + log.Printf("error while scanning: %v\n", err) + } + }() c.GetScanStatus(w, r) } func (c *Controller) GetScanStatus(w http.ResponseWriter, r *http.Request) { var trackCount int - c.DB.Model(&model.Track{}).Count(&trackCount) + c.DB. + Model(model.Track{}). + Count(&trackCount) sub := subsonic.NewResponse() sub.ScanStatus = &subsonic.ScanStatus{ Scanning: atomic.LoadInt32(&scanner.IsScanning) == 1, diff --git a/server/handler/handler_test.go b/server/handler/handler_test.go index 18acdb5..d7be352 100644 --- a/server/handler/handler_test.go +++ b/server/handler/handler_test.go @@ -44,7 +44,9 @@ func testNameToPath(name string) string { func testQueryCases(t *testing.T, handler http.HandlerFunc, cases []*queryCase) { for _, qc := range cases { + qc := qc // pin t.Run(qc.expectPath, func(t *testing.T) { + t.Parallel() // ensure the handlers give us json qc.params.Add("f", "json") req, _ := http.NewRequest("", "?"+qc.params.Encode(), nil) diff --git a/server/lastfm/lastfm.go b/server/lastfm/lastfm.go index 8c7eb44..1b1a34c 100644 --- a/server/lastfm/lastfm.go +++ b/server/lastfm/lastfm.go @@ -84,13 +84,13 @@ func makeRequest(method string, params url.Values) (*LastFM, error) { } defer resp.Body.Close() decoder := xml.NewDecoder(resp.Body) - var lastfm LastFM - err = decoder.Decode(&lastfm) + lastfm := &LastFM{} + err = decoder.Decode(lastfm) if err != nil { return nil, errors.Wrap(err, "decoding") } if lastfm.Error != nil { return nil, fmt.Errorf("parsing: %v", lastfm.Error.Value) } - return &lastfm, nil + return lastfm, nil }