0
1
mirror of https://github.com/radio95-rnt/RadioPlayer.git synced 2026-02-26 21:53:54 +01:00

procman communiator

This commit is contained in:
KubaPro010
2025-11-05 21:14:30 +01:00
parent 8b870ff1f8
commit cda38d74ef
4 changed files with 105 additions and 38 deletions

View File

@@ -1,4 +1,6 @@
import log95
from collections.abc import Sequence
from subprocess import Popen
from dataclasses import dataclass
@dataclass
@@ -10,6 +12,20 @@ class Track:
args: dict[str, str] | None
offset: float = 0.0
@dataclass
class Process:
process: Popen
track: str
started_at: float
duration: float
class Skeleton_ProcessManager:
processes: list[Process]
def _get_audio_duration(self, file_path): ...
def play(self, track_path: str, fade_in: bool=False, fade_out: bool=False, fade_time: int=5, offset: float=0.0) -> Process: ...
def anything_playing(self) -> bool: ...
def stop_all(self, timeout: float | None = None) -> None: ...
def wait_all(self, timeout: float | None = None) -> None: ...
class BaseIMCModule:
"""
This is not a module to be used but rather a placeholder IMC api to be used in other modules
@@ -18,13 +34,36 @@ class BaseIMCModule:
"""
Receive an IMC object
"""
pass
self._imc = imc
def imc_data(self, source: 'BaseIMCModule', source_name: str | None, data: object, broadcast: bool) -> object:
"""
React to IMC data
"""
return None
class ProcmanCommunicator(BaseIMCModule):
def __init__(self, procman: Skeleton_ProcessManager) -> None:
self.procman = procman
def imc(self, imc: 'InterModuleCommunication') -> None:
super().imc(imc)
self._imc.register(self, "procman")
def imc_data(self, source: BaseIMCModule, source_name: str | None, data: object, broadcast: bool) -> object:
if broadcast: return
if isinstance(data, str) and data.lower().strip() == "raw": return self.procman
elif isinstance(data, dict):
op = data.get("op")
if not op: return
if int(op) == 0: return {"op": 0, "arg": "pong"}
elif int(op) == 1:
if arg := data.get("arg"):
return {"op": 1, "arg": self.procman._get_audio_duration(arg)}
else: return
elif int(op) == 2:
self.procman.stop_all(data.get("timeout", None))
return {"op": 2}
elif int(op) == 3:
return {"op": 3, "arg": self.procman.processes}
class PlayerModule(BaseIMCModule):
"""
Simple passive observer, this allows you to send the current track the your RDS encoder, or to your website
@@ -89,21 +128,18 @@ class ActiveModifier(BaseIMCModule):
"""
pass
class InterModuleCommunication:
def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None:
self.advisor = advisor
self.active_modifier = active_modifier
self.simple_modules = simple_modules
def __init__(self, modules: Sequence[BaseIMCModule | None]) -> None:
self.modules = modules
self.names_modules: dict[str, BaseIMCModule] = {}
for module in simple_modules + [active_modifier, advisor]:
for module in modules:
if module: module.imc(self)
def broadcast(self, source: BaseIMCModule, data: object) -> None:
"""
Send data to all modules, other than ourself
"""
source_name = next((k for k, v in self.names_modules.items() if v is source), None)
if source is not self.advisor: self.advisor.imc_data(source, source_name, data, True)
if self.active_modifier and source is not self.active_modifier: self.active_modifier.imc_data(source, source_name, data, True)
for module in [f for f in self.simple_modules if f is not source]: module.imc_data(source, source_name, data, True)
for module in [f for f in self.modules if f is not source]:
if module: module.imc_data(source, source_name, data, True)
def register(self, module: BaseIMCModule, name: str) -> bool:
"""
Register our module with a name, so we can be sent data via the send function

View File

@@ -1,14 +1,8 @@
from modules import InterModuleCommunication
from . import ActiveModifier, log95, Track
import os, subprocess, glob, datetime
from . import ActiveModifier, log95, Track, InterModuleCommunication
import os, glob, datetime
from .advisor import MORNING_START, DAY_END
def get_audio_duration(file_path):
result = subprocess.run(['ffprobe', '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', file_path], capture_output=True, text=True)
if result.returncode == 0: return float(result.stdout.strip())
return None
logger = log95.log95("AC-MOD")
class Module(ActiveModifier):
@@ -62,7 +56,11 @@ class Module(ActiveModifier):
else: self.last_track = track
if self.limit_tracks:
last_track_duration = get_audio_duration(self.last_track.path)
last_track_duration = self._imc.send(self, "procman", {"op": 1, "arg": self.last_track.path})
assert isinstance(last_track_duration, dict)
last_track_duration = last_track_duration.get("arg")
if not last_track_duration: return self.last_track, False
if last_track_duration and last_track_duration > 5*60:
now = datetime.datetime.now()
timestamp = now.timestamp() + last_track_duration

View File

