diff --git a/api/libretime_api/core/models/preference.py b/api/libretime_api/core/models/preference.py index 8a99f807f..68a0f6952 100644 --- a/api/libretime_api/core/models/preference.py +++ b/api/libretime_api/core/models/preference.py @@ -18,12 +18,13 @@ class StreamPreferences(BaseModel): input_fade_transition: float message_format: MessageFormatKind message_offline: str + replay_gain_enabled: bool + replay_gain_offset: float + # input_auto_switch_off: bool # input_auto_switch_on: bool # input_main_user: str # input_main_password: str - # replay_gain_enabled: bool - # replay_gain_offset: float # track_fade_in: float # track_fade_out: float # track_fade_transition: float @@ -82,6 +83,8 @@ class Preference(models.Model): int(entries.get("stream_label_format") or 0) ), message_offline=entries.get("off_air_meta") or "Offline", + replay_gain_enabled=entries.get("enable_replay_gain") == "1", + replay_gain_offset=float(entries.get("replay_gain_modifier") or 0.0), ) @classmethod diff --git a/api/libretime_api/core/serializers/stream.py b/api/libretime_api/core/serializers/stream.py index c0c265f34..f3c6e5588 100644 --- a/api/libretime_api/core/serializers/stream.py +++ b/api/libretime_api/core/serializers/stream.py @@ -6,6 +6,8 @@ class StreamPreferencesSerializer(serializers.Serializer): input_fade_transition = serializers.FloatField(read_only=True) message_format = serializers.IntegerField(read_only=True) message_offline = serializers.CharField(read_only=True) + replay_gain_enabled = serializers.BooleanField(read_only=True) + replay_gain_offset = serializers.FloatField(read_only=True) # pylint: disable=abstract-method diff --git a/api/libretime_api/core/tests/models/test_preference.py b/api/libretime_api/core/tests/models/test_preference.py index 12efa45f9..f1683f458 100644 --- a/api/libretime_api/core/tests/models/test_preference.py +++ b/api/libretime_api/core/tests/models/test_preference.py @@ -16,6 +16,8 @@ def test_preference_get_stream_preferences(db): "input_fade_transition": 0.0, "message_format": 0, "message_offline": "LibreTime - offline", + "replay_gain_enabled": True, + "replay_gain_offset": 0.0, } diff --git a/api/libretime_api/core/tests/views/test_stream.py b/api/libretime_api/core/tests/views/test_stream.py index 7f9f9bfa0..0cd3082a0 100644 --- a/api/libretime_api/core/tests/views/test_stream.py +++ b/api/libretime_api/core/tests/views/test_stream.py @@ -9,6 +9,8 @@ def test_stream_preferences_get(db, api_client: APIClient): "input_fade_transition": 0.0, "message_format": 0, "message_offline": "LibreTime - offline", + "replay_gain_enabled": True, + "replay_gain_offset": 0.0, } diff --git a/api/libretime_api/core/views/stream.py b/api/libretime_api/core/views/stream.py index 531113aba..afa11b663 100644 --- a/api/libretime_api/core/views/stream.py +++ b/api/libretime_api/core/views/stream.py @@ -19,6 +19,8 @@ class StreamPreferencesView(views.APIView): "input_fade_transition", "message_format", "message_offline", + "replay_gain_enabled", + "replay_gain_offset", } ) ) diff --git a/api/schema.yml b/api/schema.yml index 3534846d4..8b55b0c14 100644 --- a/api/schema.yml +++ b/api/schema.yml @@ -7535,10 +7535,19 @@ components: message_offline: type: string readOnly: true + replay_gain_enabled: + type: boolean + readOnly: true + replay_gain_offset: + type: number + format: double + readOnly: true required: - input_fade_transition - message_format - message_offline + - replay_gain_enabled + - replay_gain_offset StreamState: type: object properties: diff --git a/playout/libretime_playout/liquidsoap/models.py b/playout/libretime_playout/liquidsoap/models.py index c8d9520fe..3cd4413b8 100644 --- a/playout/libretime_playout/liquidsoap/models.py +++ b/playout/libretime_playout/liquidsoap/models.py @@ -17,6 +17,8 @@ class StreamPreferences(BaseModel): input_fade_transition: float message_format: MessageFormatKind message_offline: str + replay_gain_enabled: bool + replay_gain_offset: float class StreamState(BaseModel): diff --git a/playout/libretime_playout/player/schedule.py b/playout/libretime_playout/player/schedule.py index 42bf0a5cb..9e7d4f602 100644 --- a/playout/libretime_playout/player/schedule.py +++ b/playout/libretime_playout/player/schedule.py @@ -64,15 +64,11 @@ def get_schedule(api_client: ApiClient) -> Events: if show["live_enabled"]: show_instance["starts_at"] = event_isoparse(show_instance["starts_at"]) show_instance["ends_at"] = event_isoparse(show_instance["ends_at"]) - generate_live_events( - events, - show_instance, - stream_preferences.input_fade_transition, - ) + generate_live_events(events, show_instance, stream_preferences) if item["file"]: file = api_client.get_file(item["file"]).json() - generate_file_events(events, item, file, show) + generate_file_events(events, item, file, show, stream_preferences) elif item["stream"]: webstream = api_client.get_webstream(item["stream"]).json() @@ -84,9 +80,9 @@ def get_schedule(api_client: ApiClient) -> Events: def generate_live_events( events: Events, show_instance: dict, - input_fade_transition: float, + stream_preferences: StreamPreferences, ): - transition = timedelta(seconds=input_fade_transition) + transition = timedelta(seconds=stream_preferences.input_fade_transition) switch_off = show_instance["ends_at"] - transition kick_out = show_instance["ends_at"] @@ -118,6 +114,7 @@ def generate_file_events( schedule: dict, file: dict, show: dict, + stream_preferences: StreamPreferences, ): """ Generate events for a scheduled file. @@ -143,6 +140,15 @@ def generate_file_events( replay_gain=file["replay_gain"], filesize=file["size"], ) + + if event.replay_gain is None: + event.replay_gain = 0.0 + + if stream_preferences.replay_gain_enabled: + event.replay_gain += stream_preferences.replay_gain_offset + else: + event.replay_gain = None + insert_event(events, event.start_key, event) diff --git a/playout/tests/conftest.py b/playout/tests/conftest.py index de868374c..043a8d157 100644 --- a/playout/tests/conftest.py +++ b/playout/tests/conftest.py @@ -1,6 +1,7 @@ import pytest from libretime_playout.config import Config +from libretime_playout.liquidsoap.models import StreamPreferences @pytest.fixture() @@ -36,3 +37,14 @@ def config(): }, } ) + + +@pytest.fixture() +def stream_preferences(): + return StreamPreferences( + input_fade_transition=0.0, + message_format=0, + message_offline="LibreTime - offline", + replay_gain_enabled=True, + replay_gain_offset=-3.5, + ) diff --git a/playout/tests/liquidsoap/entrypoint_test.py b/playout/tests/liquidsoap/entrypoint_test.py index c92a68ec7..3ddc91664 100644 --- a/playout/tests/liquidsoap/entrypoint_test.py +++ b/playout/tests/liquidsoap/entrypoint_test.py @@ -21,7 +21,12 @@ from .fixtures import TEST_STREAM_CONFIGS, make_config_with_stream "stream_config", TEST_STREAM_CONFIGS, ) -def test_generate_entrypoint(stream_config: Config, version, snapshot): +def test_generate_entrypoint( + stream_config: Config, + stream_preferences: StreamPreferences, + version, + snapshot, +): with mock.patch( "libretime_playout.liquidsoap.entrypoint.here", Path("/fake"), @@ -29,11 +34,7 @@ def test_generate_entrypoint(stream_config: Config, version, snapshot): found = generate_entrypoint( log_filepath=Path("/var/log/radio.log"), config=stream_config, - preferences=StreamPreferences( - input_fade_transition=0.0, - message_format=0, - message_offline="LibreTime - offline", - ), + preferences=stream_preferences, info=Info( station_name="LibreTime", ), @@ -51,7 +52,11 @@ def test_generate_entrypoint(stream_config: Config, version, snapshot): "stream_config", TEST_STREAM_CONFIGS, ) -def test_liquidsoap_syntax(tmp_path: Path, stream_config): +def test_liquidsoap_syntax( + tmp_path: Path, + stream_config: Config, + stream_preferences: StreamPreferences, +): entrypoint_filepath = tmp_path / "radio.liq" log_filepath = tmp_path / "radio.log" @@ -59,11 +64,7 @@ def test_liquidsoap_syntax(tmp_path: Path, stream_config): generate_entrypoint( log_filepath=log_filepath, config=stream_config, - preferences=StreamPreferences( - input_fade_transition=0.0, - message_format=0, - message_offline="LibreTime - offline", - ), + preferences=stream_preferences, info=Info( station_name="LibreTime", ), @@ -79,7 +80,10 @@ def test_liquidsoap_syntax(tmp_path: Path, stream_config): LIQ_VERSION >= (2, 0, 0), reason="unsupported liquidsoap >= 2.0.0", ) -def test_liquidsoap_unsupported_output_aac(tmp_path: Path): +def test_liquidsoap_unsupported_output_aac( + tmp_path: Path, + stream_preferences: StreamPreferences, +): entrypoint_filepath = tmp_path / "radio.liq" log_filepath = tmp_path / "radio.log" @@ -98,11 +102,7 @@ def test_liquidsoap_unsupported_output_aac(tmp_path: Path): ] } ), - preferences=StreamPreferences( - input_fade_transition=0.0, - message_format=0, - message_offline="LibreTime - offline", - ), + preferences=stream_preferences, info=Info( station_name="LibreTime", ), diff --git a/playout/tests/player/schedule_test.py b/playout/tests/player/schedule_test.py index ba1e1d4c5..a29b0d5ee 100644 --- a/playout/tests/player/schedule_test.py +++ b/playout/tests/player/schedule_test.py @@ -4,6 +4,7 @@ from datetime import datetime import pytest from libretime_api_client.v2 import ApiClient +from libretime_playout.liquidsoap.models import StreamPreferences from libretime_playout.player.events import ( ActionEvent, EventKind, @@ -271,13 +272,13 @@ SCHEDULE = [ ] -def test_generate_live_events(): +def test_generate_live_events(stream_preferences: StreamPreferences): show_instance_3 = SHOW_INSTANCE_3.copy() show_instance_3["starts_at"] = event_isoparse(show_instance_3["starts_at"]) show_instance_3["ends_at"] = event_isoparse(show_instance_3["ends_at"]) result = {} - generate_live_events(result, show_instance_3, 0.0) + generate_live_events(result, show_instance_3, stream_preferences) assert result == { "2022-09-05-13-00-00": ActionEvent( start=datetime(2022, 9, 5, 13, 0), @@ -288,7 +289,8 @@ def test_generate_live_events(): } result = {} - generate_live_events(result, show_instance_3, 2.0) + stream_preferences.input_fade_transition = 2.0 + generate_live_events(result, show_instance_3, stream_preferences) assert result == { "2022-09-05-12-59-58": ActionEvent( start=datetime(2022, 9, 5, 12, 59, 58), @@ -305,13 +307,13 @@ def test_generate_live_events(): } -def test_generate_file_events(): +def test_generate_file_events(stream_preferences: StreamPreferences): schedule_1 = SCHEDULE_1.copy() schedule_1["starts_at"] = event_isoparse(schedule_1["starts_at"]) schedule_1["ends_at"] = event_isoparse(schedule_1["ends_at"]) result = {} - generate_file_events(result, schedule_1, FILE_2, SHOW_1) + generate_file_events(result, schedule_1, FILE_2, SHOW_1, stream_preferences) assert result == { "2022-09-05-11-00-00": FileEvent( start=datetime(2022, 9, 5, 11, 0), @@ -328,7 +330,32 @@ def test_generate_file_events(): track_title="My Friend the Forest", artist_name="Nils Frahm", mime="audio/flac", - replay_gain=11.46, + replay_gain=11.46 - 3.5, + filesize=10000, + file_ready=False, + ) + } + + result = {} + stream_preferences.replay_gain_enabled = False + generate_file_events(result, schedule_1, FILE_2, SHOW_1, stream_preferences) + assert result == { + "2022-09-05-11-00-00": FileEvent( + start=datetime(2022, 9, 5, 11, 0), + end=datetime(2022, 9, 5, 11, 5, 2), + type=EventKind.FILE, + row_id=1, + uri=None, + id=2, + show_name="Show 1", + fade_in=500.0, + fade_out=500.0, + cue_in=13.7008, + cue_out=315.845, + track_title="My Friend the Forest", + artist_name="Nils Frahm", + mime="audio/flac", + replay_gain=None, filesize=10000, file_ready=False, ) @@ -398,6 +425,8 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient): "input_fade_transition": 2.0, "message_format": 0, "message_offline": "", + "replay_gain_enabled": True, + "replay_gain_offset": -3.5, }, ) @@ -434,7 +463,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient): track_title="My Friend the Forest", artist_name="Nils Frahm", mime="audio/flac", - replay_gain=11.46, + replay_gain=11.46 - 3.5, filesize=10000, file_ready=False, ), @@ -453,7 +482,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient): track_title="#2", artist_name="Nils Frahm", mime="audio/flac", - replay_gain=-1.65, + replay_gain=-1.65 - 3.5, filesize=10000, file_ready=False, ), @@ -472,7 +501,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient): track_title="Democracy Now! 2022-09-05 Monday", artist_name="Democracy Now! Audio", mime="audio/mp3", - replay_gain=-1.39, + replay_gain=-1.39 - 3.5, filesize=10000, file_ready=False, ), @@ -491,7 +520,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient): track_title="#2", artist_name="Nils Frahm", mime="audio/flac", - replay_gain=-1.65, + replay_gain=-1.65 - 3.5, filesize=10000, file_ready=False, ), @@ -546,7 +575,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient): track_title="All Melody", artist_name="Nils Frahm", mime="audio/flac", - replay_gain=-2.13, + replay_gain=-2.13 - 3.5, filesize=10000, file_ready=False, ), @@ -565,7 +594,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient): track_title="My Friend the Forest", artist_name="Nils Frahm", mime="audio/flac", - replay_gain=11.46, + replay_gain=11.46 - 3.5, filesize=10000, file_ready=False, ), @@ -584,7 +613,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient): track_title="The Dane", artist_name="Nils Frahm", mime="audio/flac", - replay_gain=4.52, + replay_gain=4.52 - 3.5, filesize=10000, file_ready=False, ), @@ -615,7 +644,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient): track_title="My Friend the Forest", artist_name="Nils Frahm", mime="audio/flac", - replay_gain=11.46, + replay_gain=11.46 - 3.5, filesize=10000, file_ready=False, ), @@ -634,7 +663,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient): track_title="#2", artist_name="Nils Frahm", mime="audio/flac", - replay_gain=-1.65, + replay_gain=-1.65 - 3.5, filesize=10000, file_ready=False, ),