Added support for replaygain option
This commit is contained in:
@@ -11,6 +11,7 @@ gi.require_version('Notify', '0.7')
|
|||||||
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, Notify
|
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, Notify
|
||||||
|
|
||||||
from .cache_manager import CacheManager
|
from .cache_manager import CacheManager
|
||||||
|
from .config import ReplayGainType
|
||||||
from .dbus_manager import dbus_propagate, DBusManager
|
from .dbus_manager import dbus_propagate, DBusManager
|
||||||
from .players import ChromecastPlayer, MPVPlayer, PlayerEvent
|
from .players import ChromecastPlayer, MPVPlayer, PlayerEvent
|
||||||
from .server.api_objects import Child, Directory, Playlist
|
from .server.api_objects import Child, Directory, Playlist
|
||||||
@@ -431,6 +432,8 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
'prefetch_amount'].get_value_as_int()
|
'prefetch_amount'].get_value_as_int()
|
||||||
self.state.config.concurrent_download_limit = dialog.data[
|
self.state.config.concurrent_download_limit = dialog.data[
|
||||||
'concurrent_download_limit'].get_value_as_int()
|
'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.state.save_config()
|
||||||
self.reset_state()
|
self.reset_state()
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
|
@@ -348,6 +348,7 @@ class CacheManager(metaclass=Singleton):
|
|||||||
logging.info('Migrating cache to version 1.')
|
logging.info('Migrating cache to version 1.')
|
||||||
cover_art_re = re.compile(r'(\d+)_(\d+)')
|
cover_art_re = re.compile(r'(\d+)_(\d+)')
|
||||||
abs_path = self.calculate_abs_path('cover_art/')
|
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():
|
for cover_art_file in Path(abs_path).iterdir():
|
||||||
match = cover_art_re.match(cover_art_file.name)
|
match = cover_art_re.match(cover_art_file.name)
|
||||||
if match:
|
if match:
|
||||||
|
@@ -1,10 +1,29 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import keyring
|
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:
|
class ServerConfiguration:
|
||||||
version: int
|
version: int
|
||||||
name: str
|
name: str
|
||||||
@@ -61,19 +80,23 @@ class AppConfiguration:
|
|||||||
prefetch_amount: int = 3
|
prefetch_amount: int = 3
|
||||||
concurrent_download_limit: int = 5
|
concurrent_download_limit: int = 5
|
||||||
port_number: int = 8282
|
port_number: int = 8282
|
||||||
version: int = 2
|
version: int = 3
|
||||||
serve_over_lan: bool = True
|
serve_over_lan: bool = True
|
||||||
|
replay_gain: ReplayGainType = ReplayGainType.NO
|
||||||
|
|
||||||
def to_json(self) -> Dict[str, Any]:
|
def to_json(self) -> Dict[str, Any]:
|
||||||
exclude = ('servers')
|
exclude = ('servers', 'replay_gain')
|
||||||
json_object = {
|
json_object = {
|
||||||
k: getattr(self, k)
|
k: getattr(self, k)
|
||||||
for k in self.__annotations__.keys()
|
for k in self.__annotations__.keys()
|
||||||
if k not in exclude
|
if k not in exclude
|
||||||
}
|
}
|
||||||
json_object.update({
|
json_object.update(
|
||||||
'servers': [s.__dict__ for s in self.servers],
|
{
|
||||||
})
|
'servers': [s.__dict__ for s in self.servers],
|
||||||
|
'replay_gain':
|
||||||
|
getattr(self, 'replay_gain', ReplayGainType.NO).value,
|
||||||
|
})
|
||||||
return json_object
|
return json_object
|
||||||
|
|
||||||
def migrate(self):
|
def migrate(self):
|
||||||
@@ -85,7 +108,12 @@ class AppConfiguration:
|
|||||||
logging.info('Setting serve_over_lan to True')
|
logging.info('Setting serve_over_lan to True')
|
||||||
self.serve_over_lan = 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
|
@property
|
||||||
def cache_location(self) -> str:
|
def cache_location(self) -> str:
|
||||||
|
@@ -123,11 +123,19 @@ class Player:
|
|||||||
|
|
||||||
|
|
||||||
class MPVPlayer(Player):
|
class MPVPlayer(Player):
|
||||||
def __init__(self, *args):
|
def __init__(
|
||||||
super().__init__(*args)
|
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 = mpv.MPV()
|
||||||
self.mpv.audio_client_name = 'sublime-music'
|
self.mpv.audio_client_name = 'sublime-music'
|
||||||
|
self.mpv.replaygain = config.replay_gain.as_string()
|
||||||
self.progress_value_lock = threading.Lock()
|
self.progress_value_lock = threading.Lock()
|
||||||
self.progress_value_count = 0
|
self.progress_value_count = 0
|
||||||
self._muted = False
|
self._muted = False
|
||||||
@@ -301,11 +309,7 @@ class ChromecastPlayer(Player):
|
|||||||
config: AppConfiguration,
|
config: AppConfiguration,
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
on_timepos_change,
|
on_timepos_change, on_track_end, on_player_event, config)
|
||||||
on_track_end,
|
|
||||||
on_player_event,
|
|
||||||
config,
|
|
||||||
)
|
|
||||||
self._timepos = 0.0
|
self._timepos = 0.0
|
||||||
self.time_incrementor_running = False
|
self.time_incrementor_running = False
|
||||||
self._can_hotswap_source = False
|
self._can_hotswap_source = False
|
||||||
|
@@ -160,16 +160,22 @@ class ApplicationState:
|
|||||||
os.makedirs(os.path.dirname(self.state_filename), exist_ok=True)
|
os.makedirs(os.path.dirname(self.state_filename), exist_ok=True)
|
||||||
|
|
||||||
# Save the state
|
# 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:
|
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):
|
def save_config(self):
|
||||||
# Make the necessary directories before writing the config.
|
# Make the necessary directories before writing the config.
|
||||||
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
|
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:
|
with open(self.config_file, 'w+') as f:
|
||||||
f.write(
|
f.write(config_json)
|
||||||
json.dumps(self.config.to_json(), indent=2, sort_keys=True))
|
|
||||||
|
|
||||||
def get_config(self, filename: str) -> AppConfiguration:
|
def get_config(self, filename: str) -> AppConfiguration:
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
|
@@ -4,16 +4,20 @@ import gi
|
|||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
TextFieldDescription = Tuple[str, str, bool]
|
||||||
|
BooleanFieldDescription = Tuple[str, str]
|
||||||
NumericFieldDescription = Tuple[str, str, Tuple[int, int, int], int]
|
NumericFieldDescription = Tuple[str, str, Tuple[int, int, int], int]
|
||||||
|
OptionFieldDescription = Tuple[str, str, Tuple[str, ...]]
|
||||||
|
|
||||||
|
|
||||||
class EditFormDialog(Gtk.Dialog):
|
class EditFormDialog(Gtk.Dialog):
|
||||||
entity_name: str
|
entity_name: str
|
||||||
title: str
|
title: str
|
||||||
initial_size: Tuple[int, int]
|
initial_size: Tuple[int, int]
|
||||||
text_fields: List[Tuple[str, str, bool]] = []
|
text_fields: List[TextFieldDescription] = []
|
||||||
boolean_fields: List[Tuple[str, str]] = []
|
boolean_fields: List[BooleanFieldDescription] = []
|
||||||
numeric_fields: List[NumericFieldDescription] = []
|
numeric_fields: List[NumericFieldDescription] = []
|
||||||
|
option_fields: List[OptionFieldDescription] = []
|
||||||
extra_label: Optional[str] = None
|
extra_label: Optional[str] = None
|
||||||
extra_buttons: List[Gtk.Button] = []
|
extra_buttons: List[Gtk.Button] = []
|
||||||
|
|
||||||
@@ -75,6 +79,30 @@ class EditFormDialog(Gtk.Dialog):
|
|||||||
|
|
||||||
i += 1
|
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.
|
# Add the boolean entries to the content area.
|
||||||
for label, value_field_name in self.boolean_fields:
|
for label, value_field_name in self.boolean_fields:
|
||||||
entry_label = Gtk.Label(label=label + ':')
|
entry_label = Gtk.Label(label=label + ':')
|
||||||
|
@@ -41,6 +41,9 @@ class SettingsDialog(EditFormDialog):
|
|||||||
5,
|
5,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
option_fields = [
|
||||||
|
('Replay Gain', 'replay_gain', ('Disabled', 'Track', 'Album')),
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.extra_label = Gtk.Label(
|
self.extra_label = Gtk.Label(
|
||||||
|
Reference in New Issue
Block a user