Files
lintunes/tests/test_round22.py
trav a50c39fa43 v0.1.3: start time honored when replaying the already-loaded track
QMediaPlayer.setSource() no-ops on an unchanged URL, so replaying the
track that was already the loaded source never re-fired LoadedMedia and
the armed _pending_start_ms was silently dropped — a freshly edited
start time appeared to save but playback began at 0:00. _load_current
now detects the unchanged source and stop()+seeks directly (a forced
clear+reload was tried and races the FFmpeg backend, snapping the seek
back to 0). previous()-restart likewise rewinds to the custom start
time instead of 0:00.

Verified live via an offscreen GUI harness: replay-while-playing,
replay-after-EndOfMedia, and cleared-start-time replay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 06:28:21 -04:00

123 lines
4.9 KiB
Python

"""Round 22: start-time fixes in the player.
Editing a track's start time saved fine but replaying the *already loaded*
track ignored it: QMediaPlayer.setSource() no-ops on an unchanged URL, so the
LoadedMedia status change that consumes the armed start time never re-fired
(and forcing a clear+reload races the FFmpeg backend, which snaps the seek
back to 0). _load_current now detects the unchanged source and seeks directly
— the media is already loaded. Also, "previous"-restart now rewinds to the
custom start time instead of 0:00.
"""
from unittest.mock import MagicMock, patch
from PyQt6.QtCore import QUrl
from lintunes.models import Library, Track
from lintunes import player as player_module
from lintunes.player import Player, RESTART_THRESHOLD_MS
def _track(tid, **kw):
return Track(track_id=tid, name=kw.pop("name", f"Track {tid}"), **kw)
def _player(qapp, tracks):
library = Library()
for t in tracks:
library.tracks[t.track_id] = t
manager = MagicMock()
manager.library = library
with patch.multiple(
player_module,
QMediaPlayer=MagicMock(),
QAudioOutput=MagicMock(),
QAudioBufferOutput=MagicMock(),
QMediaDevices=MagicMock(),
):
player = Player(manager)
return player, manager
class TestSameSourceReplay:
def test_unchanged_url_seeks_to_freshly_edited_start(self, qapp, tmp_path):
(tmp_path / "a.mp3").write_bytes(b"x")
loc = str(tmp_path / "a.mp3")
track = _track(1, location=loc, total_time=200_000)
player, _ = _player(qapp, [track])
url = QUrl.fromLocalFile(loc)
player.play_queue([1], 0) # first load: source was empty, plain set
assert player._media.setSource.call_args_list[-1].args == (url,)
# The track is now the loaded source; the user edits its start time
# and plays it again. The media is already loaded, so the player must
# seek directly instead of waiting for a LoadedMedia that never comes.
player._media.source.return_value = url
track.start_time = 30_000
player._media.setSource.reset_mock()
player.play_queue([1], 0)
player._media.setSource.assert_not_called()
player._media.stop.assert_called()
player._media.setPosition.assert_called_with(30_000)
assert player._pending_start_ms == 0 # consumed
player._media.play.assert_called()
def test_unchanged_url_without_start_time_restarts_at_zero(
self, qapp, tmp_path):
(tmp_path / "a.mp3").write_bytes(b"x")
loc = str(tmp_path / "a.mp3")
track = _track(1, location=loc, total_time=200_000)
player, _ = _player(qapp, [track])
player.play_queue([1], 0)
player._media.source.return_value = QUrl.fromLocalFile(loc)
player._media.setPosition.reset_mock()
player.play_queue([1], 0)
# stop() rewinds to 0; no seek should be issued.
player._media.stop.assert_called()
player._media.setPosition.assert_not_called()
def test_different_url_loads_via_setsource(self, qapp, tmp_path):
for name in ("a.mp3", "b.mp3"):
(tmp_path / name).write_bytes(b"x")
t1 = _track(1, location=str(tmp_path / "a.mp3"), total_time=200_000)
t2 = _track(2, location=str(tmp_path / "b.mp3"),
total_time=200_000, start_time=30_000)
player, _ = _player(qapp, [t1, t2])
player._media.source.return_value = QUrl.fromLocalFile(t1.location)
player.play_queue([1, 2], 1) # loading b.mp3 while a.mp3 is the source
assert [c.args for c in player._media.setSource.call_args_list] \
== [(QUrl.fromLocalFile(t2.location),)]
# New source: the seek waits for LoadedMedia as before.
assert player._pending_start_ms == 30_000
player._on_media_status(player_module.QMediaPlayer.MediaStatus.LoadedMedia)
player._media.setPosition.assert_called_with(30_000)
assert player._pending_start_ms == 0
class TestPreviousRestart:
def test_restart_seeks_to_custom_start_time(self, qapp, tmp_path):
(tmp_path / "a.mp3").write_bytes(b"x")
track = _track(1, location=str(tmp_path / "a.mp3"),
total_time=200_000, start_time=30_000)
player, _ = _player(qapp, [track])
player.play_queue([1], 0)
player._media.position.return_value = RESTART_THRESHOLD_MS + 1
player.previous()
player._media.setPosition.assert_called_with(30_000)
def test_restart_without_start_time_seeks_to_zero(self, qapp, tmp_path):
(tmp_path / "a.mp3").write_bytes(b"x")
track = _track(1, location=str(tmp_path / "a.mp3"), total_time=200_000)
player, _ = _player(qapp, [track])
player.play_queue([1], 0)
player._media.position.return_value = RESTART_THRESHOLD_MS + 1
player.previous()
player._media.setPosition.assert_called_with(0)