From 35d0dec4a887cdaea2d73dc9bee60eb6624a2aca Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 8 Feb 2024 20:29:10 +0100 Subject: [PATCH] fix: apply replay gain preferences on scheduled files (#2945) ### Description The replay gain preferences are applied in the legacy code, but the playout code was missing this feature. The replay gain was not applied when playout fetched the schedules. https://github.com/libretime/libretime/blob/37d1a7685e37e45734553a0eb4a4da793ca858cb/legacy/application/models/Schedule.php#L881-L886 --- api/libretime_api/core/models/preference.py | 7 ++- api/libretime_api/core/serializers/stream.py | 2 + .../core/tests/models/test_preference.py | 2 + .../core/tests/views/test_stream.py | 2 + api/libretime_api/core/views/stream.py | 2 + api/schema.yml | 9 +++ .../libretime_playout/liquidsoap/models.py | 2 + playout/libretime_playout/player/schedule.py | 22 ++++--- playout/tests/conftest.py | 12 ++++ playout/tests/liquidsoap/entrypoint_test.py | 36 +++++------ playout/tests/player/schedule_test.py | 59 ++++++++++++++----- 11 files changed, 112 insertions(+), 43 deletions(-) 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, ),