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 <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-06-15 09:08:54 -04:00
parent d105b15e02
commit 88e4fa4b0c
3 changed files with 75 additions and 8 deletions

View File

@ -1,6 +1,7 @@
# Copyright 2022 (c) Anna Schumaker. # Copyright 2022 (c) Anna Schumaker.
"""Set up our Application.""" """Set up our Application."""
from . import gsetup from . import gsetup
from . import audio
from . import db from . import db
from . import header from . import header
from . import mpris2 from . import mpris2
@ -20,6 +21,7 @@ class Application(Adw.Application):
db = GObject.Property(type=db.Connection) db = GObject.Property(type=db.Connection)
mpris = GObject.Property(type=mpris2.Connection) mpris = GObject.Property(type=mpris2.Connection)
player = GObject.Property(type=audio.Player)
win = GObject.Property(type=window.Window) win = GObject.Property(type=window.Window)
def __init__(self): def __init__(self):
@ -28,13 +30,24 @@ class Application(Adw.Application):
resource_base_path=gsetup.RESOURCE_PATH) resource_base_path=gsetup.RESOURCE_PATH)
self.add_main_option_entries([options.Version]) 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: def build_header(self) -> header.Header:
"""Build a new header instance.""" """Build a new header instance."""
hdr = header.Header(sql=self.db, title=VERSION_STRING) hdr = header.Header(sql=self.db, title=VERSION_STRING)
hdr.bind_property("volume", self.player, "volume")
for (setting, property) in [("audio.volume", "volume"), for (setting, property) in [("audio.volume", "volume"),
("audio.replaygain.enabled", "rg-enabled"), ("audio.replaygain.enabled", "rg-enabled"),
("audio.replaygain.mode", "rg-mode")]: ("audio.replaygain.mode", "rg-mode")]:
self.db.settings.bind_setting(setting, hdr, property) 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 return hdr
def build_window(self) -> window.Window: 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("Raise", self.win.present)
self.mpris.app.connect("Quit", self.win.close) 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: def do_handle_local_options(self, opts: GLib.VariantDict) -> int:
"""Handle any command line options.""" """Handle any command line options."""
if opts.contains("version"): if opts.contains("version"):
@ -67,6 +82,7 @@ class Application(Adw.Application):
Adw.Application.do_startup(self) Adw.Application.do_startup(self)
self.db = db.Connection() self.db = db.Connection()
self.mpris = mpris2.Connection() self.mpris = mpris2.Connection()
self.player = audio.Player()
gsetup.add_style() gsetup.add_style()
self.db.load() self.db.load()
@ -83,6 +99,9 @@ class Application(Adw.Application):
def do_shutdown(self) -> None: def do_shutdown(self) -> None:
"""Handle the Adw.Application::shutdown signal.""" """Handle the Adw.Application::shutdown signal."""
Adw.Application.do_shutdown(self) Adw.Application.do_shutdown(self)
if self.player is not None:
self.player.shutdown()
self.player = None
if self.win is not None: if self.win is not None:
self.win.close() self.win.close()
self.win = None self.win = None

View File

