backend: Refactor playlits
This commit is contained in:
1
db/db.go
1
db/db.go
@@ -45,7 +45,6 @@ func New(path string) (*DB, error) {
|
||||
model.Play{},
|
||||
model.Album{},
|
||||
model.Playlist{},
|
||||
model.PlaylistItem{},
|
||||
)
|
||||
// TODO: don't log if user already exists
|
||||
db.FirstOrCreate(&model.User{}, model.User{
|
||||
|
||||
@@ -3,6 +3,8 @@ package model
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"senan.xyz/g/gonic/mime"
|
||||
@@ -117,14 +119,28 @@ type Playlist struct {
|
||||
UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"`
|
||||
Name string
|
||||
Comment string
|
||||
TrackCount int `sql:"-"`
|
||||
TrackCount int
|
||||
Items string
|
||||
}
|
||||
|
||||
type PlaylistItem struct {
|
||||
ID int `gorm:"primary_key"`
|
||||
CreatedAt time.Time
|
||||
Playlist Playlist
|
||||
PlaylistID int `sql:"default: null; type:int REFERENCES playlists(id) ON DELETE CASCADE"`
|
||||
Track Track
|
||||
TrackID int `sql:"default: null; type:int REFERENCES tracks(id) ON DELETE CASCADE"`
|
||||
func (p *Playlist) GetItems() []int {
|
||||
if len(p.Items) == 0 {
|
||||
return []int{}
|
||||
}
|
||||
parts := strings.Split(p.Items, ",")
|
||||
ret := make([]int, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
i, _ := strconv.Atoi(p)
|
||||
ret = append(ret, i)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *Playlist) SetItems(items []int) {
|
||||
strs := make([]string, 0, len(items))
|
||||
for _, p := range items {
|
||||
strs = append(strs, strconv.Itoa(p))
|
||||
}
|
||||
p.TrackCount = len(items)
|
||||
p.Items = strings.Join(strs, ",")
|
||||
}
|
||||
|
||||
@@ -22,13 +22,11 @@ func (c *Controller) ServeLogin(r *http.Request) *Response {
|
||||
|
||||
func (c *Controller) ServeHome(r *http.Request) *Response {
|
||||
data := &templateData{}
|
||||
//
|
||||
// stats box
|
||||
// ** begin stats box
|
||||
c.DB.Table("artists").Count(&data.ArtistCount)
|
||||
c.DB.Table("albums").Count(&data.AlbumCount)
|
||||
c.DB.Table("tracks").Count(&data.TrackCount)
|
||||
//
|
||||
// lastfm box
|
||||
// ** begin lastfm box
|
||||
scheme := firstExisting(
|
||||
"http", // fallback
|
||||
r.Header.Get("X-Forwarded-Proto"),
|
||||
@@ -42,11 +40,9 @@ func (c *Controller) ServeHome(r *http.Request) *Response {
|
||||
)
|
||||
data.RequestRoot = fmt.Sprintf("%s://%s", scheme, host)
|
||||
data.CurrentLastFMAPIKey = c.DB.GetSetting("lastfm_api_key")
|
||||
//
|
||||
// users box
|
||||
// ** begin users box
|
||||
c.DB.Find(&data.AllUsers)
|
||||
//
|
||||
// recent folders box
|
||||
// ** begin recent folders box
|
||||
c.DB.
|
||||
Where("tag_artist_id IS NOT NULL").
|
||||
Order("modified_at DESC").
|
||||
@@ -57,17 +53,10 @@ func (c *Controller) ServeHome(r *http.Request) *Response {
|
||||
i, _ := strconv.ParseInt(tStr, 10, 64)
|
||||
data.LastScanTime = time.Unix(i, 0)
|
||||
}
|
||||
//
|
||||
// playlists box
|
||||
// ** begin playlists box
|
||||
user := r.Context().Value(CtxUser).(*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)
|
||||
//
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"senan.xyz/g/gonic/model"
|
||||
)
|
||||
|
||||
func playlistParseLine(c *Controller, playlistID int, path string) error {
|
||||
func playlistParseLine(c *Controller, path string) (int, error) {
|
||||
if strings.HasPrefix(path, "#") || strings.TrimSpace(path) == "" {
|
||||
return nil
|
||||
return 0, nil
|
||||
}
|
||||
track := &model.Track{}
|
||||
var track model.Track
|
||||
query := c.DB.Raw(`
|
||||
SELECT tracks.id FROM TRACKS
|
||||
JOIN albums ON tracks.album_id = albums.id
|
||||
@@ -25,15 +25,12 @@ func playlistParseLine(c *Controller, playlistID int, path string) error {
|
||||
err := query.First(&track).Error
|
||||
switch {
|
||||
case gorm.IsRecordNotFoundError(err):
|
||||
return fmt.Errorf("couldn't match track %q", path)
|
||||
return 0, fmt.Errorf("couldn't match track %q", path)
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "while matching")
|
||||
return 0, errors.Wrap(err, "while matching")
|
||||
default:
|
||||
return track.ID, nil
|
||||
}
|
||||
c.DB.Create(&model.PlaylistItem{
|
||||
PlaylistID: playlistID,
|
||||
TrackID: track.ID,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func playlistParseUpload(c *Controller, userID int, header *multipart.FileHeader) ([]string, bool) {
|
||||
@@ -49,23 +46,28 @@ func playlistParseUpload(c *Controller, userID int, header *multipart.FileHeader
|
||||
if !(contentType == "audio/x-mpegurl" || contentType == "application/octet-stream") {
|
||||
return []string{fmt.Sprintf("invalid content-type %q", contentType)}, false
|
||||
}
|
||||
var trackIDs []int
|
||||
var errors []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
trackID, err := playlistParseLine(c, scanner.Text())
|
||||
if err != nil {
|
||||
// trim length of error to not overflow cookie flash
|
||||
errors = append(errors, fmt.Sprintf("%.100s", err.Error()))
|
||||
}
|
||||
if trackID != 0 {
|
||||
trackIDs = append(trackIDs, trackID)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return []string{fmt.Sprintf("iterating playlist file: %v", err)}, true
|
||||
}
|
||||
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 {
|
||||
// trim length of error to not overflow cookie flash
|
||||
errors = append(errors, fmt.Sprintf("%.100s", err.Error()))
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return []string{fmt.Sprintf("scanning line of playlist: %v", err)}, true
|
||||
}
|
||||
playlist.SetItems(trackIDs)
|
||||
c.DB.Save(playlist)
|
||||
return errors, true
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package ctrlsubsonic
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode"
|
||||
@@ -24,55 +25,6 @@ func lowerUDecOrHash(in string) string {
|
||||
return string(lower)
|
||||
}
|
||||
|
||||
type playlistOpValues struct {
|
||||
c *Controller
|
||||
r *http.Request
|
||||
user *model.User
|
||||
id int
|
||||
}
|
||||
|
||||
func playlistDelete(opts playlistOpValues) {
|
||||
indexes, ok := opts.r.URL.Query()["songIndexToRemove"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
trackIDs := []int{}
|
||||
opts.c.DB.
|
||||
Order("created_at").
|
||||
Model(&model.PlaylistItem{}).
|
||||
Where("playlist_id = ?", opts.id).
|
||||
Pluck("track_id", &trackIDs)
|
||||
for _, indexStr := range indexes {
|
||||
i, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
opts.c.DB.Delete(&model.PlaylistItem{},
|
||||
"track_id = ?", trackIDs[i])
|
||||
}
|
||||
}
|
||||
|
||||
func playlistAdd(opts playlistOpValues) {
|
||||
var toAdd []string
|
||||
for _, val := range []string{"songId", "songIdToAdd"} {
|
||||
var ok bool
|
||||
toAdd, ok = opts.r.URL.Query()[val]
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, trackIDStr := range toAdd {
|
||||
trackID, err := strconv.Atoi(trackIDStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
opts.c.DB.Save(&model.PlaylistItem{
|
||||
PlaylistID: opts.id,
|
||||
TrackID: trackID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetLicence(r *http.Request) *spec.Response {
|
||||
sub := spec.NewResponse()
|
||||
sub.Licence = &spec.Licence{
|
||||
@@ -170,9 +122,7 @@ func (c *Controller) ServeNotFound(r *http.Request) *spec.Response {
|
||||
func (c *Controller) ServeGetPlaylists(r *http.Request) *spec.Response {
|
||||
user := r.Context().Value(CtxUser).(*model.User)
|
||||
var playlists []*model.Playlist
|
||||
c.DB.
|
||||
Where("user_id = ?", user.ID).
|
||||
Find(&playlists)
|
||||
c.DB.Where("user_id = ?", user.ID).Find(&playlists)
|
||||
sub := spec.NewResponse()
|
||||
sub.Playlists = &spec.Playlists{
|
||||
List: make([]*spec.Playlist, len(playlists)),
|
||||
@@ -180,6 +130,7 @@ func (c *Controller) ServeGetPlaylists(r *http.Request) *spec.Response {
|
||||
for i, playlist := range playlists {
|
||||
sub.Playlists.List[i] = spec.NewPlaylist(playlist)
|
||||
sub.Playlists.List[i].Owner = user.Name
|
||||
sub.Playlists.List[i].SongCount = playlist.TrackCount
|
||||
}
|
||||
return sub
|
||||
}
|
||||
@@ -198,50 +149,58 @@ func (c *Controller) ServeGetPlaylist(r *http.Request) *spec.Response {
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
return spec.NewError(70, "playlist with id `%d` not found", playlistID)
|
||||
}
|
||||
var tracks []*model.Track
|
||||
c.DB.
|
||||
Joins(`
|
||||
JOIN playlist_items
|
||||
ON playlist_items.track_id = tracks.id
|
||||
`).
|
||||
Where("playlist_items.playlist_id = ?", playlistID).
|
||||
Group("tracks.id").
|
||||
Order("playlist_items.created_at").
|
||||
Preload("Album").
|
||||
Find(&tracks)
|
||||
user := r.Context().Value(CtxUser).(*model.User)
|
||||
sub := spec.NewResponse()
|
||||
sub.Playlist = spec.NewPlaylist(&playlist)
|
||||
sub.Playlist.Owner = user.Name
|
||||
sub.Playlist.List = make([]*spec.TrackChild, len(tracks))
|
||||
for i, track := range tracks {
|
||||
sub.Playlist.List[i] = spec.NewTCTrackByFolder(track, track.Album)
|
||||
sub.Playlist.SongCount = playlist.TrackCount
|
||||
trackIDs := playlist.GetItems()
|
||||
sub.Playlist.List = make([]*spec.TrackChild, len(trackIDs))
|
||||
for i, id := range trackIDs {
|
||||
track := model.Track{}
|
||||
c.DB.
|
||||
Where("id = ?", id).
|
||||
Preload("Album").
|
||||
Find(&track)
|
||||
sub.Playlist.List[i] = spec.NewTCTrackByFolder(&track, track.Album)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response {
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
user := r.Context().Value(CtxUser).(*model.User)
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
var playlistID int
|
||||
for _, key := range []string{"id", "playlistId"} {
|
||||
if val, err := params.GetInt(key); err != nil {
|
||||
playlistID = val
|
||||
}
|
||||
if p := params.GetFirstList("id", "playlistId"); p != nil {
|
||||
playlistID, _ = strconv.Atoi(p[0])
|
||||
}
|
||||
playlist := model.Playlist{ID: playlistID}
|
||||
c.DB.Where(playlist).First(&playlist)
|
||||
// playlistID may be 0 from above. in that case we get a new playlist
|
||||
// as intended
|
||||
playlist := &model.Playlist{ID: playlistID}
|
||||
c.DB.Where(playlist).First(playlist)
|
||||
// ** begin update meta info
|
||||
playlist.UserID = user.ID
|
||||
if val := r.URL.Query().Get("name"); val != "" {
|
||||
if val := params.Get("name"); val != "" {
|
||||
playlist.Name = val
|
||||
}
|
||||
if val := r.URL.Query().Get("comment"); val != "" {
|
||||
if val := params.Get("comment"); val != "" {
|
||||
playlist.Comment = val
|
||||
}
|
||||
c.DB.Save(&playlist)
|
||||
opts := playlistOpValues{c, r, user, playlist.ID}
|
||||
playlistDelete(opts)
|
||||
playlistAdd(opts)
|
||||
trackIDs := playlist.GetItems()
|
||||
// ** begin delete items
|
||||
if p := params.GetFirstListInt("songIndexToRemove"); p != nil {
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(trackIDs)))
|
||||
for _, i := range p {
|
||||
trackIDs = append(trackIDs[:i], trackIDs[i+1:]...)
|
||||
}
|
||||
}
|
||||
// ** begin add items
|
||||
if p := params.GetFirstListInt("songId", "songIdToAdd"); p != nil {
|
||||
trackIDs = append(trackIDs, p...)
|
||||
}
|
||||
//
|
||||
playlist.SetItems(trackIDs)
|
||||
c.DB.Save(playlist)
|
||||
return spec.NewResponse()
|
||||
}
|
||||
|
||||
|
||||
@@ -55,3 +55,25 @@ func (p Params) GetIntOr(key string, or int) int {
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (p Params) GetFirstList(keys ...string) []string {
|
||||
for _, key := range keys {
|
||||
if v, ok := p.values[key]; ok && len(v) > 0 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Params) GetFirstListInt(keys ...string) []int {
|
||||
v := p.GetFirstList(keys...)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
ret := make([]int, 0, len(v))
|
||||
for _, p := range v {
|
||||
i, _ := strconv.Atoi(p)
|
||||
ret = append(ret, i)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
|
||||
func NewAlbumByFolder(f *model.Album) *Album {
|
||||
return &Album{
|
||||
Artist: f.Parent.RightPath,
|
||||
CoverID: f.ID,
|
||||
ID: f.ID,
|
||||
IsDir: true,
|
||||
ParentID: f.ParentID,
|
||||
Title: f.RightPath,
|
||||
Artist: f.Parent.RightPath,
|
||||
CoverID: f.ID,
|
||||
ID: f.ID,
|
||||
IsDir: true,
|
||||
ParentID: f.ParentID,
|
||||
Title: f.RightPath,
|
||||
TrackCount: f.ChildCount,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
|
||||
func NewAlbumByTags(a *model.Album, artist *model.Artist) *Album {
|
||||
ret := &Album{
|
||||
Created: a.ModifiedAt,
|
||||
ID: a.ID,
|
||||
Name: a.TagTitle,
|
||||
Created: a.ModifiedAt,
|
||||
ID: a.ID,
|
||||
Name: a.TagTitle,
|
||||
TrackCount: a.ChildCount,
|
||||
}
|
||||
if a.Cover != "" {
|
||||
ret.CoverID = a.ID
|
||||
|
||||
Reference in New Issue
Block a user