Compare commits
2 Commits
c3818a2b18
...
02c52dc68c
Author | SHA1 | Date |
---|---|---|
Anna Schumaker | 02c52dc68c | |
Anna Schumaker | db6ae0ecb6 |
|
@ -5,6 +5,7 @@ from gi.repository import GObject
|
|||
from gi.repository import GLib
|
||||
from gi.repository import Gst
|
||||
from . import filter
|
||||
from . import stopwatch
|
||||
from .. import path
|
||||
from .. import tmpdir
|
||||
|
||||
|
@ -33,7 +34,7 @@ class Player(GObject.GObject):
|
|||
have_track = GObject.Property(type=bool, default=False)
|
||||
almost_done = GObject.Property(type=bool, default=False)
|
||||
playtime = GObject.Property(type=float)
|
||||
savedtime = GObject.Property(type=float)
|
||||
stopwatch = GObject.Property(type=stopwatch.StopWatch)
|
||||
|
||||
bg_enabled = GObject.Property(type=bool, default=False)
|
||||
bg_volume = GObject.Property(type=float, default=0.5)
|
||||
|
@ -41,7 +42,7 @@ class Player(GObject.GObject):
|
|||
|
||||
def __init__(self):
|
||||
"""Initialize the audio Player."""
|
||||
super().__init__()
|
||||
super().__init__(stopwatch=stopwatch.StopWatch())
|
||||
self._filter = filter.Filter()
|
||||
self._timeout = None
|
||||
|
||||
|
@ -70,12 +71,6 @@ class Player(GObject.GObject):
|
|||
if not self.almost_done:
|
||||
self.emit("about-to-finish")
|
||||
|
||||
def __get_current_playtime(self) -> float:
|
||||
if not self._playbin.clock:
|
||||
return 0.0
|
||||
time = self._playbin.clock.get_time() - self._playbin.base_time
|
||||
return time / Gst.SECOND
|
||||
|
||||
def __msg_async_done(self, bus: Gst.Bus, message: Gst.Message) -> None:
|
||||
self.__update_position()
|
||||
|
||||
|
@ -95,15 +90,18 @@ class Player(GObject.GObject):
|
|||
print("audio: state changed to 'playing'")
|
||||
self.status = "Playing"
|
||||
self.playing = True
|
||||
self.stopwatch.start()
|
||||
case (_, Gst.State.PAUSED, Gst.State.VOID_PENDING):
|
||||
print("audio: state changed to 'paused'")
|
||||
self.status = "Paused"
|
||||
self.playing = False
|
||||
self.stopwatch.stop()
|
||||
case (_, Gst.State.READY, Gst.State.VOID_PENDING) | \
|
||||
(_, Gst.State.NULL, Gst.State.VOID_PENDING):
|
||||
print("audio: state changed to 'stopped'")
|
||||
self.status = "Stopped"
|
||||
self.playing = False
|
||||
self.stopwatch.stop()
|
||||
|
||||
self.__update_timeout()
|
||||
|
||||
|
@ -142,9 +140,10 @@ class Player(GObject.GObject):
|
|||
for tag in ["artist", "album-artist", "album", "title"]:
|
||||
self.set_property(tag, "")
|
||||
for tag in ["album-disc-number", "track-number",
|
||||
"position", "playtime", "savedtime"]:
|
||||
"position", "playtime"]:
|
||||
self.set_property(tag, 0)
|
||||
|
||||
self.stopwatch.reset()
|
||||
self.almost_done = False
|
||||
self.pause_on_load = False
|
||||
self.artwork = artwork
|
||||
|
@ -153,7 +152,7 @@ class Player(GObject.GObject):
|
|||
def __update_position(self) -> bool:
|
||||
(res, pos) = self._playbin.query_position(Gst.Format.TIME)
|
||||
self.position = pos / Gst.USECOND if res else 0
|
||||
self.playtime = self.__get_current_playtime() + self.savedtime
|
||||
self.playtime = self.stopwatch.elapsed_time()
|
||||
self.__check_last_second()
|
||||
return GLib.SOURCE_CONTINUE
|
||||
|
||||
|
@ -189,7 +188,6 @@ class Player(GObject.GObject):
|
|||
|
||||
def seek(self, newpos: float, *args) -> None:
|
||||
"""Seek to a different point in the stream."""
|
||||
self.savedtime += self.__get_current_playtime()
|
||||
self._playbin.seek_simple(Gst.Format.TIME, SEEK_FLAGS,
|
||||
newpos * Gst.USECOND)
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# Copyright 2024 (c) Anna Schumaker.
|
||||
"""A custom StopWatch object for tracking play time."""
|
||||
import datetime
|
||||
from gi.repository import GObject
|
||||
|
||||
|
||||
class StopWatch(GObject.GObject):
|
||||
"""A StopWatch object."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the StopWatch."""
|
||||
super().__init__()
|
||||
self._saved = None
|
||||
self._started = None
|
||||
|
||||
def elapsed_time(self) -> float:
|
||||
"""Get the elapsed time (in seconds)."""
|
||||
total = datetime.timedelta()
|
||||
if self._saved is not None:
|
||||
total += self._saved
|
||||
if self._started is not None:
|
||||
total += datetime.datetime.now() - self._started
|
||||
return total.total_seconds()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the StopWatch."""
|
||||
self._saved = None
|
||||
self._started = None
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the StopWatch."""
|
||||
self._started = datetime.datetime.now()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the StopWatch."""
|
||||
if self._started is not None:
|
||||
delta = datetime.datetime.now() - self._started
|
||||
if self._saved is None:
|
||||
self._saved = delta
|
||||
else:
|
||||
self._saved += delta
|
||||
self._started = None
|
|
@ -81,9 +81,9 @@ class TestAudio(unittest.TestCase):
|
|||
self.player.duration = 10
|
||||
self.player.position = 8
|
||||
self.player.playtime = 6
|
||||
self.player.savedtime = 4
|
||||
self.player.almost_done = True
|
||||
self.player.artwork = pathlib.Path("/a/b/c.jpg")
|
||||
self.player.stopwatch.reset = unittest.mock.Mock()
|
||||
|
||||
eos = Gst.Message.new_eos(self.player._playbin)
|
||||
self.player._playbin.get_bus().post(eos)
|
||||
|
@ -94,9 +94,10 @@ class TestAudio(unittest.TestCase):
|
|||
for prop in ["artist", "album-artist", "album", "title"]:
|
||||
self.assertEqual(self.player.get_property(prop), "")
|
||||
for prop in ["album-disc-number", "track-number",
|
||||
"position", "duration", "playtime", "savedtime"]:
|
||||
"position", "duration", "playtime"]:
|
||||
self.assertEqual(self.player.get_property(prop), 0)
|
||||
self.assertIsNone(self.player.artwork)
|
||||
self.player.stopwatch.reset.assert_called()
|
||||
|
||||
self.assertEqual(self.player.get_state(), Gst.State.READY)
|
||||
self.assertEqual(self.player.status, "Stopped")
|
||||
|
@ -151,21 +152,24 @@ class TestAudio(unittest.TestCase):
|
|||
"audio: state changed to 'paused'\n")
|
||||
|
||||
self.player.playtime = 6
|
||||
self.player.savedtime = 4
|
||||
self.player.stopwatch.reset = unittest.mock.Mock()
|
||||
self.player.emit("file-loaded", tests.util.TRACK_OGG)
|
||||
for prop in ["artist", "album-artist", "album", "title"]:
|
||||
self.assertEqual(self.player.get_property(prop), "")
|
||||
for prop in ["album-disc-number", "track-number",
|
||||
"playtime", "savedtime"]:
|
||||
for prop in ["album-disc-number", "track-number", "playtime"]:
|
||||
self.assertEqual(self.player.get_property(prop), 0)
|
||||
self.player.stopwatch.reset.assert_called()
|
||||
|
||||
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
|
||||
def test_play(self, mock_stdout: io.StringIO):
|
||||
"""Test that the play() function works as expected."""
|
||||
self.player.stopwatch.start = unittest.mock.Mock()
|
||||
|
||||
self.player.play()
|
||||
self.main_loop()
|
||||
self.assertFalse(self.player.playing)
|
||||
self.assertEqual(self.player.status, "Stopped")
|
||||
self.player.stopwatch.start.assert_not_called()
|
||||
|
||||
self.player.file = tests.util.TRACK_OGG
|
||||
self.player.play()
|
||||
|
@ -175,13 +179,17 @@ class TestAudio(unittest.TestCase):
|
|||
self.assertEqual(self.player.get_state(), Gst.State.PLAYING)
|
||||
self.assertRegex(mock_stdout.getvalue(),
|
||||
"audio: state changed to 'playing'$")
|
||||
self.player.stopwatch.start.assert_called()
|
||||
|
||||
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
|
||||
def test_pause(self, mock_stdout: io.StringIO):
|
||||
"""Test that the pause() function works as expected."""
|
||||
self.player.stopwatch.stop = unittest.mock.Mock()
|
||||
|
||||
self.player.pause()
|
||||
self.main_loop()
|
||||
self.assertEqual(self.player.status, "Stopped")
|
||||
self.player.stopwatch.stop.assert_not_called()
|
||||
|
||||
self.player.playing = True
|
||||
self.player.file = tests.util.TRACK_OGG
|
||||
|
@ -192,6 +200,7 @@ class TestAudio(unittest.TestCase):
|
|||
self.assertEqual(self.player.get_state(), Gst.State.PAUSED)
|
||||
self.assertRegex(mock_stdout.getvalue(),
|
||||
"audio: state changed to 'paused'$")
|
||||
self.player.stopwatch.stop.assert_called()
|
||||
|
||||
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
|
||||
def test_play_pause(self, mock_stdout: io.StringIO):
|
||||
|
@ -222,6 +231,7 @@ class TestAudio(unittest.TestCase):
|
|||
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
|
||||
def test_stop(self, mock_stdout: io.StringIO):
|
||||
"""Test that the stop() function works as expected."""
|
||||
self.player.stopwatch.stop = unittest.mock.Mock()
|
||||
self.player.file = tests.util.TRACK_OGG
|
||||
self.player.play()
|
||||
self.main_loop()
|
||||
|
@ -234,6 +244,7 @@ class TestAudio(unittest.TestCase):
|
|||
self.assertRegex(mock_stdout.getvalue(),
|
||||
"audio: state changed to 'playing'\n"
|
||||
"audio: state changed to 'stopped'\n$")
|
||||
self.player.stopwatch.stop.assert_called()
|
||||
|
||||
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
|
||||
def test_tags(self, mock_stdout: io.StringIO):
|
||||
|
@ -282,30 +293,15 @@ class TestAudio(unittest.TestCase):
|
|||
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
|
||||
def test_playtime(self, mock_stdout: io.StringIO):
|
||||
"""Test the play time property."""
|
||||
self.assertIsInstance(self.player.stopwatch,
|
||||
emmental.audio.stopwatch.StopWatch)
|
||||
self.assertEqual(self.player.playtime, 0.0)
|
||||
self.assertEqual(self.player.savedtime, 0.0)
|
||||
self.assertEqual(self.player._Player__get_current_playtime(), 0.0)
|
||||
|
||||
self.player.file = tests.util.TRACK_OGG
|
||||
self.player.play()
|
||||
self.main_loop()
|
||||
self.assertIsNotNone(self.player._playbin.clock)
|
||||
|
||||
base_time = unittest.mock.PropertyMock(return_value=Gst.SECOND)
|
||||
type(self.player._playbin).base_time = base_time
|
||||
get_time = unittest.mock.Mock(return_value=3 * Gst.SECOND)
|
||||
self.player._playbin.clock.get_time = get_time
|
||||
|
||||
self.assertEqual(self.player._Player__get_current_playtime(), 2.0)
|
||||
self.player._Player__update_position()
|
||||
self.assertEqual(self.player.playtime, 2)
|
||||
|
||||
with unittest.mock.patch.object(self.player._playbin, "seek_simple"):
|
||||
self.player.seek(5)
|
||||
self.assertEqual(self.player.savedtime, 2)
|
||||
|
||||
self.player._Player__update_position()
|
||||
self.assertEqual(self.player.playtime, 4)
|
||||
with unittest.mock.patch.object(self.player.stopwatch,
|
||||
"elapsed_time") as mock_elapsed:
|
||||
mock_elapsed.return_value = 2.0
|
||||
self.player._Player__update_position()
|
||||
self.assertEqual(self.player.playtime, 2.0)
|
||||
|
||||
def test_volume(self):
|
||||
"""Test that the volume property works as expected."""
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
# Copyright 2024 (c) Anna Schumaker.
|
||||
"""Tests our StopWatch object."""
|
||||
import datetime
|
||||
import emmental.audio.stopwatch
|
||||
import unittest
|
||||
from gi.repository import GObject
|
||||
|
||||
|
||||
@unittest.mock.patch.object(emmental.audio.stopwatch, "datetime")
|
||||
class TestStopwatch(unittest.TestCase):
|
||||
"""Our stopwatch test case."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
self.now = datetime.datetime.now()
|
||||
self.newdelta = datetime.timedelta
|
||||
self.stopwatch = emmental.audio.stopwatch.StopWatch()
|
||||
|
||||
def test_init(self, mock_datetime: unittest.mock.Mock):
|
||||
"""Test that the StopWatch was created properly."""
|
||||
self.assertIsInstance(self.stopwatch, GObject.GObject)
|
||||
|
||||
def test_elapsed_time(self, mock_datetime: unittest.mock.Mock):
|
||||
"""Test the elapsed_time() function."""
|
||||
mock_datetime.timedelta = self.newdelta
|
||||
self.assertEqual(self.stopwatch.elapsed_time(), 0.0)
|
||||
|
||||
mock_datetime.datetime.now.return_value = self.now
|
||||
self.stopwatch.start()
|
||||
|
||||
soon = self.now + datetime.timedelta(seconds=12.345)
|
||||
mock_datetime.datetime.now.return_value = soon
|
||||
self.assertEqual(self.stopwatch.elapsed_time(), 12.345)
|
||||
|
||||
self.stopwatch._saved = datetime.timedelta(seconds=2)
|
||||
self.assertEqual(self.stopwatch.elapsed_time(), 14.345)
|
||||
|
||||
self.stopwatch.stop()
|
||||
self.assertEqual(self.stopwatch.elapsed_time(), 14.345)
|
||||
|
||||
self.stopwatch.reset()
|
||||
self.assertEqual(self.stopwatch.elapsed_time(), 0.0)
|
||||
|
||||
def test_reset(self, mock_datetime: unittest.mock.Mock):
|
||||
"""Test resetting the StopWatch."""
|
||||
mock_datetime.datetime.now.return_value = self.now
|
||||
self.stopwatch.start()
|
||||
soon = self.now + datetime.timedelta(seconds=12.345)
|
||||
mock_datetime.datetime.now.return_value = soon
|
||||
self.stopwatch.stop()
|
||||
|
||||
self.stopwatch.reset()
|
||||
self.assertIsNone(self.stopwatch._saved)
|
||||
self.assertIsNone(self.stopwatch._started)
|
||||
|
||||
def test_start(self, mock_datetime: unittest.mock.Mock):
|
||||
"""Test starting the StopWatch."""
|
||||
self.assertIsNone(self.stopwatch._started)
|
||||
|
||||
mock_datetime.datetime.now.return_value = self.now
|
||||
self.stopwatch.start()
|
||||
self.assertEqual(self.stopwatch._started, self.now)
|
||||
|
||||
def test_stop(self, mock_datetime: unittest.mock.Mock):
|
||||
"""Test stopping the StopWatch."""
|
||||
self.assertIsNone(self.stopwatch._saved)
|
||||
self.stopwatch.stop()
|
||||
self.assertIsNone(self.stopwatch._saved)
|
||||
|
||||
mock_datetime.datetime.now.return_value = self.now
|
||||
self.stopwatch.start()
|
||||
delta1 = datetime.timedelta(seconds=12.345)
|
||||
mock_datetime.datetime.now.return_value = self.now + delta1
|
||||
self.stopwatch.stop()
|
||||
self.assertEqual(self.stopwatch._saved, delta1)
|
||||
self.assertIsNone(self.stopwatch._started)
|
||||
|
||||
now = self.now + delta1 + datetime.timedelta(seconds=2)
|
||||
mock_datetime.datetime.now.return_value = now
|
||||
self.stopwatch.start()
|
||||
delta2 = datetime.timedelta(seconds=3)
|
||||
mock_datetime.datetime.now.return_value = now + delta2
|
||||
self.stopwatch.stop()
|
||||
self.assertEqual(self.stopwatch._saved, delta1 + delta2)
|
||||
|
||||
self.stopwatch.stop()
|
||||
self.assertEqual(self.stopwatch._saved, delta1 + delta2)
|
Loading…
Reference in New Issue