From ef1de341118c5b442894c6dc467c366dc1d072ee Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 6 Sep 2022 14:09:04 +0200 Subject: [PATCH] fix(playout): missing live show events (#2087) --- api/libretime_api/schedule/models/show.py | 6 ++ .../schedule/serializers/show.py | 1 + .../schedule/tests/models/test_show.py | 15 +++++ api/schema.yml | 7 ++ playout/libretime_playout/player/schedule.py | 42 ++++++++++++ playout/tests/player/schedule_test.py | 65 +++++++++++++++++-- 6 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 api/libretime_api/schedule/tests/models/test_show.py diff --git a/api/libretime_api/schedule/models/show.py b/api/libretime_api/schedule/models/show.py index fa71d00a7..ce13f689e 100644 --- a/api/libretime_api/schedule/models/show.py +++ b/api/libretime_api/schedule/models/show.py @@ -26,11 +26,13 @@ class Show(models.Model): ) live_auth_registered = models.BooleanField( + default=False, blank=True, null=True, db_column="live_stream_using_airtime_auth", ) live_auth_custom = models.BooleanField( + default=False, blank=True, null=True, db_column="live_stream_using_custom_auth", @@ -48,6 +50,10 @@ class Show(models.Model): db_column="live_stream_pass", ) + @property + def live_enabled(self) -> bool: + return any((self.live_auth_registered, self.live_auth_custom)) + # A show is linkable if it has never been linked before. Once # a show becomes unlinked it can not be linked again. linked = models.BooleanField() diff --git a/api/libretime_api/schedule/serializers/show.py b/api/libretime_api/schedule/serializers/show.py index 2ef8ee09d..f25e926cc 100644 --- a/api/libretime_api/schedule/serializers/show.py +++ b/api/libretime_api/schedule/serializers/show.py @@ -15,6 +15,7 @@ class ShowSerializer(serializers.ModelSerializer): "image", "foreground_color", "background_color", + "live_enabled", "linked", "linkable", "auto_playlist", diff --git a/api/libretime_api/schedule/tests/models/test_show.py b/api/libretime_api/schedule/tests/models/test_show.py new file mode 100644 index 000000000..dceac902d --- /dev/null +++ b/api/libretime_api/schedule/tests/models/test_show.py @@ -0,0 +1,15 @@ +from ...models import Show + + +def test_show_live_enabled(): + show = Show( + name="My Test Show", + description="My test show description", + ) + assert not show.live_enabled + + show.live_auth_registered = True + assert show.live_enabled + + show.live_auth_custom = True + assert show.live_enabled diff --git a/api/schema.yml b/api/schema.yml index a271f77bd..4551f86e2 100644 --- a/api/schema.yml +++ b/api/schema.yml @@ -6379,6 +6379,9 @@ components: type: string nullable: true maxLength: 6 + live_enabled: + type: boolean + readOnly: true linked: type: boolean linkable: @@ -7185,6 +7188,9 @@ components: type: string nullable: true maxLength: 6 + live_enabled: + type: boolean + readOnly: true linked: type: boolean linkable: @@ -7202,6 +7208,7 @@ components: - id - linkable - linked + - live_enabled - name ShowDays: type: object diff --git a/playout/libretime_playout/player/schedule.py b/playout/libretime_playout/player/schedule.py index c89ba1138..667357255 100644 --- a/playout/libretime_playout/player/schedule.py +++ b/playout/libretime_playout/player/schedule.py @@ -10,6 +10,7 @@ from libretime_shared.datetime import ( time_in_seconds, ) +from ..liquidsoap.models import StreamPreferences from .events import EventKind EVENT_KEY_FORMAT = "%Y-%m-%d-%H-%M-%S" @@ -36,6 +37,8 @@ def insert_event(events: dict, event_key: str, event: dict): def get_schedule(api_client: ApiClient): + stream_preferences = StreamPreferences(**api_client.get_stream_preferences().json()) + current_time = datetime.utcnow() end_time = current_time + timedelta(days=1) @@ -59,6 +62,15 @@ def get_schedule(api_client: ApiClient): show_instance = api_client.get_show_instance(item["instance"]).json() show = api_client.get_show(show_instance["show"]).json() + if show["live_enabled"]: + show_instance["starts_at"] = isoparse(show_instance["starts_at"]) + show_instance["ends_at"] = isoparse(show_instance["ends_at"]) + generate_live_events( + events, + show_instance, + stream_preferences.input_fade_transition, + ) + if item["file"]: file = api_client.get_file(item["file"]).json() generate_file_events(events, item, file, show) @@ -70,6 +82,36 @@ def get_schedule(api_client: ApiClient): return {"media": dict(sorted(events.items()))} +def generate_live_events( + events: dict, + show_instance: dict, + input_fade_transition: float, +): + transition = timedelta(seconds=input_fade_transition) + + switch_off_event_key = datetime_to_event_key(show_instance["ends_at"] - transition) + kick_out_event_key = datetime_to_event_key(show_instance["ends_at"]) + + # If enabled, fade the input source out + if switch_off_event_key != kick_out_event_key: + switch_off_event = { + "type": EventKind.EVENT, + "event_type": "switch_off", + "start": switch_off_event_key, + "end": switch_off_event_key, + } + insert_event(events, switch_off_event_key, switch_off_event) + + # Then kick the source out + kick_out_event = { + "type": EventKind.EVENT, + "event_type": "kick_out", + "start": kick_out_event_key, + "end": kick_out_event_key, + } + insert_event(events, kick_out_event_key, kick_out_event) + + def generate_file_events( events: dict, schedule: dict, diff --git a/playout/tests/player/schedule_test.py b/playout/tests/player/schedule_test.py index bc81c381d..ffd86e7a0 100644 --- a/playout/tests/player/schedule_test.py +++ b/playout/tests/player/schedule_test.py @@ -1,4 +1,5 @@ import random +from datetime import timedelta import pytest from dateutil.parser import isoparse @@ -7,6 +8,7 @@ from libretime_api_client.v2 import ApiClient from libretime_playout.player.events import EventKind from libretime_playout.player.schedule import ( generate_file_events, + generate_live_events, generate_webstream_events, get_schedule, ) @@ -18,10 +20,10 @@ def _api_client_fixture(): return ApiClient(base_url=base_url, api_key="test_key") -SHOW_1 = {"id": 1, "name": "Show 1"} -SHOW_2 = {"id": 2, "name": "Show 2"} -SHOW_3 = {"id": 3, "name": "Show 3"} -SHOW_4 = {"id": 4, "name": "Show 4"} +SHOW_1 = {"id": 1, "name": "Show 1", "live_enabled": False} +SHOW_2 = {"id": 2, "name": "Show 2", "live_enabled": False} +SHOW_3 = {"id": 3, "name": "Show 3", "live_enabled": True} +SHOW_4 = {"id": 4, "name": "Show 4", "live_enabled": False} SHOW_INSTANCE_1 = { "id": 1, @@ -264,6 +266,40 @@ SCHEDULE = [ ] +def test_generate_live_events(): + show_instance_3 = SHOW_INSTANCE_3.copy() + show_instance_3["starts_at"] = isoparse(show_instance_3["starts_at"]) + show_instance_3["ends_at"] = isoparse(show_instance_3["ends_at"]) + + result = {} + generate_live_events(result, show_instance_3, 0.0) + assert result == { + "2022-09-05-13-00-00": { + "type": EventKind.EVENT, + "event_type": "kick_out", + "start": "2022-09-05-13-00-00", + "end": "2022-09-05-13-00-00", + } + } + + result = {} + generate_live_events(result, show_instance_3, 2.0) + assert result == { + "2022-09-05-12-59-58": { + "type": EventKind.EVENT, + "event_type": "switch_off", + "start": "2022-09-05-12-59-58", + "end": "2022-09-05-12-59-58", + }, + "2022-09-05-13-00-00": { + "type": EventKind.EVENT, + "event_type": "kick_out", + "start": "2022-09-05-13-00-00", + "end": "2022-09-05-13-00-00", + }, + } + + def test_generate_file_events(): schedule_1 = SCHEDULE_1.copy() schedule_1["starts_at"] = isoparse(schedule_1["starts_at"]) @@ -349,6 +385,15 @@ def test_generate_webstream_events(): def test_get_schedule(schedule, requests_mock, api_client: ApiClient): base_url = api_client.base_url + requests_mock.get( + f"{base_url}/api/v2/stream/preferences", + json={ + "input_fade_transition": 2.0, + "message_format": 0, + "message_offline": "", + }, + ) + requests_mock.get(f"{base_url}/api/v2/schedule", json=schedule) requests_mock.get(f"{base_url}/api/v2/shows/1", json=SHOW_1) @@ -541,7 +586,19 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient): "replay_gain": "4.52", "filesize": 10000, }, + "2022-09-05-12-59-58": { + "type": EventKind.EVENT, + "event_type": "switch_off", + "start": "2022-09-05-12-59-58", + "end": "2022-09-05-12-59-58", + }, "2022-09-05-13-00-00": { + "type": EventKind.EVENT, + "event_type": "kick_out", + "start": "2022-09-05-13-00-00", + "end": "2022-09-05-13-00-00", + }, + "2022-09-05-13-00-00_0": { "type": EventKind.FILE, "row_id": 9, "start": "2022-09-05-13-00-00",