construct subsonic models elsewhere

This commit is contained in:
sentriz
2019-05-28 14:22:44 +01:00
parent 6eb1041ad8
commit 74c55bd509
13 changed files with 243 additions and 192 deletions

View File

@@ -13,8 +13,8 @@ import "time"
type Album struct { type Album struct {
IDBase IDBase
CrudBase CrudBase
AlbumArtist AlbumArtist Artist Artist
AlbumArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES album_artists(id) ON DELETE CASCADE"` ArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
Title string `gorm:"not null; index"` Title string `gorm:"not null; index"`
// an Album having a `Path` is a little weird when browsing by tags // 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 // (for the most part - the library's folder structure is treated as
@@ -28,8 +28,8 @@ type Album struct {
IsNew bool `gorm:"-"` IsNew bool `gorm:"-"`
} }
// AlbumArtist represents the AlbumArtists table // Artist represents the Artists table
type AlbumArtist struct { type Artist struct {
IDBase IDBase
CrudBase CrudBase
Name string `gorm:"not null; unique_index"` Name string `gorm:"not null; unique_index"`
@@ -42,9 +42,9 @@ type Track struct {
CrudBase CrudBase
Album Album Album Album
AlbumID int `gorm:"index" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` AlbumID int `gorm:"index" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"`
AlbumArtist AlbumArtist Artist Artist
AlbumArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES album_artists(id) ON DELETE CASCADE"` ArtistID int `gorm:"index" sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
Artist string TrackArtist string
Bitrate int Bitrate int
Codec string Codec string
DiscNumber int DiscNumber int

View File

@@ -31,7 +31,7 @@ type Scanner struct {
curTracks []model.Track curTracks []model.Track
curCover model.Cover curCover model.Cover
curAlbum model.Album curAlbum model.Album
curAArtist model.AlbumArtist curAArtist model.Artist
} }
func New(db *gorm.DB, musicPath string) *Scanner { 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), curTracks: make([]model.Track, 0),
curCover: model.Cover{}, curCover: model.Cover{},
curAlbum: model.Album{}, curAlbum: model.Album{},
curAArtist: model.AlbumArtist{}, curAArtist: model.Artist{},
} }
} }
@@ -105,7 +105,7 @@ func (s *Scanner) MigrateDB() error {
defer s.tx.Commit() defer s.tx.Commit()
s.tx.AutoMigrate( s.tx.AutoMigrate(
model.Album{}, model.Album{},
model.AlbumArtist{}, model.Artist{},
model.Track{}, model.Track{},
model.Cover{}, model.Cover{},
model.User{}, model.User{},

View File

@@ -81,7 +81,7 @@ func (s *Scanner) callbackPost(path string, info *godirwalk.Dirent) error {
s.curTracks = make([]model.Track, 0) s.curTracks = make([]model.Track, 0)
s.curCover = model.Cover{} s.curCover = model.Cover{}
s.curAlbum = model.Album{} s.curAlbum = model.Album{}
s.curAArtist = model.AlbumArtist{} s.curAArtist = model.Artist{}
// //
log.Printf("processed folder `%s`\n", path) log.Printf("processed folder `%s`\n", path)
return nil return nil
@@ -156,7 +156,7 @@ func (s *Scanner) handleTrack(it *item) error {
track.ContentType = it.track.mime track.ContentType = it.track.mime
track.Size = int(it.stat.Size()) track.Size = int(it.stat.Size())
track.Title = tags.Title() track.Title = tags.Title()
track.Artist = tags.Artist() track.TrackArtist = tags.Artist()
track.Year = tags.Year() track.Year = tags.Year()
track.FolderID = s.curFolders.PeekID() track.FolderID = s.curFolders.PeekID()
// //
@@ -168,7 +168,7 @@ func (s *Scanner) handleTrack(it *item) error {
s.curAArtist.Name = tags.AlbumArtist() s.curAArtist.Name = tags.AlbumArtist()
s.tx.Save(&s.curAArtist) 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 // set album if this is the first track in the folder
if len(s.curTracks) > 0 { if len(s.curTracks) > 0 {
@@ -189,7 +189,7 @@ func (s *Scanner) handleTrack(it *item) error {
s.curAlbum.Path = directory s.curAlbum.Path = directory
s.curAlbum.Title = tags.Album() s.curAlbum.Title = tags.Album()
s.curAlbum.Year = tags.Year() s.curAlbum.Year = tags.Year()
s.curAlbum.AlbumArtistID = s.curAArtist.ID s.curAlbum.ArtistID = s.curAArtist.ID
s.curAlbum.IsNew = true s.curAlbum.IsNew = true
return nil return nil
} }

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -49,7 +49,7 @@ func (c *Controller) ServeLogout(w http.ResponseWriter, r *http.Request) {
func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) { func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) {
var data templateData 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("albums").Count(&data.AlbumCount)
c.DB.Table("tracks").Count(&data.TrackCount) c.DB.Table("tracks").Count(&data.TrackCount)
c.DB.Find(&data.AllUsers) c.DB.Find(&data.AllUsers)

View File

@@ -13,7 +13,7 @@ import (
func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) { func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) {
// we are browsing by folder, but the subsonic docs show sub <artist> elements // we are browsing by folder, but the subsonic docs show sub <artist> elements
// for this, so we're going to return root directories as "artists" // 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) c.DB.Where("parent_id = ?", 1).Find(&folders)
var indexMap = make(map[rune]*subsonic.Index) var indexMap = make(map[rune]*subsonic.Index)
var indexes []*subsonic.Index var indexes []*subsonic.Index
@@ -28,10 +28,8 @@ func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) {
indexMap[i] = index indexMap[i] = index
indexes = append(indexes, index) indexes = append(indexes, index)
} }
index.Artists = append(index.Artists, &subsonic.Artist{ index.Artists = append(index.Artists,
ID: folder.ID, makeArtistFromFolder(&folder))
Name: folder.Name,
})
} }
sub := subsonic.NewResponse() sub := subsonic.NewResponse()
sub.Indexes = &subsonic.Indexes{ sub.Indexes = &subsonic.Indexes{
@@ -48,61 +46,42 @@ func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) {
return return
} }
childrenObj := []*subsonic.Child{} childrenObj := []*subsonic.Child{}
var cFolder model.Folder var folder model.Folder
c.DB.First(&cFolder, id) c.DB.First(&folder, id)
// //
// start looking for child folders in the current dir // start looking for child childFolders in the current dir
var folders []*model.Folder var childFolders []model.Folder
c.DB. c.DB.
Where("parent_id = ?", id). Where("parent_id = ?", id).
Find(&folders) Find(&childFolders)
for _, folder := range folders { for _, c := range childFolders {
childrenObj = append(childrenObj, &subsonic.Child{ childrenObj = append(childrenObj,
Parent: cFolder.ID, makeChildFromFolder(&c, &folder))
ID: folder.ID,
Title: folder.Name,
IsDir: true,
CoverID: folder.CoverID,
})
} }
// //
// start looking for child tracks in the current dir // start looking for child childTracks in the current dir
var tracks []*model.Track var childTracks []model.Track
c.DB. c.DB.
Where("folder_id = ?", id). Where("folder_id = ?", id).
Preload("Album"). Preload("Album").
Order("title"). Order("title").
Find(&tracks) Find(&childTracks)
for _, track := range tracks { for _, c := range childTracks {
if getStrParam(r, "c") == "Jamstash" { if getStrParam(r, "c") == "Jamstash" {
// jamstash thinks it can't play flacs // jamstash thinks it can't play flacs
track.ContentType = "audio/mpeg" c.ContentType = "audio/mpeg"
track.Suffix = "mp3" c.Suffix = "mp3"
} }
childrenObj = append(childrenObj, &subsonic.Child{ childrenObj = append(childrenObj,
ID: track.ID, makeChildFromTrack(&c, &folder))
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",
})
} }
// //
// respond section // respond section
sub := subsonic.NewResponse() sub := subsonic.NewResponse()
sub.Directory = &subsonic.Directory{ sub.Directory = &subsonic.Directory{
ID: cFolder.ID, ID: folder.ID,
Parent: cFolder.ParentID, Parent: folder.ParentID,
Name: cFolder.Name, Name: folder.Name,
Children: childrenObj, Children: childrenObj,
} }
respond(w, r, sub) respond(w, r, sub)
@@ -152,28 +131,18 @@ func (c *Controller) GetAlbumList(w http.ResponseWriter, r *http.Request) {
)) ))
return return
} }
var folders []*model.Folder var folders []model.Folder
q. q.
Where("folders.has_tracks = 1"). Where("folders.has_tracks = 1").
Offset(getIntParamOr(r, "offset", 0)). Offset(getIntParamOr(r, "offset", 0)).
Limit(getIntParamOr(r, "size", 10)). Limit(getIntParamOr(r, "size", 10)).
Preload("Parent"). Preload("Parent").
Find(&folders) 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 := subsonic.NewResponse()
sub.Albums = &subsonic.Albums{ sub.Albums = &subsonic.Albums{}
List: listObj, for _, folder := range folders {
sub.Albums.List = append(sub.Albums.List,
makeAlbumFromFolder(&folder))
} }
respond(w, r, sub) respond(w, r, sub)
} }

