feat(subsonic): add support for podcast episodes in both playlists and play queues

This commit is contained in:
Brian Doherty
2022-11-11 11:29:27 -06:00
committed by sentriz
parent ae5bc2e149
commit aecee3d2d8
6 changed files with 184 additions and 69 deletions

View File

@@ -45,6 +45,7 @@ func (db *DB) Migrate(ctx MigrationContext) error {
construct(ctx, "202206011628", migrateInternetRadioStations), construct(ctx, "202206011628", migrateInternetRadioStations),
construct(ctx, "202206101425", migrateUser), construct(ctx, "202206101425", migrateUser),
construct(ctx, "202207251148", migrateStarRating), construct(ctx, "202207251148", migrateStarRating),
construct(ctx, "202211111057", migratePlaylistsQueuesToFullID),
} }
return gormigrate. return gormigrate.
@@ -386,3 +387,57 @@ func migrateStarRating(tx *gorm.DB, _ MigrationContext) error {
). ).
Error Error
} }
func migratePlaylistsQueuesToFullID(tx *gorm.DB, _ MigrationContext) error {
step := tx.Exec(`
UPDATE playlists SET items=('tr-' || items) WHERE items IS NOT NULL;
`)
if err := step.Error; err != nil {
return fmt.Errorf("step migrate playlists to full id: %w", err)
}
step = tx.Exec(`
UPDATE playlists SET items=REPLACE(items,',',',tr-') WHERE items IS NOT NULL;
`)
if err := step.Error; err != nil {
return fmt.Errorf("step migrate playlists to full id: %w", err)
}
step = tx.Exec(`
UPDATE play_queues SET items=('tr-' || items) WHERE items IS NOT NULL;
`)
if err := step.Error; err != nil {
return fmt.Errorf("step migrate play_queues to full id: %w", err)
}
step = tx.Exec(`
UPDATE play_queues SET items=REPLACE(items,',',',tr-') WHERE items IS NOT NULL;
`)
if err := step.Error; err != nil {
return fmt.Errorf("step migrate play_queues to full id: %w", err)
}
step = tx.Exec(`
ALTER TABLE play_queues ADD COLUMN newcurrent varchar[255];
`)
if err := step.Error; err != nil {
return fmt.Errorf("step migrate play_queues to full id: %w", err)
}
step = tx.Exec(`
UPDATE play_queues SET newcurrent=('tr-' || CAST(current AS varchar(10)));
`)
if err := step.Error; err != nil {
return fmt.Errorf("step migrate play_queues to full id: %w", err)
}
step = tx.Exec(`
ALTER TABLE play_queues DROP COLUMN current;
`)
if err := step.Error; err != nil {
return fmt.Errorf("step migrate play_queues to full id: %w", err)
}
step = tx.Exec(`
ALTER TABLE play_queues RENAME COLUMN newcurrent TO "current";
`)
if err := step.Error; err != nil {
return fmt.Errorf("step migrate play_queues to full id: %w", err)
}
return nil
}

View File