@@ -16,6 +16,20 @@ class Track:
args: dict[str, str] | None
offset: float = 0.0
@dataclass
class Process:
process: Popen
track: str
started_at: float
duration: float
class Skeleton_ProcessManager:
processes: list[Process]
def _get_audio_duration(self, file_path): ...
def play(self, track_path: str, fade_in: bool=False, fade_out: bool=False, fade_time: int=5, offset: float=0.0) -> Process: ...
def anything_playing(self) -> bool: ...
def stop_all(self, timeout: float | None = None) -> None: ...
def wait_all(self, timeout: float | None = None) -> None: ...
class BaseIMCModule:
"""
This is not a module to be used but rather a placeholder IMC api to be used in other modules
@@ -24,13 +38,36 @@ class BaseIMCModule:
"""
Receive an IMC object
"""
pass
self._imc = imc
def imc_data(self, source: 'BaseIMCModule', source_name: str | None, data: object, broadcast: bool) -> object:
"""
React to IMC data
"""
return None
class ProcmanCommunicator(BaseIMCModule):
def __init__(self, procman: Skeleton_ProcessManager) -> None:
self.procman = procman
def imc(self, imc: 'InterModuleCommunication') -> None:
super().imc(imc)
self._imc.register(self, "procman")
def imc_data(self, source: BaseIMCModule, source_name: str | None, data: object, broadcast: bool) -> object:
if broadcast: return
if isinstance(data, str) and data.lower().strip() == "raw": return self.procman
elif isinstance(data, dict):
op = data.get("op")
if not op: return
if int(op) == 0: return {"op": 0, "arg": "pong"}
elif int(op) == 1:
if arg := data.get("arg"):
return {"op": 1, "arg": self.procman._get_audio_duration(arg)}
else: return
elif int(op) == 2:
self.procman.stop_all(data.get("timeout", None))
return {"op": 2}
elif int(op) == 3:
return {"op": 3, "arg": self.procman.processes}
class PlayerModule(BaseIMCModule):
"""
Simple passive observer, this allows you to send the current track the your RDS encoder, or to your website
@@ -46,6 +83,8 @@ class PlayerModule(BaseIMCModule):
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 play it for
Runs at a frequency around 1 Hz
Please don't put any blocking or code that takes time
"""
pass
class PlaylistModifierModule:
@@ -93,19 +132,18 @@ class ActiveModifier(BaseIMCModule):
"""
pass
class InterModuleCommunication:
def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None:
self.advisor = advisor
self.active_modifier = active_modifier
self.simple_modules = simple_modules
def __init__(self, modules: Sequence[BaseIMCModule | None]) -> None:
self.modules = modules
self.names_modules: dict[str, BaseIMCModule] = {}
for module in modules:
if module: module.imc(self)
def broadcast(self, source: BaseIMCModule, data: object) -> None:
"""
Send data to all modules, other than ourself
"""
source_name = next((k for k, v in self.names_modules.items() if v is source), None)
if source is not self.advisor: self.advisor.imc_data(source, source_name, data, True)
if self.active_modifier and source is not self.active_modifier: self.active_modifier.imc_data(source, source_name, data, True)
for module in [f for f in self.simple_modules if f is not source]: module.imc_data(source, source_name, data, True)
for module in [f for f in self.modules if f is not source]:
if module: module.imc_data(source, source_name, data, True)
def register(self, module: BaseIMCModule, name: str) -> bool:
"""
Register our module with a name, so we can be sent data via the send function
@@ -125,4 +163,6 @@ Each module shall have a python script in the modules directory. Each of the mod
- module (list['PlayerModule'] or 'PlayerModule'), this shall be just the list or one passive observer class
- playlistmod ('PlaylistModifierModule', list['PlaylistModifierModule'], tuple['PlaylistModifierModule' | list['PlaylistModifierModule'], int]), module itself, list of modules or the module itself and list of them with an index integer which sets the order of modifiers (0 is first)
- advisor ('PlaylistAdvisor')
- activemod ('ActiveModifier')
- activemod ('ActiveModifier')
NEW! The procman communicator allows you to get the track duration, but also STOP WHATEVER IS PLAYING! That means we can skip tracks WHILE THEY ARE PLAYING

View File

@@ -22,14 +22,7 @@ exit_pending = False
intr_time = 0
exit_lock = threading.Lock()
@dataclass
class Process:
process: subprocess.Popen
track: str
started_at: float
duration: float
class ProcessManager:
class ProcessManager(Skeleton_ProcessManager):
def __init__(self) -> None:
self.lock = threading.Lock()
self.processes: list[Process] = []
@@ -58,7 +51,7 @@ class ProcessManager:
cmd.append(track_path)
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
proc = Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
pr = Process(proc, track_path, time.monotonic(), duration - offset)
with self.lock: self.processes.append(pr)
return pr
@@ -209,7 +202,7 @@ def play_playlist(playlist_path, starting_index: int = 0):
end_time = pr.started_at + ttw
while end_time >= time.monotonic():
while end_time >= time.monotonic() and pr.process.poll() is None:
start = time.monotonic()
for module in simple_modules: module.progress(song_i, track, time.monotonic() - pr.started_at, pr.duration, ttw)
@@ -271,7 +264,7 @@ def main():
logger.critical_error("Playlist advisor was not found")
raise SystemExit(1)
InterModuleCommunication(playlist_advisor, active_modifier, simple_modules)
InterModuleCommunication(simple_modules + [playlist_advisor, ProcmanCommunicator(procman), active_modifier])
logger.info("Starting playback.")