add playlist support

This commit is contained in:
sentriz
2019-11-27 01:46:13 +00:00
parent 605c587fd9
commit dd93aa5e74
7 changed files with 129 additions and 17 deletions

View File

@@ -40,12 +40,14 @@ func main() {
log.Fatalf("error opening database: %v\n", err) log.Fatalf("error opening database: %v\n", err)
} }
defer db.Close() defer db.Close()
s := server.New(server.ServerOptions{ serverOptions := server.ServerOptions{
DB: db, DB: db,
MusicPath: *musicPath, MusicPath: *musicPath,
ListenAddr: *listenAddr, ListenAddr: *listenAddr,
ScanInterval: time.Duration(*scanInterval) * time.Minute, ScanInterval: time.Duration(*scanInterval) * time.Minute,
}) }
log.Printf("using opts %+v\n", serverOptions)
s := server.New(serverOptions)
if err = s.SetupAdmin(); err != nil { if err = s.SetupAdmin(); err != nil {
log.Fatalf("error setting up admin routes: %v\n", err) log.Fatalf("error setting up admin routes: %v\n", err)
} }

View File

@@ -111,12 +111,13 @@ func (a *Album) IndexRightPath() string {
type Playlist struct { type Playlist struct {
ID int `gorm:"primary_key"` ID int `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
ModifiedAt time.Time
User *User User *User
UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
Name string Name string
Comment string Comment string
TrackCount int `sql:"-"`
} }
type PlaylistItem struct { type PlaylistItem struct {

View File

@@ -97,6 +97,7 @@ type templateData struct {
AllUsers []*model.User AllUsers []*model.User
LastScanTime time.Time LastScanTime time.Time
IsScanning bool IsScanning bool
Playlists []*model.Playlist
// //
CurrentLastFMAPIKey string CurrentLastFMAPIKey string
CurrentLastFMAPISecret string CurrentLastFMAPISecret string

View File

@@ -59,6 +59,19 @@ func (c *Controller) ServeHome(r *http.Request) *Response {
data.LastScanTime = time.Unix(i, 0) 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{ return &Response{
template: "home.tmpl", template: "home.tmpl",
data: data, data: data,
@@ -257,6 +270,36 @@ func (c *Controller) ServeStartScanDo(r *http.Request) *Response {
}() }()
return &Response{ return &Response{
redirect: "/admin/home", 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,
} }
} }

View File

@@ -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
}

View File

@@ -172,7 +172,6 @@ func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response {
func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response { func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response {
playlistID, _ := parsing.GetFirstIntParamOf(r, "id", "playlistId") playlistID, _ := parsing.GetFirstIntParamOf(r, "id", "playlistId")
//
// begin updating meta // begin updating meta
playlist := &model.Playlist{} playlist := &model.Playlist{}
c.DB. c.DB.
@@ -187,7 +186,6 @@ func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response {
playlist.Comment = comment playlist.Comment = comment
} }
c.DB.Save(playlist) c.DB.Save(playlist)
//
// begin delete tracks // begin delete tracks
if indexes, ok := r.URL.Query()["songIndexToRemove"]; ok { if indexes, ok := r.URL.Query()["songIndexToRemove"]; ok {
trackIDs := []int{} trackIDs := []int{}
@@ -205,7 +203,6 @@ func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response {
"track_id = ?", trackIDs[i]) "track_id = ?", trackIDs[i])
} }
} }
//
// begin add tracks // begin add tracks
if toAdd := parsing.GetFirstParamOf(r, "songId", "songIdToAdd"); toAdd != nil { if toAdd := parsing.GetFirstParamOf(r, "songId", "songIdToAdd"); toAdd != nil {
for _, trackIDStr := range toAdd { for _, trackIDStr := range toAdd {

View File

@@ -27,12 +27,13 @@ type ServerOptions struct {
type Server struct { type Server struct {
*http.Server *http.Server
router *mux.Router router *mux.Router
ctrlBase *ctrlbase.Controller ctrlBase *ctrlbase.Controller
ScanInterval time.Duration opts ServerOptions
} }
func New(opts ServerOptions) *Server { func New(opts ServerOptions) *Server {
opts.MusicPath = filepath.Clean(opts.MusicPath)
ctrlBase := &ctrlbase.Controller{ ctrlBase := &ctrlbase.Controller{
DB: opts.DB, DB: opts.DB,
MusicPath: opts.MusicPath, MusicPath: opts.MusicPath,
@@ -62,10 +63,10 @@ func New(opts ServerOptions) *Server {
IdleTimeout: 15 * time.Second, IdleTimeout: 15 * time.Second,
} }
return &Server{ return &Server{
Server: server, Server: server,
router: router, router: router,
ctrlBase: ctrlBase, ctrlBase: ctrlBase,
ScanInterval: opts.ScanInterval, opts: opts,
} }
} }
@@ -96,6 +97,7 @@ func (s *Server) SetupAdmin() error {
routUser.Handle("/change_own_password_do", ctrl.H(ctrl.ServeChangeOwnPasswordDo)) routUser.Handle("/change_own_password_do", ctrl.H(ctrl.ServeChangeOwnPasswordDo))
routUser.Handle("/link_lastfm_do", ctrl.H(ctrl.ServeLinkLastFMDo)) routUser.Handle("/link_lastfm_do", ctrl.H(ctrl.ServeLinkLastFMDo))
routUser.Handle("/unlink_lastfm_do", ctrl.H(ctrl.ServeUnlinkLastFMDo)) 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) // begin admin routes (if session is valid, and is admin)
routAdmin := routUser.NewRoute().Subrouter() routAdmin := routUser.NewRoute().Subrouter()
@@ -153,7 +155,7 @@ func (s *Server) SetupSubsonic() error {
} }
func (s *Server) scanTick() { func (s *Server) scanTick() {
ticker := time.NewTicker(s.ScanInterval) ticker := time.NewTicker(s.opts.ScanInterval)
for range ticker.C { for range ticker.C {
if err := s.ctrlBase.Scanner.Start(); err != nil { if err := s.ctrlBase.Scanner.Start(); err != nil {
log.Printf("error while scanner: %v", err) log.Printf("error while scanner: %v", err)
@@ -162,8 +164,8 @@ func (s *Server) scanTick() {
} }
func (s *Server) Start() error { func (s *Server) Start() error {
if s.ScanInterval > 0 { if s.opts.ScanInterval > 0 {
log.Printf("will be scanning at intervals of %s", s.ScanInterval) log.Printf("will be scanning at intervals of %s", s.opts.ScanInterval)
go s.scanTick() go s.scanTick()
} }
return s.ListenAndServe() return s.ListenAndServe()