View File

@@ -11,7 +11,7 @@ import (
) )
func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) { func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) {
var artists []*model.AlbumArtist var artists []model.Artist
c.DB.Find(&artists) c.DB.Find(&artists)
var indexMap = make(map[rune]*subsonic.Index) var indexMap = make(map[rune]*subsonic.Index)
var indexes subsonic.Artists var indexes subsonic.Artists
@@ -26,10 +26,8 @@ func (c *Controller) GetArtists(w http.ResponseWriter, r *http.Request) {
indexMap[i] = index indexMap[i] = index
indexes.List = append(indexes.List, index) indexes.List = append(indexes.List, index)
} }
index.Artists = append(index.Artists, &subsonic.Artist{ index.Artists = append(index.Artists,
ID: artist.ID, makeArtistFromArtist(&artist))
Name: artist.Name,
})
} }
sub := subsonic.NewResponse() sub := subsonic.NewResponse()
sub.Artists = &indexes 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") respondError(w, r, 10, "please provide an `id` parameter")
return return
} }
var artist model.AlbumArtist var artist model.Artist
c.DB. c.DB.
Preload("Albums"). Preload("Albums").
First(&artist, id) 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 := subsonic.NewResponse()
sub.Artist = &subsonic.Artist{ sub.Artist = makeArtistFromArtist(&artist)
ID: artist.ID, for _, album := range artist.Albums {
Name: artist.Name, sub.Artist.Albums = append(sub.Artist.Albums,
Albums: albumsObj, makeAlbumFromAlbum(&album, &artist))
} }
respond(w, r, sub) respond(w, r, sub)
} }
@@ -73,39 +60,22 @@ func (c *Controller) GetAlbum(w http.ResponseWriter, r *http.Request) {
return return
} }
var album model.Album var album model.Album
c.DB. err = c.DB.
Preload("AlbumArtist"). Preload("Artist").
Preload("Tracks", func(db *gorm.DB) *gorm.DB { Preload("Tracks", func(db *gorm.DB) *gorm.DB {
return db.Order("tracks.track_number") return db.Order("tracks.track_number")
}). }).
First(&album, id) First(&album, id).
tracksObj := []*subsonic.Track{} Error
for _, track := range album.Tracks { if gorm.IsRecordNotFoundError(err) {
tracksObj = append(tracksObj, &subsonic.Track{ respondError(w, r, 10, "couldn't find an album with that id")
ID: track.ID, return
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",
})
} }
sub := subsonic.NewResponse() sub := subsonic.NewResponse()
sub.Album = &subsonic.Album{ sub.Album = makeAlbumFromAlbum(&album, &album.Artist)
ID: album.ID, for _, track := range album.Tracks {
Name: album.Title, sub.Album.Tracks = append(sub.Album.Tracks,
CoverID: album.CoverID, makeTrackFromTrack(&track, &album))
Created: album.CreatedAt,
Artist: album.AlbumArtist.Name,
Tracks: tracksObj,
} }
respond(w, r, sub) respond(w, r, sub)
} }
@@ -122,9 +92,9 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
switch listType { switch listType {
case "alphabeticalByArtist": case "alphabeticalByArtist":
q = q.Joins(` q = q.Joins(`
JOIN album_artists JOIN artists
ON albums.album_artist_id = album_artists.id`) ON albums.artist_id = artists.id`)
q = q.Order("album_artists.name") q = q.Order("artists.name")
case "alphabeticalByName": case "alphabeticalByName":
q = q.Order("title") q = q.Order("title")
case "byYear": case "byYear":
@@ -157,26 +127,17 @@ func (c *Controller) GetAlbumListTwo(w http.ResponseWriter, r *http.Request) {
)) ))
return return
} }
var albums []*model.Album var albums []model.Album
q. q.
Offset(getIntParamOr(r, "offset", 0)). Offset(getIntParamOr(r, "offset", 0)).
Limit(getIntParamOr(r, "size", 10)). Limit(getIntParamOr(r, "size", 10)).
Preload("AlbumArtist"). Preload("Artist").
Find(&albums) 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 := subsonic.NewResponse()
sub.AlbumsTwo = &subsonic.Albums{ sub.AlbumsTwo = &subsonic.Albums{}
List: listObj, for _, album := range albums {
sub.AlbumsTwo.List = append(sub.AlbumsTwo.List,
makeAlbumFromAlbum(&album, &album.Artist))
} }
respond(w, r, sub) respond(w, r, sub)
} }

