2020-11-01 02:35:14 +00:00
|
|
|
"""
|
|
|
|
BAPSicle Server
|
|
|
|
Next-gen audio playout server for University Radio York playout,
|
|
|
|
based on WebStudio interface.
|
|
|
|
|
|
|
|
Audio Player
|
|
|
|
|
|
|
|
Authors:
|
|
|
|
Matthew Stratford
|
|
|
|
Michael Grace
|
|
|
|
|
|
|
|
Date:
|
|
|
|
October, November 2020
|
|
|
|
"""
|
|
|
|
|
2020-10-30 19:31:18 +00:00
|
|
|
# This is the player. Reliability is critical here, so we're catching
|
|
|
|
# literally every exception possible and handling it.
|
|
|
|
|
|
|
|
# It is key that whenever the parent server tells us to do something
|
|
|
|
# that we respond with something, FAIL or OKAY. The server doesn't like to be kept waiting.
|
|
|
|
|
2021-04-08 20:15:15 +00:00
|
|
|
# Stop the Pygame Hello message.
|
|
|
|
import os
|
|
|
|
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
|
|
|
|
|
2020-10-29 21:23:37 +00:00
|
|
|
from queue import Empty
|
|
|
|
import multiprocessing
|
|
|
|
import setproctitle
|
|
|
|
import copy
|
|
|
|
import json
|
|
|
|
import time
|
2020-12-19 14:57:37 +00:00
|
|
|
from typing import Any, Callable, Dict, List, Optional
|
2021-04-07 19:14:12 +00:00
|
|
|
from pygame import mixer
|
2020-10-25 01:23:24 +00:00
|
|
|
from mutagen.mp3 import MP3
|
2021-04-18 02:14:14 +00:00
|
|
|
from syncer import sync
|
2021-04-22 22:00:31 +00:00
|
|
|
from threading import Timer
|
2020-10-30 22:15:51 +00:00
|
|
|
|
2020-11-05 23:53:27 +00:00
|
|
|
from helpers.myradio_api import MyRadioAPI
|
2020-10-30 22:06:03 +00:00
|
|
|
from helpers.state_manager import StateManager
|
|
|
|
from helpers.logging_manager import LoggingManager
|
2021-04-12 21:59:51 +00:00
|
|
|
from baps_types.plan import PlanItem
|
|
|
|
from baps_types.marker import Marker
|
2020-10-23 20:10:32 +00:00
|
|
|
|
2021-03-22 00:33:14 +00:00
|
|
|
# TODO ENUM
|
2021-04-05 23:32:58 +00:00
|
|
|
VALID_MESSAGE_SOURCES = ["WEBSOCKET", "UI", "CONTROLLER", "TEST", "ALL"]
|
2021-04-22 22:00:31 +00:00
|
|
|
TRACKLISTING_DELAYED_S = 20
|
2021-04-08 19:53:51 +00:00
|
|
|
|
2021-04-08 21:32:16 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
class Player:
|
2021-04-04 21:34:46 +00:00
|
|
|
out_q: multiprocessing.Queue
|
|
|
|
last_msg: str
|
|
|
|
last_msg_source: str
|
2021-02-14 00:29:47 +00:00
|
|
|
last_time_update = None
|
2021-04-04 21:34:46 +00:00
|
|
|
|
|
|
|
state: StateManager
|
|
|
|
logger: LoggingManager
|
|
|
|
api: MyRadioAPI
|
|
|
|
|
|
|
|
running: bool = False
|
2021-04-07 19:14:12 +00:00
|
|
|
|
|
|
|
stopped_manually: bool = False
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
tracklist_start_timer: Optional[Timer] = None
|
|
|
|
tracklist_end_timer: Optional[Timer] = None
|
|
|
|
|
2020-10-24 20:31:52 +00:00
|
|
|
__default_state = {
|
2020-10-30 00:32:34 +00:00
|
|
|
"initialised": False,
|
2020-11-01 02:35:14 +00:00
|
|
|
"loaded_item": None,
|
2020-10-24 20:31:52 +00:00
|
|
|
"channel": -1,
|
|
|
|
"playing": False,
|
2020-10-30 19:31:18 +00:00
|
|
|
"paused": False,
|
2020-10-29 22:25:17 +00:00
|
|
|
"loaded": False,
|
2020-10-24 20:31:52 +00:00
|
|
|
"pos": 0,
|
2020-10-30 19:31:18 +00:00
|
|
|
"pos_offset": 0,
|
|
|
|
"pos_true": 0,
|
2020-10-24 20:31:52 +00:00
|
|
|
"remaining": 0,
|
|
|
|
"length": 0,
|
2020-11-03 01:07:25 +00:00
|
|
|
"auto_advance": True,
|
2021-02-14 17:53:28 +00:00
|
|
|
"repeat": "none", # none, one or all
|
2020-11-03 01:07:25 +00:00
|
|
|
"play_on_load": False,
|
2020-11-01 02:35:14 +00:00
|
|
|
"output": None,
|
2021-04-08 19:53:51 +00:00
|
|
|
"show_plan": [],
|
2021-04-22 22:00:31 +00:00
|
|
|
"tracklist_mode": "off",
|
|
|
|
"tracklist_id": None,
|
2020-10-24 20:31:52 +00:00
|
|
|
}
|
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
__rate_limited_params = ["pos", "pos_offset", "pos_true", "remaining"]
|
2020-11-04 01:19:56 +00:00
|
|
|
|
2020-10-29 22:25:17 +00:00
|
|
|
@property
|
2020-10-24 20:31:52 +00:00
|
|
|
def isInit(self):
|
|
|
|
try:
|
2020-10-25 01:23:24 +00:00
|
|
|
mixer.music.get_busy()
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2020-10-24 20:31:52 +00:00
|
|
|
return False
|
2020-10-29 22:25:17 +00:00
|
|
|
|
|
|
|
return True
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2020-10-29 21:23:37 +00:00
|
|
|
@property
|
2021-04-12 21:59:51 +00:00
|
|
|
def isPlaying(self) -> bool:
|
2020-10-29 22:25:17 +00:00
|
|
|
if self.isInit:
|
2020-10-30 19:31:18 +00:00
|
|
|
return (not self.isPaused) and bool(mixer.music.get_busy())
|
2020-10-29 22:25:17 +00:00
|
|
|
return False
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2020-10-30 19:31:18 +00:00
|
|
|
@property
|
2020-12-19 14:57:37 +00:00
|
|
|
def isPaused(self) -> bool:
|
2021-04-18 19:27:54 +00:00
|
|
|
return self.state.get()["paused"]
|
2020-10-30 19:31:18 +00:00
|
|
|
|
2020-10-29 21:23:37 +00:00
|
|
|
@property
|
|
|
|
def isLoaded(self):
|
2021-04-12 21:59:51 +00:00
|
|
|
return self._isLoaded()
|
|
|
|
|
|
|
|
def _isLoaded(self, short_test: bool = False):
|
2021-04-18 19:27:54 +00:00
|
|
|
if not self.state.get()["loaded_item"]:
|
2020-10-29 22:25:17 +00:00
|
|
|
return False
|
|
|
|
if self.isPlaying:
|
|
|
|
return True
|
|
|
|
|
2021-04-12 21:59:51 +00:00
|
|
|
# If we don't want to do any testing if it's really loaded, fine.
|
|
|
|
if short_test:
|
|
|
|
return True
|
|
|
|
|
2020-10-30 00:32:34 +00:00
|
|
|
# Because Pygame/SDL is annoying
|
|
|
|
# We're not playing now, so we can quickly test run
|
|
|
|
# If that works, we're loaded.
|
2020-10-29 22:25:17 +00:00
|
|
|
try:
|
2020-10-30 00:32:34 +00:00
|
|
|
mixer.music.set_volume(0)
|
|
|
|
mixer.music.play(0)
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2020-10-30 00:32:34 +00:00
|
|
|
try:
|
|
|
|
mixer.music.set_volume(1)
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2021-04-08 19:53:51 +00:00
|
|
|
self.logger.log.exception(
|
|
|
|
"Failed to reset volume after attempting loaded test."
|
|
|
|
)
|
2020-10-30 00:32:34 +00:00
|
|
|
pass
|
2020-10-29 22:25:17 +00:00
|
|
|
return False
|
2021-04-22 22:00:31 +00:00
|
|
|
finally:
|
|
|
|
mixer.music.stop()
|
|
|
|
|
2020-10-30 00:32:34 +00:00
|
|
|
mixer.music.set_volume(1)
|
|
|
|
return True
|
2020-10-29 22:25:17 +00:00
|
|
|
|
2021-04-12 21:59:51 +00:00
|
|
|
@property
|
|
|
|
def isCued(self):
|
|
|
|
# Don't mess with playback, we only care about if it's supposed to be loaded.
|
|
|
|
if not self._isLoaded(short_test=True):
|
|
|
|
return False
|
2021-04-18 19:27:54 +00:00
|
|
|
return (self.state.get()["pos_true"] == self.state.get()["loaded_item"].cue and not self.isPlaying)
|
2021-04-12 21:59:51 +00:00
|
|
|
|
2020-10-30 00:32:34 +00:00
|
|
|
@property
|
|
|
|
def status(self):
|
2020-11-01 02:35:14 +00:00
|
|
|
state = copy.copy(self.state.state)
|
|
|
|
|
|
|
|
# Not the biggest fan of this, but maybe I'll get a better solution for this later
|
2021-04-08 19:53:51 +00:00
|
|
|
state["loaded_item"] = (
|
|
|
|
state["loaded_item"].__dict__ if state["loaded_item"] else None
|
|
|
|
)
|
2020-11-03 00:32:43 +00:00
|
|
|
state["show_plan"] = [repr.__dict__ for repr in state["show_plan"]]
|
2020-11-01 02:35:14 +00:00
|
|
|
|
|
|
|
res = json.dumps(state)
|
2020-10-30 00:32:34 +00:00
|
|
|
return res
|
2020-10-29 21:23:37 +00:00
|
|
|
|
2020-11-15 19:34:13 +00:00
|
|
|
# Audio Playout Related Methods
|
2020-11-03 01:07:25 +00:00
|
|
|
|
2020-12-19 14:57:37 +00:00
|
|
|
def play(self, pos: float = 0):
|
2021-03-15 20:19:45 +00:00
|
|
|
if not self.isLoaded:
|
2021-02-14 20:10:32 +00:00
|
|
|
return
|
2021-04-07 19:14:12 +00:00
|
|
|
|
2020-10-30 19:31:18 +00:00
|
|
|
try:
|
|
|
|
mixer.music.play(0, pos)
|
|
|
|
self.state.update("pos_offset", pos)
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2020-11-03 22:48:11 +00:00
|
|
|
self.logger.log.exception("Failed to play at pos: " + str(pos))
|
2020-10-30 19:31:18 +00:00
|
|
|
return False
|
|
|
|
self.state.update("paused", False)
|
2021-04-22 22:00:31 +00:00
|
|
|
self._potentially_tracklist()
|
2021-04-07 19:14:12 +00:00
|
|
|
self.stopped_manually = False
|
2020-10-30 19:31:18 +00:00
|
|
|
return True
|
2020-10-24 20:31:52 +00:00
|
|
|
|
|
|
|
def pause(self):
|
2020-10-30 19:31:18 +00:00
|
|
|
try:
|
2021-04-12 21:59:51 +00:00
|
|
|
mixer.music.stop()
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2020-11-03 22:48:11 +00:00
|
|
|
self.logger.log.exception("Failed to pause.")
|
2020-10-30 19:31:18 +00:00
|
|
|
return False
|
2021-04-07 19:14:12 +00:00
|
|
|
|
|
|
|
self.stopped_manually = True
|
2020-10-30 19:31:18 +00:00
|
|
|
self.state.update("paused", True)
|
|
|
|
return True
|
2020-10-24 20:31:52 +00:00
|
|
|
|
|
|
|
def unpause(self):
|
2020-10-29 22:25:17 +00:00
|
|
|
if not self.isPlaying:
|
2021-04-18 19:27:54 +00:00
|
|
|
position: float = self.state.get()["pos_true"]
|
2020-10-29 22:25:17 +00:00
|
|
|
try:
|
2020-11-03 22:48:11 +00:00
|
|
|
self.play(position)
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2021-04-08 19:53:51 +00:00
|
|
|
self.logger.log.exception(
|
|
|
|
"Failed to unpause from pos: " + str(position)
|
|
|
|
)
|
2020-10-29 22:25:17 +00:00
|
|
|
return False
|
2021-04-07 19:14:12 +00:00
|
|
|
|
2020-10-30 19:31:18 +00:00
|
|
|
self.state.update("paused", False)
|
2020-10-29 22:25:17 +00:00
|
|
|
return True
|
|
|
|
return False
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2021-04-12 21:59:51 +00:00
|
|
|
def stop(self, user_initiated: bool = False):
|
2020-10-30 19:31:18 +00:00
|
|
|
try:
|
|
|
|
mixer.music.stop()
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2020-11-03 22:48:11 +00:00
|
|
|
self.logger.log.exception("Failed to stop playing.")
|
2020-10-30 19:31:18 +00:00
|
|
|
return False
|
|
|
|
self.state.update("paused", False)
|
2020-12-20 01:10:19 +00:00
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
if user_initiated:
|
|
|
|
self._potentially_end_tracklist()
|
|
|
|
|
2021-04-07 19:14:12 +00:00
|
|
|
self.stopped_manually = True
|
2021-02-14 13:23:51 +00:00
|
|
|
|
2021-04-18 19:27:54 +00:00
|
|
|
if not self.state.get()["loaded_item"]:
|
2021-04-12 21:59:51 +00:00
|
|
|
self.logger.log.warning("Tried to stop without a loaded item.")
|
|
|
|
return True
|
|
|
|
|
|
|
|
# This lets users toggle (using the stop button) between cue point and 0.
|
|
|
|
if user_initiated and not self.isCued:
|
|
|
|
# if there's a cue point ant we're not at it, go there.
|
2021-04-18 19:27:54 +00:00
|
|
|
self.seek(self.state.get()["loaded_item"].cue)
|
2021-04-12 21:59:51 +00:00
|
|
|
else:
|
|
|
|
# Otherwise, let's go to 0.
|
|
|
|
self.state.update("pos", 0)
|
|
|
|
self.state.update("pos_offset", 0)
|
|
|
|
self.state.update("pos_true", 0)
|
|
|
|
|
2020-10-30 19:31:18 +00:00
|
|
|
return True
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2020-12-19 14:57:37 +00:00
|
|
|
def seek(self, pos: float) -> bool:
|
2020-10-29 22:25:17 +00:00
|
|
|
if self.isPlaying:
|
|
|
|
try:
|
2020-10-30 19:31:18 +00:00
|
|
|
self.play(pos)
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2020-11-03 22:48:11 +00:00
|
|
|
self.logger.log.exception("Failed to seek to pos: " + str(pos))
|
2020-10-29 22:25:17 +00:00
|
|
|
return False
|
|
|
|
return True
|
2020-10-30 19:31:18 +00:00
|
|
|
else:
|
2021-04-12 21:59:51 +00:00
|
|
|
self.stopped_manually = True # Don't trigger _ended() on seeking.
|
2020-10-30 19:31:18 +00:00
|
|
|
self.state.update("paused", True)
|
|
|
|
self._updateState(pos=pos)
|
|
|
|
return True
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2021-02-14 17:53:28 +00:00
|
|
|
def set_auto_advance(self, message: bool) -> bool:
|
|
|
|
self.state.update("auto_advance", message)
|
|
|
|
return True
|
|
|
|
|
2020-11-03 01:07:25 +00:00
|
|
|
def set_repeat(self, message: str) -> bool:
|
2021-02-14 17:53:28 +00:00
|
|
|
if message in ["all", "one", "none"]:
|
2020-11-03 01:07:25 +00:00
|
|
|
self.state.update("repeat", message)
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
2021-02-14 17:53:28 +00:00
|
|
|
def set_play_on_load(self, message: bool) -> bool:
|
|
|
|
self.state.update("play_on_load", message)
|
|
|
|
return True
|
2020-11-03 01:07:25 +00:00
|
|
|
|
2020-11-15 19:34:13 +00:00
|
|
|
# Show Plan Related Methods
|
2021-02-14 00:29:47 +00:00
|
|
|
def get_plan(self, message: int):
|
2021-04-18 02:14:14 +00:00
|
|
|
plan = sync(self.api.get_showplan(message))
|
2021-02-14 00:29:47 +00:00
|
|
|
self.clear_channel_plan()
|
2021-04-18 19:27:54 +00:00
|
|
|
channel = self.state.get()["channel"]
|
2021-03-13 22:32:04 +00:00
|
|
|
self.logger.log.info(plan)
|
2021-02-14 00:29:47 +00:00
|
|
|
if len(plan) > channel:
|
|
|
|
for plan_item in plan[str(channel)]:
|
|
|
|
try:
|
2021-02-14 20:10:32 +00:00
|
|
|
self.add_to_plan(plan_item)
|
|
|
|
except Exception as e:
|
2021-04-08 19:53:51 +00:00
|
|
|
self.logger.log.critical(
|
|
|
|
"Failed to add item to show plan: {}".format(e)
|
|
|
|
)
|
2021-02-14 00:29:47 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
2020-12-19 14:57:37 +00:00
|
|
|
def add_to_plan(self, new_item: Dict[str, Any]) -> bool:
|
2021-03-13 22:32:04 +00:00
|
|
|
new_item_obj = PlanItem(new_item)
|
2021-04-18 19:27:54 +00:00
|
|
|
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
|
2021-03-13 22:32:04 +00:00
|
|
|
# Shift any plan items after the new position down one to make space.
|
|
|
|
for item in plan_copy:
|
|
|
|
if item.weight >= new_item_obj.weight:
|
|
|
|
item.weight += 1
|
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
plan_copy += [new_item_obj] # Add the new item.
|
2021-03-13 22:32:04 +00:00
|
|
|
|
|
|
|
def sort_weight(e: PlanItem):
|
|
|
|
return e.weight
|
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
plan_copy.sort(key=sort_weight) # Sort into weighted order.
|
2021-03-13 22:32:04 +00:00
|
|
|
|
|
|
|
self.state.update("show_plan", plan_copy)
|
2020-11-01 02:35:14 +00:00
|
|
|
return True
|
|
|
|
|
2021-02-14 20:10:32 +00:00
|
|
|
def remove_from_plan(self, weight: int) -> bool:
|
2021-04-18 19:27:54 +00:00
|
|
|
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
|
2021-03-13 22:32:04 +00:00
|
|
|
found = False
|
2020-12-19 14:57:37 +00:00
|
|
|
for i in plan_copy:
|
2021-02-14 20:10:32 +00:00
|
|
|
if i.weight == weight:
|
2020-11-02 23:06:45 +00:00
|
|
|
plan_copy.remove(i)
|
2021-03-13 22:32:04 +00:00
|
|
|
found = True
|
2021-04-08 19:53:51 +00:00
|
|
|
elif (
|
|
|
|
i.weight > weight
|
|
|
|
): # Shuffle up the weights of the items following the deleted one.
|
2021-03-13 22:32:04 +00:00
|
|
|
i.weight -= 1
|
|
|
|
if found:
|
|
|
|
self.state.update("show_plan", plan_copy)
|
|
|
|
return True
|
2020-11-02 23:06:45 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
def clear_channel_plan(self) -> bool:
|
|
|
|
self.state.update("show_plan", [])
|
|
|
|
return True
|
|
|
|
|
2021-02-14 20:10:32 +00:00
|
|
|
def load(self, weight: int):
|
2020-10-29 22:25:17 +00:00
|
|
|
if not self.isPlaying:
|
2020-10-30 19:31:18 +00:00
|
|
|
self.unload()
|
2020-10-29 22:25:17 +00:00
|
|
|
|
2021-04-18 19:27:54 +00:00
|
|
|
showplan = self.state.get()["show_plan"]
|
2020-12-08 19:41:11 +00:00
|
|
|
|
2020-12-19 14:57:37 +00:00
|
|
|
loaded_item: Optional[PlanItem] = None
|
2020-12-08 19:41:11 +00:00
|
|
|
|
|
|
|
for i in range(len(showplan)):
|
2021-02-14 20:10:32 +00:00
|
|
|
if showplan[i].weight == weight:
|
2020-12-08 19:41:11 +00:00
|
|
|
loaded_item = showplan[i]
|
2020-11-01 02:35:14 +00:00
|
|
|
break
|
|
|
|
|
2021-04-08 21:05:25 +00:00
|
|
|
if loaded_item is None:
|
2021-04-08 20:15:15 +00:00
|
|
|
self.logger.log.error(
|
|
|
|
"Failed to find weight: {}".format(weight))
|
2020-11-01 02:35:14 +00:00
|
|
|
return False
|
|
|
|
|
2021-04-07 21:52:33 +00:00
|
|
|
reload = False
|
2021-04-08 21:05:25 +00:00
|
|
|
if loaded_item.filename == "" or loaded_item.filename is None:
|
2021-04-08 20:15:15 +00:00
|
|
|
self.logger.log.info(
|
|
|
|
"Filename is not specified, loading from API.")
|
2021-04-07 21:52:33 +00:00
|
|
|
reload = True
|
|
|
|
elif not os.path.exists(loaded_item.filename):
|
2021-04-08 19:53:51 +00:00
|
|
|
self.logger.log.warn(
|
|
|
|
"Filename given doesn't exist. Re-loading from API."
|
|
|
|
)
|
2021-04-07 21:52:33 +00:00
|
|
|
reload = True
|
|
|
|
|
|
|
|
if reload:
|
2021-04-18 02:14:14 +00:00
|
|
|
loaded_item.filename = sync(self.api.get_filename(item=loaded_item))
|
2020-12-08 19:41:11 +00:00
|
|
|
|
2020-12-19 14:57:37 +00:00
|
|
|
if not loaded_item.filename:
|
|
|
|
return False
|
|
|
|
|
2020-12-08 19:41:11 +00:00
|
|
|
self.state.update("loaded_item", loaded_item)
|
2020-10-29 22:25:17 +00:00
|
|
|
|
2020-12-08 19:41:11 +00:00
|
|
|
for i in range(len(showplan)):
|
2021-02-14 20:10:32 +00:00
|
|
|
if showplan[i].weight == weight:
|
2020-12-08 19:41:11 +00:00
|
|
|
self.state.update("show_plan", index=i, value=loaded_item)
|
|
|
|
break
|
2021-04-12 21:59:51 +00:00
|
|
|
# TODO: Update the show plan filenames???
|
2020-10-29 22:25:17 +00:00
|
|
|
|
|
|
|
try:
|
2021-04-08 20:15:15 +00:00
|
|
|
self.logger.log.info("Loading file: " +
|
|
|
|
str(loaded_item.filename))
|
2020-12-08 19:41:11 +00:00
|
|
|
mixer.music.load(loaded_item.filename)
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2020-10-29 22:25:17 +00:00
|
|
|
# We couldn't load that file.
|
2021-04-08 19:53:51 +00:00
|
|
|
self.logger.log.exception(
|
|
|
|
"Couldn't load file: " + str(loaded_item.filename)
|
|
|
|
)
|
2020-10-29 22:25:17 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
try:
|
2020-12-19 14:57:37 +00:00
|
|
|
if ".mp3" in loaded_item.filename:
|
|
|
|
song = MP3(loaded_item.filename)
|
2020-10-29 22:25:17 +00:00
|
|
|
self.state.update("length", song.info.length)
|
|
|
|
else:
|
2021-04-08 19:53:51 +00:00
|
|
|
self.state.update(
|
2021-04-08 20:15:15 +00:00
|
|
|
"length", mixer.Sound(
|
|
|
|
loaded_item.filename).get_length() / 1000
|
2021-04-08 19:53:51 +00:00
|
|
|
)
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2021-04-08 20:15:15 +00:00
|
|
|
self.logger.log.exception(
|
|
|
|
"Failed to update the length of item.")
|
2020-10-29 22:25:17 +00:00
|
|
|
return False
|
2021-02-14 13:23:51 +00:00
|
|
|
|
2021-04-12 21:59:51 +00:00
|
|
|
if loaded_item.cue > 0:
|
|
|
|
self.seek(loaded_item.cue)
|
|
|
|
|
2021-04-18 19:27:54 +00:00
|
|
|
if self.state.get()["play_on_load"]:
|
2021-02-14 13:23:51 +00:00
|
|
|
self.play()
|
|
|
|
|
2020-10-29 22:25:17 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
def unload(self):
|
|
|
|
if not self.isPlaying:
|
|
|
|
try:
|
|
|
|
mixer.music.unload()
|
2020-10-30 19:31:18 +00:00
|
|
|
self.state.update("paused", False)
|
2020-11-01 02:35:14 +00:00
|
|
|
self.state.update("loaded_item", None)
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2020-11-03 22:48:11 +00:00
|
|
|
self.logger.log.exception("Failed to unload channel.")
|
2020-10-29 22:25:17 +00:00
|
|
|
return False
|
2021-04-22 22:00:31 +00:00
|
|
|
|
|
|
|
self._potentially_end_tracklist()
|
|
|
|
# If we unloaded successfully, reset the tracklist_id, ready for the next item.
|
|
|
|
if not self.isLoaded:
|
|
|
|
self.state.update("tracklist_id", None)
|
|
|
|
|
2020-10-29 22:25:17 +00:00
|
|
|
return not self.isLoaded
|
2020-10-24 20:31:52 +00:00
|
|
|
|
|
|
|
def quit(self):
|
2020-11-03 22:48:11 +00:00
|
|
|
try:
|
|
|
|
mixer.quit()
|
|
|
|
self.state.update("paused", False)
|
2021-04-04 21:34:46 +00:00
|
|
|
self.logger.log.info("Quit mixer.")
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2020-11-03 22:48:11 +00:00
|
|
|
self.logger.log.exception("Failed to quit mixer.")
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2020-12-19 14:57:37 +00:00
|
|
|
def output(self, name: Optional[str] = None):
|
2021-04-18 19:27:54 +00:00
|
|
|
wasPlaying = self.state.get()["playing"]
|
2021-04-07 19:14:39 +00:00
|
|
|
|
|
|
|
name = None if (not name or name.lower() == "none") else name
|
2020-11-05 18:59:28 +00:00
|
|
|
|
2020-10-24 20:31:52 +00:00
|
|
|
self.quit()
|
2020-10-30 00:32:34 +00:00
|
|
|
self.state.update("output", name)
|
2020-10-24 20:31:52 +00:00
|
|
|
try:
|
|
|
|
if name:
|
2020-11-01 03:27:26 +00:00
|
|
|
mixer.init(44100, -16, 2, 1024, devicename=name)
|
2020-10-24 20:31:52 +00:00
|
|
|
else:
|
2020-11-01 03:27:26 +00:00
|
|
|
mixer.init(44100, -16, 2, 1024)
|
2021-04-08 21:32:16 +00:00
|
|
|
except Exception:
|
2021-04-08 19:53:51 +00:00
|
|
|
self.logger.log.exception(
|
|
|
|
"Failed to init mixer with device name: " + str(name)
|
|
|
|
)
|
2020-10-30 00:32:34 +00:00
|
|
|
return False
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2021-04-18 19:27:54 +00:00
|
|
|
loadedItem = self.state.get()["loaded_item"]
|
2021-04-08 19:53:51 +00:00
|
|
|
if loadedItem:
|
2021-02-14 20:10:32 +00:00
|
|
|
self.load(loadedItem.weight)
|
2020-11-01 03:19:21 +00:00
|
|
|
if wasPlaying:
|
|
|
|
self.unpause()
|
|
|
|
|
2020-10-30 00:32:34 +00:00
|
|
|
return True
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2021-04-17 17:27:36 +00:00
|
|
|
# Timeslotitemid can be a ghost (un-submitted item), so may be "IXXX"
|
|
|
|
def set_marker(self, timeslotitemid: str, marker_str: str):
|
2021-04-10 21:56:53 +00:00
|
|
|
set_loaded = False
|
|
|
|
success = True
|
|
|
|
try:
|
|
|
|
marker = Marker(marker_str)
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.log.error("Failed to create Marker instance with {} {}: {}".format(timeslotitemid, marker_str, e))
|
|
|
|
return False
|
|
|
|
|
2021-04-17 17:27:36 +00:00
|
|
|
if timeslotitemid == "-1":
|
2021-04-10 21:56:53 +00:00
|
|
|
set_loaded = True
|
|
|
|
if not self.isLoaded:
|
|
|
|
return False
|
2021-04-18 19:27:54 +00:00
|
|
|
timeslotitemid = self.state.get()["loaded_item"].timeslotitemid
|
2021-04-10 21:56:53 +00:00
|
|
|
|
2021-04-18 19:27:54 +00:00
|
|
|
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
|
|
|
|
for i in range(len(self.state.get()["show_plan"])):
|
2021-04-12 21:59:51 +00:00
|
|
|
|
|
|
|
item = plan_copy[i]
|
2021-04-10 21:56:53 +00:00
|
|
|
|
2021-04-17 17:27:36 +00:00
|
|
|
if str(item.timeslotitemid) == str(timeslotitemid):
|
2021-04-10 21:56:53 +00:00
|
|
|
try:
|
2021-04-12 21:59:51 +00:00
|
|
|
new_item = item.set_marker(marker)
|
|
|
|
self.state.update("show_plan", new_item, index=i)
|
2021-04-10 21:56:53 +00:00
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.log.error(
|
|
|
|
"Failed to set marker on item {}: {} with marker \n{}".format(timeslotitemid, e, marker))
|
|
|
|
success = False
|
|
|
|
|
|
|
|
if set_loaded:
|
|
|
|
try:
|
2021-04-18 19:27:54 +00:00
|
|
|
self.state.update("loaded_item", self.state.get()["loaded_item"].set_marker(marker))
|
2021-04-10 21:56:53 +00:00
|
|
|
except Exception as e:
|
|
|
|
self.logger.log.error(
|
|
|
|
"Failed to set marker on loaded_item {}: {} with marker \n{}".format(timeslotitemid, e, marker))
|
|
|
|
success = False
|
|
|
|
|
|
|
|
return success
|
|
|
|
|
|
|
|
# Helper functions
|
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
# This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading.
|
|
|
|
def _potentially_tracklist(self):
|
|
|
|
mode: TracklistMode = self.state.get()["tracklist_mode"]
|
|
|
|
|
|
|
|
time: int = -1
|
|
|
|
if mode == "on":
|
|
|
|
time = 1 # Let's do it pretty quickly.
|
|
|
|
elif mode == "delayed":
|
|
|
|
# Let's do it in a bit, once we're sure it's been playing. (Useful if we've got no idea if it's live or cueing.)
|
|
|
|
time = TRACKLISTING_DELAYED_S
|
|
|
|
|
|
|
|
if time >= 0 and not self.tracklist_start_timer:
|
|
|
|
self.logger.log.info("Setting timer for tracklisting in {} secs due to Mode: {}".format(time, mode))
|
|
|
|
self.tracklist_start_timer = Timer(time, self._tracklist_start)
|
|
|
|
self.tracklist_start_timer.start()
|
|
|
|
elif self.tracklist_start_timer:
|
|
|
|
self.logger.log.error("Failed to potentially tracklist, timer already busy.")
|
|
|
|
|
|
|
|
# This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading.
|
|
|
|
def _potentially_end_tracklist(self):
|
|
|
|
|
|
|
|
# Make a copy of the tracklist_id, it will get reset as we load the next item.
|
|
|
|
tracklist_id = self.state.get()["tracklist_id"]
|
2021-04-23 20:12:31 +00:00
|
|
|
if not tracklist_id:
|
|
|
|
self.logger.log.info("No tracklist to end.")
|
|
|
|
return
|
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
self.logger.log.info("Setting timer for ending tracklist_id {}".format(tracklist_id))
|
|
|
|
if tracklist_id:
|
|
|
|
self.logger.log.info("Attempting to end tracklist_id {}".format(tracklist_id))
|
|
|
|
if self.tracklist_end_timer:
|
|
|
|
self.logger.log.error("Failed to potentially end tracklist, timer already busy.")
|
|
|
|
return
|
|
|
|
# This threads it, so it won't hang track loading if it fails.
|
|
|
|
self.tracklist_end_timer = Timer(1, self._tracklist_end, [tracklist_id])
|
|
|
|
self.tracklist_end_timer.start()
|
|
|
|
else:
|
|
|
|
self.logger.log.warning("Failed to potentially end tracklist, no tracklist started.")
|
|
|
|
|
|
|
|
def _tracklist_start(self):
|
|
|
|
loaded_item = self.state.get()["loaded_item"]
|
|
|
|
if not loaded_item:
|
|
|
|
self.logger.log.error("Tried to call _tracklist_start() with no loaded item!")
|
|
|
|
return
|
|
|
|
|
|
|
|
tracklist_id = self.state.get()["tracklist_id"]
|
|
|
|
if (not tracklist_id):
|
|
|
|
self.logger.log.info("Tracklisting item: {}".format(loaded_item.name))
|
|
|
|
tracklist_id = self.api.post_tracklist_start(loaded_item)
|
|
|
|
if not tracklist_id:
|
|
|
|
self.logger.log.error("Failed to tracklist {}".format(loaded_item.name))
|
|
|
|
else:
|
|
|
|
self.logger.log.info("Tracklist id: {}".format(tracklist_id))
|
|
|
|
self.state.update("tracklist_id", tracklist_id)
|
|
|
|
else:
|
|
|
|
self.logger.log.info("Not tracklisting item {}, already got tracklistid: {}".format(
|
|
|
|
loaded_item.name, tracklist_id))
|
|
|
|
|
|
|
|
self.tracklist_start_timer = None
|
|
|
|
|
|
|
|
def _tracklist_end(self, tracklist_id):
|
|
|
|
|
|
|
|
if tracklist_id:
|
|
|
|
self.logger.log.info("Attempting to end tracklist_id {}".format(tracklist_id))
|
|
|
|
self.api.post_tracklist_end(tracklist_id)
|
|
|
|
else:
|
|
|
|
self.logger.log.error("Tracklist_id to _tracklist_end() missing. Failed to end tracklist.")
|
|
|
|
|
|
|
|
self.tracklist_end_timer = None
|
|
|
|
|
2021-04-10 21:56:53 +00:00
|
|
|
def _ended(self):
|
2021-04-22 22:00:31 +00:00
|
|
|
self._potentially_end_tracklist()
|
|
|
|
|
2021-04-18 19:27:54 +00:00
|
|
|
loaded_item = self.state.get()["loaded_item"]
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-12 21:59:51 +00:00
|
|
|
if not loaded_item:
|
|
|
|
return
|
|
|
|
|
2021-02-14 00:29:47 +00:00
|
|
|
# Track has ended
|
2021-04-07 19:14:12 +00:00
|
|
|
print("Finished", loaded_item.name, loaded_item.weight)
|
2021-02-14 00:29:47 +00:00
|
|
|
|
|
|
|
# Repeat 1
|
2021-04-07 19:14:12 +00:00
|
|
|
# TODO ENUM
|
2021-04-18 19:27:54 +00:00
|
|
|
if self.state.get()["repeat"] == "one":
|
2021-02-14 00:29:47 +00:00
|
|
|
self.play()
|
2021-04-07 19:14:12 +00:00
|
|
|
return
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-07 19:14:12 +00:00
|
|
|
loaded_new_item = False
|
2021-02-14 00:29:47 +00:00
|
|
|
# Auto Advance
|
2021-04-18 19:27:54 +00:00
|
|
|
if self.state.get()["auto_advance"]:
|
|
|
|
for i in range(len(self.state.get()["show_plan"])):
|
|
|
|
if self.state.get()["show_plan"][i].weight == loaded_item.weight:
|
|
|
|
if len(self.state.get()["show_plan"]) > i + 1:
|
|
|
|
self.load(self.state.get()["show_plan"][i + 1].weight)
|
2021-04-07 19:14:12 +00:00
|
|
|
loaded_new_item = True
|
2021-02-14 00:29:47 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
# Repeat All
|
2021-04-07 19:14:12 +00:00
|
|
|
# TODO ENUM
|
2021-04-18 19:27:54 +00:00
|
|
|
elif self.state.get()["repeat"] == "all":
|
|
|
|
self.load(self.state.get()["show_plan"][0].weight)
|
2021-04-07 19:14:12 +00:00
|
|
|
loaded_new_item = True
|
|
|
|
break
|
2021-02-14 00:29:47 +00:00
|
|
|
|
|
|
|
# Play on Load
|
2021-04-18 19:27:54 +00:00
|
|
|
if self.state.get()["play_on_load"] and loaded_new_item:
|
2021-02-14 00:29:47 +00:00
|
|
|
self.play()
|
2021-04-07 19:14:12 +00:00
|
|
|
return
|
2021-02-14 13:23:51 +00:00
|
|
|
|
2021-04-07 19:14:12 +00:00
|
|
|
# No automations, just stop playing.
|
|
|
|
self.stop()
|
|
|
|
if self.out_q:
|
2021-04-08 19:53:51 +00:00
|
|
|
self._retAll("STOPPED") # Tell clients that we've stopped playing.
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2020-12-19 14:57:37 +00:00
|
|
|
def _updateState(self, pos: Optional[float] = None):
|
2020-11-04 01:19:56 +00:00
|
|
|
|
2020-10-30 00:32:34 +00:00
|
|
|
self.state.update("initialised", self.isInit)
|
|
|
|
if self.isInit:
|
2021-04-08 19:53:51 +00:00
|
|
|
if pos:
|
2020-11-04 01:19:56 +00:00
|
|
|
self.state.update("pos", max(0, pos))
|
|
|
|
elif self.isPlaying:
|
2020-10-30 19:31:18 +00:00
|
|
|
# Get one last update in, incase we're about to pause/stop it.
|
2021-04-08 19:53:51 +00:00
|
|
|
self.state.update("pos", max(0, mixer.music.get_pos() / 1000))
|
2021-04-17 17:27:36 +00:00
|
|
|
# TODO this is wrong now we don't pause the mixer.
|
2021-03-21 13:06:09 +00:00
|
|
|
elif not self.isPaused:
|
2021-04-08 19:53:51 +00:00
|
|
|
self.state.update("pos", 0) # Reset back to 0 if stopped.
|
2021-03-21 13:06:09 +00:00
|
|
|
self.state.update("pos_offset", 0)
|
2021-04-07 19:14:12 +00:00
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
# If the state is changing from playing to not playing, and the user didn't stop it, the item must have ended.
|
2021-04-08 19:53:51 +00:00
|
|
|
if (
|
2021-04-18 19:27:54 +00:00
|
|
|
self.state.get()["playing"]
|
2021-04-08 19:53:51 +00:00
|
|
|
and not self.isPlaying
|
|
|
|
and not self.stopped_manually
|
|
|
|
):
|
2021-04-10 21:56:53 +00:00
|
|
|
self._ended()
|
2021-04-07 19:14:12 +00:00
|
|
|
|
2020-10-30 00:32:34 +00:00
|
|
|
self.state.update("playing", self.isPlaying)
|
|
|
|
self.state.update("loaded", self.isLoaded)
|
2020-10-30 19:31:18 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
self.state.update(
|
|
|
|
"pos_true",
|
|
|
|
min(
|
2021-04-18 19:27:54 +00:00
|
|
|
self.state.get()["length"],
|
|
|
|
self.state.get()["pos"] + self.state.get()["pos_offset"],
|
2021-04-08 19:53:51 +00:00
|
|
|
),
|
|
|
|
)
|
2020-10-30 19:31:18 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
self.state.update(
|
|
|
|
"remaining",
|
2021-04-18 19:27:54 +00:00
|
|
|
max(0, (self.state.get()["length"] -
|
|
|
|
self.state.get()["pos_true"])),
|
2021-04-08 19:53:51 +00:00
|
|
|
)
|
2020-10-30 00:32:34 +00:00
|
|
|
|
2021-02-14 00:29:47 +00:00
|
|
|
def _ping_times(self):
|
2021-03-21 13:06:09 +00:00
|
|
|
|
|
|
|
UPDATES_FREQ_SECS = 0.2
|
2021-04-08 19:53:51 +00:00
|
|
|
if (
|
2021-04-08 21:05:25 +00:00
|
|
|
self.last_time_update is None
|
2021-04-08 19:53:51 +00:00
|
|
|
or self.last_time_update + UPDATES_FREQ_SECS < time.time()
|
|
|
|
):
|
2021-02-14 00:29:47 +00:00
|
|
|
self.last_time_update = time.time()
|
2021-04-18 19:27:54 +00:00
|
|
|
self._retAll("POS:" + str(self.state.get()["pos_true"]))
|
2020-11-03 01:07:25 +00:00
|
|
|
|
2021-03-22 00:33:14 +00:00
|
|
|
def _retAll(self, msg):
|
|
|
|
self.out_q.put("ALL:" + msg)
|
2020-11-03 01:07:25 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
def _retMsg(
|
|
|
|
self, msg: Any, okay_str: bool = False, custom_prefix: Optional[str] = None
|
|
|
|
):
|
2021-03-22 00:33:14 +00:00
|
|
|
# Make sure to add the message source back, so that it can be sent to the correct destination in the main server.
|
|
|
|
if custom_prefix:
|
|
|
|
response = custom_prefix
|
|
|
|
else:
|
|
|
|
response = "{}:{}:".format(self.last_msg_source, self.last_msg)
|
2021-04-08 21:05:25 +00:00
|
|
|
if msg is True:
|
2020-10-30 00:32:34 +00:00
|
|
|
response += "OKAY"
|
|
|
|
elif isinstance(msg, str):
|
|
|
|
if okay_str:
|
|
|
|
response += "OKAY:" + msg
|
|
|
|
else:
|
|
|
|
response += "FAIL:" + msg
|
2020-10-24 20:31:52 +00:00
|
|
|
else:
|
2020-10-30 00:32:34 +00:00
|
|
|
response += "FAIL"
|
2021-02-14 17:53:28 +00:00
|
|
|
self.logger.log.debug(("Preparing to send: {}".format(response)))
|
2020-10-30 00:32:34 +00:00
|
|
|
if self.out_q:
|
2021-04-17 17:27:36 +00:00
|
|
|
self.logger.log.debug(("Sending: {}".format(response)))
|
2020-10-30 00:32:34 +00:00
|
|
|
self.out_q.put(response)
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2021-02-14 13:57:07 +00:00
|
|
|
def _send_status(self):
|
2021-03-22 00:33:14 +00:00
|
|
|
# TODO This is hacky
|
2021-04-08 20:15:15 +00:00
|
|
|
self._retMsg(str(self.status), okay_str=True,
|
|
|
|
custom_prefix="ALL:STATUS:")
|
2021-02-14 13:57:07 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
def __init__(
|
2021-04-22 22:00:31 +00:00
|
|
|
self, channel: int, in_q: multiprocessing.Queue, out_q: multiprocessing.Queue, server_state: StateManager
|
2021-04-08 19:53:51 +00:00
|
|
|
):
|
2020-10-30 00:32:34 +00:00
|
|
|
|
2020-10-30 23:14:29 +00:00
|
|
|
process_title = "Player: Channel " + str(channel)
|
|
|
|
setproctitle.setproctitle(process_title)
|
|
|
|
multiprocessing.current_process().name = process_title
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2020-10-30 22:06:03 +00:00
|
|
|
self.running = True
|
|
|
|
self.out_q = out_q
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2021-04-05 21:41:22 +00:00
|
|
|
self.logger = LoggingManager("Player" + str(channel))
|
2020-10-30 22:06:03 +00:00
|
|
|
|
2021-04-22 22:00:31 +00:00
|
|
|
self.api = MyRadioAPI(self.logger, server_state)
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
self.state = StateManager(
|
|
|
|
"Player" + str(channel),
|
|
|
|
self.logger,
|
|
|
|
self.__default_state,
|
|
|
|
self.__rate_limited_params,
|
|
|
|
)
|
2021-02-14 13:57:07 +00:00
|
|
|
|
|
|
|
self.state.add_callback(self._send_status)
|
|
|
|
|
2020-10-30 23:59:58 +00:00
|
|
|
self.state.update("channel", channel)
|
2021-04-22 22:00:31 +00:00
|
|
|
self.state.update("tracklist_mode", server_state.get()["tracklist_mode"])
|
2020-10-30 23:59:58 +00:00
|
|
|
|
2020-10-24 20:31:52 +00:00
|
|
|
loaded_state = copy.copy(self.state.state)
|
|
|
|
|
|
|
|
if loaded_state["output"]:
|
2021-04-08 20:15:15 +00:00
|
|
|
self.logger.log.info("Setting output to: " +
|
|
|
|
str(loaded_state["output"]))
|
2020-10-24 20:31:52 +00:00
|
|
|
self.output(loaded_state["output"])
|
|
|
|
else:
|
2020-10-30 22:06:03 +00:00
|
|
|
self.logger.log.info("Using default output device.")
|
2020-10-24 20:31:52 +00:00
|
|
|
self.output()
|
|
|
|
|
2020-12-19 14:57:37 +00:00
|
|
|
loaded_item = loaded_state["loaded_item"]
|
|
|
|
if loaded_item:
|
2021-04-08 20:15:15 +00:00
|
|
|
self.logger.log.info("Loading filename: " +
|
|
|
|
str(loaded_item.filename))
|
2021-02-14 20:10:32 +00:00
|
|
|
self.load(loaded_item.weight)
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2021-04-12 21:59:51 +00:00
|
|
|
# Load may jump to the cue point, as it would do on a regular load.
|
|
|
|
# If we were at a different state before, we have to override it now.
|
2020-10-30 19:31:18 +00:00
|
|
|
if loaded_state["pos_true"] != 0:
|
2021-04-08 19:53:51 +00:00
|
|
|
self.logger.log.info(
|
|
|
|
"Seeking to pos_true: " + str(loaded_state["pos_true"])
|
|
|
|
)
|
2020-10-30 19:31:18 +00:00
|
|
|
self.seek(loaded_state["pos_true"])
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2021-04-08 21:05:25 +00:00
|
|
|
if loaded_state["playing"] is True:
|
2020-10-30 22:06:03 +00:00
|
|
|
self.logger.log.info("Resuming.")
|
2021-04-12 21:59:51 +00:00
|
|
|
self.unpause() # Use un-pause as we don't want to jump to a new position.
|
2020-10-29 21:23:37 +00:00
|
|
|
else:
|
2020-10-30 22:06:03 +00:00
|
|
|
self.logger.log.info("No file was previously loaded.")
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2021-04-04 21:34:46 +00:00
|
|
|
try:
|
|
|
|
while self.running:
|
2021-04-04 22:14:08 +00:00
|
|
|
time.sleep(0.02)
|
2021-04-04 21:34:46 +00:00
|
|
|
self._updateState()
|
|
|
|
self._ping_times()
|
2020-10-29 21:23:37 +00:00
|
|
|
try:
|
2021-03-22 00:33:14 +00:00
|
|
|
message = in_q.get_nowait()
|
|
|
|
source = message.split(":")[0]
|
|
|
|
if source not in VALID_MESSAGE_SOURCES:
|
|
|
|
self.last_msg_source = ""
|
|
|
|
self.last_msg = ""
|
2021-04-08 19:53:51 +00:00
|
|
|
self.logger.log.warn(
|
2021-04-08 20:15:15 +00:00
|
|
|
"Message from unknown sender source: {}".format(
|
|
|
|
source)
|
2021-04-08 19:53:51 +00:00
|
|
|
)
|
2021-03-22 00:33:14 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
self.last_msg_source = source
|
|
|
|
self.last_msg = message.split(":", 1)[1]
|
|
|
|
|
2021-04-17 17:27:36 +00:00
|
|
|
self.logger.log.debug(
|
2021-04-08 19:53:51 +00:00
|
|
|
"Recieved message from source {}: {}".format(
|
|
|
|
self.last_msg_source, self.last_msg
|
|
|
|
)
|
|
|
|
)
|
2020-10-29 21:23:37 +00:00
|
|
|
except Empty:
|
|
|
|
# The incomming message queue was empty,
|
|
|
|
# skip message processing
|
|
|
|
pass
|
|
|
|
else:
|
2020-10-30 00:32:34 +00:00
|
|
|
|
2020-10-29 21:23:37 +00:00
|
|
|
# We got a message.
|
2020-10-29 22:25:17 +00:00
|
|
|
|
2020-10-30 00:32:34 +00:00
|
|
|
# Output re-inits the mixer, so we can do this any time.
|
2021-04-08 19:53:51 +00:00
|
|
|
if self.last_msg.startswith("OUTPUT"):
|
2020-10-30 00:32:34 +00:00
|
|
|
split = self.last_msg.split(":")
|
|
|
|
self._retMsg(self.output(split[1]))
|
|
|
|
|
|
|
|
elif self.isInit:
|
2021-04-08 19:53:51 +00:00
|
|
|
message_types: Dict[
|
|
|
|
str, Callable[..., Any]
|
|
|
|
] = { # TODO Check Types
|
2021-02-14 00:29:47 +00:00
|
|
|
"STATUS": lambda: self._retMsg(self.status, True),
|
2020-11-03 01:07:25 +00:00
|
|
|
# Audio Playout
|
2021-04-12 21:59:51 +00:00
|
|
|
# Unpause, so we don't jump to 0, we play from the current pos.
|
|
|
|
"PLAY": lambda: self._retMsg(self.unpause()),
|
2020-11-15 19:34:13 +00:00
|
|
|
"PAUSE": lambda: self._retMsg(self.pause()),
|
|
|
|
"UNPAUSE": lambda: self._retMsg(self.unpause()),
|
2021-04-12 21:59:51 +00:00
|
|
|
"STOP": lambda: self._retMsg(self.stop(user_initiated=True)),
|
2021-04-08 19:53:51 +00:00
|
|
|
"SEEK": lambda: self._retMsg(
|
|
|
|
self.seek(float(self.last_msg.split(":")[1]))
|
|
|
|
),
|
|
|
|
"AUTOADVANCE": lambda: self._retMsg(
|
|
|
|
self.set_auto_advance(
|
|
|
|
(self.last_msg.split(":")[1] == "True")
|
|
|
|
)
|
|
|
|
),
|
|
|
|
"REPEAT": lambda: self._retMsg(
|
|
|
|
self.set_repeat(self.last_msg.split(":")[1])
|
|
|
|
),
|
|
|
|
"PLAYONLOAD": lambda: self._retMsg(
|
|
|
|
self.set_play_on_load(
|
|
|
|
(self.last_msg.split(":")[1] == "True")
|
|
|
|
)
|
|
|
|
),
|
2020-11-03 01:07:25 +00:00
|
|
|
# Show Plan Items
|
2021-04-08 19:53:51 +00:00
|
|
|
"GET_PLAN": lambda: self._retMsg(
|
|
|
|
self.get_plan(int(self.last_msg.split(":")[1]))
|
|
|
|
),
|
|
|
|
"LOAD": lambda: self._retMsg(
|
|
|
|
self.load(int(self.last_msg.split(":")[1]))
|
|
|
|
),
|
2020-11-15 19:34:13 +00:00
|
|
|
"LOADED?": lambda: self._retMsg(self.isLoaded),
|
|
|
|
"UNLOAD": lambda: self._retMsg(self.unload()),
|
2021-04-08 19:53:51 +00:00
|
|
|
"ADD": lambda: self._retMsg(
|
|
|
|
self.add_to_plan(
|
2021-04-08 20:15:15 +00:00
|
|
|
json.loads(
|
|
|
|
":".join(self.last_msg.split(":")[1:]))
|
2021-04-08 19:53:51 +00:00
|
|
|
)
|
|
|
|
),
|
|
|
|
"REMOVE": lambda: self._retMsg(
|
2021-04-08 20:15:15 +00:00
|
|
|
self.remove_from_plan(
|
|
|
|
int(self.last_msg.split(":")[1]))
|
2021-04-08 19:53:51 +00:00
|
|
|
),
|
|
|
|
"CLEAR": lambda: self._retMsg(self.clear_channel_plan()),
|
2021-04-17 17:27:36 +00:00
|
|
|
"SETMARKER": lambda: self._retMsg(self.set_marker(self.last_msg.split(":")[1], self.last_msg.split(":", 2)[2])),
|
2020-11-01 02:37:56 +00:00
|
|
|
}
|
|
|
|
|
2020-11-03 00:32:43 +00:00
|
|
|
message_type: str = self.last_msg.split(":")[0]
|
|
|
|
|
|
|
|
if message_type in message_types.keys():
|
|
|
|
message_types[message_type]()
|
2020-10-30 00:32:34 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
elif self.last_msg == "QUIT":
|
2021-04-05 23:32:58 +00:00
|
|
|
self._retMsg(True)
|
2020-10-29 21:23:37 +00:00
|
|
|
self.running = False
|
2020-10-30 00:32:34 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
else:
|
|
|
|
self._retMsg("Unknown Command")
|
|
|
|
else:
|
2021-02-14 00:29:47 +00:00
|
|
|
|
2021-04-08 19:53:51 +00:00
|
|
|
if self.last_msg == "STATUS":
|
2020-10-30 00:32:34 +00:00
|
|
|
self._retMsg(self.status)
|
|
|
|
else:
|
|
|
|
self._retMsg(False)
|
2020-10-29 21:23:37 +00:00
|
|
|
|
2021-04-04 21:34:46 +00:00
|
|
|
# Catch the player being killed externally.
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
self.logger.log.info("Received KeyboardInterupt")
|
|
|
|
except SystemExit:
|
|
|
|
self.logger.log.info("Received SystemExit")
|
|
|
|
except Exception as e:
|
2021-04-08 20:15:15 +00:00
|
|
|
self.logger.log.exception(
|
2021-04-08 21:05:47 +00:00
|
|
|
"Received unexpected Exception: {}".format(e))
|
2020-10-24 20:31:52 +00:00
|
|
|
|
2021-04-04 21:34:46 +00:00
|
|
|
self.logger.log.info("Quiting player " + str(channel))
|
2020-10-29 21:23:37 +00:00
|
|
|
self.quit()
|
2021-04-05 23:32:58 +00:00
|
|
|
self._retAll("QUIT")
|
2021-04-04 21:34:46 +00:00
|
|
|
del self.logger
|
|
|
|
os._exit(0)
|
2020-10-29 21:23:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2021-04-08 19:53:51 +00:00
|
|
|
raise Exception(
|
|
|
|
"This BAPSicle Player is a subcomponenet, it will not run individually."
|
|
|
|
)
|