support subsonic bookmarks

This commit is contained in:
sentriz
2021-02-03 22:51:16 +00:00
parent f027d5a486
commit 7a1d57a43c
7 changed files with 168 additions and 42 deletions

View File

@@ -0,0 +1,79 @@
package ctrlsubsonic
import (
"net/http"
"github.com/jinzhu/gorm"
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
"go.senan.xyz/gonic/server/db"
)
func (c *Controller) ServeGetBookmarks(r *http.Request) *spec.Response {
user := r.Context().Value(CtxUser).(*db.User)
bookmarks := []*db.Bookmark{}
err := c.DB.
Where("user_id=?", user.ID).
Find(&bookmarks).
Error
if gorm.IsRecordNotFoundError(err) {
return spec.NewResponse()
}
sub := spec.NewResponse()
sub.Bookmarks = &spec.Bookmarks{
List: []*spec.Bookmark{},
}
for _, bookmark := range bookmarks {
specid := &specid.ID{
Type: specid.IDT(bookmark.EntryIDType),
Value: bookmark.EntryID,
}
entries := []*spec.BookmarkEntry{{
ID: specid,
Type: bookmark.EntryIDType,
}}
sub.Bookmarks.List = append(sub.Bookmarks.List, &spec.Bookmark{
Username: user.Name,
Position: bookmark.Position,
Comment: bookmark.Comment,
Created: bookmark.CreatedAt,
Changed: bookmark.UpdatedAt,
Entries: entries,
})
}
return sub
}
func (c *Controller) ServeCreateBookmark(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
id, err := params.GetID("id")
if err != nil {
return spec.NewError(10, "please provide an `id` parameter")
}
bookmark := &db.Bookmark{}
c.DB.FirstOrCreate(bookmark, db.Bookmark{
UserID: user.ID,
EntryIDType: string(id.Type),
EntryID: id.Value,
})
bookmark.Comment = params.GetOr("comment", "")
bookmark.Position = params.GetOrInt("position", 0)
c.DB.Save(bookmark)
return spec.NewResponse()
}
func (c *Controller) ServeDeleteBookmark(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params)
user := r.Context().Value(CtxUser).(*db.User)
id, err := params.GetID("id")
if err != nil {
return spec.NewError(10, "please provide an `id` parameter")
}
c.DB.
Where("user_id=? AND entry_id_type=? AND entry_id=?", user.ID, id.Type, id.Value).
Delete(&db.Bookmark{})
return spec.NewResponse()
}

View File