View File

@@ -96,14 +96,14 @@ func (c *Controller) Scrobble(w http.ResponseWriter, r *http.Request) {
// fetch user to get lastfm session // fetch user to get lastfm session
user := r.Context().Value(contextUserKey).(*model.User) user := r.Context().Value(contextUserKey).(*model.User)
if user.LastFMSession == "" { 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 return
} }
// fetch track for getting info to send to last.fm function // fetch track for getting info to send to last.fm function
var track model.Track var track model.Track
c.DB. c.DB.
Preload("Album"). Preload("Album").
Preload("AlbumArtist"). Preload("Artist").
First(&track, id) First(&track, id)
// scrobble with above info // scrobble with above info
err = lastfm.Scrobble( err = lastfm.Scrobble(

View File

@@ -33,12 +33,12 @@ func checkCredentialsToken(password, token, salt string) bool {
return token == expToken return token == expToken
} }
func checkCredentialsBasic(password, givenPassword string) bool { func checkCredentialsBasic(password, given string) bool {
if givenPassword[:4] == "enc:" { if given[:4] == "enc:" {
bytes, _ := hex.DecodeString(givenPassword[4:]) bytes, _ := hex.DecodeString(given[4:])
givenPassword = string(bytes) given = string(bytes)
} }
return password == givenPassword return password == given
} }
func (c *Controller) WithValidSubsonicArgs(next http.HandlerFunc) http.HandlerFunc { 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) user := c.GetUserFromName(username)
if user == nil { if user == nil {
// the user does not exist
respondError(w, r, 40, "invalid username") respondError(w, r, 40, "invalid username")
return return
} }

View File

@@ -48,10 +48,10 @@ func Scrobble(apiKey, secret, session string, track *model.Track,
} }
params.Add("api_key", apiKey) params.Add("api_key", apiKey)
params.Add("sk", session) params.Add("sk", session)
params.Add("artist", track.Artist) params.Add("artist", track.TrackArtist)
params.Add("track", track.Title) params.Add("track", track.Title)
params.Add("album", track.Album.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("trackNumber", strconv.Itoa(track.TrackNumber))
params.Add("api_sig", getParamSignature(params, secret)) params.Add("api_sig", getParamSignature(params, secret))
_, err := makeRequest("POST", params) _, err := makeRequest("POST", params)

View File

@@ -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)
}
}

