From 88e4fa4b0ca07f862f5d295f1628537337619c80 Mon Sep 17 00:00:00 2001 From: Anna Schumaker Date: Wed, 15 Jun 2022 09:08:54 -0400 Subject: [PATCH] emmental: Add a Player instance to the application And wire it up to the Header and Mpris.Player so we can apply volume & replaygain changes as they happen. Implements: #42 ("Remove global audio.Player instance") Signed-off-by: Anna Schumaker --- emmental/__init__.py | 19 +++++++++++++++++++ tests/test_emmental.py | 31 ++++++++++++++++++++++++++++--- tests/test_settings.py | 33 ++++++++++++++++++++++++++++----- 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/emmental/__init__.py b/emmental/__init__.py index 26eb758..024f183 100644 --- a/emmental/__init__.py +++ b/emmental/__init__.py @@ -1,6 +1,7 @@ # Copyright 2022 (c) Anna Schumaker. """Set up our Application.""" from . import gsetup +from . import audio from . import db from . import header from . import mpris2 @@ -20,6 +21,7 @@ class Application(Adw.Application): db = GObject.Property(type=db.Connection) mpris = GObject.Property(type=mpris2.Connection) + player = GObject.Property(type=audio.Player) win = GObject.Property(type=window.Window) def __init__(self): @@ -28,13 +30,24 @@ class Application(Adw.Application): resource_base_path=gsetup.RESOURCE_PATH) self.add_main_option_entries([options.Version]) + def __set_replaygain(self, *args) -> None: + enabled = self.db.settings["audio.replaygain.enabled"] + mode = self.db.settings["audio.replaygain.mode"] + mode = "track" if mode == "auto" else mode + self.player.set_replaygain(enabled, mode) + def build_header(self) -> header.Header: """Build a new header instance.""" hdr = header.Header(sql=self.db, title=VERSION_STRING) + hdr.bind_property("volume", self.player, "volume") for (setting, property) in [("audio.volume", "volume"), ("audio.replaygain.enabled", "rg-enabled"), ("audio.replaygain.mode", "rg-mode")]: self.db.settings.bind_setting(setting, hdr, property) + + hdr.connect("notify::rg-enabled", self.__set_replaygain) + hdr.connect("notify::rg-mode", self.__set_replaygain) + self.__set_replaygain() return hdr def build_window(self) -> window.Window: @@ -54,6 +67,8 @@ class Application(Adw.Application): self.mpris.app.connect("Raise", self.win.present) self.mpris.app.connect("Quit", self.win.close) + self.mpris.player.link_property("Volume", self.win.header, "volume") + def do_handle_local_options(self, opts: GLib.VariantDict) -> int: """Handle any command line options.""" if opts.contains("version"): @@ -67,6 +82,7 @@ class Application(Adw.Application): Adw.Application.do_startup(self) self.db = db.Connection() self.mpris = mpris2.Connection() + self.player = audio.Player() gsetup.add_style() self.db.load() @@ -83,6 +99,9 @@ class Application(Adw.Application): def do_shutdown(self) -> None: """Handle the Adw.Application::shutdown signal.""" Adw.Application.do_shutdown(self) + if self.player is not None: + self.player.shutdown() + self.player = None if self.win is not None: self.win.close() self.win = None diff --git a/tests/test_emmental.py b/tests/test_emmental.py index f132a86..2f04c12 100644 --- a/tests/test_emmental.py +++ b/tests/test_emmental.py @@ -1,5 +1,6 @@ # Copyright 2022 (c) Anna Schumaker. """Test as much as we can of the Emmental Application.""" +import io import unittest import unittest.mock import gi @@ -31,31 +32,37 @@ class TestEmmental(unittest.TestCase): self.assertEqual(self.application.get_property("resource-base-path"), "/com/nowheycreamery/emmental") + @unittest.mock.patch("sys.stdout") @unittest.mock.patch("gi.repository.Adw.Application.add_window") @unittest.mock.patch("emmental.db.Connection.load") @unittest.mock.patch("gi.repository.Adw.Application.do_startup") def test_startup(self, mock_startup: unittest.mock.Mock, mock_load: unittest.mock.Mock, - mock_add_window: unittest.mock.Mock): + mock_add_window: unittest.mock.Mock, + mock_stdout: unittest.mock.Mock): """Test that the startup signal works as expected.""" self.assertIsNone(self.application.db) self.assertIsNone(self.application.mpris) + self.assertIsNone(self.application.player) self.assertIsNone(self.application.win) self.application.emit("startup") self.assertIsInstance(self.application.db, emmental.db.Connection) self.assertIsInstance(self.application.mpris, emmental.mpris2.Connection) + self.assertIsInstance(self.application.player, emmental.audio.Player) self.assertIsInstance(self.application.win, emmental.window.Window) mock_startup.assert_called() mock_load.assert_called() mock_add_window.assert_called_with(self.application.win) + @unittest.mock.patch("sys.stdout") @unittest.mock.patch("gi.repository.Adw.Application.add_window") @unittest.mock.patch("gi.repository.Adw.Application.do_startup") def test_activate(self, mock_startup: unittest.mock.Mock, - mock_add_window: unittest.mock.Mock): + mock_add_window: unittest.mock.Mock, + mock_stdout: unittest.mock.Mock): """Test activating the application.""" self.application.emit("startup") @@ -70,21 +77,39 @@ class TestEmmental(unittest.TestCase): db = self.application.db = emmental.db.Connection() mpris = self.application.mpris = emmental.mpris2.Connection() self.application.win = emmental.window.Window("Test 1.2.3") + player = self.application.player = emmental.audio.Player() self.application.emit("shutdown") self.assertIsNone(self.application.db) self.assertIsNone(self.application.mpris) + self.assertIsNone(self.application.player) self.assertIsNone(self.application.win) self.assertIsNone(mpris.dbus) self.assertFalse(db.connected) + self.assertEqual(player.get_state(), gi.repository.Gst.State.NULL) mock_close.assert_called() - def test_window_widgets(self): + @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) + def test_window_widgets(self, mock_stdout: io.StringIO): """Check that the window widgets are added properly.""" self.application.db = emmental.db.Connection() + self.application.player = emmental.audio.Player() win = self.application.build_window() self.assertIsInstance(win, emmental.window.Window) self.assertIsInstance(win.header, emmental.header.Header) self.assertEqual(win.header.title, emmental.VERSION_STRING) + + @unittest.mock.patch("sys.stdout", new_callable=io.StringIO) + def test_replaygain(self, mock_stdout: io.StringIO): + """Test setting replaygain modes.""" + self.application.db = emmental.db.Connection() + self.application.player = emmental.audio.Player() + win = self.application.build_window() + player = self.application.player + + win.header.rg_enabled = True + self.assertEqual(player.get_replaygain(), (True, "track")) + win.header.rg_mode = "album" + self.assertEqual(player.get_replaygain(), (True, "album")) diff --git a/tests/test_settings.py b/tests/test_settings.py index 0fcaf21..db117e3 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,9 +1,12 @@ # Copyright 2022 (c) Anna Schumaker. """Test saving and loading Emmental settings.""" +import io import unittest +import unittest.mock import emmental +@unittest.mock.patch("sys.stdout", new_callable=io.StringIO) class TestSettings(unittest.TestCase): """Emmental settings test case.""" @@ -13,15 +16,17 @@ class TestSettings(unittest.TestCase): mock_add_window: unittest.mock.Mock): """Set up common variables.""" self.app = emmental.Application() - self.app.do_startup() + with unittest.mock.patch("sys.stdout"): + self.app.do_startup() self.settings = self.app.db.settings self.win = self.app.win + self.player = self.app.player def tearDown(self): """Clean up.""" self.app.do_shutdown() - def test_save_window_size(self): + def test_save_window_size(self, new_callable=io.StringIO): """Check saving and loading window size from the database.""" self.assertEqual(self.settings["window.width"], 1600) self.assertEqual(self.settings["window.height"], 900) @@ -33,25 +38,43 @@ class TestSettings(unittest.TestCase): win = self.app.build_window() self.assertEqual(win.get_default_size(), (100, 200)) - def test_save_volume(self): + def test_save_volume(self, mock_stdout: io.StringIO): """Check saving and loading volume from the database.""" self.assertEqual(self.settings["audio.volume"], 1.0) + self.assertEqual(self.player.volume, 1.0) + self.win.header.volume = 0.5 self.assertEqual(self.settings["audio.volume"], 0.5) + self.assertEqual(self.player.volume, 0.5) + self.player.volume = 0.0 self.assertEqual(self.app.build_header().volume, 0.5) + self.assertEqual(self.player.volume, 0.5) - def test_save_replaygain(self): + def test_save_replaygain(self, mock_stdout: io.StringIO): """Check saving and loading replaygain state from the database.""" self.assertFalse(self.settings["audio.replaygain.enabled"]) self.assertEqual(self.settings["audio.replaygain.mode"], "auto") + self.assertEqual(self.player.get_replaygain(), (False, None)) self.win.header.rg_enabled = True self.assertTrue(self.settings["audio.replaygain.enabled"]) self.win.header.rg_mode = "track" self.assertEqual(self.settings["audio.replaygain.mode"], "track") + self.assertEqual(self.player.get_replaygain(), (True, "track")) + self.win.header.rg_mode = "album" + self.assertEqual(self.settings["audio.replaygain.mode"], "album") + self.assertEqual(self.player.get_replaygain(), (True, "album")) + + self.player.set_replaygain(False, None) header = self.app.build_header() self.assertTrue(header.rg_enabled) - self.assertEqual(header.rg_mode, "track") + self.assertEqual(header.rg_mode, "album") + self.assertEqual(self.player.get_replaygain(), (True, "album")) + + header.rg_enabled = False + self.assertFalse(self.settings["audio.replaygain.enabled"]) + self.assertEqual(self.settings["audio.replaygain.mode"], "album") + self.assertEqual(self.player.get_replaygain(), (False, None))