From 161f2ccdcb1a77a5d4878e9a7663a60167defd01 Mon Sep 17 00:00:00 2001 From: jo Date: Thu, 23 Mar 2023 02:11:02 +0100 Subject: [PATCH] refactor(playout): merge liquidsoap modules --- .../libretime_playout/player/liquidsoap.py | 115 ++++++++++++++++- .../player/liquidsoap_gateway.py | 121 ------------------ .../tests/player/liquidsoap_gateway_test.py | 45 ------- playout/tests/player/liquidsoap_test.py | 49 ++++++- 4 files changed, 161 insertions(+), 169 deletions(-) delete mode 100644 playout/libretime_playout/player/liquidsoap_gateway.py delete mode 100644 playout/tests/player/liquidsoap_gateway_test.py diff --git a/playout/libretime_playout/player/liquidsoap.py b/playout/libretime_playout/player/liquidsoap.py index aa073bf9f..c614f4f4e 100644 --- a/playout/libretime_playout/player/liquidsoap.py +++ b/playout/libretime_playout/player/liquidsoap.py @@ -6,11 +6,124 @@ from typing import Dict, List, Optional, Set from ..liquidsoap.client import LiquidsoapClient from ..utils import seconds_between from .events import ActionEvent, AnyEvent, EventKind, FileEvent, WebStreamEvent -from .liquidsoap_gateway import TelnetLiquidsoap logger = logging.getLogger(__name__) +def create_liquidsoap_annotation(file_event: FileEvent) -> str: + # We need liq_start_next value in the annotate. That is the value that controls + # overlap duration of crossfade. + annotations = { + "media_id": file_event.id, + "schedule_table_id": file_event.row_id, + "liq_start_next": "0", + "liq_fade_in": file_event.fade_in / 1000, + "liq_fade_out": file_event.fade_out / 1000, + "liq_cue_in": file_event.cue_in, + "liq_cue_out": file_event.cue_out, + } + + if file_event.replay_gain is not None: + annotations["replay_gain"] = f"{file_event.replay_gain} dB" + + # Override the the artist/title that Liquidsoap extracts from a file's metadata with + # the metadata we get from Airtime. (You can modify metadata in Airtime's library, + # which doesn't get saved back to the file.) + if file_event.artist_name: + annotations["artist"] = file_event.artist_name.replace('"', '\\"') + + if file_event.track_title: + annotations["title"] = file_event.track_title.replace('"', '\\"') + + annotations_str = ",".join(f'{key}="{value}"' for key, value in annotations.items()) + + return "annotate:" + annotations_str + ":" + str(file_event.local_filepath) + + +class TelnetLiquidsoap: + current_prebuffering_stream_id: Optional[int] = None + + def __init__( + self, + liq_client: LiquidsoapClient, + queues: List[int], + ): + self.liq_client = liq_client + self.queues = queues + + def queue_clear_all(self): + try: + self.liq_client.queues_remove(*self.queues) + except OSError as exception: + logger.exception(exception) + + def queue_remove(self, queue_id: int): + try: + self.liq_client.queues_remove(queue_id) + except OSError as exception: + logger.exception(exception) + + def queue_push(self, queue_id: int, file_event: FileEvent): + try: + annotation = create_liquidsoap_annotation(file_event) + self.liq_client.queue_push(queue_id, annotation, file_event.show_name) + except OSError as exception: + logger.exception(exception) + + def stop_web_stream_buffer(self): + try: + self.liq_client.web_stream_stop_buffer() + except OSError as exception: + logger.exception(exception) + + def stop_web_stream_output(self): + try: + self.liq_client.web_stream_stop() + except OSError as exception: + logger.exception(exception) + + def start_web_stream(self): + try: + self.liq_client.web_stream_start() + self.current_prebuffering_stream_id = None + except OSError as exception: + logger.exception(exception) + + def start_web_stream_buffer(self, event: WebStreamEvent): + try: + self.liq_client.web_stream_start_buffer(event.row_id, event.uri) + self.current_prebuffering_stream_id = event.row_id + except OSError as exception: + logger.exception(exception) + + def get_current_stream_id(self) -> str: + try: + return self.liq_client.web_stream_get_id() + except OSError as exception: + logger.exception(exception) + return "-1" + + def disconnect_source(self, sourcename): + if sourcename not in ("master_dj", "live_dj"): + raise ValueError(f"invalid source name: {sourcename}") + + try: + logger.debug("Disconnecting source: %s", sourcename) + self.liq_client.source_disconnect(sourcename) + except OSError as exception: + logger.exception(exception) + + def switch_source(self, sourcename, status): + if sourcename not in ("master_dj", "live_dj", "scheduled_play"): + raise ValueError(f"invalid source name: {sourcename}") + + try: + logger.debug('Switching source: %s to "%s" status', sourcename, status) + self.liq_client.source_switch_status(sourcename, status == "on") + except OSError as exception: + logger.exception(exception) + + class PypoLiquidsoap: def __init__(self, liq_client: LiquidsoapClient): self.liq_queue_tracker: Dict[int, Optional[FileEvent]] = { diff --git a/playout/libretime_playout/player/liquidsoap_gateway.py b/playout/libretime_playout/player/liquidsoap_gateway.py deleted file mode 100644 index 52d09e09c..000000000 --- a/playout/libretime_playout/player/liquidsoap_gateway.py +++ /dev/null @@ -1,121 +0,0 @@ -import logging -from typing import List, Optional - -from ..liquidsoap.client import LiquidsoapClient -from .events import FileEvent, WebStreamEvent - -logger = logging.getLogger(__name__) - - -def create_liquidsoap_annotation(file_event: FileEvent) -> str: - # We need liq_start_next value in the annotate. That is the value that controls - # overlap duration of crossfade. - annotations = { - "media_id": file_event.id, - "schedule_table_id": file_event.row_id, - "liq_start_next": "0", - "liq_fade_in": file_event.fade_in / 1000, - "liq_fade_out": file_event.fade_out / 1000, - "liq_cue_in": file_event.cue_in, - "liq_cue_out": file_event.cue_out, - } - - if file_event.replay_gain is not None: - annotations["replay_gain"] = f"{file_event.replay_gain} dB" - - # Override the the artist/title that Liquidsoap extracts from a file's metadata with - # the metadata we get from Airtime. (You can modify metadata in Airtime's library, - # which doesn't get saved back to the file.) - if file_event.artist_name: - annotations["artist"] = file_event.artist_name.replace('"', '\\"') - - if file_event.track_title: - annotations["title"] = file_event.track_title.replace('"', '\\"') - - annotations_str = ",".join(f'{key}="{value}"' for key, value in annotations.items()) - - return "annotate:" + annotations_str + ":" + str(file_event.local_filepath) - - -class TelnetLiquidsoap: - current_prebuffering_stream_id: Optional[int] = None - - def __init__( - self, - liq_client: LiquidsoapClient, - queues: List[int], - ): - self.liq_client = liq_client - self.queues = queues - - def queue_clear_all(self): - try: - self.liq_client.queues_remove(*self.queues) - except OSError as exception: - logger.exception(exception) - - def queue_remove(self, queue_id: int): - try: - self.liq_client.queues_remove(queue_id) - except OSError as exception: - logger.exception(exception) - - def queue_push(self, queue_id: int, file_event: FileEvent): - try: - annotation = create_liquidsoap_annotation(file_event) - self.liq_client.queue_push(queue_id, annotation, file_event.show_name) - except OSError as exception: - logger.exception(exception) - - def stop_web_stream_buffer(self): - try: - self.liq_client.web_stream_stop_buffer() - except OSError as exception: - logger.exception(exception) - - def stop_web_stream_output(self): - try: - self.liq_client.web_stream_stop() - except OSError as exception: - logger.exception(exception) - - def start_web_stream(self): - try: - self.liq_client.web_stream_start() - self.current_prebuffering_stream_id = None - except OSError as exception: - logger.exception(exception) - - def start_web_stream_buffer(self, event: WebStreamEvent): - try: - self.liq_client.web_stream_start_buffer(event.row_id, event.uri) - self.current_prebuffering_stream_id = event.row_id - except OSError as exception: - logger.exception(exception) - - def get_current_stream_id(self) -> str: - try: - return self.liq_client.web_stream_get_id() - except OSError as exception: - logger.exception(exception) - return "-1" - - def disconnect_source(self, sourcename): - if sourcename not in ("master_dj", "live_dj"): - raise ValueError(f"invalid source name: {sourcename}") - - try: - logger.debug("Disconnecting source: %s", sourcename) - self.liq_client.source_disconnect(sourcename) - except OSError as exception: - logger.exception(exception) - - def switch_source(self, sourcename, status): - if sourcename not in ("master_dj", "live_dj", "scheduled_play"): - raise ValueError(f"invalid source name: {sourcename}") - - try: - logger.debug('Switching source: %s to "%s" status', sourcename, status) - self.liq_client.source_switch_status(sourcename, status == "on") - except OSError as exception: - logger.exception(exception) diff --git a/playout/tests/player/liquidsoap_gateway_test.py b/playout/tests/player/liquidsoap_gateway_test.py deleted file mode 100644 index ea5cf5843..000000000 --- a/playout/tests/player/liquidsoap_gateway_test.py +++ /dev/null @@ -1,45 +0,0 @@ -from datetime import datetime -from pathlib import Path -from unittest import mock - -from dateutil.tz import tzutc - -from libretime_playout.player.events import EventKind, FileEvent -from libretime_playout.player.liquidsoap_gateway import create_liquidsoap_annotation - - -@mock.patch("libretime_playout.player.events.CACHE_DIR", Path("/fake")) -def test_create_liquidsoap_annotation(): - file_event = FileEvent( - type=EventKind.FILE, - row_id=1, - start=datetime(2022, 9, 5, 11, tzinfo=tzutc()), - end=datetime(2022, 9, 5, 11, 5, 2, tzinfo=tzutc()), - 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=11.46, - filesize=10000, - ) - - assert create_liquidsoap_annotation(file_event) == ( - "annotate:" - 'media_id="2",' - 'schedule_table_id="1",' - 'liq_start_next="0",' - 'liq_fade_in="0.5",' - 'liq_fade_out="0.5",' - 'liq_cue_in="13.7008",' - 'liq_cue_out="315.845",' - 'replay_gain="11.46 dB",' - 'artist="Nils Frahm",' - 'title="My Friend the \\"Forest\\""' - ":/fake/2.flac" - ) diff --git a/playout/tests/player/liquidsoap_test.py b/playout/tests/player/liquidsoap_test.py index b1028ffd2..fe1aa10bf 100644 --- a/playout/tests/player/liquidsoap_test.py +++ b/playout/tests/player/liquidsoap_test.py @@ -1,6 +1,51 @@ -from unittest.mock import MagicMock +from datetime import datetime +from pathlib import Path +from unittest.mock import MagicMock, patch -from libretime_playout.player.liquidsoap import PypoLiquidsoap +from dateutil.tz import tzutc + +from libretime_playout.player.events import EventKind, FileEvent +from libretime_playout.player.liquidsoap import ( + PypoLiquidsoap, + create_liquidsoap_annotation, +) + + +@patch("libretime_playout.player.events.CACHE_DIR", Path("/fake")) +def test_create_liquidsoap_annotation(): + file_event = FileEvent( + type=EventKind.FILE, + row_id=1, + start=datetime(2022, 9, 5, 11, tzinfo=tzutc()), + end=datetime(2022, 9, 5, 11, 5, 2, tzinfo=tzutc()), + 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=11.46, + filesize=10000, + ) + + assert create_liquidsoap_annotation(file_event) == ( + "annotate:" + 'media_id="2",' + 'schedule_table_id="1",' + 'liq_start_next="0",' + 'liq_fade_in="0.5",' + 'liq_fade_out="0.5",' + 'liq_cue_in="13.7008",' + 'liq_cue_out="315.845",' + 'replay_gain="11.46 dB",' + 'artist="Nils Frahm",' + 'title="My Friend the \\"Forest\\""' + ":/fake/2.flac" + ) def test_liquidsoap():