From 81f38fc9c65b25b90aace7c15dc1ec43c0f71afa Mon Sep 17 00:00:00 2001 From: jo Date: Fri, 4 Mar 2022 14:00:14 +0100 Subject: [PATCH] fix(api_client): comply to legacy schedule events - remove 5 seconds to stream_buffer_start start event key, - use explicit variables names, - split schedule events into multiple functions - stream_output_start ends at the end of the scheduled period (fix #1556). --- api_client/libretime_api_client/version2.py | 189 ++++++++++------ api_client/tests/version2_test.py | 236 ++++++++++++++++---- 2 files changed, 312 insertions(+), 113 deletions(-) diff --git a/api_client/libretime_api_client/version2.py b/api_client/libretime_api_client/version2.py index 12bd702bd..54463759c 100644 --- a/api_client/libretime_api_client/version2.py +++ b/api_client/libretime_api_client/version2.py @@ -8,6 +8,7 @@ ############################################################################### import logging from datetime import datetime, timedelta +from typing import Dict from dateutil.parser import isoparse @@ -15,6 +16,12 @@ from ._config import Config from .utils import RequestProvider, fromisoformat, time_in_milliseconds, time_in_seconds LIBRETIME_API_VERSION = "2.0" +EVENT_KEY_FORMAT = "%Y-%m-%d-%H-%M-%S" + + +def datetime_to_event_key(value: datetime) -> str: + return value.strftime(EVENT_KEY_FORMAT) + api_endpoints = {} @@ -47,93 +54,131 @@ class AirtimeApiClient: current_time = datetime.utcnow() end_time = current_time + timedelta(days=1) - str_current = current_time.isoformat(timespec="seconds") - str_end = end_time.isoformat(timespec="seconds") - data = self.services.schedule_url( + current_time_str = current_time.isoformat(timespec="seconds") + end_time_str = end_time.isoformat(timespec="seconds") + + schedule = self.services.schedule_url( params={ - "ends__range": (f"{str_current}Z,{str_end}Z"), + "ends__range": (f"{current_time_str}Z,{end_time_str}Z"), "is_valid": True, "playout_status__gt": 0, } ) - result = {} - for item in data: - start = isoparse(item["starts"]) - start_key = start.strftime("%Y-%m-%d-%H-%M-%S") - end = isoparse(item["ends"]) - end_key = end.strftime("%Y-%m-%d-%H-%M-%S") + + events = {} + for item in schedule: + item["starts"] = isoparse(item["starts"]) + item["ends"] = isoparse(item["ends"]) show_instance = self.services.show_instance_url(id=item["instance_id"]) show = self.services.show_url(id=show_instance["show_id"]) - result[start_key] = { - "start": start_key, - "end": end_key, - "row_id": item["id"], - "show_name": show["name"], - } - current = result[start_key] if item["file"]: - current["independent_event"] = False - current["type"] = "file" - current["id"] = item["file_id"] + file = self.services.file_url(id=item["file_id"]) + events.update(generate_file_events(item, file, show)) - fade_in = time_in_milliseconds(fromisoformat(item["fade_in"])) - fade_out = time_in_milliseconds(fromisoformat(item["fade_out"])) - - cue_in = time_in_seconds(fromisoformat(item["cue_in"])) - cue_out = time_in_seconds(fromisoformat(item["cue_out"])) - - current["fade_in"] = fade_in - current["fade_out"] = fade_out - current["cue_in"] = cue_in - current["cue_out"] = cue_out - - info = self.services.file_url(id=item["file_id"]) - current["metadata"] = info - current["uri"] = item["file"] - current["replay_gain"] = info["replay_gain"] - current["filesize"] = info["filesize"] elif item["stream"]: - current["independent_event"] = True - current["id"] = item["stream_id"] - info = self.services.webstream_url(id=item["stream_id"]) - current["uri"] = info["url"] - current["type"] = "stream_buffer_start" - # Stream events are instantaneous - current["end"] = current["start"] + webstream = self.services.webstream_url(id=item["stream_id"]) + events.update(generate_webstream_events(item, webstream, show)) - result[f"{start_key}_0"] = { - "id": current["id"], - "type": "stream_output_start", - "start": current["start"], - "end": current["start"], - "uri": current["uri"], - "row_id": current["row_id"], - "independent_event": current["independent_event"], - } - - result[end_key] = { - "type": "stream_buffer_end", - "start": current["end"], - "end": current["end"], - "uri": current["uri"], - "row_id": current["row_id"], - "independent_event": current["independent_event"], - } - - result[f"{end_key}_0"] = { - "type": "stream_output_end", - "start": current["end"], - "end": current["end"], - "uri": current["uri"], - "row_id": current["row_id"], - "independent_event": current["independent_event"], - } - - return {"media": result} + return {"media": events} def update_file(self, file_id, payload): data = self.services.file_url(id=file_id) data.update(payload) return self.services.file_url(id=file_id, _put_data=data) + + +def generate_file_events( + 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"]) + schedule_end_event_key = datetime_to_event_key(schedule["ends"]) + + events[schedule_start_event_key] = { + "type": "file", + "independent_event": False, + "row_id": schedule["id"], + "start": schedule_start_event_key, + "end": schedule_end_event_key, + "uri": file["url"], + "id": file["id"], + # Show data + "show_name": show["name"], + # Extra data + "fade_in": time_in_milliseconds(fromisoformat(schedule["fade_in"])), + "fade_out": time_in_milliseconds(fromisoformat(schedule["fade_out"])), + "cue_in": time_in_seconds(fromisoformat(schedule["cue_in"])), + "cue_out": time_in_seconds(fromisoformat(schedule["cue_out"])), + "metadata": file, + "replay_gain": file["replay_gain"], + "filesize": file["filesize"], + } + + return events + + +def generate_webstream_events( + 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"]) + schedule_end_event_key = datetime_to_event_key(schedule["ends"]) + + events[schedule_start_event_key] = { + "type": "stream_buffer_start", + "independent_event": True, + "row_id": schedule["id"], + "start": datetime_to_event_key(schedule["starts"] - timedelta(seconds=5)), + "end": datetime_to_event_key(schedule["starts"] - timedelta(seconds=5)), + "uri": webstream["url"], + "id": webstream["id"], + } + + events[f"{schedule_start_event_key}_0"] = { + "type": "stream_output_start", + "independent_event": True, + "row_id": schedule["id"], + "start": schedule_start_event_key, + "end": schedule_end_event_key, + "uri": webstream["url"], + "id": webstream["id"], + # Show data + "show_name": show["name"], + } + + # NOTE: stream_*_end were previously triggerered 1 second before the schedule end. + events[schedule_end_event_key] = { + "type": "stream_buffer_end", + "independent_event": True, + "row_id": schedule["id"], + "start": schedule_end_event_key, + "end": schedule_end_event_key, + "uri": webstream["url"], + "id": webstream["id"], + } + + events[f"{schedule_end_event_key}_0"] = { + "type": "stream_output_end", + "independent_event": True, + "row_id": schedule["id"], + "start": schedule_end_event_key, + "end": schedule_end_event_key, + "uri": webstream["url"], + "id": webstream["id"], + } + + return events diff --git a/api_client/tests/version2_test.py b/api_client/tests/version2_test.py index 86de06b8c..f2f5acd7b 100644 --- a/api_client/tests/version2_test.py +++ b/api_client/tests/version2_test.py @@ -25,62 +25,176 @@ class MockRequestProvider: def schedule_url(_post_data=None, params=None, **kwargs): return [ { - "id": 1, - "starts": "2021-07-05T11:00:00Z", - "ends": "2021-07-05T11:01:00.5000Z", - "instance_id": 2, - "file": "http://localhost/api/v2/file/3", - "file_id": 3, + "item_url": "http://192.168.10.100:8081/api/v2/schedule/17/", + "id": 17, + "starts": "2022-03-04T15:30:00Z", + "ends": "2022-03-04T15:33:50.674340Z", + "file": "http://192.168.10.100:8081/api/v2/files/1/", + "file_id": 1, + "stream": None, + "clip_length": "00:03:50.674340", "fade_in": "00:00:00.500000", - "fade_out": "00:00:01", - "cue_in": "00:00:00.142404", - "cue_out": "01:58:04.463583", + "fade_out": "00:00:00.500000", + "cue_in": "00:00:01.310660", + "cue_out": "00:03:51.985000", + "media_item_played": False, + "instance": "http://192.168.10.100:8081/api/v2/show-instances/3/", + "instance_id": 3, + "playout_status": 1, + "broadcasted": 0, + "position": 0, + }, + { + "item_url": "http://192.168.10.100:8081/api/v2/schedule/18/", + "id": 18, + "starts": "2022-03-04T15:33:50.674340Z", + "ends": "2022-03-04T16:03:50.674340Z", + "file": None, + "stream": "http://192.168.10.100:8081/api/v2/webstreams/1/", + "stream_id": 1, + "clip_length": "00:30:00", + "fade_in": "00:00:00.500000", + "fade_out": "00:00:00.500000", + "cue_in": "00:00:00", + "cue_out": "00:30:00", + "media_item_played": False, + "instance": "http://192.168.10.100:8081/api/v2/show-instances/3/", + "instance_id": 3, + "playout_status": 1, + "broadcasted": 0, + "position": 1, }, ] @staticmethod def show_instance_url(_post_data=None, params=None, **kwargs): return { - "show_id": 4, + "item_url": "http://192.168.10.100:8081/api/v2/show-instances/3/", + "id": 3, + "description": "", + "starts": "2022-03-04T15:30:00Z", + "ends": "2022-03-04T16:30:00Z", + "record": 0, + "rebroadcast": 0, + "time_filled": "00:33:50.674340", + "created": "2022-03-04T15:05:36Z", + "last_scheduled": "2022-03-04T15:05:46Z", + "modified_instance": False, + "autoplaylist_built": False, + "show": "http://192.168.10.100:8081/api/v2/shows/3/", + "show_id": 3, + "instance": None, + "file": None, } @staticmethod def show_url(_post_data=None, params=None, **kwargs): return { - "name": "Test show", + "item_url": "http://192.168.10.100:8081/api/v2/shows/3/", + "id": 3, + "name": "Test", + "url": "", + "genre": "", + "description": "", + "color": "", + "background_color": "", + "linked": False, + "is_linkable": True, + "image_path": "", + "has_autoplaylist": False, + "autoplaylist_repeat": False, + "autoplaylist": None, } @staticmethod def file_url(_post_data=None, params=None, **kwargs): return { - "item_url": "http://localhost/api/v2/files/3/", + "item_url": "http://192.168.10.100:8081/api/v2/files/1/", + "id": 1, "name": "", "mime": "audio/mp3", "ftype": "audioclip", - "filepath": "imported/1/test.mp3", + "filepath": "imported/1/Bag Raiders/Bag Raiders/03 - Bag Raiders - Shooting Stars.mp3", "import_status": 0, "currently_accessing": 0, - "mtime": "2021-07-01T23:13:43Z", - "utime": "2021-07-01T23:12:46Z", - "md5": "202ae33a642ce475bd8b265ddb11c139", - "track_title": "Test file.mp3", + "mtime": "2022-03-04T13:03:13Z", + "utime": "2022-03-04T13:03:11Z", + "lptime": "2022-03-04T14:26:18Z", + "md5": "65c497bdc702881be607c7961ae814fa", + "track_title": "Shooting Stars", + "artist_name": "Bag Raiders", "bit_rate": 320000, "sample_rate": 44100, - "length": "01:58:04.463600", - "genre": "Test", + "format": None, + "length": "00:03:55.859578", + "album_title": "Bag Raiders", + "genre": "Electro", + "comments": None, + "year": "2010", + "track_number": 3, "channels": 2, + "url": None, + "bpm": None, + "rating": None, + "encoded_by": None, + "disc_number": None, + "mood": None, + "label": None, + "composer": None, + "encoder": None, + "checksum": None, + "lyrics": None, + "orchestra": None, + "conductor": None, + "lyricist": None, + "original_lyricist": None, + "radio_station_name": None, + "info_url": None, + "artist_url": None, + "audio_source_url": None, + "radio_station_url": None, + "buy_this_url": None, + "isrc_number": None, + "catalog_number": None, + "original_artist": None, + "copyright": None, + "report_datetime": None, + "report_location": None, + "report_organization": None, + "subject": None, + "contributor": None, + "language": None, "file_exists": True, - "replay_gain": "-5.68", - "cuein": "00:00:00.142404", - "cueout": "01:58:04.463583", + "replay_gain": "-8.77", + "cuein": "00:00:01.310660", + "cueout": "00:03:51.985000", "silan_check": False, "hidden": False, "is_scheduled": True, "is_playlist": False, - "filesize": 283379568, - "track_type": "MUS", - "directory": "http://localhost/api/v2/music-dirs/1/", - "owner": "http://localhost/api/v2/users/1/", + "filesize": 9505222, + "description": None, + "artwork": "imported/1/artwork/03 - Bag Raiders - Shooting Stars", + "track_type": "TEST", + "directory": "http://192.168.10.100:8081/api/v2/music-dirs/1/", + "edited_by": None, + "owner": "http://192.168.10.100:8081/api/v2/users/1/", + } + + @staticmethod + def webstream_url(_post_data=None, params=None, **kwargs): + return { + "item_url": "http://192.168.10.100:8081/api/v2/webstreams/1/", + "id": 1, + "name": "Test", + "description": "", + "url": "http://some-other-radio:8800/main.ogg", + "length": "00:30:00", + "creator_id": 1, + "mtime": "2022-03-04T13:11:20Z", + "utime": "2022-03-04T13:11:20Z", + "lptime": None, + "mime": "application/ogg", } @@ -90,22 +204,62 @@ def test_get_schedule(monkeypatch, config_filepath): schedule = client.get_schedule() assert schedule == { "media": { - "2021-07-05-11-00-00": { - "id": 3, + "2022-03-04-15-30-00": { "type": "file", - "metadata": MockRequestProvider.file_url(), - "row_id": 1, - "uri": "http://localhost/api/v2/file/3", - "fade_in": 500.0, - "fade_out": 1000.0, - "cue_in": 0.142404, - "cue_out": 7084.463583, - "start": "2021-07-05-11-00-00", - "end": "2021-07-05-11-01-00", - "show_name": "Test show", - "replay_gain": "-5.68", "independent_event": False, - "filesize": 283379568, + "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. + "uri": None, + "id": 1, + "show_name": "Test", + "fade_in": 500.0, + "fade_out": 500.0, + "cue_in": 1.31066, + "cue_out": 231.985, + "metadata": MockRequestProvider.file_url(), + "replay_gain": "-8.77", + "filesize": 9505222, }, - }, + "2022-03-04-15-33-50": { + "type": "stream_buffer_start", + "independent_event": True, + "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", + "id": 1, + }, + "2022-03-04-15-33-50_0": { + "type": "stream_output_start", + "independent_event": True, + "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", + "id": 1, + "show_name": "Test", + }, + "2022-03-04-16-03-50": { + "type": "stream_buffer_end", + "independent_event": True, + "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", + "id": 1, + }, + "2022-03-04-16-03-50_0": { + "type": "stream_output_end", + "independent_event": True, + "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", + "id": 1, + }, + } }