139 lines
3.2 KiB
Go
139 lines
3.2 KiB
Go
package model
|
|
|
|
import (
|
|
"time"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"github.com/bjarneo/cliamp/luaplugin"
|
|
"github.com/bjarneo/cliamp/ui"
|
|
)
|
|
|
|
// seekDebounceTicks is how many ticks to wait after the last seek keypress
|
|
// before actually executing the yt-dlp seek (restart).
|
|
const seekDebounceTicks = 8 // ~800ms at 100ms tick interval
|
|
|
|
// seekTickMsg fires when the async seek completes.
|
|
type seekTickMsg struct{}
|
|
|
|
type ytdlUnpauseReconnectMsg struct{ err error }
|
|
|
|
// doSeek handles a seek keypress. For yt-dlp streams, accumulates into a
|
|
// single target position and debounces. For HTTP seekable streams, dispatches
|
|
// the seek asynchronously to avoid blocking the UI. For local files, seeks
|
|
// immediately.
|
|
func (m *Model) doSeek(d time.Duration) tea.Cmd {
|
|
return m.seekRelative(d, seekDebounceTicks)
|
|
}
|
|
|
|
func (m *Model) streamSeekRelative(delta time.Duration) tea.Cmd {
|
|
p := m.player
|
|
return func() tea.Msg {
|
|
p.Seek(delta)
|
|
return seekTickMsg{}
|
|
}
|
|
}
|
|
|
|
func (m *Model) streamSeekAbsolute(target time.Duration) tea.Cmd {
|
|
p := m.player
|
|
return func() tea.Msg {
|
|
p.Seek(target - p.Position())
|
|
return seekTickMsg{}
|
|
}
|
|
}
|
|
|
|
func (m *Model) seekRelative(d time.Duration, debounceTicks int) tea.Cmd {
|
|
if m.player.IsStreamSeek() {
|
|
return m.streamSeekRelative(d)
|
|
}
|
|
if !m.player.IsYTDLSeek() {
|
|
m.player.Seek(d)
|
|
m.finishSeek()
|
|
return nil
|
|
}
|
|
|
|
target := m.player.Position()
|
|
if m.seek.active && debounceTicks > 0 {
|
|
target = m.seek.targetPos
|
|
}
|
|
return m.queueYTDLSeekTarget(target+d, debounceTicks)
|
|
}
|
|
|
|
func (m *Model) seekAbsolute(target time.Duration) tea.Cmd {
|
|
if m.player.IsStreamSeek() {
|
|
return m.streamSeekAbsolute(target)
|
|
}
|
|
if !m.player.IsYTDLSeek() {
|
|
m.player.Seek(target - m.player.Position())
|
|
m.finishSeek()
|
|
return nil
|
|
}
|
|
return m.queueYTDLSeekTarget(target, 0)
|
|
}
|
|
|
|
func (m *Model) queueYTDLSeekTarget(target time.Duration, debounceTicks int) tea.Cmd {
|
|
m.seek.active = true
|
|
m.seek.targetPos = m.clampPosition(target)
|
|
|
|
m.player.CancelSeekYTDL()
|
|
|
|
if debounceTicks > 0 {
|
|
m.seek.timer = debounceTicks
|
|
m.seek.timerFor = 0
|
|
return nil
|
|
}
|
|
|
|
m.seek.timer = 0
|
|
m.seek.timerFor = 0
|
|
return m.commitPendingYTDLSeek()
|
|
}
|
|
|
|
func (m *Model) finishSeek() {
|
|
m.notifyAll()
|
|
if m.notifier != nil {
|
|
m.notifier.Seeked(m.player.Position())
|
|
}
|
|
m.emitPlugin(luaplugin.EventPlayerSeek, map[string]any{
|
|
"position": m.player.Position().Seconds(),
|
|
"duration": m.player.Duration().Seconds(),
|
|
})
|
|
}
|
|
|
|
func (m *Model) commitPendingYTDLSeek() tea.Cmd {
|
|
target := m.seek.targetPos
|
|
curPos := m.player.Position()
|
|
d := target - curPos
|
|
|
|
p := m.player
|
|
return func() tea.Msg {
|
|
p.SeekYTDL(d)
|
|
return seekTickMsg{}
|
|
}
|
|
}
|
|
|
|
func (m *Model) clampPosition(pos time.Duration) time.Duration {
|
|
if pos < 0 {
|
|
return 0
|
|
}
|
|
dur := m.player.Duration()
|
|
if dur > 0 && pos >= dur {
|
|
return dur - time.Second
|
|
}
|
|
return pos
|
|
}
|
|
|
|
// tickSeek is called from the main tick loop. It advances the debounce timer with elapsed
|
|
// time and runs the yt-dlp seek when the countdown reaches zero.
|
|
func (m *Model) tickSeek(dt time.Duration) tea.Cmd {
|
|
if !m.seek.active || m.seek.timer <= 0 {
|
|
m.seek.timerFor = 0
|
|
return nil
|
|
}
|
|
if advanceTickUnits(&m.seek.timer, &m.seek.timerFor, dt, ui.TickFast) == 0 || m.seek.timer > 0 {
|
|
return nil
|
|
}
|
|
|
|
// Timer expired — fire the seek to the target position.
|
|
return m.commitPendingYTDLSeek()
|
|
}
|