From dd93aa5e74a697413e6252f16d668ad0e986c3a6 Mon Sep 17 00:00:00 2001 From: sentriz Date: Wed, 27 Nov 2019 01:46:13 +0000 Subject: [PATCH] add playlist support --- cmd/gonic/main.go | 6 ++- model/model.go | 3 +- server/ctrladmin/ctrl.go | 1 + server/ctrladmin/handlers.go | 45 +++++++++++++++++- server/ctrladmin/playlist.go | 66 ++++++++++++++++++++++++++ server/ctrlsubsonic/handlers_common.go | 3 -- server/server.go | 22 +++++---- 7 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 server/ctrladmin/playlist.go diff --git a/cmd/gonic/main.go b/cmd/gonic/main.go index ee15c103..32ad7f09 100644 --- a/cmd/gonic/main.go +++ b/cmd/gonic/main.go @@ -40,12 +40,14 @@ func main() { log.Fatalf("error opening database: %v\n", err) } defer db.Close() - s := server.New(server.ServerOptions{ + serverOptions := server.ServerOptions{ DB: db, MusicPath: *musicPath, ListenAddr: *listenAddr, ScanInterval: time.Duration(*scanInterval) * time.Minute, - }) + } + log.Printf("using opts %+v\n", serverOptions) + s := server.New(serverOptions) if err = s.SetupAdmin(); err != nil { log.Fatalf("error setting up admin routes: %v\n", err) } diff --git a/model/model.go b/model/model.go index 2401b2c2..8a4bfb97 100644 --- a/model/model.go +++ b/model/model.go @@ -111,12 +111,13 @@ func (a *Album) IndexRightPath() string { type Playlist struct { ID int `gorm:"primary_key"` + CreatedAt time.Time UpdatedAt time.Time - ModifiedAt time.Time User *User UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` Name string Comment string + TrackCount int `sql:"-"` } type PlaylistItem struct { diff --git a/server/ctrladmin/ctrl.go b/server/ctrladmin/ctrl.go index 01fb3a4b..f608ceda 100644 --- a/server/ctrladmin/ctrl.go +++ b/server/ctrladmin/ctrl.go @@ -97,6 +97,7 @@ type templateData struct { AllUsers []*model.User LastScanTime time.Time IsScanning bool + Playlists []*model.Playlist // CurrentLastFMAPIKey string CurrentLastFMAPISecret string diff --git a/server/ctrladmin/handlers.go b/server/ctrladmin/handlers.go index cbb4e4cb..620a38ea 100644 --- a/server/ctrladmin/handlers.go +++ b/server/ctrladmin/handlers.go @@ -59,6 +59,19 @@ func (c *Controller) ServeHome(r *http.Request) *Response { data.LastScanTime = time.Unix(i, 0) } // + // playlists box + user := r.Context().Value(key.User).(*model.User) + c.DB. + Select("*, count(items.id) as track_count"). + Joins(` + LEFT JOIN playlist_items items + ON items.playlist_id = playlists.id + `). + Where("user_id = ?", user.ID). + Group("playlists.id"). + Limit(20). + Find(&data.Playlists) + // return &Response{ template: "home.tmpl", data: data, @@ -257,6 +270,36 @@ func (c *Controller) ServeStartScanDo(r *http.Request) *Response { }() return &Response{ redirect: "/admin/home", - flashN: "scan started. refresh for results", + flashN: []string{"scan started. refresh for results"}, + } +} + +func (c *Controller) ServeUploadPlaylist(r *http.Request) *Response { + return &Response{template: "upload_playlist.tmpl"} +} + +func (c *Controller) ServeUploadPlaylistDo(r *http.Request) *Response { + if err := r.ParseMultipartForm((1 << 10) * 24); nil != err { + return &Response{ + err: "couldn't parse mutlipart", + code: 500, + } + } + user := r.Context().Value(key.User).(*model.User) + var playlistCount int + var errors []string + for _, headers := range r.MultipartForm.File { + for _, header := range headers { + headerErrors, created := playlistParseUpload(c, user.ID, header) + if created { + playlistCount++ + } + errors = append(errors, headerErrors...) + } + } + return &Response{ + redirect: "/admin/home", + flashN: []string{fmt.Sprintf("%d playlist(s) created", playlistCount)}, + flashW: errors, } } diff --git a/server/ctrladmin/playlist.go b/server/ctrladmin/playlist.go new file mode 100644 index 00000000..fd3d3fe5 --- /dev/null +++ b/server/ctrladmin/playlist.go @@ -0,0 +1,66 @@ +package ctrladmin + +import ( + "bufio" + "fmt" + "mime/multipart" + "strings" + + "github.com/jinzhu/gorm" + "github.com/pkg/errors" + + "senan.xyz/g/gonic/model" +) + +func playlistParseLine(c *Controller, playlistID int, path string) error { + if strings.HasPrefix(path, "#") || strings.TrimSpace(path) == "" { + return nil + } + track := &model.Track{} + query := c.DB.Raw(` + SELECT tracks.id FROM TRACKS + JOIN albums ON tracks.album_id = albums.id + WHERE (? || '/' || albums.left_path || albums.right_path || '/' || tracks.filename) = ? + `, c.MusicPath, path) + err := query.First(&track).Error + switch { + case gorm.IsRecordNotFoundError(err): + return fmt.Errorf("couldn't match track %q", path) + case err != nil: + return errors.Wrap(err, "while matching") + } + c.DB.Create(&model.PlaylistItem{ + PlaylistID: playlistID, + TrackID: track.ID, + }) + return nil +} + +func playlistParseUpload(c *Controller, userID int, header *multipart.FileHeader) ([]string, bool) { + file, err := header.Open() + if err != nil { + return []string{fmt.Sprintf("couldn't open file %q", header.Filename)}, false + } + playlistName := strings.TrimSuffix(header.Filename, ".m3u8") + if playlistName == "" { + return []string{fmt.Sprintf("invalid filename %q", header.Filename)}, false + } + playlist := &model.Playlist{} + c.DB.FirstOrCreate(playlist, model.Playlist{ + Name: playlistName, + UserID: userID, + }) + c.DB.Delete(&model.PlaylistItem{}, "playlist_id = ?", playlist.ID) + var errors []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + path := scanner.Text() + if err := playlistParseLine(c, playlist.ID, path); err != nil { + errors = append(errors, err.Error()) + } + } + if err := scanner.Err(); err != nil { + return []string{fmt.Sprintf("scanning line of playlist: %v", err)}, true + } + return errors, true +} diff --git a/server/ctrlsubsonic/handlers_common.go b/server/ctrlsubsonic/handlers_common.go index 37651c22..353d60fe 100644 --- a/server/ctrlsubsonic/handlers_common.go +++ b/server/ctrlsubsonic/handlers_common.go @@ -172,7 +172,6 @@ func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response { func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response { playlistID, _ := parsing.GetFirstIntParamOf(r, "id", "playlistId") - // // begin updating meta playlist := &model.Playlist{} c.DB. @@ -187,7 +186,6 @@ func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response { playlist.Comment = comment } c.DB.Save(playlist) - // // begin delete tracks if indexes, ok := r.URL.Query()["songIndexToRemove"]; ok { trackIDs := []int{} @@ -205,7 +203,6 @@ func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response { "track_id = ?", trackIDs[i]) } } - // // begin add tracks if toAdd := parsing.GetFirstParamOf(r, "songId", "songIdToAdd"); toAdd != nil { for _, trackIDStr := range toAdd { diff --git a/server/server.go b/server/server.go index 7a585118..63c8bf31 100644 --- a/server/server.go +++ b/server/server.go @@ -27,12 +27,13 @@ type ServerOptions struct { type Server struct { *http.Server - router *mux.Router - ctrlBase *ctrlbase.Controller - ScanInterval time.Duration + router *mux.Router + ctrlBase *ctrlbase.Controller + opts ServerOptions } func New(opts ServerOptions) *Server { + opts.MusicPath = filepath.Clean(opts.MusicPath) ctrlBase := &ctrlbase.Controller{ DB: opts.DB, MusicPath: opts.MusicPath, @@ -62,10 +63,10 @@ func New(opts ServerOptions) *Server { IdleTimeout: 15 * time.Second, } return &Server{ - Server: server, - router: router, - ctrlBase: ctrlBase, - ScanInterval: opts.ScanInterval, + Server: server, + router: router, + ctrlBase: ctrlBase, + opts: opts, } } @@ -96,6 +97,7 @@ func (s *Server) SetupAdmin() error { routUser.Handle("/change_own_password_do", ctrl.H(ctrl.ServeChangeOwnPasswordDo)) routUser.Handle("/link_lastfm_do", ctrl.H(ctrl.ServeLinkLastFMDo)) routUser.Handle("/unlink_lastfm_do", ctrl.H(ctrl.ServeUnlinkLastFMDo)) + routUser.Handle("/upload_playlist_do", ctrl.H(ctrl.ServeUploadPlaylistDo)) // // begin admin routes (if session is valid, and is admin) routAdmin := routUser.NewRoute().Subrouter() @@ -153,7 +155,7 @@ func (s *Server) SetupSubsonic() error { } func (s *Server) scanTick() { - ticker := time.NewTicker(s.ScanInterval) + ticker := time.NewTicker(s.opts.ScanInterval) for range ticker.C { if err := s.ctrlBase.Scanner.Start(); err != nil { log.Printf("error while scanner: %v", err) @@ -162,8 +164,8 @@ func (s *Server) scanTick() { } func (s *Server) Start() error { - if s.ScanInterval > 0 { - log.Printf("will be scanning at intervals of %s", s.ScanInterval) + if s.opts.ScanInterval > 0 { + log.Printf("will be scanning at intervals of %s", s.opts.ScanInterval) go s.scanTick() } return s.ListenAndServe()