some updates

This commit is contained in:
2026-04-15 19:56:40 +02:00
parent 2f4202a096
commit 32c61996f9
16 changed files with 126 additions and 221 deletions
+12 -27
View File
@@ -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
View File
@@ -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)
+2 -2
View File
@@ -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.
+4 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
+8
View File
@@ -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
+1 -2
View File
@@ -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
View File
@@ -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
-55
View File
@@ -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
View File
@@ -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
View File
@@ -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.
-43
View File
@@ -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
View File
@@ -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
View File
@@ -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.