Files
wehub-resource-sync ead81af521
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

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()
}