Jukebox mode (#2289)
* Adding cache directory to ignore-list
* Adding jukebox-related config options
* Adding DevEnableJukebox config option pls. dummy server
* Adding types and routers
* Now without panic
* First draft on parsing the action
* Some cleanups
* Adding playback server
* Verify audio device configuration
* Adding debug-build target to have full symbol support
* Adding beep sound library pls some example code. Not working yet
* Play a fixed mp3 on any interface access for testing purposes
* Put action code into separate file, adding stringer, more debug output, prepare structs, validation
* Put action parameter parser code where it belongs
* Have a single Action transporting all information
* User fmt.Errorf for error-generation
* Adding wide playback interface
* Use action map for parsing, stringer instead switch stmt.
* Use but only one switch case and direct dispatch, refactoring
* Add error handling and pushing to client
* send decent errormessage, no internal server error
* Adding playback devices slice and load it from config
* Combine config-verification and structure init
* Return user-specific device
* Separate playback server from device
* Use dataStore to retrieve mediafile by id
* WIP: Playlist and start/stop handling. Doing start/stop the hard way as of now
* WIP: set, start and stop work on one single song. More to come
* Dont need to wait for the end
* Merge jukebox_action.go into jukebox.go
* Remove getParameterAsInt64(). Use existing requiredParamInt() instead
* Dont need to call newFailure() explicitly
* Remove int64, use int instead.
* Add and set action now accept multiple ids
* Kickout copy of childFromMediaFile(). It is not needed here.
* Refactoring devices and playbackServer
* Turn (internal) playback.DeviceStatus into subsonic JukeboxStatus when rendering output. Indexes int64 -> int
* Now we have a position and playing status
* Switching gain to float32, xs:float is defined as 32 bit. Fixing nasty copy/pointer bug
* Now with volume control
* Start working the queue
* Remove user from device interface
* Rename function GetDevice -> GetDeviceForUser to make intention clearer
* Have a nice stringer for the queue
* User Prepared boolean for now to allow pause/unpause
* Skipping works, but without offsets
* Make ChildFromMediaFile public to be used in jukebox get() implementation
* Return position in seconds and implement offset-skip in seconds
* Default offset to 0
* Adding a simple setGain implementation
* Prepare for transcoding AAC
* WIP: transcode to WAV to use beeps wav decoder. Not done yet.
* WIP: out of sheer desparation: convert to MP3 (which works) rather than WAV to troubleshoot issue.
* Use FLAC as intermediate format to play Apple AAC
* A bit of cleanup
* Catching the end-of-stream event for further reactions
* Have a trackSwitching goroutine waiting on channel when track ends
* Move decoder code into own file. Restructure code a bit
* Now with going on to play the next song in the playlist
* Adding shuffle feature
* Implementing remove action
* Cleanup code
* Remove templates for ffmpeg mp3 generation. Not needed anymore.
* Adding some documentation
* Check whether offset into track is in range. Fixing potential remove track bug. Documentation
* Make golangci-lint happy: handling return values
* Adding test suite and example dummy for playback package
* Adding some basic queue tests
* Only use Jukebox.Enabled config option
* Adding stream closing handling
* Pass context.Context to all PlaybackDevice methods
* Remove unneeded function
* Correct spelling
* Reduce visibility of ChildFromMediaFile
* Decomplicate action-parsing
* Adding simple tempfile-based AAC->FLAC transcoding. No parallel reading and writing yet.
* Try to optimize pipe-writing, tempfile-handling and reading. Not done yet.
* Do a synchronous copy of the tempfile. Racecondition detected
* More debugging statements and fixing the play/pause bug. More work needed
* Start the trackSwitcher() with each device once. Return JSON position even if its 0. More debug-output
* Moving all track-handling code into own module
* Fix typo. Do not pass ctx around when not applicable
* WIP: More refactoring, debugging output
* Fix nil pointer
* Repairing MP3 playback by pinning indirect dependencies: hajimehoshi/go-mp3 and hajimehoshi/oto
* Do not forget to cleanup after a skip action
* Make resync with master easy
* Adding missing mocks
* Adding missing error-handling found by linter
* Updating github.com/hajimehoshi/oto
* Removing duplicate function
* Move BEEP-related code into own package
* Juggle beep-related code around as preparation for interface access
* More refactoring for interface separation
* Gather CloseDevice() behind Track interface.
* Adding skeleton, draft audio-interface using mpv.io
* Adding majority of interface commands using messages to mpv socket.
* Adding end-of-stream handling
* MPV: start/stop are working
* postition is given in float in mpv
* Unify Close() and CloseDevice(). Using temp filename for controlling socket
* Wait until control-socket shows up. Cleanup socket in Close()
* Use canceable command. Rename to Executor
* Skipping tracks works now
* Now with actually setting the position
* Fix regain
* Add missing error-handling found by linter
* Adding retry mode on time-pos property getter
* Remove unneeded code on queue
* Putting build-tag beep onto beep files
* Remove deprecated call to rand.Seed()
"As of Go 1.20 there is no reason to call Seed with a random value. Programs that call Seed with a known value to get a specific sequence of results should use New(NewSource(seed)) to obtain a local random generator."
* Using int32 to conform to Subsonic API spec
* Fix merge error
* Minor style changes
* Get username from context
---------
Co-authored-by: Deluan <deluan@navidrome.org>
2023-09-10 07:25:22 -08:00
|
|
|
package playback
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"math/rand"
|
|
|
|
|
|
|
|
|
|
"github.com/navidrome/navidrome/log"
|
|
|
|
|
"github.com/navidrome/navidrome/model"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Queue struct {
|
|
|
|
|
Index int
|
|
|
|
|
Items model.MediaFiles
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewQueue() *Queue {
|
|
|
|
|
return &Queue{
|
|
|
|
|
Index: -1,
|
|
|
|
|
Items: model.MediaFiles{},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (pd *Queue) String() string {
|
|
|
|
|
filenames := ""
|
|
|
|
|
for idx, item := range pd.Items {
|
|
|
|
|
filenames += fmt.Sprint(idx) + ":" + item.Path + " "
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("#Items: %d, idx: %d, files: %s", len(pd.Items), pd.Index, filenames)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// returns the current mediafile or nil
|
|
|
|
|
func (pd *Queue) Current() *model.MediaFile {
|
|
|
|
|
if pd.Index == -1 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if pd.Index >= len(pd.Items) {
|
|
|
|
|
log.Error("internal error: current song index out of bounds", "idx", pd.Index, "length", len(pd.Items))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &pd.Items[pd.Index]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// returns the whole queue
|
|
|
|
|
func (pd *Queue) Get() model.MediaFiles {
|
|
|
|
|
return pd.Items
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (pd *Queue) Size() int {
|
|
|
|
|
return len(pd.Items)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (pd *Queue) IsEmpty() bool {
|
|
|
|
|
return len(pd.Items) < 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// set is similar to a clear followed by a add, but will not change the currently playing track.
|
|
|
|
|
func (pd *Queue) Set(items model.MediaFiles) {
|
|
|
|
|
pd.Clear()
|
|
|
|
|
pd.Items = append(pd.Items, items...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// adding mediafiles to the queue
|
|
|
|
|
func (pd *Queue) Add(items model.MediaFiles) {
|
|
|
|
|
pd.Items = append(pd.Items, items...)
|
|
|
|
|
if pd.Index == -1 && len(pd.Items) > 0 {
|
|
|
|
|
pd.Index = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// empties whole queue
|
|
|
|
|
func (pd *Queue) Clear() {
|
|
|
|
|
pd.Index = -1
|
|
|
|
|
pd.Items = nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// idx Zero-based index of the song to skip to or remove.
|
|
|
|
|
func (pd *Queue) Remove(idx int) {
|
|
|
|
|
current := pd.Current()
|
|
|
|
|
backupID := ""
|
|
|
|
|
if current != nil {
|
|
|
|
|
backupID = current.ID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pd.Items = append(pd.Items[:idx], pd.Items[idx+1:]...)
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
pd.Index, err = pd.getMediaFileIndexByID(backupID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// we seem to have deleted the current id, setting to default:
|
|
|
|
|
pd.Index = -1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (pd *Queue) Shuffle() {
|
|
|
|
|
current := pd.Current()
|
|
|
|
|
backupID := ""
|
|
|
|
|
if current != nil {
|
|
|
|
|
backupID = current.ID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rand.Shuffle(len(pd.Items), func(i, j int) { pd.Items[i], pd.Items[j] = pd.Items[j], pd.Items[i] })
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
pd.Index, err = pd.getMediaFileIndexByID(backupID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error("Could not find ID while shuffling: " + backupID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (pd *Queue) getMediaFileIndexByID(id string) (int, error) {
|
|
|
|
|
for idx, item := range pd.Items {
|
|
|
|
|
if item.ID == id {
|
|
|
|
|
return idx, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-08-19 13:47:54 -08:00
|
|
|
return -1, fmt.Errorf("ID not found in playlist: %s", id)
|
Jukebox mode (#2289)
* Adding cache directory to ignore-list
* Adding jukebox-related config options
* Adding DevEnableJukebox config option pls. dummy server
* Adding types and routers
* Now without panic
* First draft on parsing the action
* Some cleanups
* Adding playback server
* Verify audio device configuration
* Adding debug-build target to have full symbol support
* Adding beep sound library pls some example code. Not working yet
* Play a fixed mp3 on any interface access for testing purposes
* Put action code into separate file, adding stringer, more debug output, prepare structs, validation
* Put action parameter parser code where it belongs
* Have a single Action transporting all information
* User fmt.Errorf for error-generation
* Adding wide playback interface
* Use action map for parsing, stringer instead switch stmt.
* Use but only one switch case and direct dispatch, refactoring
* Add error handling and pushing to client
* send decent errormessage, no internal server error
* Adding playback devices slice and load it from config
* Combine config-verification and structure init
* Return user-specific device
* Separate playback server from device
* Use dataStore to retrieve mediafile by id
* WIP: Playlist and start/stop handling. Doing start/stop the hard way as of now
* WIP: set, start and stop work on one single song. More to come
* Dont need to wait for the end
* Merge jukebox_action.go into jukebox.go
* Remove getParameterAsInt64(). Use existing requiredParamInt() instead
* Dont need to call newFailure() explicitly
* Remove int64, use int instead.
* Add and set action now accept multiple ids
* Kickout copy of childFromMediaFile(). It is not needed here.
* Refactoring devices and playbackServer
* Turn (internal) playback.DeviceStatus into subsonic JukeboxStatus when rendering output. Indexes int64 -> int
* Now we have a position and playing status
* Switching gain to float32, xs:float is defined as 32 bit. Fixing nasty copy/pointer bug
* Now with volume control
* Start working the queue
* Remove user from device interface
* Rename function GetDevice -> GetDeviceForUser to make intention clearer
* Have a nice stringer for the queue
* User Prepared boolean for now to allow pause/unpause
* Skipping works, but without offsets
* Make ChildFromMediaFile public to be used in jukebox get() implementation
* Return position in seconds and implement offset-skip in seconds
* Default offset to 0
* Adding a simple setGain implementation
* Prepare for transcoding AAC
* WIP: transcode to WAV to use beeps wav decoder. Not done yet.
* WIP: out of sheer desparation: convert to MP3 (which works) rather than WAV to troubleshoot issue.
* Use FLAC as intermediate format to play Apple AAC
* A bit of cleanup
* Catching the end-of-stream event for further reactions
* Have a trackSwitching goroutine waiting on channel when track ends
* Move decoder code into own file. Restructure code a bit
* Now with going on to play the next song in the playlist
* Adding shuffle feature
* Implementing remove action
* Cleanup code
* Remove templates for ffmpeg mp3 generation. Not needed anymore.
* Adding some documentation
* Check whether offset into track is in range. Fixing potential remove track bug. Documentation
* Make golangci-lint happy: handling return values
* Adding test suite and example dummy for playback package
* Adding some basic queue tests
* Only use Jukebox.Enabled config option
* Adding stream closing handling
* Pass context.Context to all PlaybackDevice methods
* Remove unneeded function
* Correct spelling
* Reduce visibility of ChildFromMediaFile
* Decomplicate action-parsing
* Adding simple tempfile-based AAC->FLAC transcoding. No parallel reading and writing yet.
* Try to optimize pipe-writing, tempfile-handling and reading. Not done yet.
* Do a synchronous copy of the tempfile. Racecondition detected
* More debugging statements and fixing the play/pause bug. More work needed
* Start the trackSwitcher() with each device once. Return JSON position even if its 0. More debug-output
* Moving all track-handling code into own module
* Fix typo. Do not pass ctx around when not applicable
* WIP: More refactoring, debugging output
* Fix nil pointer
* Repairing MP3 playback by pinning indirect dependencies: hajimehoshi/go-mp3 and hajimehoshi/oto
* Do not forget to cleanup after a skip action
* Make resync with master easy
* Adding missing mocks
* Adding missing error-handling found by linter
* Updating github.com/hajimehoshi/oto
* Removing duplicate function
* Move BEEP-related code into own package
* Juggle beep-related code around as preparation for interface access
* More refactoring for interface separation
* Gather CloseDevice() behind Track interface.
* Adding skeleton, draft audio-interface using mpv.io
* Adding majority of interface commands using messages to mpv socket.
* Adding end-of-stream handling
* MPV: start/stop are working
* postition is given in float in mpv
* Unify Close() and CloseDevice(). Using temp filename for controlling socket
* Wait until control-socket shows up. Cleanup socket in Close()
* Use canceable command. Rename to Executor
* Skipping tracks works now
* Now with actually setting the position
* Fix regain
* Add missing error-handling found by linter
* Adding retry mode on time-pos property getter
* Remove unneeded code on queue
* Putting build-tag beep onto beep files
* Remove deprecated call to rand.Seed()
"As of Go 1.20 there is no reason to call Seed with a random value. Programs that call Seed with a known value to get a specific sequence of results should use New(NewSource(seed)) to obtain a local random generator."
* Using int32 to conform to Subsonic API spec
* Fix merge error
* Minor style changes
* Get username from context
---------
Co-authored-by: Deluan <deluan@navidrome.org>
2023-09-10 07:25:22 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sets the index to a new, valid value inside the Items. Values lower than zero are going to be zero,
|
|
|
|
|
// values above will be limited by number of items.
|
|
|
|
|
func (pd *Queue) SetIndex(idx int) {
|
|
|
|
|
pd.Index = max(0, min(idx, len(pd.Items)-1))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Are we at the last track?
|
|
|
|
|
func (pd *Queue) IsAtLastElement() bool {
|
|
|
|
|
return (pd.Index + 1) >= len(pd.Items)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Goto next index
|
|
|
|
|
func (pd *Queue) IncreaseIndex() {
|
|
|
|
|
if !pd.IsAtLastElement() {
|
|
|
|
|
pd.SetIndex(pd.Index + 1)
|
|
|
|
|
}
|
|
|
|
|
}
|