View File

@@ -1,6 +1,8 @@
package subsonic package subsonic
import "time" import (
"time"
)
type Albums struct { type Albums struct {
List []*Album `xml:"album" json:"album,omitempty"` List []*Album `xml:"album" json:"album,omitempty"`
@@ -37,7 +39,7 @@ type Track struct {
Artist string `xml:"artist,attr,omitempty" json:"artist"` Artist string `xml:"artist,attr,omitempty" json:"artist"`
IsDir bool `xml:"isDir,attr,omitempty" json:"isDir"` IsDir bool `xml:"isDir,attr,omitempty" json:"isDir"`
CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt"` CoverID int `xml:"coverArt,attr,omitempty" json:"coverArt"`
Created time.Time `xml:"created,attr,omitempty" json:"created"` CreatedAt time.Time `xml:"created,attr,omitempty" json:"created"`
Duration int `xml:"duration,attr,omitempty" json:"duration"` Duration int `xml:"duration,attr,omitempty" json:"duration"`
Genre string `xml:"genre,attr,omitempty" json:"genre"` Genre string `xml:"genre,attr,omitempty" json:"genre"`
Bitrate int `xml:"bitRate,attr,omitempty" json:"bitRate"` Bitrate int `xml:"bitRate,attr,omitempty" json:"bitRate"`
@@ -48,7 +50,7 @@ type Track struct {
Path string `xml:"path,attr,omitempty" json:"path"` Path string `xml:"path,attr,omitempty" json:"path"`
AlbumID int `xml:"albumId,attr,omitempty" json:"albumId"` AlbumID int `xml:"albumId,attr,omitempty" json:"albumId"`
ArtistID int `xml:"artistId,attr,omitempty" json:"artistId"` ArtistID int `xml:"artistId,attr,omitempty" json:"artistId"`
TrackNo int `xml:"track,attr,omitempty" json:"track"` TrackNumber int `xml:"track,attr,omitempty" json:"track"`
Type string `xml:"type,attr,omitempty" json:"type"` Type string `xml:"type,attr,omitempty" json:"type"`
} }
@@ -84,7 +86,7 @@ type Directory struct {
type Child struct { type Child struct {
ID int `xml:"id,attr,omitempty" json:"id,omitempty"` 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"` Title string `xml:"title,attr,omitempty" json:"title,omitempty"`
IsDir bool `xml:"isDir,attr,omitempty" json:"isDir,omitempty"` IsDir bool `xml:"isDir,attr,omitempty" json:"isDir,omitempty"`
Album string `xml:"album,attr,omitempty" json:"album,omitempty"` Album string `xml:"album,attr,omitempty" json:"album,omitempty"`