diff --git a/sublime/app.py b/sublime/app.py index ffa0b59..6f5dc4a 100644 --- a/sublime/app.py +++ b/sublime/app.py @@ -11,6 +11,7 @@ gi.require_version('Notify', '0.7') from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, Notify from .cache_manager import CacheManager +from .config import ReplayGainType from .dbus_manager import dbus_propagate, DBusManager from .players import ChromecastPlayer, MPVPlayer, PlayerEvent from .server.api_objects import Child, Directory, Playlist @@ -431,6 +432,8 @@ class SublimeMusicApp(Gtk.Application): 'prefetch_amount'].get_value_as_int() self.state.config.concurrent_download_limit = dialog.data[ 'concurrent_download_limit'].get_value_as_int() + self.state.config.replay_gain = ReplayGainType.from_string( + dialog.data['replay_gain'].get_active_id()) self.state.save_config() self.reset_state() dialog.destroy() diff --git a/sublime/cache_manager.py b/sublime/cache_manager.py index fd3c796..9664251 100644 --- a/sublime/cache_manager.py +++ b/sublime/cache_manager.py @@ -348,6 +348,7 @@ class CacheManager(metaclass=Singleton): logging.info('Migrating cache to version 1.') cover_art_re = re.compile(r'(\d+)_(\d+)') abs_path = self.calculate_abs_path('cover_art/') + abs_path.mkdir(parents=True, exist_ok=True) for cover_art_file in Path(abs_path).iterdir(): match = cover_art_re.match(cover_art_file.name) if match: diff --git a/sublime/config.py b/sublime/config.py index f4ac15a..94192d3 100644 --- a/sublime/config.py +++ b/sublime/config.py @@ -1,10 +1,29 @@ import logging import os +from enum import Enum from typing import Any, Dict, List, Optional import keyring +class ReplayGainType(Enum): + NO = 0 + TRACK = 1 + ALBUM = 2 + + def as_string(self) -> str: + return ['no', 'track', 'album'][self.value] + + @staticmethod + def from_string(replay_gain_type: str) -> 'ReplayGainType': + return { + 'no': ReplayGainType.NO, + 'disabled': ReplayGainType.NO, + 'track': ReplayGainType.TRACK, + 'album': ReplayGainType.ALBUM, + }[replay_gain_type.lower()] + + class ServerConfiguration: version: int name: str @@ -61,19 +80,23 @@ class AppConfiguration: prefetch_amount: int = 3 concurrent_download_limit: int = 5 port_number: int = 8282 - version: int = 2 + version: int = 3 serve_over_lan: bool = True + replay_gain: ReplayGainType = ReplayGainType.NO def to_json(self) -> Dict[str, Any]: - exclude = ('servers') + exclude = ('servers', 'replay_gain') json_object = { k: getattr(self, k) for k in self.__annotations__.keys() if k not in exclude } - json_object.update({ - 'servers': [s.__dict__ for s in self.servers], - }) + json_object.update( + { + 'servers': [s.__dict__ for s in self.servers], + 'replay_gain': + getattr(self, 'replay_gain', ReplayGainType.NO).value, + }) return json_object def migrate(self): @@ -85,7 +108,12 @@ class AppConfiguration: logging.info('Setting serve_over_lan to True') self.serve_over_lan = True - self.version = 2 + if (getattr(self, 'version') or 0) < 3: + logging.info('Migrating app configuration to version 3.') + logging.info('Setting replay_gain to ReplayGainType.NO') + self.replay_gain = ReplayGainType.NO + + self.version = 3 @property def cache_location(self) -> str: diff --git a/sublime/players.py b/sublime/players.py index bb59a98..aa32757 100644 --- a/sublime/players.py +++ b/sublime/players.py @@ -123,11 +123,19 @@ class Player: class MPVPlayer(Player): - def __init__(self, *args): - super().__init__(*args) + def __init__( + self, + on_timepos_change: Callable[[Optional[float]], None], + on_track_end: Callable[[], None], + on_player_event: Callable[[PlayerEvent], None], + config: AppConfiguration, + ): + super().__init__( + on_timepos_change, on_track_end, on_player_event, config) self.mpv = mpv.MPV() self.mpv.audio_client_name = 'sublime-music' + self.mpv.replaygain = config.replay_gain.as_string() self.progress_value_lock = threading.Lock() self.progress_value_count = 0 self._muted = False @@ -301,11 +309,7 @@ class ChromecastPlayer(Player): config: AppConfiguration, ): super().__init__( - on_timepos_change, - on_track_end, - on_player_event, - config, - ) + on_timepos_change, on_track_end, on_player_event, config) self._timepos = 0.0 self.time_incrementor_running = False self._can_hotswap_source = False diff --git a/sublime/state_manager.py b/sublime/state_manager.py index f5d68c2..5252cdb 100644 --- a/sublime/state_manager.py +++ b/sublime/state_manager.py @@ -160,16 +160,22 @@ class ApplicationState: os.makedirs(os.path.dirname(self.state_filename), exist_ok=True) # Save the state + state_json = json.dumps(self.to_json(), indent=2, sort_keys=True) + if not state_json: + return with open(self.state_filename, 'w+') as f: - f.write(json.dumps(self.to_json(), indent=2, sort_keys=True)) + f.write(state_json) def save_config(self): # Make the necessary directories before writing the config. os.makedirs(os.path.dirname(self.config_file), exist_ok=True) + config_json = json.dumps( + self.config.to_json(), indent=2, sort_keys=True) + if not config_json: + return with open(self.config_file, 'w+') as f: - f.write( - json.dumps(self.config.to_json(), indent=2, sort_keys=True)) + f.write(config_json) def get_config(self, filename: str) -> AppConfiguration: if not os.path.exists(filename): diff --git a/sublime/ui/common/edit_form_dialog.py b/sublime/ui/common/edit_form_dialog.py index f0166fc..4da8b31 100644 --- a/sublime/ui/common/edit_form_dialog.py +++ b/sublime/ui/common/edit_form_dialog.py @@ -4,16 +4,20 @@ import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk +TextFieldDescription = Tuple[str, str, bool] +BooleanFieldDescription = Tuple[str, str] NumericFieldDescription = Tuple[str, str, Tuple[int, int, int], int] +OptionFieldDescription = Tuple[str, str, Tuple[str, ...]] class EditFormDialog(Gtk.Dialog): entity_name: str title: str initial_size: Tuple[int, int] - text_fields: List[Tuple[str, str, bool]] = [] - boolean_fields: List[Tuple[str, str]] = [] + text_fields: List[TextFieldDescription] = [] + boolean_fields: List[BooleanFieldDescription] = [] numeric_fields: List[NumericFieldDescription] = [] + option_fields: List[OptionFieldDescription] = [] extra_label: Optional[str] = None extra_buttons: List[Gtk.Button] = [] @@ -75,6 +79,30 @@ class EditFormDialog(Gtk.Dialog): i += 1 + for label, value_field_name, options in self.option_fields: + entry_label = Gtk.Label(label=label + ':') + entry_label.set_halign(Gtk.Align.START) + content_grid.attach(entry_label, 0, i, 1, 1) + + options_store = Gtk.ListStore(str) + for option in options: + options_store.append([option]) + + combo = Gtk.ComboBox.new_with_model(options_store) + combo.set_id_column(0) + renderer_text = Gtk.CellRendererText() + combo.pack_start(renderer_text, True) + combo.add_attribute(renderer_text, "text", 0) + + field_value = getattr(existing_object, value_field_name) + if field_value: + combo.set_active(field_value.value) + + content_grid.attach(combo, 1, i, 1, 1) + self.data[value_field_name] = combo + + i += 1 + # Add the boolean entries to the content area. for label, value_field_name in self.boolean_fields: entry_label = Gtk.Label(label=label + ':') diff --git a/sublime/ui/settings.py b/sublime/ui/settings.py index e7c6a56..2303389 100644 --- a/sublime/ui/settings.py +++ b/sublime/ui/settings.py @@ -41,6 +41,9 @@ class SettingsDialog(EditFormDialog): 5, ), ] + option_fields = [ + ('Replay Gain', 'replay_gain', ('Disabled', 'Track', 'Album')), + ] def __init__(self, *args, **kwargs): self.extra_label = Gtk.Label(