diff --git a/db/migrations.go b/db/migrations.go index a84025b..ae180aa 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -45,6 +45,7 @@ func (db *DB) Migrate(ctx MigrationContext) error { construct(ctx, "202206011628", migrateInternetRadioStations), construct(ctx, "202206101425", migrateUser), construct(ctx, "202207251148", migrateStarRating), + construct(ctx, "202211111057", migratePlaylistsQueuesToFullID), } return gormigrate. @@ -386,3 +387,57 @@ func migrateStarRating(tx *gorm.DB, _ MigrationContext) 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 +} diff --git a/db/model.go b/db/model.go index 5831bb8..b552012 100644 --- a/db/model.go +++ b/db/model.go @@ -9,7 +9,6 @@ package db import ( "path" "path/filepath" - "strconv" "strings" "time" @@ -19,26 +18,26 @@ import ( "go.senan.xyz/gonic/server/ctrlsubsonic/specid" ) -func splitInt(in, sep string) []int { +func splitIDs(in, sep string) []specid.ID { if in == "" { - return []int{} + return []specid.ID{} } parts := strings.Split(in, sep) - ret := make([]int, 0, len(parts)) + ret := make([]specid.ID, 0, len(parts)) for _, p := range parts { - i, _ := strconv.Atoi(p) - ret = append(ret, i) + id, _ := specid.New(p) + ret = append(ret, id) } return ret } -func joinInt(in []int, sep string) string { +func joinIds(in []specid.ID, sep string) string { if in == nil { return "" } strs := make([]string, 0, len(in)) - for _, i := range in { - strs = append(strs, strconv.Itoa(i)) + for _, id := range in { + strs = append(strs, id.String()) } return strings.Join(strs, sep) } @@ -256,12 +255,12 @@ type Playlist struct { IsPublic bool `sql:"default: null"` } -func (p *Playlist) GetItems() []int { - return splitInt(p.Items, ",") +func (p *Playlist) GetItems() []specid.ID { + return splitIDs(p.Items, ",") } -func (p *Playlist) SetItems(items []int) { - p.Items = joinInt(items, ",") +func (p *Playlist) SetItems(items []specid.ID) { + p.Items = joinIds(items, ",") p.TrackCount = len(items) } @@ -271,22 +270,23 @@ type PlayQueue struct { UpdatedAt time.Time User *User UserID int `sql:"default: null; type:int REFERENCES users(id) ON DELETE CASCADE"` - Current int + Current string Position int ChangedBy string Items string } 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 { - return splitInt(p.Items, ",") +func (p *PlayQueue) GetItems() []specid.ID { + return splitIDs(p.Items, ",") } -func (p *PlayQueue) SetItems(items []int) { - p.Items = joinInt(items, ",") +func (p *PlayQueue) SetItems(items []specid.ID) { + p.Items = joinIds(items, ",") } type TranscodePreference struct { diff --git a/server/ctrladmin/handlers_playlist.go b/server/ctrladmin/handlers_playlist.go index 656be10..4074378 100644 --- a/server/ctrladmin/handlers_playlist.go +++ b/server/ctrladmin/handlers_playlist.go @@ -13,15 +13,16 @@ import ( "github.com/jinzhu/gorm" "go.senan.xyz/gonic/db" + "go.senan.xyz/gonic/server/ctrlsubsonic/specid" ) var ( 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) == "" { - return 0, nil + return nil, nil } var track db.Track 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)=?`, string(os.PathSeparator), string(os.PathSeparator), absPath) err := query.First(&track).Error - switch { - case errors.Is(err, gorm.ErrRecordNotFound): - 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 err == nil { + return &specid.ID{Type: specid.Track, Value: 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 { @@ -65,7 +75,7 @@ func playlistParseUpload(c *Controller, userID int, header *multipart.FileHeader if !playlistCheckContentType(contentType) { return []string{fmt.Sprintf("invalid content-type %q", contentType)}, false } - var trackIDs []int + var trackIDs []specid.ID var errors []string scanner := bufio.NewScanner(file) 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 errors = append(errors, fmt.Sprintf("%.100s", err.Error())) } - if trackID != 0 { - trackIDs = append(trackIDs, trackID) + if trackID.Value != 0 { + trackIDs = append(trackIDs, *trackID) } } if err := scanner.Err(); err != nil { diff --git a/server/ctrlsubsonic/handlers_common.go b/server/ctrlsubsonic/handlers_common.go index b2f03fe..7de1b41 100644 --- a/server/ctrlsubsonic/handlers_common.go +++ b/server/ctrlsubsonic/handlers_common.go @@ -167,16 +167,31 @@ func (c *Controller) ServeGetPlayQueue(r *http.Request) *spec.Response { transcodeMIME, transcodeSuffix := streamGetTransPrefProfile(c.DB, user.ID, params.GetOr("c", "")) for i, id := range trackIDs { - track := db.Track{} - c.DB. - Where("id=?", id). - Preload("Album"). - Preload("TrackStar", "user_id=?", user.ID). - Preload("TrackRating", "user_id=?", user.ID). - Find(&track) - sub.PlayQueue.List[i] = spec.NewTCTrackByFolder(&track, track.Album) - sub.PlayQueue.List[i].TranscodedContentType = transcodeMIME - sub.PlayQueue.List[i].TranscodedSuffix = transcodeSuffix + switch id.Type { + case specid.Track: + track := db.Track{} + c.DB. + Where("id=?", id.Value). + Preload("Album"). + Preload("TrackStar", "user_id=?", user.ID). + Preload("TrackRating", "user_id=?", user.ID). + Find(&track) + 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 } @@ -187,11 +202,10 @@ func (c *Controller) ServeSavePlayQueue(r *http.Request) *spec.Response { if err != nil { return spec.NewError(10, "please provide some `id` parameters") } - // TODO: support other play queue entries other than tracks - trackIDs := make([]int, 0, len(tracks)) + trackIDs := make([]specid.ID, 0, len(tracks)) for _, id := range tracks { - if id.Type == specid.Track { - trackIDs = append(trackIDs, id.Value) + if (id.Type == specid.Track) || (id.Type == specid.PodcastEpisode) { + trackIDs = append(trackIDs, id) } } if len(trackIDs) == 0 { @@ -201,7 +215,7 @@ func (c *Controller) ServeSavePlayQueue(r *http.Request) *spec.Response { var queue db.PlayQueue c.DB.Where("user_id=?", user.ID).First(&queue) 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.ChangedBy = params.GetOr("c", "") // must exist, middleware checks queue.SetItems(trackIDs) diff --git a/server/ctrlsubsonic/handlers_playlist.go b/server/ctrlsubsonic/handlers_playlist.go index 7e77b0e..afabd12 100644 --- a/server/ctrlsubsonic/handlers_playlist.go +++ b/server/ctrlsubsonic/handlers_playlist.go @@ -11,6 +11,7 @@ import ( "go.senan.xyz/gonic/db" "go.senan.xyz/gonic/server/ctrlsubsonic/params" "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 { @@ -33,23 +34,47 @@ func playlistRender(c *Controller, playlist *db.Playlist, params params.Params) transcodeMIME, transcodeSuffix := streamGetTransPrefProfile(c.DB, user.ID, params.GetOr("c", "")) for i, id := range trackIDs { - track := db.Track{} - err := c.DB. - Where("id=?", id). - Preload("Album"). - Preload("Album.TagArtist"). - Preload("TrackStar", "user_id=?", user.ID). - Preload("TrackRating", "user_id=?", user.ID). - Find(&track). - Error - if errors.Is(err, gorm.ErrRecordNotFound) { - log.Printf("wasn't able to find track with id %d", id) - continue + switch id.Type { + case specid.Track: + track := db.Track{} + err := c.DB. + Where("id=?", id.Value). + Preload("Album"). + Preload("Album.TagArtist"). + Preload("TrackStar", "user_id=?", user.ID). + Preload("TrackRating", "user_id=?", user.ID). + Find(&track). + Error + 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].TranscodedSuffix = transcodeSuffix - resp.Duration += track.Length } return resp } @@ -109,12 +134,7 @@ func (c *Controller) ServeCreatePlaylist(r *http.Request) *spec.Response { } // replace song IDs - var trackIDs []int - if p, err := params.GetIDList("songId"); err == nil { - for _, i := range p { - trackIDs = append(trackIDs, i.Value) - } - } + trackIDs, _ := params.GetIDList("songId") // Set the items of the playlist playlist.SetItems(trackIDs) c.DB.Save(playlist) @@ -161,9 +181,7 @@ func (c *Controller) ServeUpdatePlaylist(r *http.Request) *spec.Response { // add items if p, err := params.GetIDList("songIdToAdd"); err == nil { - for _, i := range p { - trackIDs = append(trackIDs, i.Value) - } + trackIDs = append(trackIDs, p...) } playlist.SetItems(trackIDs) diff --git a/server/ctrlsubsonic/spec/construct_by_folder.go b/server/ctrlsubsonic/spec/construct_by_folder.go index 02e140b..9cc65a6 100644 --- a/server/ctrlsubsonic/spec/construct_by_folder.go +++ b/server/ctrlsubsonic/spec/construct_by_folder.go @@ -95,6 +95,24 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild { 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 { // the db is structued around "browse by tags", and where // an album is also a folder. so we're constructing an artist