support subsonic bookmarks
This commit is contained in:
79
server/ctrlsubsonic/handlers_bookmark.go
Normal file
79
server/ctrlsubsonic/handlers_bookmark.go
Normal 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()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user