From 510d55c7c15ca37d91d13a42e99fdde8fb3c6e77 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 5 Sep 2022 18:41:04 +0200 Subject: [PATCH] feat(playout): improve generate_*_events (#2088) --- playout/libretime_playout/player/schedule.py | 56 +- playout/tests/player/schedule_test.py | 656 ++++++++++++++++--- 2 files changed, 586 insertions(+), 126 deletions(-) diff --git a/playout/libretime_playout/player/schedule.py b/playout/libretime_playout/player/schedule.py index c351d2bf5..c89ba1138 100644 --- a/playout/libretime_playout/player/schedule.py +++ b/playout/libretime_playout/player/schedule.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from operator import itemgetter from typing import Dict from dateutil.parser import isoparse @@ -18,6 +19,22 @@ def datetime_to_event_key(value: datetime) -> str: return value.strftime(EVENT_KEY_FORMAT) +def insert_event(events: dict, event_key: str, event: dict): + key = event_key + + # Search for an empty slot + index = 0 + while key in events: + # Ignore duplicate event + if event == events[key]: + return + + key = f"{event_key}_{index}" + index += 1 + + events[key] = event + + def get_schedule(api_client: ApiClient): current_time = datetime.utcnow() end_time = current_time + timedelta(days=1) @@ -34,8 +51,8 @@ def get_schedule(api_client: ApiClient): } ).json() - events = {} - for item in schedule: + events: Dict[str, dict] = {} + for item in sorted(schedule, key=itemgetter("starts_at")): item["starts_at"] = isoparse(item["starts_at"]) item["ends_at"] = isoparse(item["ends_at"]) @@ -44,29 +61,28 @@ def get_schedule(api_client: ApiClient): if item["file"]: file = api_client.get_file(item["file"]).json() - events.update(generate_file_events(item, file, show)) + generate_file_events(events, item, file, show) elif item["stream"]: webstream = api_client.get_webstream(item["stream"]).json() - events.update(generate_webstream_events(item, webstream, show)) + generate_webstream_events(events, item, webstream, show) - return {"media": events} + return {"media": dict(sorted(events.items()))} def generate_file_events( + events: dict, schedule: dict, file: dict, show: dict, -) -> Dict[str, dict]: +): """ Generate events for a scheduled file. """ - events = {} - schedule_start_event_key = datetime_to_event_key(schedule["starts_at"]) schedule_end_event_key = datetime_to_event_key(schedule["ends_at"]) - events[schedule_start_event_key] = { + event = { "type": EventKind.FILE, "row_id": schedule["id"], "start": schedule_start_event_key, @@ -88,24 +104,22 @@ def generate_file_events( "replay_gain": file["replay_gain"], "filesize": file["size"], } - - return events + insert_event(events, schedule_start_event_key, event) def generate_webstream_events( + events: dict, schedule: dict, webstream: dict, show: dict, -) -> Dict[str, dict]: +): """ Generate events for a scheduled webstream. """ - events = {} - schedule_start_event_key = datetime_to_event_key(schedule["starts_at"]) schedule_end_event_key = datetime_to_event_key(schedule["ends_at"]) - events[schedule_start_event_key] = { + stream_buffer_start_event = { "type": EventKind.STREAM_BUFFER_START, "row_id": schedule["id"], "start": datetime_to_event_key(schedule["starts_at"] - timedelta(seconds=5)), @@ -113,8 +127,9 @@ def generate_webstream_events( "uri": webstream["url"], "id": webstream["id"], } + insert_event(events, schedule_start_event_key, stream_buffer_start_event) - events[f"{schedule_start_event_key}_0"] = { + stream_output_start_event = { "type": EventKind.STREAM_OUTPUT_START, "row_id": schedule["id"], "start": schedule_start_event_key, @@ -124,10 +139,11 @@ def generate_webstream_events( # Show data "show_name": show["name"], } + insert_event(events, schedule_start_event_key, stream_output_start_event) # NOTE: stream_*_end were previously triggered 1 second before # the schedule end. - events[schedule_end_event_key] = { + stream_buffer_end_event = { "type": EventKind.STREAM_BUFFER_END, "row_id": schedule["id"], "start": schedule_end_event_key, @@ -135,8 +151,9 @@ def generate_webstream_events( "uri": webstream["url"], "id": webstream["id"], } + insert_event(events, schedule_end_event_key, stream_buffer_end_event) - events[f"{schedule_end_event_key}_0"] = { + stream_output_end_event = { "type": EventKind.STREAM_OUTPUT_END, "row_id": schedule["id"], "start": schedule_end_event_key, @@ -144,5 +161,4 @@ def generate_webstream_events( "uri": webstream["url"], "id": webstream["id"], } - - return events + insert_event(events, schedule_end_event_key, stream_output_end_event) diff --git a/playout/tests/player/schedule_test.py b/playout/tests/player/schedule_test.py index 8bd09836d..bc81c381d 100644 --- a/playout/tests/player/schedule_test.py +++ b/playout/tests/player/schedule_test.py @@ -1,141 +1,585 @@ +import random + import pytest +from dateutil.parser import isoparse from libretime_api_client.v2 import ApiClient -from libretime_playout.player.schedule import get_schedule +from libretime_playout.player.events import EventKind +from libretime_playout.player.schedule import ( + generate_file_events, + generate_webstream_events, + get_schedule, +) -@pytest.fixture(name="api_client_mock") -def _api_client_mock(requests_mock): +@pytest.fixture(name="api_client") +def _api_client_fixture(): base_url = "http://localhost" - api_client = ApiClient(base_url=base_url, api_key="test_key") + return ApiClient(base_url=base_url, api_key="test_key") - requests_mock.get( - f"{base_url}/api/v2/schedule", - json=[ - { - "id": 17, - "starts_at": "2022-03-04T15:30:00Z", - "ends_at": "2022-03-04T15:33:50.674340Z", - "file": 1, - "stream": None, - "fade_in": "00:00:00.500000", - "fade_out": "00:00:00.500000", - "cue_in": "00:00:01.310660", - "cue_out": "00:03:51.985000", - "instance": 3, + +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_INSTANCE_1 = { + "id": 1, + "starts_at": "2022-09-05T11:00:00Z", + "ends_at": "2022-09-05T11:10:00Z", + "show": 1, +} +SHOW_INSTANCE_2 = { + "id": 2, + "starts_at": "2022-09-05T11:10:00Z", + "ends_at": "2022-09-05T12:10:00Z", + "show": 2, +} +SHOW_INSTANCE_3 = { + "id": 3, + "starts_at": "2022-09-05T12:10:00Z", + "ends_at": "2022-09-05T13:00:00Z", + "show": 3, +} +SHOW_INSTANCE_4 = { + "id": 4, + "starts_at": "2022-09-05T13:00:00Z", + "ends_at": "2022-09-05T14:10:00Z", + "show": 4, +} + +FILE_1 = { + "id": 1, + "mime": "audio/flac", + "length": "00:03:41.041723", + "replay_gain": "4.52", + "cue_in": "00:00:08.252450", + "cue_out": "00:03:27.208000", + "artist_name": "Nils Frahm", + "album_title": "Tripping with Nils Frahm", + "track_title": "The Dane", + "url": None, + "size": 10000, +} +FILE_2 = { + "id": 2, + "mime": "audio/flac", + "length": "00:06:08.668798", + "replay_gain": "11.46", + "cue_in": "00:00:13.700800", + "cue_out": "00:05:15.845000", + "artist_name": "Nils Frahm", + "album_title": "Tripping with Nils Frahm", + "track_title": "My Friend the Forest", + "url": None, + "size": 10000, +} +FILE_3 = { + "id": 3, + "mime": "audio/flac", + "length": "00:14:18.400000", + "replay_gain": "-2.13", + "cue_in": "00:00:55.121100", + "cue_out": "00:14:18.400000", + "artist_name": "Nils Frahm", + "album_title": "Tripping with Nils Frahm", + "track_title": "All Melody", + "url": None, + "size": 10000, +} +FILE_4 = { + "id": 4, + "mime": "audio/flac", + "length": "00:10:45.472200", + "replay_gain": "-1.65", + "cue_in": "00:00:00", + "cue_out": "00:10:26.891000", + "artist_name": "Nils Frahm", + "album_title": "Tripping with Nils Frahm", + "track_title": "#2", + "url": None, + "size": 10000, +} +FILE_5 = { + "id": 5, + "mime": "audio/mp3", + "length": "00:59:04.989000", + "replay_gain": "-1.39", + "cue_in": "00:00:00", + "cue_out": "00:58:59.130000", + "artist_name": "Democracy Now! Audio", + "album_title": "Democracy Now! Audio", + "track_title": "Democracy Now! 2022-09-05 Monday", + "url": None, + "size": 10000, +} + +WEBSTREAM_1 = { + "id": 1, + "name": "External radio", + "url": "http://stream.radio.org/main.ogg", +} + +SCHEDULE_1 = { + "id": 1, + "starts_at": "2022-09-05T11:00:00Z", + "ends_at": "2022-09-05T11:05:02.144200Z", + "cue_in": "00:00:13.700800", + "cue_out": "00:05:15.845000", + "fade_in": "00:00:00.500000", + "fade_out": "00:00:00.500000", + "file": 2, + "instance": 1, + "length": "00:05:02.144200", + "stream": None, +} +SCHEDULE_2 = { + "id": 2, + "starts_at": "2022-09-05T11:05:02.144200Z", + "ends_at": "2022-09-05T11:10:00Z", + "cue_in": "00:00:00", + "cue_out": "00:04:57.855800", + "fade_in": "00:00:00.500000", + "fade_out": "00:00:00.500000", + "file": 4, + "instance": 1, + "length": "00:10:26.891000", + "stream": None, +} +SCHEDULE_3 = { + "id": 3, + "starts_at": "2022-09-05T11:10:00Z", + "ends_at": "2022-09-05T12:08:59Z", + "cue_in": "00:00:00", + "cue_out": "00:58:59.130000", + "fade_in": "00:00:00.500000", + "fade_out": "00:00:00.500000", + "file": 5, + "instance": 2, + "length": "00:58:59.130000", + "stream": None, +} +SCHEDULE_4 = { + "id": 4, + "starts_at": "2022-09-05T12:08:59Z", + "ends_at": "2022-09-05T12:10:00Z", + "cue_in": "00:00:00", + "cue_out": "00:01:01", + "fade_in": "00:00:00.500000", + "fade_out": "00:00:00.500000", + "file": 4, + "instance": 2, + "length": "00:10:26.891000", + "stream": None, +} +SCHEDULE_5 = { + "id": 5, + "starts_at": "2022-09-05T12:10:00Z", + "ends_at": "2022-09-05T12:40:00Z", + "cue_in": "00:00:00", + "cue_out": "00:30:00", + "fade_in": "00:00:00.500000", + "fade_out": "00:00:00.500000", + "file": None, + "instance": 3, + "length": "00:30:00", + "stream": 1, +} +SCHEDULE_6 = { + "id": 6, + "starts_at": "2022-09-05T12:40:00Z", + "ends_at": "2022-09-05T12:53:23Z", + "cue_in": "00:00:55.121100", + "cue_out": "00:14:18.400000", + "fade_in": "00:00:00.500000", + "fade_out": "00:00:00.500000", + "file": 3, + "instance": 3, + "length": "00:13:23.278900", + "stream": None, +} +SCHEDULE_7 = { + "id": 7, + "starts_at": "2022-09-05T12:53:23Z", + "ends_at": "2022-09-05T12:58:25Z", + "cue_in": "00:00:13.700800", + "cue_out": "00:05:15.845000", + "fade_in": "00:00:00.500000", + "fade_out": "00:00:00.500000", + "file": 2, + "instance": 3, + "length": "00:05:02.144200", + "stream": None, +} +SCHEDULE_8 = { + "id": 8, + "starts_at": "2022-09-05T12:58:25Z", + "ends_at": "2022-09-05T13:00:00Z", + "cue_in": "00:00:08.252450", + "cue_out": "00:01:35", + "fade_in": "00:00:00.500000", + "fade_out": "00:00:00.500000", + "file": 1, + "instance": 3, + "length": "00:03:18.955550", + "stream": None, +} +SCHEDULE_9 = { + "id": 9, + "starts_at": "2022-09-05T13:00:00Z", + "ends_at": "2022-09-05T13:05:02.144200Z", + "cue_in": "00:00:13.700800", + "cue_out": "00:05:15.845000", + "fade_in": "00:00:00.500000", + "fade_out": "00:00:00.500000", + "file": 2, + "instance": 4, + "length": "00:05:02.144200", + "stream": None, +} +SCHEDULE_10 = { + "id": 10, + "starts_at": "2022-09-05T13:05:02.144200Z", + "ends_at": "2022-09-05T13:10:00Z", + "cue_in": "00:00:00", + "cue_out": "00:04:57.855800", + "fade_in": "00:00:00.500000", + "fade_out": "00:00:00.500000", + "file": 4, + "instance": 4, + "length": "00:10:26.891000", + "stream": None, +} +SCHEDULE = [ + SCHEDULE_1, + SCHEDULE_2, + SCHEDULE_3, + SCHEDULE_4, + SCHEDULE_5, + SCHEDULE_6, + SCHEDULE_7, + SCHEDULE_8, + SCHEDULE_9, + SCHEDULE_10, +] + + +def test_generate_file_events(): + schedule_1 = SCHEDULE_1.copy() + schedule_1["starts_at"] = isoparse(schedule_1["starts_at"]) + schedule_1["ends_at"] = isoparse(schedule_1["ends_at"]) + + result = {} + generate_file_events(result, schedule_1, FILE_2, SHOW_1) + assert result == { + "2022-09-05-11-00-00": { + "type": EventKind.FILE, + "row_id": 1, + "start": "2022-09-05-11-00-00", + "end": "2022-09-05-11-05-02", + "uri": None, + "id": 2, + "show_name": "Show 1", + "fade_in": 500.0, + "fade_out": 500.0, + "cue_in": 13.7008, + "cue_out": 315.845, + "metadata": { + "track_title": "My Friend the Forest", + "artist_name": "Nils Frahm", + "mime": "audio/flac", }, - { - "id": 18, - "starts_at": "2022-03-04T15:33:50.674340Z", - "ends_at": "2022-03-04T16:03:50.674340Z", - "file": None, - "stream": 1, - "fade_in": "00:00:00.500000", - "fade_out": "00:00:00.500000", - "cue_in": "00:00:00", - "cue_out": "00:30:00", - "instance": 3, - }, - ], - ) + "replay_gain": "11.46", + "filesize": 10000, + } + } - requests_mock.get( - f"{base_url}/api/v2/show-instances/3", - json={ - "show": 3, - }, - ) - requests_mock.get( - f"{base_url}/api/v2/shows/3", - json={ - "name": "Test", - }, - ) +def test_generate_webstream_events(): + schedule_5 = SCHEDULE_5.copy() + schedule_5["starts_at"] = isoparse(schedule_5["starts_at"]) + schedule_5["ends_at"] = isoparse(schedule_5["ends_at"]) - requests_mock.get( - f"{base_url}/api/v2/files/1", - json={ + result = {} + generate_webstream_events(result, schedule_5, WEBSTREAM_1, SHOW_3) + assert result == { + "2022-09-05-12-10-00": { + "type": EventKind.STREAM_BUFFER_START, + "row_id": 5, + "start": "2022-09-05-12-09-55", + "end": "2022-09-05-12-09-55", + "uri": "http://stream.radio.org/main.ogg", "id": 1, - "url": None, - "replay_gain": "-8.77", - "size": 9505222, - "artist_name": "Bag Raiders", - "track_title": "Shooting Stars", - "mime": "audio/mp3", }, - ) - - requests_mock.get( - f"{base_url}/api/v2/webstreams/1", - json={ + "2022-09-05-12-10-00_0": { + "type": EventKind.STREAM_OUTPUT_START, + "row_id": 5, + "start": "2022-09-05-12-10-00", + "end": "2022-09-05-12-40-00", + "uri": "http://stream.radio.org/main.ogg", "id": 1, - "name": "Test", - "url": "http://some-other-radio:8800/main.ogg", + "show_name": "Show 3", }, - ) - - return api_client + "2022-09-05-12-40-00": { + "type": EventKind.STREAM_BUFFER_END, + "row_id": 5, + "start": "2022-09-05-12-40-00", + "end": "2022-09-05-12-40-00", + "uri": "http://stream.radio.org/main.ogg", + "id": 1, + }, + "2022-09-05-12-40-00_0": { + "type": EventKind.STREAM_OUTPUT_END, + "row_id": 5, + "start": "2022-09-05-12-40-00", + "end": "2022-09-05-12-40-00", + "uri": "http://stream.radio.org/main.ogg", + "id": 1, + }, + } -def test_get_schedule(api_client_mock: ApiClient): - assert get_schedule(api_client_mock) == { +@pytest.mark.parametrize( + "schedule", + [ + (SCHEDULE), + (random.sample(SCHEDULE, len(SCHEDULE))), + ], +) +def test_get_schedule(schedule, requests_mock, api_client: ApiClient): + base_url = api_client.base_url + + requests_mock.get(f"{base_url}/api/v2/schedule", json=schedule) + + requests_mock.get(f"{base_url}/api/v2/shows/1", json=SHOW_1) + requests_mock.get(f"{base_url}/api/v2/shows/2", json=SHOW_2) + requests_mock.get(f"{base_url}/api/v2/shows/3", json=SHOW_3) + requests_mock.get(f"{base_url}/api/v2/shows/4", json=SHOW_4) + requests_mock.get(f"{base_url}/api/v2/show-instances/1", json=SHOW_INSTANCE_1) + requests_mock.get(f"{base_url}/api/v2/show-instances/2", json=SHOW_INSTANCE_2) + requests_mock.get(f"{base_url}/api/v2/show-instances/3", json=SHOW_INSTANCE_3) + requests_mock.get(f"{base_url}/api/v2/show-instances/4", json=SHOW_INSTANCE_4) + requests_mock.get(f"{base_url}/api/v2/files/1", json=FILE_1) + requests_mock.get(f"{base_url}/api/v2/files/2", json=FILE_2) + requests_mock.get(f"{base_url}/api/v2/files/3", json=FILE_3) + requests_mock.get(f"{base_url}/api/v2/files/4", json=FILE_4) + requests_mock.get(f"{base_url}/api/v2/files/5", json=FILE_5) + requests_mock.get(f"{base_url}/api/v2/webstreams/1", json=WEBSTREAM_1) + + assert get_schedule(api_client) == { "media": { - "2022-03-04-15-30-00": { - "type": "file", - "row_id": 17, - "start": "2022-03-04-15-30-00", - "end": "2022-03-04-15-33-50", - # NOTE: The legacy schedule generator creates an url, - # but playout download the file using the file id, so - # we can safely ignore it here. + "2022-09-05-11-00-00": { + "type": EventKind.FILE, + "row_id": 1, + "start": "2022-09-05-11-00-00", + "end": "2022-09-05-11-05-02", "uri": None, - "id": 1, - "show_name": "Test", + "id": 2, + "show_name": "Show 1", "fade_in": 500.0, "fade_out": 500.0, - "cue_in": 1.31066, - "cue_out": 231.985, + "cue_in": 13.7008, + "cue_out": 315.845, "metadata": { - "artist_name": "Bag Raiders", - "track_title": "Shooting Stars", + "track_title": "My Friend the Forest", + "artist_name": "Nils Frahm", + "mime": "audio/flac", + }, + "replay_gain": "11.46", + "filesize": 10000, + }, + "2022-09-05-11-05-02": { + "type": EventKind.FILE, + "row_id": 2, + "start": "2022-09-05-11-05-02", + "end": "2022-09-05-11-10-00", + "uri": None, + "id": 4, + "show_name": "Show 1", + "fade_in": 500.0, + "fade_out": 500.0, + "cue_in": 0.0, + "cue_out": 297.8558, + "metadata": { + "track_title": "#2", + "artist_name": "Nils Frahm", + "mime": "audio/flac", + }, + "replay_gain": "-1.65", + "filesize": 10000, + }, + "2022-09-05-11-10-00": { + "type": EventKind.FILE, + "row_id": 3, + "start": "2022-09-05-11-10-00", + "end": "2022-09-05-12-08-59", + "uri": None, + "id": 5, + "show_name": "Show 2", + "fade_in": 500.0, + "fade_out": 500.0, + "cue_in": 0.0, + "cue_out": 3539.13, + "metadata": { + "track_title": "Democracy Now! 2022-09-05 Monday", + "artist_name": "Democracy Now! Audio", "mime": "audio/mp3", }, - "replay_gain": "-8.77", - "filesize": 9505222, + "replay_gain": "-1.39", + "filesize": 10000, }, - "2022-03-04-15-33-50": { - "type": "stream_buffer_start", - "row_id": 18, - "start": "2022-03-04-15-33-45", - "end": "2022-03-04-15-33-45", - "uri": "http://some-other-radio:8800/main.ogg", + "2022-09-05-12-08-59": { + "type": EventKind.FILE, + "row_id": 4, + "start": "2022-09-05-12-08-59", + "end": "2022-09-05-12-10-00", + "uri": None, + "id": 4, + "show_name": "Show 2", + "fade_in": 500.0, + "fade_out": 500.0, + "cue_in": 0.0, + "cue_out": 61.0, + "metadata": { + "track_title": "#2", + "artist_name": "Nils Frahm", + "mime": "audio/flac", + }, + "replay_gain": "-1.65", + "filesize": 10000, + }, + "2022-09-05-12-10-00": { + "type": EventKind.STREAM_BUFFER_START, + "row_id": 5, + "start": "2022-09-05-12-09-55", + "end": "2022-09-05-12-09-55", + "uri": "http://stream.radio.org/main.ogg", "id": 1, }, - "2022-03-04-15-33-50_0": { - "type": "stream_output_start", - "row_id": 18, - "start": "2022-03-04-15-33-50", - "end": "2022-03-04-16-03-50", - "uri": "http://some-other-radio:8800/main.ogg", + "2022-09-05-12-10-00_0": { + "type": EventKind.STREAM_OUTPUT_START, + "row_id": 5, + "start": "2022-09-05-12-10-00", + "end": "2022-09-05-12-40-00", + "uri": "http://stream.radio.org/main.ogg", "id": 1, - "show_name": "Test", + "show_name": "Show 3", }, - "2022-03-04-16-03-50": { - "type": "stream_buffer_end", - "row_id": 18, - "start": "2022-03-04-16-03-50", - "end": "2022-03-04-16-03-50", - "uri": "http://some-other-radio:8800/main.ogg", + "2022-09-05-12-40-00": { + "type": EventKind.STREAM_BUFFER_END, + "row_id": 5, + "start": "2022-09-05-12-40-00", + "end": "2022-09-05-12-40-00", + "uri": "http://stream.radio.org/main.ogg", "id": 1, }, - "2022-03-04-16-03-50_0": { - "type": "stream_output_end", - "row_id": 18, - "start": "2022-03-04-16-03-50", - "end": "2022-03-04-16-03-50", - "uri": "http://some-other-radio:8800/main.ogg", + "2022-09-05-12-40-00_0": { + "type": EventKind.STREAM_OUTPUT_END, + "row_id": 5, + "start": "2022-09-05-12-40-00", + "end": "2022-09-05-12-40-00", + "uri": "http://stream.radio.org/main.ogg", "id": 1, }, + "2022-09-05-12-40-00_1": { + "type": EventKind.FILE, + "row_id": 6, + "start": "2022-09-05-12-40-00", + "end": "2022-09-05-12-53-23", + "uri": None, + "id": 3, + "show_name": "Show 3", + "fade_in": 500.0, + "fade_out": 500.0, + "cue_in": 55.1211, + "cue_out": 858.4, + "metadata": { + "track_title": "All Melody", + "artist_name": "Nils Frahm", + "mime": "audio/flac", + }, + "replay_gain": "-2.13", + "filesize": 10000, + }, + "2022-09-05-12-53-23": { + "type": EventKind.FILE, + "row_id": 7, + "start": "2022-09-05-12-53-23", + "end": "2022-09-05-12-58-25", + "uri": None, + "id": 2, + "show_name": "Show 3", + "fade_in": 500.0, + "fade_out": 500.0, + "cue_in": 13.7008, + "cue_out": 315.845, + "metadata": { + "track_title": "My Friend the Forest", + "artist_name": "Nils Frahm", + "mime": "audio/flac", + }, + "replay_gain": "11.46", + "filesize": 10000, + }, + "2022-09-05-12-58-25": { + "type": EventKind.FILE, + "row_id": 8, + "start": "2022-09-05-12-58-25", + "end": "2022-09-05-13-00-00", + "uri": None, + "id": 1, + "show_name": "Show 3", + "fade_in": 500.0, + "fade_out": 500.0, + "cue_in": 8.25245, + "cue_out": 95.0, + "metadata": { + "track_title": "The Dane", + "artist_name": "Nils Frahm", + "mime": "audio/flac", + }, + "replay_gain": "4.52", + "filesize": 10000, + }, + "2022-09-05-13-00-00": { + "type": EventKind.FILE, + "row_id": 9, + "start": "2022-09-05-13-00-00", + "end": "2022-09-05-13-05-02", + "uri": None, + "id": 2, + "show_name": "Show 4", + "fade_in": 500.0, + "fade_out": 500.0, + "cue_in": 13.7008, + "cue_out": 315.845, + "metadata": { + "track_title": "My Friend the Forest", + "artist_name": "Nils Frahm", + "mime": "audio/flac", + }, + "replay_gain": "11.46", + "filesize": 10000, + }, + "2022-09-05-13-05-02": { + "type": EventKind.FILE, + "row_id": 10, + "start": "2022-09-05-13-05-02", + "end": "2022-09-05-13-10-00", + "uri": None, + "id": 4, + "show_name": "Show 4", + "fade_in": 500.0, + "fade_out": 500.0, + "cue_in": 0.0, + "cue_out": 297.8558, + "metadata": { + "track_title": "#2", + "artist_name": "Nils Frahm", + "mime": "audio/flac", + }, + "replay_gain": "-1.65", + "filesize": 10000, + }, } }