add playlist support
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
server/ctrladmin/playlist.go
Normal file
66
server/ctrladmin/playlist.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user