@@ -9,7 +9,6 @@ package db
import ( import (
"path" "path"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
@@ -19,26 +18,26 @@ import (
"go.senan.xyz/gonic/server/ctrlsubsonic/specid" "go.senan.xyz/gonic/server/ctrlsubsonic/specid"
) )
func splitInt(in, sep string) []int { func splitIDs(in, sep string) []specid.ID {
if in == "" { if in == "" {
return []int{} return []specid.ID{}
} }
parts := strings.Split(in, sep) parts := strings.Split(in, sep)
ret := make([]int, 0, len(parts)) ret := make([]specid.ID, 0, len(parts))
for _, p := range parts { for _, p := range parts {
i, _ := strconv.Atoi(p) id, _ := specid.New(p)
ret = append(ret, i) ret = append(ret, id)
} }
return ret return ret
} }
func joinInt(in []int, sep string) string { func joinIds(in []specid.ID, sep string) string {
if in == nil { if in == nil {
return "" return ""
} }
strs := make([]string, 0, len(in)) strs := make([]string, 0, len(in))
for _, i := range in { for _, id := range in {
strs = append(strs, strconv.Itoa(i)) strs = append(strs, id.String())
} }
return strings.Join(strs, sep) return strings.Join(strs, sep)
} }
@@ -256,12 +255,12 @@ type Playlist struct {
IsPublic bool `sql:"default: null"` IsPublic bool `sql:"default: null"`
} }
func (p *Playlist) GetItems() []int { func (p *Playlist) GetItems() []specid.ID {
return splitInt(p.Items, ",") return splitIDs(p.Items, ",")
} }
func (p *Playlist) SetItems(items []int) { func (p *Playlist) SetItems(items []specid.ID) {
p.Items = joinInt(items, ",") p.Items = joinIds(items, ",")
p.TrackCount = len(items) p.TrackCount = len(items)
} }
@@ -271,22 +270,23 @@ type PlayQueue struct {
UpdatedAt time.Time UpdatedAt 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"`
Current int Current string
Position int Position int
ChangedBy string ChangedBy string
Items string Items string
} }
func (p *PlayQueue) CurrentSID() *specid.ID { func (p *PlayQueue) CurrentSID() *specid.ID {
return &specid.ID{Type: specid.Track, Value: p.Current} id, _ := specid.New(p.Current)
return &id
} }
func (p *PlayQueue) GetItems() []int { func (p *PlayQueue) GetItems() []specid.ID {
return splitInt(p.Items, ",") return splitIDs(p.Items, ",")
} }
func (p *PlayQueue) SetItems(items []int) { func (p *PlayQueue) SetItems(items []specid.ID) {
p.Items = joinInt(items, ",") p.Items = joinIds(items, ",")
} }
type TranscodePreference struct { type TranscodePreference struct {

View File

@@ -13,15 +13,16 @@ import (
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"go.senan.xyz/gonic/db" "go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
) )
var ( var (
errPlaylistNoMatch = errors.New("couldn't match track") errPlaylistNoMatch = errors.New("couldn't match track")
) )
func playlistParseLine(c *Controller, absPath string) (int, error) { func playlistParseLine(c *Controller, absPath string) (*specid.ID, error) {
if strings.HasPrefix(absPath, "#") || strings.TrimSpace(absPath) == "" { if strings.HasPrefix(absPath, "#") || strings.TrimSpace(absPath) == "" {
return 0, nil return nil, nil
} }
var track db.Track var track db.Track
query := c.DB.Raw(` query := c.DB.Raw(`
@@ -30,14 +31,23 @@ func playlistParseLine(c *Controller, absPath string) (int, error) {
WHERE (albums.root_dir || ? || albums.left_path || albums.right_path || ? || tracks.filename)=?`, WHERE (albums.root_dir || ? || albums.left_path || albums.right_path || ? || tracks.filename)=?`,
string(os.PathSeparator), string(os.PathSeparator), absPath) string(os.PathSeparator), string(os.PathSeparator), absPath)
err := query.First(&track).Error err := query.First(&track).Error
switch { if err == nil {
case errors.Is(err, gorm.ErrRecordNotFound): return &specid.ID{Type: specid.Track, Value: track.ID}, nil
return 0, fmt.Errorf("%v: %w", err, errPlaylistNoMatch)
case err != nil:
return 0, fmt.Errorf("while matching: %w", err)
default:
return track.ID, nil
} }
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("while matching: %w", err)
}
var pe db.PodcastEpisode
err = c.DB.Where("path=?", absPath).First(&pe).Error
if err == nil {
return &specid.ID{Type: specid.PodcastEpisode, Value: pe.ID}, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("while matching: %w", err)
}
return nil, fmt.Errorf("%v: %w", err, errPlaylistNoMatch)
} }
func playlistCheckContentType(contentType string) bool { func playlistCheckContentType(contentType string) bool {
@@ -65,7 +75,7 @@ func playlistParseUpload(c *Controller, userID int, header *multipart.FileHeader
if !playlistCheckContentType(contentType) { if !playlistCheckContentType(contentType) {
return []string{fmt.Sprintf("invalid content-type %q", contentType)}, false return []string{fmt.Sprintf("invalid content-type %q", contentType)}, false
} }
var trackIDs []int var trackIDs []specid.ID
var errors []string var errors []string
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
@@ -74,8 +84,8 @@ func playlistParseUpload(c *Controller, userID int, header *multipart.FileHeader
// trim length of error to not overflow cookie flash // trim length of error to not overflow cookie flash
errors = append(errors, fmt.Sprintf("%.100s", err.Error())) errors = append(errors, fmt.Sprintf("%.100s", err.Error()))
} }
if trackID != 0 { if trackID.Value != 0 {
trackIDs = append(trackIDs, trackID) trackIDs = append(trackIDs, *trackID)
} }
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {

View File

@@ -167,16 +167,31 @@ func (c *Controller) ServeGetPlayQueue(r *http.Request) *spec.Response {
transcodeMIME, transcodeSuffix := streamGetTransPrefProfile(c.DB, user.ID, params.GetOr("c", "")) transcodeMIME, transcodeSuffix := streamGetTransPrefProfile(c.DB, user.ID, params.GetOr("c", ""))
for i, id := range trackIDs { for i, id := range trackIDs {
track := db.Track{} switch id.Type {
c.DB. case specid.Track:
Where("id=?", id). track := db.Track{}
Preload("Album"). c.DB.
Preload("TrackStar", "user_id=?", user.ID). Where("id=?", id.Value).
Preload("TrackRating", "user_id=?", user.ID). Preload("Album").
Find(&track) Preload("TrackStar", "user_id=?", user.ID).
sub.PlayQueue.List[i] = spec.NewTCTrackByFolder(&track, track.Album) Preload("TrackRating", "user_id=?", user.ID).
sub.PlayQueue.List[i].TranscodedContentType = transcodeMIME Find(&track)
sub.PlayQueue.List[i].TranscodedSuffix = transcodeSuffix sub.PlayQueue.List[i] = spec.NewTCTrackByFolder(&track, track.Album)
sub.PlayQueue.List[i].TranscodedContentType = transcodeMIME
sub.PlayQueue.List[i].TranscodedSuffix = transcodeSuffix
case specid.PodcastEpisode:
pe := db.PodcastEpisode{}
c.DB.
Where("id=?", id.Value).
Find(&pe)
p := db.Podcast{}
c.DB.
Where("id=?", pe.PodcastID).
Find(&p)
sub.PlayQueue.List[i] = spec.NewTCPodcastEpisode(&pe, &p)
sub.PlayQueue.List[i].TranscodedContentType = transcodeMIME
sub.PlayQueue.List[i].TranscodedSuffix = transcodeSuffix
}
} }
return sub return sub
} }
@@ -187,11 +202,10 @@ 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([]specid.ID, 0, len(tracks))
trackIDs := make([]int, 0, len(tracks))
for _, id := range tracks { for _, id := range tracks {
if id.Type == specid.Track { if (id.Type == specid.Track) || (id.Type == specid.PodcastEpisode) {
trackIDs = append(trackIDs, id.Value) trackIDs = append(trackIDs, id)
} }
} }
if len(trackIDs) == 0 { if len(trackIDs) == 0 {
@@ -201,7 +215,7 @@ func (c *Controller) ServeSavePlayQueue(r *http.Request) *spec.Response {
var queue db.PlayQueue var queue db.PlayQueue
c.DB.Where("user_id=?", user.ID).First(&queue) c.DB.Where("user_id=?", user.ID).First(&queue)
queue.UserID = user.ID queue.UserID = user.ID
queue.Current = params.GetOrID("current", specid.ID{}).Value queue.Current = params.GetOrID("current", specid.ID{}).String()
queue.Position = params.GetOrInt("position", 0) queue.Position = params.GetOrInt("position", 0)
queue.ChangedBy = params.GetOr("c", "") // must exist, middleware checks queue.ChangedBy = params.GetOr("c", "") // must exist, middleware checks
queue.SetItems(trackIDs) queue.SetItems(trackIDs)

View File

@@ -11,6 +11,7 @@ import (
"go.senan.xyz/gonic/db" "go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/server/ctrlsubsonic/params" "go.senan.xyz/gonic/server/ctrlsubsonic/params"
"go.senan.xyz/gonic/server/ctrlsubsonic/spec" "go.senan.xyz/gonic/server/ctrlsubsonic/spec"
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
) )
func playlistRender(c *Controller, playlist *db.Playlist, params params.Params) *spec.Playlist { func playlistRender(c *Controller, playlist *db.Playlist, params params.Params) *spec.Playlist {
@@ -33,23 +34,47 @@ func playlistRender(c *Controller, playlist *db.Playlist, params params.Params)
transcodeMIME, transcodeSuffix := streamGetTransPrefProfile(c.DB, user.ID, params.GetOr("c", "")) transcodeMIME, transcodeSuffix := streamGetTransPrefProfile(c.DB, user.ID, params.GetOr("c", ""))
for i, id := range trackIDs { for i, id := range trackIDs {
track := db.Track{} switch id.Type {
err := c.DB. case specid.Track:
Where("id=?", id). track := db.Track{}
Preload("Album"). err := c.DB.
Preload("Album.TagArtist"). Where("id=?", id.Value).
Preload("TrackStar", "user_id=?", user.ID). Preload("Album").
Preload("TrackRating", "user_id=?", user.ID). Preload("Album.TagArtist").
Find(&track). Preload("TrackStar", "user_id=?", user.ID).
Error Preload("TrackRating", "user_id=?", user.ID).
if errors.Is(err, gorm.ErrRecordNotFound) { Find(&track).
log.Printf("wasn't able to find track with id %d", id) Error
continue if errors.Is(err, gorm.ErrRecordNotFound) {
log.Printf("wasn't able to find track with id %d", id.Value)
continue
}
resp.List[i] = spec.NewTCTrackByFolder(&track, track.Album)
resp.Duration += track.Length
case specid.PodcastEpisode:
pe := db.PodcastEpisode{}
err := c.DB.
Where("id=?", id.Value).
Find(&pe).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Printf("wasn't able to find podcast episode with id %d", id.Value)
continue
}
p := db.Podcast{}
err = c.DB.
Where("id=?", pe.PodcastID).
Find(&p).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
log.Printf("wasn't able to find podcast with id %d", pe.PodcastID)
continue
}
resp.List[i] = spec.NewTCPodcastEpisode(&pe, &p)
resp.Duration += pe.Length
} }
resp.List[i] = spec.NewTCTrackByFolder(&track, track.Album)
resp.List[i].TranscodedContentType = transcodeMIME resp.List[i].TranscodedContentType = transcodeMIME
resp.List[i].TranscodedSuffix = transcodeSuffix resp.List[i].TranscodedSuffix = transcodeSuffix
resp.Duration += track.Length
} }
return resp return resp
} }
@@ -109,12 +134,7 @@ func (c *Controller) ServeCreatePlaylist(r *http.Request) *spec.Response {
} }
// replace song IDs // replace song IDs
var trackIDs []int trackIDs, _ := params.GetIDList("songId")
if p, err := params.GetIDList("songId"); err == nil {
for _, i := range p {
trackIDs = append(trackIDs, i.Value)
}
}
// Set the items of the playlist // Set the items of the playlist
playlist.SetItems(trackIDs) playlist.SetItems(trackIDs)
c.DB.Save(playlist) c.DB.Save(playlist)
@@ -161,9 +181,7 @@ func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response {
// add items // add items
if p, err := params.GetIDList("songIdToAdd"); err == nil { if p, err := params.GetIDList("songIdToAdd"); err == nil {
for _, i := range p { trackIDs = append(trackIDs, p...)
trackIDs = append(trackIDs, i.Value)
}
} }
playlist.SetItems(trackIDs) playlist.SetItems(trackIDs)

View File

@@ -95,6 +95,24 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild {
return trCh return trCh
} }
func NewTCPodcastEpisode(pe *db.PodcastEpisode, parent *db.Podcast) *TrackChild {
trCh := &TrackChild{
ID: pe.SID(),
ContentType: pe.MIME(),
Suffix: pe.Ext(),
Size: pe.Size,
Title: pe.Title,
Path: pe.Path,
ParentID: parent.SID(),
Duration: pe.Length,
Bitrate: pe.Bitrate,
IsDir: false,
Type: "podcastepisode",
CreatedAt: pe.CreatedAt,
}
return trCh
}
func NewArtistByFolder(f *db.Album) *Artist { func NewArtistByFolder(f *db.Album) *Artist {
// the db is structued around "browse by tags", and where // the db is structued around "browse by tags", and where
// an album is also a folder. so we're constructing an artist // an album is also a folder. so we're constructing an artist