@@ -153,10 +153,13 @@ func (c *Controller) ServeSavePlayQueue(r *http.Request) *spec.Response {
if err != nil { if err != nil {
return spec.NewError(10, "please provide some `id` parameters") return spec.NewError(10, "please provide some `id` parameters")
} }
// TODO: support other play queue entries other than tracks
trackIDs := make([]int, 0, len(tracks)) trackIDs := make([]int, 0, len(tracks))
for _, id := range tracks { for _, id := range tracks {
if id.Type == specid.Track {
trackIDs = append(trackIDs, id.Value) trackIDs = append(trackIDs, id.Value)
} }
}
user := r.Context().Value(CtxUser).(*db.User) user := r.Context().Value(CtxUser).(*db.User)
queue := &db.PlayQueue{UserID: user.ID} queue := &db.PlayQueue{UserID: user.ID}
c.DB.Where(queue).First(queue) c.DB.Where(queue).First(queue)

View File

@@ -45,6 +45,7 @@ type Response struct {
JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus" json:"jukeboxStatus,omitempty"` JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus" json:"jukeboxStatus,omitempty"`
JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist" json:"jukeboxPlaylist,omitempty"` JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist" json:"jukeboxPlaylist,omitempty"`
Podcasts *Podcasts `xml:"podcasts" json:"podcasts,omitempty"` Podcasts *Podcasts `xml:"podcasts" json:"podcasts,omitempty"`
Bookmarks *Bookmarks `xml:"bookmarks" json:"bookmarks,omitempty"`
} }
func NewResponse() *Response { func NewResponse() *Response {
@@ -120,6 +121,7 @@ type TracksByGenre struct {
} }
type TrackChild struct { type TrackChild struct {
ID *specid.ID `xml:"id,attr,omitempty" json:"id,omitempty"`
Album string `xml:"album,attr,omitempty" json:"album,omitempty"` Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
AlbumID *specid.ID `xml:"albumId,attr,omitempty" json:"albumId,omitempty"` AlbumID *specid.ID `xml:"albumId,attr,omitempty" json:"albumId,omitempty"`
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"` Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
@@ -130,7 +132,6 @@ type TrackChild struct {
CreatedAt time.Time `xml:"created,attr,omitempty" json:"created,omitempty"` CreatedAt time.Time `xml:"created,attr,omitempty" json:"created,omitempty"`
Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"` Duration int `xml:"duration,attr,omitempty" json:"duration,omitempty"`
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"` Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
ID *specid.ID `xml:"id,attr,omitempty" json:"id,omitempty"`
IsDir bool `xml:"isDir,attr" json:"isDir"` IsDir bool `xml:"isDir,attr" json:"isDir"`
IsVideo bool `xml:"isVideo,attr" json:"isVideo"` IsVideo bool `xml:"isVideo,attr" json:"isVideo"`
ParentID *specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"` ParentID *specid.ID `xml:"parent,attr,omitempty" json:"parent,omitempty"`
@@ -322,3 +323,21 @@ type PodcastEpisode struct {
BitRate int `xml:"bitRate,attr" json:"bitrate"` BitRate int `xml:"bitRate,attr" json:"bitrate"`
Path string `xml:"path,attr" json:"path"` Path string `xml:"path,attr" json:"path"`
} }
type Bookmarks struct {
List []*Bookmark `xml:"bookmark" json:"bookmark"`
}
type Bookmark struct {
Entries []*BookmarkEntry `xml:"entry,omitempty" json:"entry,omitempty"`
Username string `xml:"username,attr" json:"username"`
Position int `xml:"position,attr" json:"position"`
Comment string `xml:"comment,attr" json:"comment"`
Created time.Time `xml:"created,attr" json:"created"`
Changed time.Time `xml:"changed,attr" json:"changed"`
}
type BookmarkEntry struct {
ID *specid.ID `xml:"id,attr" json:"id"`
Type string `xml:"type,attr" json:"type"`
}

View File

@@ -80,6 +80,7 @@ func New(path string) (*DB, error) {
migrateMultiGenre(), migrateMultiGenre(),
migrateListenBrainz(), migrateListenBrainz(),
migratePodcast(), migratePodcast(),
migrateBookmarks(),
)) ))
if err = migr.Migrate(); err != nil { if err = migr.Migrate(); err != nil {
return nil, fmt.Errorf("migrating to latest version: %w", err) return nil, fmt.Errorf("migrating to latest version: %w", err)

View File

@@ -211,7 +211,6 @@ func migrateMultiGenre() gormigrate.Migration {
} }
} }
func migrateListenBrainz() gormigrate.Migration { func migrateListenBrainz() gormigrate.Migration {
return gormigrate.Migration{ return gormigrate.Migration{
ID: "202101081149", ID: "202101081149",
@@ -231,11 +230,23 @@ func migratePodcast() gormigrate.Migration {
return gormigrate.Migration{ return gormigrate.Migration{
ID: "202101111537", ID: "202101111537",
Migrate: func(tx *gorm.DB) error { Migrate: func(tx *gorm.DB) error {
step := tx.AutoMigrate( return tx.AutoMigrate(
Podcast{}, Podcast{},
PodcastEpisode{}, PodcastEpisode{},
) ).
return step.Error Error
},
}
}
func migrateBookmarks() gormigrate.Migration {
return gormigrate.Migration{
ID: "202102032210",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(
Bookmark{},
).
Error
}, },
} }
} }

View File

@@ -354,3 +354,15 @@ func (pe *PodcastEpisode) MIME() string {
func (pe *PodcastEpisode) AudioBitrate() int { func (pe *PodcastEpisode) AudioBitrate() int {
return pe.Bitrate return pe.Bitrate
} }
type Bookmark struct {
ID int `gorm:"primary_key"`
User *User
UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
Position int
Comment string
EntryIDType string
EntryID int
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@@ -191,6 +191,9 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) {
r.Handle("/getRandomSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetRandomSongs)) r.Handle("/getRandomSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetRandomSongs))
r.Handle("/getSongsByGenre{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSongsByGenre)) r.Handle("/getSongsByGenre{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSongsByGenre))
r.Handle("/jukeboxControl{_:(?:\\.view)?}", ctrl.H(ctrl.ServeJukebox)) r.Handle("/jukeboxControl{_:(?:\\.view)?}", ctrl.H(ctrl.ServeJukebox))
r.Handle("/getBookmarks{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetBookmarks))
r.Handle("/createBookmark{_:(?:\\.view)?}", ctrl.H(ctrl.ServeCreateBookmark))
r.Handle("/deleteBookmark{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeleteBookmark))
// ** 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))
@@ -210,14 +213,12 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) {
r.Handle("/getGenres{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetGenres)) r.Handle("/getGenres{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetGenres))
r.Handle("/getArtistInfo{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetArtistInfo)) r.Handle("/getArtistInfo{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetArtistInfo))
// ** begin podcasts // ** begin podcasts
if ctrl.Podcasts.PodcastBasePath != "" {
r.Handle("/getPodcasts{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPodcasts)) r.Handle("/getPodcasts{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetPodcasts))
r.Handle("/downloadPodcastEpisode{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDownloadPodcastEpisode)) r.Handle("/downloadPodcastEpisode{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDownloadPodcastEpisode))
r.Handle("/createPodcastChannel{_:(?:\\.view)?}", ctrl.H(ctrl.ServeCreatePodcastChannel)) r.Handle("/createPodcastChannel{_:(?:\\.view)?}", ctrl.H(ctrl.ServeCreatePodcastChannel))
r.Handle("/refreshPodcasts{_:(?:\\.view)?}", ctrl.H(ctrl.ServeRefreshPodcasts)) r.Handle("/refreshPodcasts{_:(?:\\.view)?}", ctrl.H(ctrl.ServeRefreshPodcasts))
r.Handle("/deletePodcastChannel{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeletePodcastChannel)) r.Handle("/deletePodcastChannel{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeletePodcastChannel))
r.Handle("/deletePodcastEpisode{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeletePodcastEpisode)) r.Handle("/deletePodcastEpisode{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeletePodcastEpisode))
}
// 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)