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,9 +13,9 @@ 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
// if it were flat), but this solves the "American Football problem" // if it were flat), but this solves the "American Football problem"
@@ -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"`
@@ -40,26 +40,26 @@ type AlbumArtist struct {
type Track struct { type Track struct {
IDBase IDBase
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
Duration int Duration int
Title string Title string
TotalDiscs int TotalDiscs int
TotalTracks int TotalTracks int
TrackNumber int TrackNumber int
Year int Year int
Suffix string Suffix string
ContentType string ContentType string
Size int Size int
Folder Folder Folder Folder
FolderID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES folders(id) ON DELETE CASCADE"` FolderID int `gorm:"not null; index" sql:"default: null; type:int REFERENCES folders(id) ON DELETE CASCADE"`
Path string `gorm:"not null; unique_index"` Path string `gorm:"not null; unique_index"`
} }
// Cover represents the covers table // Cover represents the covers table

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"`
@@ -30,26 +32,26 @@ type RandomTracks struct {
} }
type Track struct { type Track struct {
ID int `xml:"id,attr,omitempty" json:"id"` ID int `xml:"id,attr,omitempty" json:"id"`
Parent int `xml:"parent,attr,omitempty" json:"parent"` Parent int `xml:"parent,attr,omitempty" json:"parent"`
Title string `xml:"title,attr,omitempty" json:"title"` Title string `xml:"title,attr,omitempty" json:"title"`
Album string `xml:"album,attr,omitempty" json:"album"` Album string `xml:"album,attr,omitempty" json:"album"`
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"`
Size int `xml:"size,attr,omitempty" json:"size"` Size int `xml:"size,attr,omitempty" json:"size"`
Suffix string `xml:"suffix,attr,omitempty" json:"suffix"` Suffix string `xml:"suffix,attr,omitempty" json:"suffix"`
ContentType string `xml:"contentType,attr,omitempty" json:"contentType"` ContentType string `xml:"contentType,attr,omitempty" json:"contentType"`
IsVideo bool `xml:"isVideo,attr,omitempty" json:"isVideo"` IsVideo bool `xml:"isVideo,attr,omitempty" json:"isVideo"`
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"`
} }
type Artists struct { type Artists struct {
@@ -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"`