Merge branch 'develop'
This commit is contained in:
1
db/db.go
1
db/db.go
@@ -41,6 +41,7 @@ func New(path string) (*DB, error) {
|
|||||||
&migrationCreateInitUser,
|
&migrationCreateInitUser,
|
||||||
&migrationMergePlaylist,
|
&migrationMergePlaylist,
|
||||||
&migrationCreateTranscode,
|
&migrationCreateTranscode,
|
||||||
|
&migrationAddGenre,
|
||||||
})
|
})
|
||||||
if err = migr.Migrate(); err != nil {
|
if err = migr.Migrate(); err != nil {
|
||||||
return nil, errors.Wrap(err, "migrating to latest version")
|
return nil, errors.Wrap(err, "migrating to latest version")
|
||||||
|
|||||||
@@ -79,3 +79,15 @@ var migrationCreateTranscode = gormigrate.Migration{
|
|||||||
Error
|
Error
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var migrationAddGenre = gormigrate.Migration{
|
||||||
|
ID: "202003121330",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
return tx.AutoMigrate(
|
||||||
|
Genre{},
|
||||||
|
Album{},
|
||||||
|
Track{},
|
||||||
|
).
|
||||||
|
Error
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
13
db/model.go
13
db/model.go
@@ -49,6 +49,15 @@ func (a *Artist) IndexName() string {
|
|||||||
return a.Name
|
return a.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Genre struct {
|
||||||
|
ID int `gorm:"primary_ket"`
|
||||||
|
Name string `gorm:"not null; unique_index"`
|
||||||
|
Albums []*Album `gorm:"foreignkey:TagGenreID"`
|
||||||
|
AlbumCount int `sql:"-"`
|
||||||
|
Tracks []*Track `gorm:"foreignkey:TagGenreID"`
|
||||||
|
TrackCount int `sql:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
type Track struct {
|
type Track struct {
|
||||||
ID int `gorm:"primary_key"`
|
ID int `gorm:"primary_key"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
@@ -67,6 +76,8 @@ type Track struct {
|
|||||||
TagTrackArtist string `sql:"default: null"`
|
TagTrackArtist string `sql:"default: null"`
|
||||||
TagTrackNumber int `sql:"default: null"`
|
TagTrackNumber int `sql:"default: null"`
|
||||||
TagDiscNumber int `sql:"default: null"`
|
TagDiscNumber int `sql:"default: null"`
|
||||||
|
TagGenre *Genre
|
||||||
|
TagGenreID int `sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"`
|
||||||
TagBrainzID string `sql:"default: null"`
|
TagBrainzID string `sql:"default: null"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +141,8 @@ type Album struct {
|
|||||||
Cover string `sql:"default: null"`
|
Cover string `sql:"default: null"`
|
||||||
TagArtist *Artist
|
TagArtist *Artist
|
||||||
TagArtistID int `sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
|
TagArtistID int `sql:"default: null; type:int REFERENCES artists(id) ON DELETE CASCADE"`
|
||||||
|
TagGenre *Genre
|
||||||
|
TagGenreID int `sql:"default: null; type:int REFERENCES genres(id) ON DELETE CASCADE"`
|
||||||
TagTitle string `sql:"default: null"`
|
TagTitle string `sql:"default: null"`
|
||||||
TagTitleUDec string `sql:"default: null"`
|
TagTitleUDec string `sql:"default: null"`
|
||||||
TagBrainzID string `sql:"default: null"`
|
TagBrainzID string `sql:"default: null"`
|
||||||
|
|||||||
@@ -362,6 +362,27 @@ func (s *Scanner) handleTrack(it *item) error {
|
|||||||
s.trTx.Save(artist)
|
s.trTx.Save(artist)
|
||||||
}
|
}
|
||||||
track.ArtistID = artist.ID
|
track.ArtistID = artist.ID
|
||||||
|
//
|
||||||
|
// set genre
|
||||||
|
genreName := func() string {
|
||||||
|
if r := trTags.Genre(); r != "" {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return "Unknown Genre"
|
||||||
|
}()
|
||||||
|
genre := &db.Genre{}
|
||||||
|
err = s.trTx.
|
||||||
|
Select("id").
|
||||||
|
Where("name=?", genreName).
|
||||||
|
First(genre).
|
||||||
|
Error
|
||||||
|
if gorm.IsRecordNotFoundError(err) {
|
||||||
|
genre.Name = genreName
|
||||||
|
s.trTx.Save(genre)
|
||||||
|
}
|
||||||
|
track.TagGenreID = genre.ID
|
||||||
|
//
|
||||||
|
// save the track
|
||||||
s.trTx.Save(track)
|
s.trTx.Save(track)
|
||||||
s.seenTracks[track.ID] = struct{}{}
|
s.seenTracks[track.ID] = struct{}{}
|
||||||
s.seenTracksNew++
|
s.seenTracksNew++
|
||||||
@@ -377,6 +398,7 @@ func (s *Scanner) handleTrack(it *item) error {
|
|||||||
folder.TagBrainzID = trTags.AlbumBrainzID()
|
folder.TagBrainzID = trTags.AlbumBrainzID()
|
||||||
folder.TagYear = trTags.Year()
|
folder.TagYear = trTags.Year()
|
||||||
folder.TagArtistID = artist.ID
|
folder.TagArtistID = artist.ID
|
||||||
|
folder.TagGenreID = genre.ID
|
||||||
folder.ReceivedTags = true
|
folder.ReceivedTags = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ func (t *Tags) Artist() string { return t.firstTag("artist") }
|
|||||||
func (t *Tags) Album() string { return t.firstTag("album") }
|
func (t *Tags) Album() string { return t.firstTag("album") }
|
||||||
func (t *Tags) AlbumArtist() string { return t.firstTag("albumartist", "album artist") }
|
func (t *Tags) AlbumArtist() string { return t.firstTag("albumartist", "album artist") }
|
||||||
func (t *Tags) AlbumBrainzID() string { return t.firstTag("musicbrainz_albumid") }
|
func (t *Tags) AlbumBrainzID() string { return t.firstTag("musicbrainz_albumid") }
|
||||||
|
func (t *Tags) Genre() string { return t.firstTag("genre") }
|
||||||
func (t *Tags) Year() int { return intSep(t.firstTag("date", "year"), "-") } // eg. 2019-6-11
|
func (t *Tags) Year() int { return intSep(t.firstTag("date", "year"), "-") } // eg. 2019-6-11
|
||||||
func (t *Tags) TrackNumber() int { return intSep(t.firstTag("tracknumber"), "/") } // eg. 5/12
|
func (t *Tags) TrackNumber() int { return intSep(t.firstTag("tracknumber"), "/") } // eg. 5/12
|
||||||
func (t *Tags) DiscNumber() int { return intSep(t.firstTag("discnumber"), "/") } // eg. 1/2
|
func (t *Tags) DiscNumber() int { return intSep(t.firstTag("discnumber"), "/") } // eg. 1/2
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ func (c *Controller) ServeGetAlbumListTwo(r *http.Request) *spec.Response {
|
|||||||
params.GetIntOr("fromYear", 1800),
|
params.GetIntOr("fromYear", 1800),
|
||||||
params.GetIntOr("toYear", 2200))
|
params.GetIntOr("toYear", 2200))
|
||||||
q = q.Order("tag_year")
|
q = q.Order("tag_year")
|
||||||
|
case "byGenre":
|
||||||
|
q = q.Joins("JOIN genres ON albums.tag_genre_id=genres.id AND genres.name=?",
|
||||||
|
params.GetOr("genre", "Unknown Genre"))
|
||||||
case "frequent":
|
case "frequent":
|
||||||
user := r.Context().Value(CtxUser).(*db.User)
|
user := r.Context().Value(CtxUser).(*db.User)
|
||||||
q = q.Joins("JOIN plays ON albums.id=plays.album_id AND plays.user_id=?",
|
q = q.Joins("JOIN plays ON albums.id=plays.album_id AND plays.user_id=?",
|
||||||
@@ -275,3 +278,51 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
|
|||||||
}
|
}
|
||||||
return sub
|
return sub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ServeGetGenres(r *http.Request) *spec.Response {
|
||||||
|
var genres []*db.Genre
|
||||||
|
c.DB.
|
||||||
|
Select(`*,
|
||||||
|
(SELECT count(id) FROM albums WHERE tag_genre_id=genres.id) album_count,
|
||||||
|
(SELECT count(id) FROM tracks WHERE tag_genre_id=genres.id) track_count`).
|
||||||
|
Group("genres.id").
|
||||||
|
Find(&genres)
|
||||||
|
|
||||||
|
sub := spec.NewResponse()
|
||||||
|
sub.Genres = &spec.Genres{
|
||||||
|
List: make([]*spec.Genre, len(genres)),
|
||||||
|
}
|
||||||
|
for i, genre := range genres {
|
||||||
|
sub.Genres.List[i] = spec.NewGenre(genre)
|
||||||
|
}
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) ServeGetSongsByGenre(r *http.Request) *spec.Response {
|
||||||
|
params := r.Context().Value(CtxParams).(params.Params)
|
||||||
|
genre := params.Get("genre")
|
||||||
|
if genre == "" {
|
||||||
|
return spec.NewError(10, "please provide an `genre` parameter")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add musicFolderId parameter:
|
||||||
|
// (Since 1.12.0) Only return albums in the music folder with the given ID.
|
||||||
|
|
||||||
|
var tracks []*db.Track
|
||||||
|
c.DB.
|
||||||
|
Joins("JOIN albums ON tracks.album_id=albums.id").
|
||||||
|
Joins("JOIN genres ON tracks.tag_genre_id=genres.id AND genres.name=?", genre).
|
||||||
|
Preload("Album").
|
||||||
|
Offset(params.GetIntOr("offset", 0)).
|
||||||
|
Limit(params.GetIntOr("count", 10)).
|
||||||
|
Find(&tracks)
|
||||||
|
|
||||||
|
sub := spec.NewResponse()
|
||||||
|
sub.TracksByGenre = &spec.TracksByGenre{
|
||||||
|
List: make([]*spec.TrackChild, len(tracks)),
|
||||||
|
}
|
||||||
|
for i, track := range tracks {
|
||||||
|
sub.TracksByGenre.List[i] = spec.NewTrackByTags(track, track.Album)
|
||||||
|
}
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|||||||
@@ -286,18 +286,28 @@ func (c *Controller) ServeGetSong(r *http.Request) *spec.Response {
|
|||||||
|
|
||||||
func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
|
func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response {
|
||||||
params := r.Context().Value(CtxParams).(params.Params)
|
params := r.Context().Value(CtxParams).(params.Params)
|
||||||
// TODO: add genre restraint here
|
|
||||||
var tracks []*db.Track
|
var tracks []*db.Track
|
||||||
c.DB.DB.
|
|
||||||
Limit(params.GetIntOr("size", 10)).
|
q := c.DB.DB.
|
||||||
Where(
|
|
||||||
"albums.tag_year BETWEEN ? AND ?",
|
|
||||||
params.GetIntOr("fromYear", 1800),
|
|
||||||
params.GetIntOr("toYear", 2200)).
|
|
||||||
Joins("JOIN albums ON tracks.album_id=albums.id").
|
Joins("JOIN albums ON tracks.album_id=albums.id").
|
||||||
|
Limit(params.GetIntOr("size", 10)).
|
||||||
Preload("Album").
|
Preload("Album").
|
||||||
Order(gorm.Expr("random()")).
|
Order(gorm.Expr("random()"))
|
||||||
Find(&tracks)
|
|
||||||
|
if year, err := params.GetInt("fromYear"); err == nil {
|
||||||
|
q = q.Where("albums.tag_year >= ?", year)
|
||||||
|
}
|
||||||
|
if year, err := params.GetInt("toYear"); err == nil {
|
||||||
|
q = q.Where("albums.tag_year <= ?", year)
|
||||||
|
}
|
||||||
|
if genre := params.Get("genre"); genre != "" {
|
||||||
|
q = q.Joins(
|
||||||
|
"JOIN genres ON tracks.tag_genre_id=genres.id AND genres.name=?",
|
||||||
|
genre,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
q.Find(&tracks)
|
||||||
|
|
||||||
sub := spec.NewResponse()
|
sub := spec.NewResponse()
|
||||||
sub.RandomTracks = &spec.RandomTracks{}
|
sub.RandomTracks = &spec.RandomTracks{}
|
||||||
sub.RandomTracks.List = make([]*spec.TrackChild, len(tracks))
|
sub.RandomTracks.List = make([]*spec.TrackChild, len(tracks))
|
||||||
|
|||||||
@@ -1,17 +1,4 @@
|
|||||||
package ctrlsubsonic
|
package ctrlsubsonic
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"senan.xyz/g/gonic/server/ctrlsubsonic/spec"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NOTE: when these are implemented, they should be moved to their
|
// NOTE: when these are implemented, they should be moved to their
|
||||||
// respective _by_folder or _by_tag file
|
// respective _by_folder or _by_tag file
|
||||||
|
|
||||||
func (c *Controller) ServeGetGenres(r *http.Request) *spec.Response {
|
|
||||||
sub := spec.NewResponse()
|
|
||||||
sub.Genres = &spec.Genres{}
|
|
||||||
sub.Genres.List = []*spec.Genre{}
|
|
||||||
return sub
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -72,3 +72,11 @@ func NewArtistByTags(a *db.Artist) *Artist {
|
|||||||
AlbumCount: a.AlbumCount,
|
AlbumCount: a.AlbumCount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewGenre(g *db.Genre) *Genre {
|
||||||
|
return &Genre{
|
||||||
|
Name: g.Name,
|
||||||
|
AlbumCount: g.AlbumCount,
|
||||||
|
SongCount: g.TrackCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type Response struct {
|
|||||||
Artist *Artist `xml:"artist" json:"artist,omitempty"`
|
Artist *Artist `xml:"artist" json:"artist,omitempty"`
|
||||||
Directory *Directory `xml:"directory" json:"directory,omitempty"`
|
Directory *Directory `xml:"directory" json:"directory,omitempty"`
|
||||||
RandomTracks *RandomTracks `xml:"randomSongs" json:"randomSongs,omitempty"`
|
RandomTracks *RandomTracks `xml:"randomSongs" json:"randomSongs,omitempty"`
|
||||||
|
TracksByGenre *TracksByGenre `xml:"songsByGenre" json:"songsByGenre,omitempty"`
|
||||||
MusicFolders *MusicFolders `xml:"musicFolders" json:"musicFolders,omitempty"`
|
MusicFolders *MusicFolders `xml:"musicFolders" json:"musicFolders,omitempty"`
|
||||||
ScanStatus *ScanStatus `xml:"scanStatus" json:"scanStatus,omitempty"`
|
ScanStatus *ScanStatus `xml:"scanStatus" json:"scanStatus,omitempty"`
|
||||||
Licence *Licence `xml:"license" json:"license,omitempty"`
|
Licence *Licence `xml:"license" json:"license,omitempty"`
|
||||||
@@ -109,6 +110,10 @@ type RandomTracks struct {
|
|||||||
List []*TrackChild `xml:"song" json:"song"`
|
List []*TrackChild `xml:"song" json:"song"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TracksByGenre struct {
|
||||||
|
List []*TrackChild `xml:"song" json:"song"`
|
||||||
|
}
|
||||||
|
|
||||||
type TrackChild struct {
|
type TrackChild struct {
|
||||||
Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
|
Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
|
||||||
AlbumID int `xml:"albumId,attr,omitempty" json:"albumId,omitempty,string"`
|
AlbumID int `xml:"albumId,attr,omitempty" json:"albumId,omitempty,string"`
|
||||||
@@ -250,8 +255,9 @@ type Genres struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Genre struct {
|
type Genre struct {
|
||||||
SongCount string `xml:"songCount,attr"`
|
Name string `xml:",chardata",json:"value"`
|
||||||
AlbumCount string `xml:"albumCount,attr"`
|
SongCount int `xml:"songCount,attr,omitempty" json:"songCount,omitempty"`
|
||||||
|
AlbumCount int `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlayQueue struct {
|
type PlayQueue struct {
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) {
|
|||||||
r.Handle("/getPlayQueue{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPlayQueue))
|
r.Handle("/getPlayQueue{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPlayQueue))
|
||||||
r.Handle("/getSong{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSong))
|
r.Handle("/getSong{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSong))
|
||||||
r.Handle("/getRandomSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetRandomSongs))
|
r.Handle("/getRandomSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetRandomSongs))
|
||||||
|
r.Handle("/getSongsByGenre{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSongsByGenre))
|
||||||
// ** begin raw
|
// ** begin raw
|
||||||
r.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeDownload))
|
r.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeDownload))
|
||||||
r.Handle("/getCoverArt{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeGetCoverArt))
|
r.Handle("/getCoverArt{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeGetCoverArt))
|
||||||
@@ -166,8 +167,8 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) {
|
|||||||
r.Handle("/getMusicDirectory{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetMusicDirectory))
|
r.Handle("/getMusicDirectory{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetMusicDirectory))
|
||||||
r.Handle("/getAlbumList{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetAlbumList))
|
r.Handle("/getAlbumList{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetAlbumList))
|
||||||
r.Handle("/search2{_:(?:\\.view)?}", ctrl.H(ctrl.ServeSearchTwo))
|
r.Handle("/search2{_:(?:\\.view)?}", ctrl.H(ctrl.ServeSearchTwo))
|
||||||
// ** begin unimplemented
|
|
||||||
r.Handle("/getGenres{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetGenres))
|
r.Handle("/getGenres{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetGenres))
|
||||||
|
// ** begin unimplemented
|
||||||
// middlewares should be run for not found handler
|
// middlewares should be run for not found handler
|
||||||
// https://github.com/gorilla/mux/issues/416
|
// https://github.com/gorilla/mux/issues/416
|
||||||
notFoundHandler := ctrl.H(ctrl.ServeNotFound)
|
notFoundHandler := ctrl.H(ctrl.ServeNotFound)
|
||||||
|
|||||||
Reference in New Issue
Block a user