Add WIP internal support for tracklisting
This commit is contained in:
parent
504f5b0145
commit
669489068a
5 changed files with 242 additions and 45 deletions
|
@ -19,8 +19,9 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import json
|
import json
|
||||||
from logging import INFO
|
from logging import INFO, ERROR, WARNING
|
||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
from baps_types.plan import PlanItem
|
from baps_types.plan import PlanItem
|
||||||
from helpers.os_environment import resolve_external_file_path
|
from helpers.os_environment import resolve_external_file_path
|
||||||
|
@ -36,42 +37,108 @@ class MyRadioAPI:
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
async def get(self, url, timeout=10):
|
async def async_call(self, url, method="GET", data=None, timeout=10):
|
||||||
|
|
||||||
async with aiohttp.ClientSession(read_timeout=timeout) as session:
|
async with aiohttp.ClientSession(read_timeout=timeout) as session:
|
||||||
async with session.get(url) as response:
|
func = session.get(url)
|
||||||
if response.status != 200:
|
status_code = -1
|
||||||
|
if method == "GET":
|
||||||
|
#func = session.get(url)
|
||||||
|
status_code = 200
|
||||||
|
elif method == "POST":
|
||||||
|
func = session.post(url, data=data)
|
||||||
|
status_code = 201
|
||||||
|
elif method == "PUT":
|
||||||
|
func = session.put(url)
|
||||||
|
status_code = 201
|
||||||
|
|
||||||
|
async with func as response:
|
||||||
|
if response.status != status_code:
|
||||||
self._logException(
|
self._logException(
|
||||||
"Failed to get API request. Status code: " + str(response.status)
|
"Failed to get API request. Status code: " + str(response.status)
|
||||||
)
|
)
|
||||||
self._logException(str(response.text()))
|
self._logException(str(response.text()))
|
||||||
return await response.read()
|
return await response.read()
|
||||||
|
|
||||||
async def get_non_api_call(self, url):
|
def call(self, url, method="GET", data=None, timeout=10, json_payload=True):
|
||||||
|
r = None
|
||||||
|
status_code = -1
|
||||||
|
if method == "GET":
|
||||||
|
r = requests.get(url, timeout=timeout)
|
||||||
|
status_code = 200
|
||||||
|
elif method == "POST":
|
||||||
|
r = requests.post(url, data, timeout=timeout)
|
||||||
|
status_code = 201
|
||||||
|
elif method == "PUT":
|
||||||
|
r = requests.put(url, data, timeout=timeout)
|
||||||
|
status_code = 200
|
||||||
|
|
||||||
url = "{}{}".format(self.config.get()["myradio_base_url"], url)
|
if r.status_code != status_code:
|
||||||
|
self._logException(
|
||||||
|
"Failed to get API request. Status code: " + str(r.status_code)
|
||||||
|
)
|
||||||
|
self._logException(str(r.text))
|
||||||
|
return json.loads(r.text) if json_payload else r.text
|
||||||
|
|
||||||
|
async def async_api_call(self, url, api_version="v2", method="GET", data=None, timeout=10):
|
||||||
|
if api_version == "v2":
|
||||||
|
url = "{}/v2{}".format(self.config.get()["myradio_api_url"], url)
|
||||||
|
elif api_version == "non":
|
||||||
|
url = "{}{}".format(self.config.get()["myradio_base_url"], url)
|
||||||
|
else:
|
||||||
|
self._logException("Invalid API version. Request not sent.")
|
||||||
|
return None
|
||||||
|
|
||||||
if "?" in url:
|
if "?" in url:
|
||||||
url += "&api_key={}".format(self.config.get()["myradio_api_key"])
|
url += "&api_key={}".format(self.config.get()["myradio_api_key"])
|
||||||
else:
|
else:
|
||||||
url += "?api_key={}".format(self.config.get()["myradio_api_key"])
|
url += "?api_key={}".format(self.config.get()["myradio_api_key"])
|
||||||
|
|
||||||
self._log("Requesting non-API URL: " + url)
|
self._log("Requesting API V2 URL with method {}: {}".format(method, url))
|
||||||
request = self.get(url)
|
|
||||||
|
request = None
|
||||||
|
if method == "GET":
|
||||||
|
request = self.async_call(url, method="GET", timeout=timeout)
|
||||||
|
elif method == "POST":
|
||||||
|
self._log("POST data: {}".format(data))
|
||||||
|
request = self.async_call(url, data=data, method="POST", timeout=timeout)
|
||||||
|
elif method == "PUT":
|
||||||
|
request = self.async_call(url, method="PUT", timeout=timeout)
|
||||||
|
else:
|
||||||
|
self._logException("Invalid API method. Request not sent.")
|
||||||
|
return None
|
||||||
self._log("Finished request.")
|
self._log("Finished request.")
|
||||||
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
async def get_apiv2_call(self, url):
|
def api_call(self, url, api_version="v2", method="GET", data=None, timeout=10):
|
||||||
|
|
||||||
url = "{}/v2{}".format(self.config.get()["myradio_api_url"], url)
|
if api_version == "v2":
|
||||||
|
url = "{}/v2{}".format(self.config.get()["myradio_api_url"], url)
|
||||||
|
elif api_version == "non":
|
||||||
|
url = "{}{}".format(self.config.get()["myradio_base_url"], url)
|
||||||
|
else:
|
||||||
|
self._logException("Invalid API version. Request not sent.")
|
||||||
|
return None
|
||||||
|
|
||||||
if "?" in url:
|
if "?" in url:
|
||||||
url += "&api_key={}".format(self.config.get()["myradio_api_key"])
|
url += "&api_key={}".format(self.config.get()["myradio_api_key"])
|
||||||
else:
|
else:
|
||||||
url += "?api_key={}".format(self.config.get()["myradio_api_key"])
|
url += "?api_key={}".format(self.config.get()["myradio_api_key"])
|
||||||
|
|
||||||
self._log("Requesting API V2 URL: " + url)
|
self._log("Requesting API V2 URL with method {}: {}".format(method, url))
|
||||||
request = self.get(url)
|
|
||||||
|
request = None
|
||||||
|
if method == "GET":
|
||||||
|
request = self.call(url, method="GET", timeout=timeout)
|
||||||
|
elif method == "POST":
|
||||||
|
self._log("POST data: {}".format(data))
|
||||||
|
request = self.call(url, data=data, method="POST", timeout=timeout)
|
||||||
|
elif method == "PUT":
|
||||||
|
request = self.call(url, method="PUT", timeout=timeout)
|
||||||
|
else:
|
||||||
|
self._logException("Invalid API method. Request not sent.")
|
||||||
|
return None
|
||||||
self._log("Finished request.")
|
self._log("Finished request.")
|
||||||
|
|
||||||
return request
|
return request
|
||||||
|
@ -80,7 +147,7 @@ class MyRadioAPI:
|
||||||
|
|
||||||
async def get_showplans(self):
|
async def get_showplans(self):
|
||||||
url = "/timeslot/currentandnextobjects?n=10"
|
url = "/timeslot/currentandnextobjects?n=10"
|
||||||
request = await self.get_apiv2_call(url)
|
request = await self.async_api_call(url)
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
self._logException("Failed to get list of show plans.")
|
self._logException("Failed to get list of show plans.")
|
||||||
|
@ -110,7 +177,7 @@ class MyRadioAPI:
|
||||||
async def get_showplan(self, timeslotid: int):
|
async def get_showplan(self, timeslotid: int):
|
||||||
|
|
||||||
url = "/timeslot/{}/showplan".format(timeslotid)
|
url = "/timeslot/{}/showplan".format(timeslotid)
|
||||||
request = await self.get_apiv2_call(url)
|
request = await self.async_api_call(url)
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
self._logException("Failed to get show plan.")
|
self._logException("Failed to get show plan.")
|
||||||
|
@ -154,7 +221,7 @@ class MyRadioAPI:
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
# File doesn't exist, download it.
|
# File doesn't exist, download it.
|
||||||
request = await self.get_non_api_call(url)
|
request = await self.async_api_call(url, api_version="non")
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
return None
|
return None
|
||||||
|
@ -171,7 +238,7 @@ class MyRadioAPI:
|
||||||
# Gets the list of managed music playlists.
|
# Gets the list of managed music playlists.
|
||||||
async def get_playlist_music(self):
|
async def get_playlist_music(self):
|
||||||
url = "/playlist/allitonesplaylists"
|
url = "/playlist/allitonesplaylists"
|
||||||
request = await self.get_apiv2_call(url)
|
request = await self.async_api_call(url)
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
self._logException("Failed to retrieve music playlists.")
|
self._logException("Failed to retrieve music playlists.")
|
||||||
|
@ -182,7 +249,7 @@ class MyRadioAPI:
|
||||||
# Gets the list of managed aux playlists (sfx, beds etc.)
|
# Gets the list of managed aux playlists (sfx, beds etc.)
|
||||||
async def get_playlist_aux(self):
|
async def get_playlist_aux(self):
|
||||||
url = "/nipswebPlaylist/allmanagedplaylists"
|
url = "/nipswebPlaylist/allmanagedplaylists"
|
||||||
request = await self.get_apiv2_call(url)
|
request = await self.async_api_call(url)
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
self._logException("Failed to retrieve music playlists.")
|
self._logException("Failed to retrieve music playlists.")
|
||||||
|
@ -197,7 +264,7 @@ class MyRadioAPI:
|
||||||
library_id = library_id[library_id.index("-") + 1:]
|
library_id = library_id[library_id.index("-") + 1:]
|
||||||
|
|
||||||
url = "/nipswebPlaylist/{}/items".format(library_id)
|
url = "/nipswebPlaylist/{}/items".format(library_id)
|
||||||
request = await self.get_apiv2_call(url)
|
request = await self.async_api_call(url)
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
self._logException(
|
self._logException(
|
||||||
|
@ -211,7 +278,7 @@ class MyRadioAPI:
|
||||||
|
|
||||||
async def get_playlist_music_items(self, library_id: str):
|
async def get_playlist_music_items(self, library_id: str):
|
||||||
url = "/playlist/{}/tracks".format(library_id)
|
url = "/playlist/{}/tracks".format(library_id)
|
||||||
request = await self.get_apiv2_call(url)
|
request = await self.async_api_call(url)
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
self._logException(
|
self._logException(
|
||||||
|
@ -227,7 +294,7 @@ class MyRadioAPI:
|
||||||
url = "/track/search?title={}&artist={}&digitised=1&limit={}".format(
|
url = "/track/search?title={}&artist={}&digitised=1&limit={}".format(
|
||||||
title if title else "", artist if artist else "", limit
|
title if title else "", artist if artist else "", limit
|
||||||
)
|
)
|
||||||
request = await self.get_apiv2_call(url)
|
request = await self.async_api_call(url)
|
||||||
|
|
||||||
if not request:
|
if not request:
|
||||||
self._logException("Failed to search for track.")
|
self._logException("Failed to search for track.")
|
||||||
|
@ -235,6 +302,43 @@ class MyRadioAPI:
|
||||||
|
|
||||||
return json.loads(await request)["payload"]
|
return json.loads(await request)["payload"]
|
||||||
|
|
||||||
|
def post_tracklist_start(self, item: PlanItem):
|
||||||
|
if item.type != "central":
|
||||||
|
self._log("Not tracklisting, {} is not a track.".format(item.name))
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._log("Tracklisting item: {}".format(item.name))
|
||||||
|
|
||||||
|
source: str = self.config.get()["myradio_api_tracklist_source"]
|
||||||
|
data = {
|
||||||
|
"trackid": item.trackid,
|
||||||
|
"sourceid": int(source) if source.isnumeric() else source
|
||||||
|
}
|
||||||
|
# Starttime and timeslotid are default in the API to current time/show.
|
||||||
|
tracklist_id = None
|
||||||
|
try:
|
||||||
|
tracklist_id = self.api_call("/tracklistItem/", method="POST", data=data)["payload"]["audiologid"]
|
||||||
|
except Exception as e:
|
||||||
|
self._logException("Failed to get tracklistid. {}".format(e))
|
||||||
|
|
||||||
|
if not tracklist_id or not isinstance(tracklist_id, int):
|
||||||
|
self._log("Failed to tracklist! API rejected tracklist.", ERROR)
|
||||||
|
return
|
||||||
|
return tracklist_id
|
||||||
|
|
||||||
|
def post_tracklist_end(self, tracklistitemid: int):
|
||||||
|
if not tracklistitemid:
|
||||||
|
self._log("Tracklistitemid is None, can't end tracklist.", WARNING)
|
||||||
|
return False
|
||||||
|
if not isinstance(tracklistitemid, int):
|
||||||
|
self._logException("Tracklistitemid '{}' is not an integer!".format(tracklistitemid))
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._log("Ending tracklistitemid {}".format(tracklistitemid))
|
||||||
|
|
||||||
|
result = self.api_call("/tracklistItem/{}/endtime".format(tracklistitemid), method="PUT")
|
||||||
|
print(result)
|
||||||
|
|
||||||
def _log(self, text: str, level: int = INFO):
|
def _log(self, text: str, level: int = INFO):
|
||||||
self.logger.log.log(level, "MyRadio API: " + text)
|
self.logger.log.log(level, "MyRadio API: " + text)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from logging import CRITICAL, INFO
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from typing import Any, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from baps_types.plan import PlanItem
|
from baps_types.plan import PlanItem
|
||||||
from helpers.logging_manager import LoggingManager
|
from helpers.logging_manager import LoggingManager
|
||||||
|
@ -24,7 +24,7 @@ class StateManager:
|
||||||
self,
|
self,
|
||||||
name,
|
name,
|
||||||
logger: LoggingManager,
|
logger: LoggingManager,
|
||||||
default_state=None,
|
default_state: Dict[str, Any] = None,
|
||||||
rate_limit_params=[],
|
rate_limit_params=[],
|
||||||
rate_limit_period_s=5,
|
rate_limit_period_s=5,
|
||||||
):
|
):
|
||||||
|
@ -57,10 +57,9 @@ class StateManager:
|
||||||
if file_state == "":
|
if file_state == "":
|
||||||
self._log("State file is empty. Setting default state.")
|
self._log("State file is empty. Setting default state.")
|
||||||
self.state = default_state
|
self.state = default_state
|
||||||
self.__state_in_file = copy(self.state)
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
file_state = json.loads(file_state)
|
file_state: Dict[str, Any] = json.loads(file_state)
|
||||||
|
|
||||||
# Turn from JSON -> PlanItem
|
# Turn from JSON -> PlanItem
|
||||||
if "channel" in file_state:
|
if "channel" in file_state:
|
||||||
|
@ -75,12 +74,18 @@ class StateManager:
|
||||||
|
|
||||||
# Now feed the loaded state into the initialised state manager.
|
# Now feed the loaded state into the initialised state manager.
|
||||||
self.state = file_state
|
self.state = file_state
|
||||||
|
|
||||||
|
# If there are any new config options in the default state, save them.
|
||||||
|
# Uses update() to save them to file too.
|
||||||
|
for key in default_state.keys():
|
||||||
|
if not key in file_state.keys():
|
||||||
|
self.update(key, default_state[key])
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self._logException(
|
self._logException(
|
||||||
"Failed to parse state JSON. Resetting to default state."
|
"Failed to parse state JSON. Resetting to default state."
|
||||||
)
|
)
|
||||||
self.state = default_state
|
self.state = default_state
|
||||||
self.__state_in_file = copy(self.state)
|
|
||||||
|
|
||||||
# Now setup the rate limiting
|
# Now setup the rate limiting
|
||||||
# Essentially rate limit all values to "now" to start with, allowing the first update
|
# Essentially rate limit all values to "now" to start with, allowing the first update
|
||||||
|
@ -103,8 +108,6 @@ class StateManager:
|
||||||
|
|
||||||
def write_to_file(self, state):
|
def write_to_file(self, state):
|
||||||
|
|
||||||
self.__state_in_file = state
|
|
||||||
|
|
||||||
# Make sure we're not manipulating state
|
# Make sure we're not manipulating state
|
||||||
state_to_json = copy(state)
|
state_to_json = copy(state)
|
||||||
|
|
||||||
|
|
104
player.py
104
player.py
|
@ -20,6 +20,7 @@
|
||||||
# that we respond with something, FAIL or OKAY. The server doesn't like to be kept waiting.
|
# that we respond with something, FAIL or OKAY. The server doesn't like to be kept waiting.
|
||||||
|
|
||||||
# Stop the Pygame Hello message.
|
# Stop the Pygame Hello message.
|
||||||
|
from baps_types.enums import TracklistMode
|
||||||
import os
|
import os
|
||||||
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
|
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@ from typing import Any, Callable, Dict, List, Optional
|
||||||
from pygame import mixer
|
from pygame import mixer
|
||||||
from mutagen.mp3 import MP3
|
from mutagen.mp3 import MP3
|
||||||
from syncer import sync
|
from syncer import sync
|
||||||
|
from threading import Timer
|
||||||
|
|
||||||
from helpers.myradio_api import MyRadioAPI
|
from helpers.myradio_api import MyRadioAPI
|
||||||
from helpers.state_manager import StateManager
|
from helpers.state_manager import StateManager
|
||||||
|
@ -42,6 +44,7 @@ from baps_types.marker import Marker
|
||||||
|
|
||||||
# TODO ENUM
|
# TODO ENUM
|
||||||
VALID_MESSAGE_SOURCES = ["WEBSOCKET", "UI", "CONTROLLER", "TEST", "ALL"]
|
VALID_MESSAGE_SOURCES = ["WEBSOCKET", "UI", "CONTROLLER", "TEST", "ALL"]
|
||||||
|
TRACKLISTING_DELAYED_S = 20
|
||||||
|
|
||||||
|
|
||||||
class Player:
|
class Player:
|
||||||
|
@ -58,6 +61,9 @@ class Player:
|
||||||
|
|
||||||
stopped_manually: bool = False
|
stopped_manually: bool = False
|
||||||
|
|
||||||
|
tracklist_start_timer: Optional[Timer] = None
|
||||||
|
tracklist_end_timer: Optional[Timer] = None
|
||||||
|
|
||||||
__default_state = {
|
__default_state = {
|
||||||
"initialised": False,
|
"initialised": False,
|
||||||
"loaded_item": None,
|
"loaded_item": None,
|
||||||
|
@ -75,6 +81,8 @@ class Player:
|
||||||
"play_on_load": False,
|
"play_on_load": False,
|
||||||
"output": None,
|
"output": None,
|
||||||
"show_plan": [],
|
"show_plan": [],
|
||||||
|
"tracklist_mode": "off",
|
||||||
|
"tracklist_id": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
__rate_limited_params = ["pos", "pos_offset", "pos_true", "remaining"]
|
__rate_limited_params = ["pos", "pos_offset", "pos_true", "remaining"]
|
||||||
|
@ -116,7 +124,6 @@ class Player:
|
||||||
# We're not playing now, so we can quickly test run
|
# We're not playing now, so we can quickly test run
|
||||||
# If that works, we're loaded.
|
# If that works, we're loaded.
|
||||||
try:
|
try:
|
||||||
position: float = self.state.get()["pos"]
|
|
||||||
mixer.music.set_volume(0)
|
mixer.music.set_volume(0)
|
||||||
mixer.music.play(0)
|
mixer.music.play(0)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -128,10 +135,9 @@ class Player:
|
||||||
)
|
)
|
||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
if position > 0:
|
finally:
|
||||||
self.pause()
|
mixer.music.stop()
|
||||||
else:
|
|
||||||
self.stop()
|
|
||||||
mixer.music.set_volume(1)
|
mixer.music.set_volume(1)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -168,7 +174,7 @@ class Player:
|
||||||
self.logger.log.exception("Failed to play at pos: " + str(pos))
|
self.logger.log.exception("Failed to play at pos: " + str(pos))
|
||||||
return False
|
return False
|
||||||
self.state.update("paused", False)
|
self.state.update("paused", False)
|
||||||
|
self._potentially_tracklist()
|
||||||
self.stopped_manually = False
|
self.stopped_manually = False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -206,6 +212,9 @@ class Player:
|
||||||
return False
|
return False
|
||||||
self.state.update("paused", False)
|
self.state.update("paused", False)
|
||||||
|
|
||||||
|
if user_initiated:
|
||||||
|
self._potentially_end_tracklist()
|
||||||
|
|
||||||
self.stopped_manually = True
|
self.stopped_manually = True
|
||||||
|
|
||||||
if not self.state.get()["loaded_item"]:
|
if not self.state.get()["loaded_item"]:
|
||||||
|
@ -394,6 +403,12 @@ class Player:
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.log.exception("Failed to unload channel.")
|
self.logger.log.exception("Failed to unload channel.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
return not self.isLoaded
|
return not self.isLoaded
|
||||||
|
|
||||||
def quit(self):
|
def quit(self):
|
||||||
|
@ -473,7 +488,78 @@ class Player:
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
loaded_item = self.state.get()["loaded_item"]
|
||||||
|
if not loaded_item:
|
||||||
|
self.logger.log.warning("Tried to call _tracklist_end() with no loaded item!")
|
||||||
|
|
||||||
|
# Make a copy of the tracklist_id, it will get reset as we load the next item.
|
||||||
|
tracklist_id = self.state.get()["tracklist_id"]
|
||||||
|
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
|
||||||
|
|
||||||
def _ended(self):
|
def _ended(self):
|
||||||
|
self._potentially_end_tracklist()
|
||||||
|
|
||||||
loaded_item = self.state.get()["loaded_item"]
|
loaded_item = self.state.get()["loaded_item"]
|
||||||
|
|
||||||
if not loaded_item:
|
if not loaded_item:
|
||||||
|
@ -529,6 +615,7 @@ class Player:
|
||||||
self.state.update("pos", 0) # Reset back to 0 if stopped.
|
self.state.update("pos", 0) # Reset back to 0 if stopped.
|
||||||
self.state.update("pos_offset", 0)
|
self.state.update("pos_offset", 0)
|
||||||
|
|
||||||
|
# If the state is changing from playing to not playing, and the user didn't stop it, the item must have ended.
|
||||||
if (
|
if (
|
||||||
self.state.get()["playing"]
|
self.state.get()["playing"]
|
||||||
and not self.isPlaying
|
and not self.isPlaying
|
||||||
|
@ -594,7 +681,7 @@ class Player:
|
||||||
custom_prefix="ALL:STATUS:")
|
custom_prefix="ALL:STATUS:")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, channel: int, in_q: multiprocessing.Queue, out_q: multiprocessing.Queue, server_config: StateManager
|
self, channel: int, in_q: multiprocessing.Queue, out_q: multiprocessing.Queue, server_state: StateManager
|
||||||
):
|
):
|
||||||
|
|
||||||
process_title = "Player: Channel " + str(channel)
|
process_title = "Player: Channel " + str(channel)
|
||||||
|
@ -606,7 +693,7 @@ class Player:
|
||||||
|
|
||||||
self.logger = LoggingManager("Player" + str(channel))
|
self.logger = LoggingManager("Player" + str(channel))
|
||||||
|
|
||||||
self.api = MyRadioAPI(self.logger, server_config)
|
self.api = MyRadioAPI(self.logger, server_state)
|
||||||
|
|
||||||
self.state = StateManager(
|
self.state = StateManager(
|
||||||
"Player" + str(channel),
|
"Player" + str(channel),
|
||||||
|
@ -618,6 +705,7 @@ class Player:
|
||||||
self.state.add_callback(self._send_status)
|
self.state.add_callback(self._send_status)
|
||||||
|
|
||||||
self.state.update("channel", channel)
|
self.state.update("channel", channel)
|
||||||
|
self.state.update("tracklist_mode", server_state.get()["tracklist_mode"])
|
||||||
|
|
||||||
loaded_state = copy.copy(self.state.state)
|
loaded_state = copy.copy(self.state.state)
|
||||||
|
|
||||||
|
|
20
server.py
20
server.py
|
@ -39,6 +39,8 @@ from controllers.mattchbox_usb import MattchBox
|
||||||
from helpers.the_terminator import Terminator
|
from helpers.the_terminator import Terminator
|
||||||
import player
|
import player
|
||||||
|
|
||||||
|
PROCESS_KILL_TIMEOUT_S = 5
|
||||||
|
|
||||||
setproctitle("server.py")
|
setproctitle("server.py")
|
||||||
|
|
||||||
""" Proxy Manager to proxy Class Objects into multiprocessing processes, instead of making a copy. """
|
""" Proxy Manager to proxy Class Objects into multiprocessing processes, instead of making a copy. """
|
||||||
|
@ -51,8 +53,8 @@ class ProxyManager(m.BaseManager):
|
||||||
class BAPSicleServer:
|
class BAPSicleServer:
|
||||||
|
|
||||||
default_state = {
|
default_state = {
|
||||||
"server_version": "",
|
"server_version": "unknown",
|
||||||
"server_build": "",
|
"server_build": "unknown",
|
||||||
"server_name": "URY BAPSicle",
|
"server_name": "URY BAPSicle",
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 13500,
|
"port": 13500,
|
||||||
|
@ -63,7 +65,9 @@ class BAPSicleServer:
|
||||||
"myradio_api_key": None,
|
"myradio_api_key": None,
|
||||||
"myradio_base_url": "https://ury.org.uk/myradio",
|
"myradio_base_url": "https://ury.org.uk/myradio",
|
||||||
"myradio_api_url": "https://ury.org.uk/api",
|
"myradio_api_url": "https://ury.org.uk/api",
|
||||||
"running_state": "running"
|
"myradio_api_tracklist_source": "",
|
||||||
|
"running_state": "running",
|
||||||
|
"tracklist_mode": "off",
|
||||||
}
|
}
|
||||||
|
|
||||||
player_to_q: List[Queue] = []
|
player_to_q: List[Queue] = []
|
||||||
|
@ -219,7 +223,7 @@ class BAPSicleServer:
|
||||||
print("Stopping Websocket Server")
|
print("Stopping Websocket Server")
|
||||||
self.websocket_to_q[0].put("WEBSOCKET:QUIT")
|
self.websocket_to_q[0].put("WEBSOCKET:QUIT")
|
||||||
if self.websockets_server:
|
if self.websockets_server:
|
||||||
self.websockets_server.join()
|
self.websockets_server.join(timeout=PROCESS_KILL_TIMEOUT_S)
|
||||||
del self.websockets_server
|
del self.websockets_server
|
||||||
|
|
||||||
print("Stopping Players")
|
print("Stopping Players")
|
||||||
|
@ -227,26 +231,26 @@ class BAPSicleServer:
|
||||||
q.put("ALL:QUIT")
|
q.put("ALL:QUIT")
|
||||||
|
|
||||||
for player in self.player:
|
for player in self.player:
|
||||||
player.join()
|
player.join(timeout=PROCESS_KILL_TIMEOUT_S)
|
||||||
|
|
||||||
del self.player
|
del self.player
|
||||||
|
|
||||||
print("Stopping Web Server")
|
print("Stopping Web Server")
|
||||||
if self.webserver:
|
if self.webserver:
|
||||||
self.webserver.terminate()
|
self.webserver.terminate()
|
||||||
self.webserver.join()
|
self.webserver.join(timeout=PROCESS_KILL_TIMEOUT_S)
|
||||||
del self.webserver
|
del self.webserver
|
||||||
|
|
||||||
print("Stopping Player Handler")
|
print("Stopping Player Handler")
|
||||||
if self.player_handler:
|
if self.player_handler:
|
||||||
self.player_handler.terminate()
|
self.player_handler.terminate()
|
||||||
self.player_handler.join()
|
self.player_handler.join(timeout=PROCESS_KILL_TIMEOUT_S)
|
||||||
del self.player_handler
|
del self.player_handler
|
||||||
|
|
||||||
print("Stopping Controllers")
|
print("Stopping Controllers")
|
||||||
if self.controller_handler:
|
if self.controller_handler:
|
||||||
self.controller_handler.terminate()
|
self.controller_handler.terminate()
|
||||||
self.controller_handler.join()
|
self.controller_handler.join(timeout=PROCESS_KILL_TIMEOUT_S)
|
||||||
del self.controller_handler
|
del self.controller_handler
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,6 @@ import asyncio
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
# , render_template, send_from_directory, request, jsonify, abort
|
|
||||||
#from flask_cors import CORS
|
|
||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Optional, List
|
from typing import Any, Optional, List
|
||||||
|
|
Loading…
Reference in a new issue