mirror of
https://github.com/radio95-rnt/RadioPlayer.git
synced 2026-07-29 15:29:14 +02:00
some updates
This commit is contained in:
+12
-27
@@ -34,18 +34,14 @@ class ABC_ProcessManager(abc.ABC):
|
||||
class BaseIMCModule:
|
||||
"""This is not a module to be used but rather a placeholder IMC api to be used in other modules"""
|
||||
def imc(self, imc: 'InterModuleCommunication') -> None:
|
||||
"""
|
||||
Receive an IMC object
|
||||
"""
|
||||
"""Receive an IMC object"""
|
||||
self._imc = imc
|
||||
def imc_data(self, source: 'BaseIMCModule', source_name: str | None, data: object, broadcast: bool) -> object:
|
||||
"""
|
||||
React to IMC data
|
||||
"""
|
||||
return None
|
||||
def imc_data(self, source: 'BaseIMCModule', source_name: str | None, data: object, broadcast: bool) -> object: """React to IMC data"""
|
||||
|
||||
class ProcmanCommunicator(BaseIMCModule):
|
||||
def __init__(self, procman: ABC_ProcessManager) -> None: self.procman = procman
|
||||
def __init__(self, procman: ABC_ProcessManager) -> None:
|
||||
self.procman = procman
|
||||
self.tinytag = tinytag.TinyTag()
|
||||
def imc(self, imc: 'InterModuleCommunication') -> None:
|
||||
super().imc(imc)
|
||||
self._imc.register(self, "procman")
|
||||
@@ -57,7 +53,7 @@ class ProcmanCommunicator(BaseIMCModule):
|
||||
|
||||
if int(op) == 0: return {"op": 0, "arg": "pong"}
|
||||
elif int(op) == 1:
|
||||
if arg := data.get("arg"): return {"op": 1, "arg": tinytag.TinyTag().get(arg, tags=False).duration}
|
||||
if arg := data.get("arg"): return {"op": 1, "arg": self.tinytag.get(arg, tags=False).duration}
|
||||
else: return
|
||||
elif int(op) == 2:
|
||||
self.procman.stop_all(data.get("timeout", None))
|
||||
@@ -71,28 +67,21 @@ class ProcmanCommunicator(BaseIMCModule):
|
||||
|
||||
class PlayerModule(BaseIMCModule):
|
||||
"""Simple passive observer, this allows you to send the current track the your RDS encoder, or to your website"""
|
||||
def on_new_playlist(self, playlist: list[Track], global_args: dict[str, str]) -> None:
|
||||
def on_new_playlist(self, playlist: list[Track], global_args: dict[str, str]) -> None:
|
||||
"""This is called every new playlist"""
|
||||
pass
|
||||
def on_new_track(self, index: int, track: Track, next_track: Track | None) -> None:
|
||||
"""Called on every track including the ones added by the active modifier, you can check for that comparing the playlists[index] and the track"""
|
||||
pass
|
||||
def progress(self, index: int, track: Track, elapsed: float, total: float, real_total: float) -> None:
|
||||
"""
|
||||
Real total and total differ in that, total is how much the track lasts, but real_total will be for how long we will focus on it (crossfade)
|
||||
Runs at a frequency around 1 Hz
|
||||
Runs at a frequency around 1 Hz (depending on other plugins it can be rarer, but never any faster)
|
||||
Please don't put any blocking or code that takes time
|
||||
"""
|
||||
pass
|
||||
def shutdown(self):
|
||||
"""Ran while shutting down"""
|
||||
pass
|
||||
def shutdown(self): """Ran while shutting down"""
|
||||
class PlaylistModifierModule:
|
||||
"""Playlist modifier, this type of module allows you to shuffle, or put jingles into your playlist"""
|
||||
def modify(self, global_args: dict, playlist: list[Track]) -> list[Track] | None:
|
||||
"""
|
||||
global_args are playlist global args (see radioPlayer_playlist_file.txt)
|
||||
"""
|
||||
"""global_args are playlist global args (see radioPlayer_playlist_file.txt)"""
|
||||
return playlist
|
||||
# No IMC, as we only run on new playlists
|
||||
class PlaylistAdvisor(BaseIMCModule):
|
||||
@@ -105,9 +94,7 @@ class PlaylistAdvisor(BaseIMCModule):
|
||||
return False
|
||||
class ActiveModifier(BaseIMCModule):
|
||||
"""This changes the next song to be played live, which means that this picks the next song, not the playlist, but this is affected by the playlist"""
|
||||
def arguments(self, arguments: str | None) -> None:
|
||||
"""Called at start up with the program arguments"""
|
||||
pass
|
||||
def arguments(self, arguments: str | None) -> None: """Called at start up with the program arguments"""
|
||||
def play(self, index: int, track: Track | None, next_track: Track | None) -> tuple[tuple[Track | None, Track | None], bool | None]:
|
||||
"""
|
||||
Returns a tuple, in the first case where a is the track and b is a bool, b corresponds to whether to extend the playlist, set to true when adding content instead of replacing it
|
||||
@@ -115,9 +102,7 @@ class ActiveModifier(BaseIMCModule):
|
||||
The second track object is the next track, which is optional which is also only used for metadata and will not be taken in as data to play
|
||||
"""
|
||||
return (track, None), False
|
||||
def on_new_playlist(self, playlist: list[Track], global_args: dict[str, str]) -> None:
|
||||
"""Same behaviour as the basic module function"""
|
||||
pass
|
||||
def on_new_playlist(self, playlist: list[Track], global_args: dict[str, str]) -> None: """Same behaviour as the basic module function"""
|
||||
class InterModuleCommunication:
|
||||
def __init__(self, modules: Sequence[BaseIMCModule | None]) -> None:
|
||||
self.modules = modules
|
||||
|
||||
+3
-4
@@ -1,7 +1,7 @@
|
||||
MORNING_START = 5
|
||||
MORNING_END = 10
|
||||
DAY_START = 10
|
||||
DAY_END = 18
|
||||
DAY_END = 19
|
||||
LATE_NIGHT_START = 0
|
||||
LATE_NIGHT_END = 5
|
||||
|
||||
@@ -51,7 +51,6 @@ class Module(PlaylistAdvisor):
|
||||
def __init__(self) -> None:
|
||||
self.last_mod_time = 0
|
||||
self.last_playlist = None
|
||||
self.class_imc = None
|
||||
|
||||
self.custom_playlist = None
|
||||
self.custom_playlist_path = Path("/tmp/radioPlayer_list")
|
||||
@@ -104,7 +103,7 @@ class Module(PlaylistAdvisor):
|
||||
logger.info(f"Playing {current_day} night playlist...")
|
||||
self.last_mod_time = Time.get_playlist_modification_time(night_playlist)
|
||||
self.last_playlist = night_playlist
|
||||
if self.class_imc: self.class_imc.send(self, "web", {"playlist": str(self.last_playlist)})
|
||||
if self._imc: self._imc.send(self, "web", {"playlist": str(self.last_playlist)})
|
||||
return self.last_playlist
|
||||
def new_playlist(self) -> bool:
|
||||
if self.custom_playlist and self.custom_playlist_path.exists():
|
||||
@@ -128,7 +127,7 @@ class Module(PlaylistAdvisor):
|
||||
return True
|
||||
return False
|
||||
def imc(self, imc: InterModuleCommunication) -> None:
|
||||
self.class_imc = imc
|
||||
self._imc = imc
|
||||
imc.register(self, "advisor")
|
||||
def imc_data(self, source: BaseIMCModule, source_name: str | None, data: object, broadcast: bool):
|
||||
return (self.custom_playlist, MORNING_START, DAY_END)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from . import PlaylistModifierModule, Track
|
||||
DEFAULT_CROSSFADE = 5.0
|
||||
DEFAULT_CROSSFADE = 6.0
|
||||
class Module(PlaylistModifierModule):
|
||||
def modify(self, global_args: dict, playlist: list[Track]) -> list[Track] | None:
|
||||
out = []
|
||||
@@ -10,7 +10,7 @@ class Module(PlaylistModifierModule):
|
||||
out.append(Track(track.path, track_crossfade, track_crossfade, do_cross_fade, track.args, focus_time_offset=-track_crossfade))
|
||||
else: out.append(track)
|
||||
return out
|
||||
playlistmod = (Module(),1)
|
||||
playlistmod = Module(), 0
|
||||
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from . import ABC_ProcessManager, Process, Track, Path, Popen, tinytag
|
||||
from . import ABC_ProcessManager, Process, Track, Popen, tinytag
|
||||
from threading import Lock
|
||||
import subprocess, time
|
||||
|
||||
@@ -6,13 +6,12 @@ class ProcessManager(ABC_ProcessManager):
|
||||
def __init__(self) -> None:
|
||||
self.lock = Lock()
|
||||
self.processes: list[Process] = []
|
||||
def _get_audio_duration(self, file_path: Path):
|
||||
return tinytag.TinyTag().get(file_path, tags=False).duration
|
||||
self.tinytag = tinytag.TinyTag()
|
||||
def play(self, track: Track) -> Process:
|
||||
assert track.path.exists()
|
||||
cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet']
|
||||
|
||||
duration = self._get_audio_duration(track.path.absolute())
|
||||
duration = self.tinytag.get(track.path.absolute(), tags=False).duration
|
||||
if not duration: raise Exception("Failed to get file duration for", track.path)
|
||||
if track.offset >= duration: track.offset = max(duration - 0.1, 0)
|
||||
if track.offset > 0: cmd.extend(['-ss', str(track.offset)])
|
||||
@@ -35,7 +34,7 @@ class ProcessManager(ABC_ProcessManager):
|
||||
try: p.process.wait(timeout=0)
|
||||
except subprocess.TimeoutExpired: pass
|
||||
self.processes = alive
|
||||
return bool(self.processes)
|
||||
return bool(alive)
|
||||
def stop_all(self, timeout: float | None = None) -> None:
|
||||
with self.lock:
|
||||
for process in self.processes:
|
||||
|
||||
+1
-2
@@ -11,8 +11,7 @@ def _parse_args(text: str) -> dict[str, str]:
|
||||
if "=" in line:
|
||||
key, val = line.split("=", 1)
|
||||
args[key.strip()] = val.strip()
|
||||
else:
|
||||
args[line] = True
|
||||
else: args[line] = True
|
||||
return args
|
||||
|
||||
class FSDBParser(PlaylistParser):
|
||||
|
||||
+8
-6
@@ -25,6 +25,8 @@ def get_jingles():
|
||||
if not master: master = jingles.pop(0)
|
||||
return master, jingles
|
||||
|
||||
def chance(one_in_n): return random.randint(1, one_in_n) == 1
|
||||
|
||||
class Module(PlaylistModifierModule):
|
||||
def modify(self, global_args: dict, playlist: list[Track]) -> list[Track] | None:
|
||||
if int(global_args.get("no_jingle", 0)) != 0: return None
|
||||
@@ -34,26 +36,26 @@ class Module(PlaylistModifierModule):
|
||||
out: list[Track] = []
|
||||
last_jingiel = True
|
||||
for track in playlist:
|
||||
if not last_jingiel and (random.randint(1,3) == 1) and (track.args is None or int(track.args.get("no_jingle", 0)) == 0):
|
||||
if not last_jingiel and chance(3) and (track.args is None or int(track.args.get("no_jingle", 0)) == 0):
|
||||
out.append(Track(track.path, 0, track.fade_in, True, track.args))
|
||||
jingle = primary
|
||||
if secondary and (random.randint(1,3) == 1): jingle = random.choice(secondary)
|
||||
if secondary and chance(2): jingle = random.choice(secondary)
|
||||
out.append(Track(jingle, 0, 0, False, {}))
|
||||
last_jingiel = True
|
||||
continue
|
||||
out.append(Track(track.path, track.fade_out, track.fade_in, True, track.args,focus_time_offset=-track.fade_out))
|
||||
last_jingiel = False
|
||||
return out
|
||||
|
||||
|
||||
class Module2(PlayerModule):
|
||||
def imc(self, imc: InterModuleCommunication) -> None:
|
||||
super().imc(imc)
|
||||
self._imc.register(self, "jingle")
|
||||
imc.register(self, "jingle")
|
||||
def imc_data(self, source: BaseIMCModule, source_name: str | None, data: bool, broadcast: bool) -> object:
|
||||
if broadcast: return
|
||||
jingle, secondary = get_jingles()
|
||||
if secondary and (random.randint(1,3) == 1): jingle = random.choice(secondary)
|
||||
if secondary and chance(2): jingle = random.choice(secondary)
|
||||
return self._imc.send(self, "activemod", {"action": "add_to_toplay", "songs": [f"!{jingle}"], "top": bool(data)})
|
||||
|
||||
module = Module2()
|
||||
playlistmod = Module()
|
||||
playlistmod = Module(), 2
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from . import PlaylistModifierModule, Track
|
||||
|
||||
class Module(PlaylistModifierModule):
|
||||
def modify(self, global_args: dict, playlist: list[Track]) -> list[Track] | None:
|
||||
playlist[0].fade_in = 0
|
||||
return playlist
|
||||
|
||||
playlistmod = Module(), -1
|
||||
@@ -34,7 +34,6 @@ class Module(PlayerModule):
|
||||
def on_new_track(self, index: int, track: Track, next_track: Track | None) -> None:
|
||||
self.counts[track.path.as_posix()] = self.counts.get(track.path.as_posix(), 0) + 1
|
||||
self._save_counts()
|
||||
def shutdown(self):
|
||||
self._save_counts()
|
||||
def shutdown(self): self._save_counts()
|
||||
|
||||
module = Module()
|
||||
+6
-17
@@ -4,32 +4,22 @@ _log_out: log95.TextIO
|
||||
assert _log_out # pyright: ignore[reportUnboundVariable]
|
||||
|
||||
def load_play_counts(file_path: Path) -> dict[str, int]:
|
||||
"""
|
||||
Loads the play counts from the file generated by the play_counter module.
|
||||
"""
|
||||
counts = {}
|
||||
try:
|
||||
with open(file_path, 'r') as file:
|
||||
for line in file:
|
||||
if line.strip() == "" or line.startswith(";"):
|
||||
continue
|
||||
if line.strip() == "" or line.startswith(";"): continue
|
||||
try:
|
||||
key, value = line.split(':', 1)
|
||||
counts[key.strip()] = int(value.strip())
|
||||
except ValueError:
|
||||
continue
|
||||
except ValueError: continue
|
||||
return counts
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
except FileNotFoundError: return {}
|
||||
|
||||
class PopularitySorterModule(PlaylistModifierModule):
|
||||
"""
|
||||
A playlist modifier that reorders tracks based on their play history.
|
||||
For every pair of tracks, it gives a 60% probability to schedule the less-played track first.
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
self.logger = log95.log95("PopSort", output=_log_out)
|
||||
self.play_counts_file = Path("/home/user/mixes/.playlist/count.txt").resolve()
|
||||
self.play_counts_file = Path("/home/user/mixes/.playlist/count.txt")
|
||||
|
||||
def modify(self, global_args: dict, playlist: list[Track]) -> list[Track]:
|
||||
self.logger.info("Applying popularity-based sorting to the playlist...")
|
||||
@@ -69,12 +59,11 @@ class PopularitySorterModule(PlaylistModifierModule):
|
||||
count1 = play_counts.get(track1.path.as_posix(), 0)
|
||||
count2 = play_counts.get(track2.path.as_posix(), 0)
|
||||
|
||||
if count1 > count2:
|
||||
if count1 > count2:
|
||||
playlist[i], playlist[i+1] = track2, track1
|
||||
|
||||
i += 2
|
||||
|
||||
self.logger.info("Popularity sorting complete.")
|
||||
return playlist
|
||||
|
||||
playlistmod = (PopularitySorterModule(), 2)
|
||||
playlistmod = PopularitySorterModule(), 2
|
||||
@@ -1,55 +0,0 @@
|
||||
import glob
|
||||
from . import log95, Path, PlaylistParser
|
||||
|
||||
_log_out: log95.TextIO
|
||||
class PlaintextParser(PlaylistParser):
|
||||
def __init__(self): self.logger = log95.log95("PARSER", output=_log_out)
|
||||
|
||||
def _check_for_imports(self, path: Path, seen=None) -> list[str]:
|
||||
if seen is None: seen = set()
|
||||
if not path.exists():
|
||||
self.logger.error(f"Playlist not found: {path.name}")
|
||||
raise Exception("Playlist doesn't exist")
|
||||
lines = [line.strip() for line in path.read_text().splitlines() if line.strip()]
|
||||
|
||||
out = []
|
||||
for line in lines:
|
||||
if line.startswith("@"):
|
||||
target = Path(line.removeprefix("@"))
|
||||
if target not in seen:
|
||||
if not target.exists():
|
||||
self.logger.error(f"Target {target.name} of {path.name} does not exist")
|
||||
continue
|
||||
seen.add(target)
|
||||
out.extend(self._check_for_imports(target, seen))
|
||||
else: out.append(line)
|
||||
return out
|
||||
|
||||
def parse(self, playlist_path: Path) -> tuple[dict[str, str], list[tuple[list[str], dict[str, str]]]]:
|
||||
lines = self._check_for_imports(playlist_path)
|
||||
out = []
|
||||
global_arguments = {}
|
||||
for line in lines:
|
||||
arguments = {}
|
||||
line = line.strip()
|
||||
if not line or line.startswith(";") or line.startswith("#"): continue
|
||||
if "|" in line:
|
||||
if line.startswith("|"): # No file name, we're defining global arguments
|
||||
args = line.removeprefix("|").split(";")
|
||||
for arg in args:
|
||||
if "=" in arg:
|
||||
key, val = arg.split("=", 1)
|
||||
arguments[key] = val
|
||||
else: arguments[arg] = True
|
||||
else:
|
||||
line, args = line.split("|", 1)
|
||||
args = args.split(";")
|
||||
for arg in args:
|
||||
if "=" in arg:
|
||||
key, val = arg.split("=", 1)
|
||||
arguments[key] = val
|
||||
else: arguments[arg] = True
|
||||
out.append(([f for f in glob.glob(line) if Path(f).is_file()], arguments))
|
||||
return global_arguments, out
|
||||
|
||||
# parser = PlaintextParser()
|
||||
+29
-1
@@ -1,3 +1,5 @@
|
||||
from modules import InterModuleCommunication
|
||||
|
||||
from . import PlayerModule, log95, Track
|
||||
import socket
|
||||
|
||||
@@ -13,7 +15,7 @@ udp_host = ("127.0.0.1", 5000)
|
||||
logger_level = log95.log95Levels.DEBUG if DEBUG else log95.log95Levels.CRITICAL_ERROR
|
||||
_log_out: log95.TextIO
|
||||
assert _log_out # pyright: ignore[reportUnboundVariable]
|
||||
logger = log95.log95("RDS-MODULE", logger_level, output=_log_out)
|
||||
logger = log95.log95("RDS", logger_level, output=_log_out)
|
||||
|
||||
def load_dict_from_custom_format(file_path: str) -> dict[str, str]:
|
||||
try:
|
||||
@@ -84,5 +86,31 @@ class Module(PlayerModule):
|
||||
self._imc.send(self, "web", {"rt": rds_rt, "rtp": rds_rtp}, False)
|
||||
logger.info(f"RT set to '{rds_rt}'")
|
||||
logger.debug(f"{rds_rtp=}")
|
||||
def imc(self, imc: InterModuleCommunication) -> None: imc.register(self, "rds")
|
||||
|
||||
module = Module()
|
||||
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
# distribute this software, either in source code form or as a compiled
|
||||
# binary, for any purpose, commercial or non-commercial, and by any
|
||||
# means.
|
||||
|
||||
# In jurisdictions that recognize copyright laws, the author or authors
|
||||
# of this software dedicate any and all copyright interest in the
|
||||
# software to the public domain. We make this dedication for the benefit
|
||||
# of the public at large and to the detriment of our heirs and
|
||||
# successors. We intend this dedication to be an overt act of
|
||||
# relinquishment in perpetuity of all present and future rights to this
|
||||
# software under copyright law.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# For more information, please refer to <https://unlicense.org>
|
||||
+1
-1
@@ -7,7 +7,7 @@ class Module(PlaylistModifierModule):
|
||||
if int(global_args.get("no_shuffle", 0)) == 0: random.shuffle(playlist)
|
||||
return playlist
|
||||
|
||||
playlistmod = (Module(), 0)
|
||||
playlistmod = Module(), 1
|
||||
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
from . import PlayerModule, log95, Track
|
||||
import os
|
||||
|
||||
_log_out: log95.TextIO
|
||||
assert _log_out # pyright: ignore[reportUnboundVariable]
|
||||
logger = log95.log95("Skipper", output=_log_out)
|
||||
|
||||
class Module(PlayerModule):
|
||||
def __init__(self) -> None: self.playlist = []
|
||||
def on_new_playlist(self, playlist: list[Track], global_args: dict[str, str]): self.playlist = [str(t.path.absolute()) for t in playlist]
|
||||
def progress(self, index: int, track: Track, elapsed: float, total: float, real_total: float) -> None:
|
||||
if os.path.exists("/tmp/radioPlayer_skip"):
|
||||
self._imc.send(self, "procman", {"op": 2}) # Ask procman to kill every track playing (usually there is one, unless we are in the default 5 seconds of the crossfade)
|
||||
os.remove("/tmp/radioPlayer_skip")
|
||||
def on_new_track(self, index: int, track: Track, next_track: Track | None):
|
||||
if next_track: logger.info("Next up:", next_track.path.name)
|
||||
|
||||
module = Module()
|
||||
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
# distribute this software, either in source code form or as a compiled
|
||||
# binary, for any purpose, commercial or non-commercial, and by any
|
||||
# means.
|
||||
|
||||
# In jurisdictions that recognize copyright laws, the author or authors
|
||||
# of this software dedicate any and all copyright interest in the
|
||||
# software to the public domain. We make this dedication for the benefit
|
||||
# of the public at large and to the detriment of our heirs and
|
||||
# successors. We intend this dedication to be an overt act of
|
||||
# relinquishment in perpetuity of all present and future rights to this
|
||||
# software under copyright law.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# For more information, please refer to <https://unlicense.org>
|
||||
+2
-3
@@ -281,9 +281,8 @@
|
||||
function handleMessage(msg){
|
||||
if(msg.event === "state"){
|
||||
const d = msg.data || {};
|
||||
applyProgressState(d.track);
|
||||
updateDirs(d.dirs);
|
||||
if(d.playlist) { playlist = d.playlist; renderPlaylist(); }
|
||||
if(d.track) applyProgressState(d.track);
|
||||
if(d.dirs) updateDirs(d.dirs);
|
||||
} else if(msg.event === "playlist"){
|
||||
playlist = msg.data || [];
|
||||
renderPlaylist();
|
||||
|
||||
+31
-42
@@ -15,13 +15,14 @@ MAIN_PATH_DIR = Path("/home/user/mixes")
|
||||
async def ws_handler(websocket: ServerConnection, shared_data: dict, imc_q: multiprocessing.Queue, ws_q: multiprocessing.Queue):
|
||||
try:
|
||||
initial = {
|
||||
"playlist": json.loads(shared_data.get("playlist", "[]")),
|
||||
"track": json.loads(shared_data.get("track", "{}")),
|
||||
"progress": json.loads(shared_data.get("progress", "{}")),
|
||||
"dirs": {"files": [i.name for i in list(MAIN_PATH_DIR.iterdir()) if i.is_file()], "dirs": [i.name for i in list(MAIN_PATH_DIR.iterdir()) if i.is_dir()], "base": str(MAIN_PATH_DIR)}
|
||||
"dirs": {"files": [i.name for i in list(MAIN_PATH_DIR.iterdir()) if i.is_file()], "dirs": [i.name for i in list(MAIN_PATH_DIR.iterdir()) if i.is_dir()], "base": str(MAIN_PATH_DIR)},
|
||||
"rds": json.loads(shared_data.get("rds", "{}"))
|
||||
}
|
||||
except Exception: initial = {"playlist": [], "track": {}, "progress": {}, "dirs": {"files": [], "dirs": [], "base": ""}}
|
||||
except Exception: initial = {"track": {}, "progress": {}, "dirs": {}, "rds": {}}
|
||||
await websocket.send(json.dumps({"event": "state", "data": initial}))
|
||||
await websocket.send(json.dumps({"event": "playlist", "data": json.loads(shared_data.get("playlist", "[]"))}))
|
||||
|
||||
async for raw in websocket:
|
||||
try: msg: dict = json.loads(raw)
|
||||
@@ -33,13 +34,10 @@ async def ws_handler(websocket: ServerConnection, shared_data: dict, imc_q: mult
|
||||
key = str(uuid.uuid4())
|
||||
imc_q.put({"name": name, "data": data, "key": key})
|
||||
start = time.monotonic()
|
||||
result = None
|
||||
while time.monotonic() - start < 1:
|
||||
if key in shared_data:
|
||||
result = shared_data.pop(key)
|
||||
break
|
||||
if key in shared_data: return shared_data.pop(key)
|
||||
await asyncio.sleep(0.1)
|
||||
return result
|
||||
return None
|
||||
|
||||
action = msg.get("action")
|
||||
if action == "skip":
|
||||
@@ -80,23 +78,6 @@ async def ws_handler(websocket: ServerConnection, shared_data: dict, imc_q: mult
|
||||
result = await get_imc("activemod", {"action": "get_toplay"})
|
||||
if result is not None:
|
||||
await asyncio.get_event_loop().run_in_executor(None, ws_q.put, {"data": result, "event": "toplay"})
|
||||
elif action == "request_state":
|
||||
# supports requesting specific parts if provided
|
||||
what = msg.get("what", "")
|
||||
try:
|
||||
if what == "playlist": payload = json.loads(shared_data.get("playlist", "[]"))
|
||||
elif what == "track": payload = json.loads(shared_data.get("track", "{}"))
|
||||
elif what == "progress": payload = json.loads(shared_data.get("progress", "{}"))
|
||||
elif what == "dirs": payload = {"files": [i.name for i in list(MAIN_PATH_DIR.iterdir()) if i.is_file()], "dirs": [i.name for i in list(MAIN_PATH_DIR.iterdir()) if i.is_dir()], "base": str(MAIN_PATH_DIR)}
|
||||
else:
|
||||
payload = {
|
||||
"playlist": json.loads(shared_data.get("playlist", "[]")),
|
||||
"track": json.loads(shared_data.get("track", "{}")),
|
||||
"progress": json.loads(shared_data.get("progress", "{}")),
|
||||
"dirs": {"files": [i.name for i in list(MAIN_PATH_DIR.iterdir()) if i.is_file()], "dirs": [i.name for i in list(MAIN_PATH_DIR.iterdir()) if i.is_dir()], "base": str(MAIN_PATH_DIR)}
|
||||
}
|
||||
except Exception: payload = {}
|
||||
await websocket.send(json.dumps({"event": "state", "data": payload}))
|
||||
elif action == "request_dir":
|
||||
what: str = msg.get("what", "")
|
||||
try:
|
||||
@@ -142,21 +123,23 @@ def websocket_server_process(shared_data: dict, imc_q: multiprocessing.Queue, ws
|
||||
Headers([("Content-Type", "text/html"), ("Content-Length", f"{len(data)}")]),
|
||||
data
|
||||
)
|
||||
if request.path == "/favicon.ico":
|
||||
data = b"Not Found"
|
||||
if request.path == "/ws":
|
||||
if not "upgrade" in request.headers.get("Connection", "").lower():
|
||||
return Response(
|
||||
426,
|
||||
"Upgrade Required",
|
||||
Headers([("Connection", "Upgrade"), ("Upgrade", "websocket")]),
|
||||
b"WebSocket upgrade required\n"
|
||||
)
|
||||
return None
|
||||
else:
|
||||
data = b"Not Found\n"
|
||||
return Response(
|
||||
404,
|
||||
"Not Found",
|
||||
Headers([("Content-Length", f"{len(data)}")]),
|
||||
data
|
||||
)
|
||||
if not "upgrade" in request.headers.get("Connection", "").lower():
|
||||
return Response(
|
||||
426,
|
||||
"Upgrade Required",
|
||||
Headers([("Connection", "Upgrade"), ("Upgrade", "websocket")]),
|
||||
b"WebSocket upgrade required\n"
|
||||
)
|
||||
|
||||
server = await websockets.serve(handler_wrapper, "0.0.0.0", 3001, server_header="RadioPlayer ws plugin", process_request=process_request)
|
||||
broadcaster = asyncio.create_task(broadcast_worker(ws_q, clients))
|
||||
@@ -179,6 +162,7 @@ class Module(PlayerModule):
|
||||
self.data["playlist"] = "[]"
|
||||
self.data["track"] = "{}"
|
||||
self.data["progress"] = "{}"
|
||||
self.data["rds"] = "{}"
|
||||
|
||||
self.ipc_thread_running = True
|
||||
self.ipc_thread = threading.Thread(target=self._ipc_worker, daemon=True)
|
||||
@@ -192,10 +176,6 @@ class Module(PlayerModule):
|
||||
except Exception: pass
|
||||
|
||||
def _ipc_worker(self):
|
||||
"""
|
||||
Listens for messages placed in imc_q by websocket process or other modules,
|
||||
forwards them to the main IPC layer and stores keyed responses into shared dict.
|
||||
"""
|
||||
while self.ipc_thread_running:
|
||||
try:
|
||||
message: dict | None = self.imc_q.get(timeout=0.5)
|
||||
@@ -215,8 +195,7 @@ class Module(PlayerModule):
|
||||
"official": track.official,
|
||||
"args": track.args,
|
||||
"offset": track.offset,
|
||||
"focus_time_offset": track.focus_time_offset,
|
||||
"global_args": global_args
|
||||
"focus_time_offset": track.focus_time_offset
|
||||
})
|
||||
self.data["playlist"] = json.dumps(api_data)
|
||||
try: self.ws_q.put({"event": "playlist", "data": api_data})
|
||||
@@ -239,9 +218,13 @@ class Module(PlayerModule):
|
||||
except Exception: pass
|
||||
|
||||
def imc_data(self, source: BaseIMCModule, source_name: str | None, data: object, broadcast: bool) -> object:
|
||||
try: self.ws_q.put({"event": "imc", "data": {"name": source_name, "data": data, "broadcast": broadcast}})
|
||||
wsdata = {"event": "imc", "data": {"name": source_name, "data": data, "broadcast": broadcast}}
|
||||
if source_name == "rds":
|
||||
self.data[source_name] = data
|
||||
wsdata = {"event": "rds", "data": data}
|
||||
try: self.ws_q.put(wsdata)
|
||||
except Exception: pass
|
||||
|
||||
|
||||
def imc(self, imc: InterModuleCommunication) -> None:
|
||||
self._imc = imc
|
||||
imc.register(self, "web")
|
||||
@@ -258,6 +241,9 @@ class Module(PlayerModule):
|
||||
self.ipc_thread.join(timeout=1)
|
||||
self.ws_process.join(timeout=1)
|
||||
|
||||
self.imc_q.close()
|
||||
self.ws_q.close()
|
||||
|
||||
if self.ws_process.is_alive():
|
||||
self.ws_process.terminate()
|
||||
self.ws_process.join(timeout=1)
|
||||
@@ -266,6 +252,9 @@ class Module(PlayerModule):
|
||||
self.ws_process.kill()
|
||||
self.ws_process.join(timeout=1)
|
||||
|
||||
self.imc_q.join_thread()
|
||||
self.ws_q.join_thread()
|
||||
|
||||
module = Module()
|
||||
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
|
||||
Reference in New Issue
Block a user