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 import log95
from collections.abc import Sequence
from subprocess import Popen
from dataclasses import dataclass from dataclasses import dataclass
@dataclass @dataclass
@@ -10,6 +12,20 @@ class Track:
args: dict[str, str] | None args: dict[str, str] | None
offset: float = 0.0 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: class BaseIMCModule:
""" """
This is not a module to be used but rather a placeholder IMC api to be used in other modules 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 Receive an IMC object
""" """
pass self._imc = imc
def imc_data(self, source: 'BaseIMCModule', source_name: str | None, data: object, broadcast: bool) -> object: def imc_data(self, source: 'BaseIMCModule', source_name: str | None, data: object, broadcast: bool) -> object:
""" """
React to IMC data React to IMC data
""" """
return None 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): class PlayerModule(BaseIMCModule):
""" """
Simple passive observer, this allows you to send the current track the your RDS encoder, or to your website 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 pass
class InterModuleCommunication: class InterModuleCommunication:
def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None: def __init__(self, modules: Sequence[BaseIMCModule | None]) -> None:
self.advisor = advisor self.modules = modules
self.active_modifier = active_modifier
self.simple_modules = simple_modules
self.names_modules: dict[str, BaseIMCModule] = {} self.names_modules: dict[str, BaseIMCModule] = {}
for module in simple_modules + [active_modifier, advisor]: for module in modules:
if module: module.imc(self) if module: module.imc(self)
def broadcast(self, source: BaseIMCModule, data: object) -> None: def broadcast(self, source: BaseIMCModule, data: object) -> None:
""" """
Send data to all modules, other than ourself 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) 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) for module in [f for f in self.modules if f is not source]:
if self.active_modifier and source is not self.active_modifier: self.active_modifier.imc_data(source, source_name, data, True) if module: module.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)
def register(self, module: BaseIMCModule, name: str) -> bool: def register(self, module: BaseIMCModule, name: str) -> bool:
""" """
Register our module with a name, so we can be sent data via the send function 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, InterModuleCommunication
from . import ActiveModifier, log95, Track import os, glob, datetime
import os, subprocess, glob, datetime
from .advisor import MORNING_START, DAY_END 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") logger = log95.log95("AC-MOD")
class Module(ActiveModifier): class Module(ActiveModifier):
@@ -62,7 +56,11 @@ class Module(ActiveModifier):
else: self.last_track = track else: self.last_track = track
if self.limit_tracks: 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: if last_track_duration and last_track_duration > 5*60:
now = datetime.datetime.now() now = datetime.datetime.now()
timestamp = now.timestamp() + last_track_duration timestamp = now.timestamp() + last_track_duration

View File

@@ -16,6 +16,20 @@ class Track:
args: dict[str, str] | None args: dict[str, str] | None
offset: float = 0.0 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: class BaseIMCModule:
""" """
This is not a module to be used but rather a placeholder IMC api to be used in other modules 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 Receive an IMC object
""" """
pass self._imc = imc
def imc_data(self, source: 'BaseIMCModule', source_name: str | None, data: object, broadcast: bool) -> object: def imc_data(self, source: 'BaseIMCModule', source_name: str | None, data: object, broadcast: bool) -> object:
""" """
React to IMC data React to IMC data
""" """
return None 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): class PlayerModule(BaseIMCModule):
""" """
Simple passive observer, this allows you to send the current track the your RDS encoder, or to your website 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: 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 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 pass
class PlaylistModifierModule: class PlaylistModifierModule:
@@ -93,19 +132,18 @@ class ActiveModifier(BaseIMCModule):
""" """
pass pass
class InterModuleCommunication: class InterModuleCommunication:
def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None: def __init__(self, modules: Sequence[BaseIMCModule | None]) -> None:
self.advisor = advisor self.modules = modules
self.active_modifier = active_modifier
self.simple_modules = simple_modules
self.names_modules: dict[str, BaseIMCModule] = {} self.names_modules: dict[str, BaseIMCModule] = {}
for module in modules:
if module: module.imc(self)
def broadcast(self, source: BaseIMCModule, data: object) -> None: def broadcast(self, source: BaseIMCModule, data: object) -> None:
""" """
Send data to all modules, other than ourself 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) 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) for module in [f for f in self.modules if f is not source]:
if self.active_modifier and source is not self.active_modifier: self.active_modifier.imc_data(source, source_name, data, True) if module: module.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)
def register(self, module: BaseIMCModule, name: str) -> bool: def register(self, module: BaseIMCModule, name: str) -> bool:
""" """
Register our module with a name, so we can be sent data via the send function 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 - 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) - 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') - 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 intr_time = 0
exit_lock = threading.Lock() exit_lock = threading.Lock()
@dataclass class ProcessManager(Skeleton_ProcessManager):
class Process:
process: subprocess.Popen
track: str
started_at: float
duration: float
class ProcessManager:
def __init__(self) -> None: def __init__(self) -> None:
self.lock = threading.Lock() self.lock = threading.Lock()
self.processes: list[Process] = [] self.processes: list[Process] = []
@@ -58,7 +51,7 @@ class ProcessManager:
cmd.append(track_path) 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) pr = Process(proc, track_path, time.monotonic(), duration - offset)
with self.lock: self.processes.append(pr) with self.lock: self.processes.append(pr)
return pr return pr
@@ -209,7 +202,7 @@ def play_playlist(playlist_path, starting_index: int = 0):
end_time = pr.started_at + ttw 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() start = time.monotonic()
for module in simple_modules: module.progress(song_i, track, time.monotonic() - pr.started_at, pr.duration, ttw) 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") logger.critical_error("Playlist advisor was not found")
raise SystemExit(1) raise SystemExit(1)
InterModuleCommunication(playlist_advisor, active_modifier, simple_modules) InterModuleCommunication(simple_modules + [playlist_advisor, ProcmanCommunicator(procman), active_modifier])
logger.info("Starting playback.") logger.info("Starting playback.")