@ -1,5 +1,6 @@
# Copyright 2022 (c) Anna Schumaker. # Copyright 2022 (c) Anna Schumaker.
"""Test as much as we can of the Emmental Application.""" """Test as much as we can of the Emmental Application."""
import io
import unittest import unittest
import unittest.mock import unittest.mock
import gi import gi
@ -31,31 +32,37 @@ class TestEmmental(unittest.TestCase):
self.assertEqual(self.application.get_property("resource-base-path"), self.assertEqual(self.application.get_property("resource-base-path"),
"/com/nowheycreamery/emmental") "/com/nowheycreamery/emmental")
@unittest.mock.patch("sys.stdout")
@unittest.mock.patch("gi.repository.Adw.Application.add_window") @unittest.mock.patch("gi.repository.Adw.Application.add_window")
@unittest.mock.patch("emmental.db.Connection.load") @unittest.mock.patch("emmental.db.Connection.load")
@unittest.mock.patch("gi.repository.Adw.Application.do_startup") @unittest.mock.patch("gi.repository.Adw.Application.do_startup")
def test_startup(self, mock_startup: unittest.mock.Mock, def test_startup(self, mock_startup: unittest.mock.Mock,
mock_load: 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.""" """Test that the startup signal works as expected."""
self.assertIsNone(self.application.db) self.assertIsNone(self.application.db)
self.assertIsNone(self.application.mpris) self.assertIsNone(self.application.mpris)
self.assertIsNone(self.application.player)
self.assertIsNone(self.application.win) self.assertIsNone(self.application.win)
self.application.emit("startup") self.application.emit("startup")
self.assertIsInstance(self.application.db, emmental.db.Connection) self.assertIsInstance(self.application.db, emmental.db.Connection)
self.assertIsInstance(self.application.mpris, self.assertIsInstance(self.application.mpris,
emmental.mpris2.Connection) emmental.mpris2.Connection)
self.assertIsInstance(self.application.player, emmental.audio.Player)
self.assertIsInstance(self.application.win, emmental.window.Window) self.assertIsInstance(self.application.win, emmental.window.Window)
mock_startup.assert_called() mock_startup.assert_called()
mock_load.assert_called() mock_load.assert_called()
mock_add_window.assert_called_with(self.application.win) 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.add_window")
@unittest.mock.patch("gi.repository.Adw.Application.do_startup") @unittest.mock.patch("gi.repository.Adw.Application.do_startup")
def test_activate(self, mock_startup: unittest.mock.Mock, 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.""" """Test activating the application."""
self.application.emit("startup") self.application.emit("startup")
@ -70,21 +77,39 @@ class TestEmmental(unittest.TestCase):
db = self.application.db = emmental.db.Connection() db = self.application.db = emmental.db.Connection()
mpris = self.application.mpris = emmental.mpris2.Connection() mpris = self.application.mpris = emmental.mpris2.Connection()
self.application.win = emmental.window.Window("Test 1.2.3") self.application.win = emmental.window.Window("Test 1.2.3")
player = self.application.player = emmental.audio.Player()
self.application.emit("shutdown") self.application.emit("shutdown")
self.assertIsNone(self.application.db) self.assertIsNone(self.application.db)
self.assertIsNone(self.application.mpris) self.assertIsNone(self.application.mpris)
self.assertIsNone(self.application.player)
self.assertIsNone(self.application.win) self.assertIsNone(self.application.win)
self.assertIsNone(mpris.dbus) self.assertIsNone(mpris.dbus)
self.assertFalse(db.connected) self.assertFalse(db.connected)
self.assertEqual(player.get_state(), gi.repository.Gst.State.NULL)
mock_close.assert_called() 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.""" """Check that the window widgets are added properly."""
self.application.db = emmental.db.Connection() self.application.db = emmental.db.Connection()
self.application.player = emmental.audio.Player()
win = self.application.build_window() win = self.application.build_window()
self.assertIsInstance(win, emmental.window.Window) self.assertIsInstance(win, emmental.window.Window)
self.assertIsInstance(win.header, emmental.header.Header) self.assertIsInstance(win.header, emmental.header.Header)
self.assertEqual(win.header.title, emmental.VERSION_STRING) 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"))

View File

@ -1,9 +1,12 @@
# Copyright 2022 (c) Anna Schumaker. # Copyright 2022 (c) Anna Schumaker.
"""Test saving and loading Emmental settings.""" """Test saving and loading Emmental settings."""
import io
import unittest import unittest
import unittest.mock
import emmental import emmental
@unittest.mock.patch("sys.stdout", new_callable=io.StringIO)
class TestSettings(unittest.TestCase): class TestSettings(unittest.TestCase):
"""Emmental settings test case.""" """Emmental settings test case."""
@ -13,15 +16,17 @@ class TestSettings(unittest.TestCase):
mock_add_window: unittest.mock.Mock): mock_add_window: unittest.mock.Mock):
"""Set up common variables.""" """Set up common variables."""
self.app = emmental.Application() 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.settings = self.app.db.settings
self.win = self.app.win self.win = self.app.win
self.player = self.app.player
def tearDown(self): def tearDown(self):
"""Clean up.""" """Clean up."""
self.app.do_shutdown() 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.""" """Check saving and loading window size from the database."""
self.assertEqual(self.settings["window.width"], 1600) self.assertEqual(self.settings["window.width"], 1600)
self.assertEqual(self.settings["window.height"], 900) self.assertEqual(self.settings["window.height"], 900)
@ -33,25 +38,43 @@ class TestSettings(unittest.TestCase):
win = self.app.build_window() win = self.app.build_window()
self.assertEqual(win.get_default_size(), (100, 200)) 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.""" """Check saving and loading volume from the database."""
self.assertEqual(self.settings["audio.volume"], 1.0) self.assertEqual(self.settings["audio.volume"], 1.0)
self.assertEqual(self.player.volume, 1.0)
self.win.header.volume = 0.5 self.win.header.volume = 0.5
self.assertEqual(self.settings["audio.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.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.""" """Check saving and loading replaygain state from the database."""
self.assertFalse(self.settings["audio.replaygain.enabled"]) self.assertFalse(self.settings["audio.replaygain.enabled"])
self.assertEqual(self.settings["audio.replaygain.mode"], "auto") self.assertEqual(self.settings["audio.replaygain.mode"], "auto")
self.assertEqual(self.player.get_replaygain(), (False, None))
self.win.header.rg_enabled = True self.win.header.rg_enabled = True
self.assertTrue(self.settings["audio.replaygain.enabled"]) self.assertTrue(self.settings["audio.replaygain.enabled"])
self.win.header.rg_mode = "track" self.win.header.rg_mode = "track"
self.assertEqual(self.settings["audio.replaygain.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() header = self.app.build_header()
self.assertTrue(header.rg_enabled) 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))