diff --git a/model/model.go b/model/model.go index 2c1613d..62ea379 100644 --- a/model/model.go +++ b/model/model.go @@ -13,9 +13,9 @@ import "time" type Album struct { IDBase CrudBase - AlbumArtist AlbumArtist - AlbumArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES album_artists(id) ON DELETE CASCADE"` - Title string `gorm:"not null; index"` + Artist Artist + ArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` + Title string `gorm:"not null; index"` // an Album having a `Path` is a little weird when browsing by tags // (for the most part - the library's folder structure is treated as // if it were flat), but this solves the "American Football problem" @@ -28,8 +28,8 @@ type Album struct { IsNew bool `gorm:"-"` } -// AlbumArtist represents the AlbumArtists table -type AlbumArtist struct { +// Artist represents the Artists table +type Artist struct { IDBase CrudBase Name string `gorm:"not null; unique_index"` @@ -40,26 +40,26 @@ type AlbumArtist struct { type Track struct { IDBase CrudBase - Album Album - AlbumID int `gorm:"index" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` - AlbumArtist AlbumArtist - AlbumArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES album_artists(id) ON DELETE CASCADE"` - Artist string - Bitrate int - Codec string - DiscNumber int - Duration int - Title string - TotalDiscs int - TotalTracks int - TrackNumber int - Year int - Suffix string - ContentType string - Size int - Folder Folder - FolderID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES folders(id) ON DELETE CASCADE"` - Path string `gorm:"not null; unique_index"` + Album Album + AlbumID int `gorm:"index" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` + Artist Artist + ArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"` + TrackArtist string + Bitrate int + Codec string + DiscNumber int + Duration int + Title string + TotalDiscs int + TotalTracks int + TrackNumber int + Year int + Suffix string + ContentType string + Size int + Folder Folder + FolderID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES folders(id) ON DELETE CASCADE"` + Path string `gorm:"not null; unique_index"` } // Cover represents the covers table diff --git a/scanner/scanner.go b/scanner/scanner.go index 7b0f6fc..b82f5ea 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -31,7 +31,7 @@ type Scanner struct { curTracks []model.Track curCover model.Cover curAlbum model.Album - curAArtist model.AlbumArtist + curAArtist model.Artist } func New(db *gorm.DB, musicPath string) *Scanner { @@ -43,7 +43,7 @@ func New(db *gorm.DB, musicPath string) *Scanner { curTracks: make([]model.Track, 0), curCover: model.Cover{}, curAlbum: model.Album{}, - curAArtist: model.AlbumArtist{}, + curAArtist: model.Artist{}, } } @@ -105,7 +105,7 @@ func (s *Scanner) MigrateDB() error { defer s.tx.Commit() s.tx.AutoMigrate( model.Album{}, - model.AlbumArtist{}, + model.Artist{}, model.Track{}, model.Cover{}, model.User{}, diff --git a/scanner/walk.go b/scanner/walk.go index a759772..fb15d75 100644 --- a/scanner/walk.go +++ b/scanner/walk.go @@ -81,7 +81,7 @@ func (s *Scanner) callbackPost(path string, info *godirwalk.Dirent) error { s.curTracks = make([]model.Track, 0) s.curCover = model.Cover{} s.curAlbum = model.Album{} - s.curAArtist = model.AlbumArtist{} + s.curAArtist = model.Artist{} // log.Printf("processed folder `%s`\n", path) return nil @@ -156,7 +156,7 @@ func (s *Scanner) handleTrack(it *item) error { track.ContentType = it.track.mime track.Size = int(it.stat.Size()) track.Title = tags.Title() - track.Artist = tags.Artist() + track.TrackArtist = tags.Artist() track.Year = tags.Year() track.FolderID = s.curFolders.PeekID() // @@ -168,7 +168,7 @@ func (s *Scanner) handleTrack(it *item) error { s.curAArtist.Name = tags.AlbumArtist() s.tx.Save(&s.curAArtist) } - track.AlbumArtistID = s.curAArtist.ID + track.ArtistID = s.curAArtist.ID // // set album if this is the first track in the folder if len(s.curTracks) > 0 { @@ -189,7 +189,7 @@ func (s *Scanner) handleTrack(it *item) error { s.curAlbum.Path = directory s.curAlbum.Title = tags.Album() s.curAlbum.Year = tags.Year() - s.curAlbum.AlbumArtistID = s.curAArtist.ID + s.curAlbum.ArtistID = s.curAArtist.ID s.curAlbum.IsNew = true return nil } diff --git a/server/handler/construct_sub_by_folder.go b/server/handler/construct_sub_by_folder.go new file mode 100644 index 0000000..59b7f9a --- /dev/null +++ b/server/handler/construct_sub_by_folder.go @@ -0,0 +1,54 @@ +package handler + +import ( + "github.com/sentriz/gonic/model" + "github.com/sentriz/gonic/server/subsonic" +) + +func makeChildFromFolder(f *model.Folder, parent *model.Folder) *subsonic.Child { + return &subsonic.Child{ + ID: f.ID, + Title: f.Name, + CoverID: f.CoverID, + ParentID: parent.ID, + IsDir: true, + } +} + +func makeChildFromTrack(t *model.Track, parent *model.Folder) *subsonic.Child { + return &subsonic.Child{ + ID: t.ID, + Album: t.Album.Title, + Artist: t.TrackArtist, + ContentType: t.ContentType, + Path: t.Path, + Size: t.Size, + Suffix: t.Suffix, + Title: t.Title, + Track: t.TrackNumber, + ParentID: parent.ID, + CoverID: parent.CoverID, + Duration: 0, + IsDir: false, + Type: "music", + } +} + +func makeAlbumFromFolder(f *model.Folder) *subsonic.Album { + return &subsonic.Album{ + ID: f.ID, + Title: f.Name, + Album: f.Name, + CoverID: f.CoverID, + ParentID: f.ParentID, + Artist: f.Parent.Name, + IsDir: true, + } +} + +func makeArtistFromFolder(f *model.Folder) *subsonic.Artist { + return &subsonic.Artist{ + ID: f.ID, + Name: f.Name, + } +} diff --git a/server/handler/construct_sub_by_tags.go b/server/handler/construct_sub_by_tags.go new file mode 100644 index 0000000..7d66da0 --- /dev/null +++ b/server/handler/construct_sub_by_tags.go @@ -0,0 +1,43 @@ +package handler + +import ( + "github.com/sentriz/gonic/model" + "github.com/sentriz/gonic/server/subsonic" +) + +func makeAlbumFromAlbum(a *model.Album, artist *model.Artist) *subsonic.Album { + return &subsonic.Album{ + ID: a.ID, + Name: a.Title, + Created: a.CreatedAt, + CoverID: a.CoverID, + Artist: artist.Name, + ArtistID: artist.ID, + } +} + +func makeTrackFromTrack(t *model.Track, album *model.Album) *subsonic.Track { + return &subsonic.Track{ + ID: t.ID, + Title: t.Title, + Artist: t.TrackArtist, + TrackNumber: t.TrackNumber, + ContentType: t.ContentType, + Path: t.Path, + Suffix: t.Suffix, + CreatedAt: t.CreatedAt, + Size: t.Size, + Album: album.Title, + AlbumID: album.ID, + ArtistID: album.Artist.ID, + CoverID: album.CoverID, + Type: "music", + } +} + +func makeArtistFromArtist(a *model.Artist) *subsonic.Artist { + return &subsonic.Artist{ + ID: a.ID, + Name: a.Name, + } +} diff --git a/server/handler/handler_admin.go b/server/handler/handler_admin.go index bfdf2b2..12a79b3 100644 --- a/server/handler/handler_admin.go +++ b/server/handler/handler_admin.go @@ -49,7 +49,7 @@ 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("album_artists").Count(&data.ArtistCount) + 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) diff --git a/server/handler/handler_sub_by_folder.go b/server/handler/handler_sub_by_folder.go index a89d3db..f51b10d 100644 --- a/server/handler/handler_sub_by_folder.go +++ b/server/handler/handler_sub_by_folder.go @@ -13,7 +13,7 @@ import ( func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) { // we are browsing by folder, but the subsonic docs show sub elements // for this, so we're going to return root directories as "artists" - var folders []*model.Folder + var folders []model.Folder c.DB.Where("parent_id = ?", 1).Find(&folders) var indexMap = make(map[rune]*subsonic.Index) var indexes []*subsonic.Index @@ -28,10 +28,8 @@ func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) { indexMap[i] = index indexes = append(indexes, index) } - index.Artists = append(index.Artists, &subsonic.Artist{ - ID: folder.ID, - Name: folder.Name, - }) + index.Artists = append(index.Artists, + makeArtistFromFolder(&folder)) } sub := subsonic.NewResponse() sub.Indexes = &subsonic.Indexes{ @@ -48,61 +46,42 @@ func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) { return } childrenObj := []*subsonic.Child{} - var cFolder model.Folder - c.DB.First(&cFolder, id) + var folder model.Folder + c.DB.First(&folder, id) // - // start looking for child folders in the current dir - var folders []*model.Folder + // start looking for child childFolders in the current dir + var childFolders []model.Folder c.DB. Where("parent_id = ?", id). - Find(&folders) - for _, folder := range folders { - childrenObj = append(childrenObj, &subsonic.Child{ - Parent: cFolder.ID, - ID: folder.ID, - Title: folder.Name, - IsDir: true, - CoverID: folder.CoverID, - }) + Find(&childFolders) + for _, c := range childFolders { + childrenObj = append(childrenObj, + makeChildFromFolder(&c, &folder)) } // - // start looking for child tracks in the current dir - var tracks []*model.Track + // start looking for child childTracks in the current dir + var childTracks []model.Track c.DB. Where("folder_id = ?", id). Preload("Album"). Order("title"). - Find(&tracks) - for _, track := range tracks { + Find(&childTracks) + for _, c := range childTracks { if getStrParam(r, "c") == "Jamstash" { // jamstash thinks it can't play flacs - track.ContentType = "audio/mpeg" - track.Suffix = "mp3" + c.ContentType = "audio/mpeg" + c.Suffix = "mp3" } - childrenObj = append(childrenObj, &subsonic.Child{ - ID: track.ID, - Album: track.Album.Title, - Artist: track.Artist, - ContentType: track.ContentType, - CoverID: cFolder.CoverID, - Duration: 0, - IsDir: false, - Parent: cFolder.ID, - Path: track.Path, - Size: track.Size, - Suffix: track.Suffix, - Title: track.Title, - Track: track.TrackNumber, - Type: "music", - }) + childrenObj = append(childrenObj, + makeChildFromTrack(&c, &folder)) } // // respond section sub := subsonic.NewResponse() sub.Directory = &subsonic.Directory{ - ID: cFolder.ID, - Parent: cFolder.ParentID, - Name: cFolder.Name, + ID: folder.ID, + Parent: folder.ParentID, + Name: folder.Name, Children: childrenObj, } respond(w, r, sub) @@ -152,28 +131,18 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) { )) return } - var folders []*model.Folder + var folders []model.Folder q. Where("folders.has_tracks = 1"). Offset(getIntParamOr(r, "offset", 0)). Limit(getIntParamOr(r, "size", 10)). Preload("Parent"). Find(&folders) - listObj := []*subsonic.Album{} - for _, folder := range folders { - listObj = append(listObj, &subsonic.Album{ - ID: folder.ID, - Title: folder.Name, - Album: folder.Name, - CoverID: folder.CoverID, - ParentID: folder.ParentID, - IsDir: true, - Artist: folder.Parent.Name, - }) - } sub := subsonic.NewResponse() - sub.Albums = &subsonic.Albums{ - List: listObj, + sub.Albums = &subsonic.Albums{} + for _, folder := range folders { + sub.Albums.List = append(sub.Albums.List, + makeAlbumFromFolder(&folder)) } respond(w, r, sub) } diff --git a/server/handler/handler_sub_by_tags.go b/server/handler/handler_sub_by_tags.go index c4fcba9..f9648a1 100644 --- a/server/handler/handler_sub_by_tags.go +++ b/server/handler/handler_sub_by_tags.go @@ -11,7 +11,7 @@ import ( ) func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) { - var artists []*model.AlbumArtist + var artists []model.Artist c.DB.Find(&artists) var indexMap = make(map[rune]*subsonic.Index) var indexes subsonic.Artists @@ -26,10 +26,8 @@ func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) { indexMap[i] = index indexes.List = append(indexes.List, index) } - index.Artists = append(index.Artists, &subsonic.Artist{ - ID: artist.ID, - Name: artist.Name, - }) + index.Artists = append(index.Artists, + makeArtistFromArtist(&artist)) } sub := subsonic.NewResponse() sub.Artists = &indexes @@ -42,26 +40,15 @@ func (c *Controller) GetArtist(w http.ResponseWriter, r *http.Request) { respondError(w, r, 10, "please provide an `id` parameter") return } - var artist model.AlbumArtist + var artist model.Artist c.DB. Preload("Albums"). First(&artist, id) - albumsObj := []*subsonic.Album{} - for _, album := range artist.Albums { - albumsObj = append(albumsObj, &subsonic.Album{ - ID: album.ID, - Name: album.Title, - Created: album.CreatedAt, - Artist: artist.Name, - ArtistID: artist.ID, - CoverID: album.CoverID, - }) - } sub := subsonic.NewResponse() - sub.Artist = &subsonic.Artist{ - ID: artist.ID, - Name: artist.Name, - Albums: albumsObj, + sub.Artist = makeArtistFromArtist(&artist) + for _, album := range artist.Albums { + sub.Artist.Albums = append(sub.Artist.Albums, + makeAlbumFromAlbum(&album, &artist)) } respond(w, r, sub) } @@ -73,39 +60,22 @@ func (c *Controller) GetAlbum(w http.ResponseWriter, r *http.Request) { return } var album model.Album - c.DB. - Preload("AlbumArtist"). + err = c.DB. + Preload("Artist"). Preload("Tracks", func(db *gorm.DB) *gorm.DB { - return db.Order("tracks.track_number") - }). - First(&album, id) - tracksObj := []*subsonic.Track{} - for _, track := range album.Tracks { - tracksObj = append(tracksObj, &subsonic.Track{ - ID: track.ID, - Title: track.Title, - Artist: track.Artist, // track artist - TrackNo: track.TrackNumber, - ContentType: track.ContentType, - Path: track.Path, - Suffix: track.Suffix, - Created: track.CreatedAt, - Size: track.Size, - Album: album.Title, - AlbumID: album.ID, - ArtistID: album.AlbumArtist.ID, // album artist - CoverID: album.CoverID, - Type: "music", - }) + return db.Order("tracks.track_number") + }). + 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 = &subsonic.Album{ - ID: album.ID, - Name: album.Title, - CoverID: album.CoverID, - Created: album.CreatedAt, - Artist: album.AlbumArtist.Name, - Tracks: tracksObj, + sub.Album = makeAlbumFromAlbum(&album, &album.Artist) + for _, track := range album.Tracks { + sub.Album.Tracks = append(sub.Album.Tracks, + makeTrackFromTrack(&track, &album)) } respond(w, r, sub) } @@ -122,9 +92,9 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) { switch listType { case "alphabeticalByArtist": q = q.Joins(` - JOIN album_artists - ON albums.album_artist_id = album_artists.id`) - q = q.Order("album_artists.name") + JOIN artists + ON albums.artist_id = artists.id`) + q = q.Order("artists.name") case "alphabeticalByName": q = q.Order("title") case "byYear": @@ -157,26 +127,17 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) { )) return } - var albums []*model.Album + var albums []model.Album q. Offset(getIntParamOr(r, "offset", 0)). Limit(getIntParamOr(r, "size", 10)). - Preload("AlbumArtist"). + Preload("Artist"). Find(&albums) - listObj := []*subsonic.Album{} - for _, album := range albums { - listObj = append(listObj, &subsonic.Album{ - ID: album.ID, - Name: album.Title, - Created: album.CreatedAt, - CoverID: album.CoverID, - Artist: album.AlbumArtist.Name, - ArtistID: album.AlbumArtist.ID, - }) - } sub := subsonic.NewResponse() - sub.AlbumsTwo = &subsonic.Albums{ - List: listObj, + sub.AlbumsTwo = &subsonic.Albums{} + for _, album := range albums { + sub.AlbumsTwo.List = append(sub.AlbumsTwo.List, + makeAlbumFromAlbum(&album, &album.Artist)) } respond(w, r, sub) } diff --git a/server/handler/handler_sub_common.go b/server/handler/handler_sub_common.go index 748da6b..752e78a 100644 --- a/server/handler/handler_sub_common.go +++ b/server/handler/handler_sub_common.go @@ -96,14 +96,14 @@ func (c *Controller) Scrobble(w http.ResponseWriter, r *http.Request) { // fetch user to get lastfm session user := r.Context().Value(contextUserKey).(*model.User) if user.LastFMSession == "" { - respondError(w, r, 0, fmt.Sprintf("no last.fm session for this user: %v", err)) + respondError(w, r, 0, "you don't have a last.fm session") return } // fetch track for getting info to send to last.fm function var track model.Track c.DB. Preload("Album"). - Preload("AlbumArtist"). + Preload("Artist"). First(&track, id) // scrobble with above info err = lastfm.Scrobble( diff --git a/server/handler/middleware_sub.go b/server/handler/middleware_sub.go index 65a97e6..8f4388e 100644 --- a/server/handler/middleware_sub.go +++ b/server/handler/middleware_sub.go @@ -33,12 +33,12 @@ func checkCredentialsToken(password, token, salt string) bool { return token == expToken } -func checkCredentialsBasic(password, givenPassword string) bool { - if givenPassword[:4] == "enc:" { - bytes, _ := hex.DecodeString(givenPassword[4:]) - givenPassword = string(bytes) +func checkCredentialsBasic(password, given string) bool { + if given[:4] == "enc:" { + bytes, _ := hex.DecodeString(given[4:]) + given = string(bytes) } - return password == givenPassword + return password == given } func (c *Controller) WithValidSubsonicArgs(next http.HandlerFunc) http.HandlerFunc { @@ -62,7 +62,6 @@ func (c *Controller) WithValidSubsonicArgs(next http.HandlerFunc) http.HandlerFu } user := c.GetUserFromName(username) if user == nil { - // the user does not exist respondError(w, r, 40, "invalid username") return } diff --git a/server/lastfm/lastfm.go b/server/lastfm/lastfm.go index b0b275e..bebfaa0 100644 --- a/server/lastfm/lastfm.go +++ b/server/lastfm/lastfm.go @@ -48,10 +48,10 @@ func Scrobble(apiKey, secret, session string, track *model.Track, } params.Add("api_key", apiKey) params.Add("sk", session) - params.Add("artist", track.Artist) + params.Add("artist", track.TrackArtist) params.Add("track", track.Title) params.Add("album", track.Album.Title) - params.Add("albumArtist", track.AlbumArtist.Name) + params.Add("albumArtist", track.Artist.Name) params.Add("trackNumber", strconv.Itoa(track.TrackNumber)) params.Add("api_sig", getParamSignature(params, secret)) _, err := makeRequest("POST", params) diff --git a/server/lastfm/lastfm_test.go b/server/lastfm/lastfm_test.go new file mode 100644 index 0000000..4713a6d --- /dev/null +++ b/server/lastfm/lastfm_test.go @@ -0,0 +1,23 @@ +package lastfm + +import ( + "crypto/md5" + "fmt" + "net/url" + "testing" +) + +func TestGetParamSignature(t *testing.T) { + params := url.Values{} + params.Add("ccc", "CCC") + params.Add("bbb", "BBB") + params.Add("aaa", "AAA") + params.Add("ddd", "DDD") + actual := getParamSignature(params, "secret") + expected := fmt.Sprintf("%x", md5.Sum([]byte( + "aaaAAAbbbBBBcccCCCdddDDDsecret", + ))) + if actual != expected { + t.Errorf("expected %x, got %s", expected, actual) + } +} diff --git a/server/subsonic/media.go b/server/subsonic/media.go index cf02cd1..fde1bd5 100644 --- a/server/subsonic/media.go +++ b/server/subsonic/media.go @@ -1,6 +1,8 @@ package subsonic -import "time" +import ( + "time" +) type Albums struct { List []*Album `xml:"album" json:"album,omitempty"` @@ -30,26 +32,26 @@ type RandomTracks struct { } type Track struct { - ID int `xml:"id,attr,omitempty" json:"id"` - Parent int `xml:"parent,attr,omitempty" json:"parent"` - Title string `xml:"title,attr,omitempty" json:"title"` - Album string `xml:"album,attr,omitempty" json:"album"` - Artist string `xml:"artist,attr,omitempty" json:"artist"` - IsDir bool `xml:"isDir,attr,omitempty" json:"isDir"` - CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt"` - Created time.Time `xml:"created,attr,omitempty" json:"created"` - Duration int `xml:"duration,attr,omitempty" json:"duration"` - Genre string `xml:"genre,attr,omitempty" json:"genre"` - Bitrate int `xml:"bitRate,attr,omitempty" json:"bitRate"` - Size int `xml:"size,attr,omitempty" json:"size"` - Suffix string `xml:"suffix,attr,omitempty" json:"suffix"` - ContentType string `xml:"contentType,attr,omitempty" json:"contentType"` - IsVideo bool `xml:"isVideo,attr,omitempty" json:"isVideo"` - Path string `xml:"path,attr,omitempty" json:"path"` - AlbumID int `xml:"albumId,attr,omitempty" json:"albumId"` - ArtistID int `xml:"artistId,attr,omitempty" json:"artistId"` - TrackNo int `xml:"track,attr,omitempty" json:"track"` - Type string `xml:"type,attr,omitempty" json:"type"` + ID int `xml:"id,attr,omitempty" json:"id"` + Parent int `xml:"parent,attr,omitempty" json:"parent"` + Title string `xml:"title,attr,omitempty" json:"title"` + Album string `xml:"album,attr,omitempty" json:"album"` + Artist string `xml:"artist,attr,omitempty" json:"artist"` + IsDir bool `xml:"isDir,attr,omitempty" json:"isDir"` + CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt"` + CreatedAt time.Time `xml:"created,attr,omitempty" json:"created"` + Duration int `xml:"duration,attr,omitempty" json:"duration"` + Genre string `xml:"genre,attr,omitempty" json:"genre"` + Bitrate int `xml:"bitRate,attr,omitempty" json:"bitRate"` + Size int `xml:"size,attr,omitempty" json:"size"` + Suffix string `xml:"suffix,attr,omitempty" json:"suffix"` + ContentType string `xml:"contentType,attr,omitempty" json:"contentType"` + IsVideo bool `xml:"isVideo,attr,omitempty" json:"isVideo"` + Path string `xml:"path,attr,omitempty" json:"path"` + AlbumID int `xml:"albumId,attr,omitempty" json:"albumId"` + ArtistID int `xml:"artistId,attr,omitempty" json:"artistId"` + TrackNumber int `xml:"track,attr,omitempty" json:"track"` + Type string `xml:"type,attr,omitempty" json:"type"` } type Artists struct { @@ -84,7 +86,7 @@ type Directory struct { type Child struct { ID int `xml:"id,attr,omitempty" json:"id,omitempty"` - Parent int `xml:"parent,attr,omitempty" json:"parent,omitempty"` + ParentID int `xml:"parent,attr,omitempty" json:"parent,omitempty"` Title string `xml:"title,attr,omitempty" json:"title,omitempty"` IsDir bool `xml:"isDir,attr,omitempty" json:"isDir,omitempty"` Album string `xml:"album,attr,omitempty" json:"album,omitempty"`