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

this is a beast

This commit is contained in:
KubaPro010
2025-11-12 22:05:37 +01:00
parent 76325d0dfa
commit 5fd56bfa41
5 changed files with 207 additions and 209 deletions

View File

@@ -2,10 +2,10 @@ from . import ActiveModifier, log95, Track, Path
import os, glob, datetime import os, glob, datetime
from typing import TextIO from typing import TextIO
_log_file: TextIO _log_out: TextIO
assert _log_file # pyright: ignore[reportUnboundVariable] assert _log_out # pyright: ignore[reportUnboundVariable]
logger = log95.log95("AC-MOD", output=_log_file) logger = log95.log95("AC-MOD", output=_log_out)
class Module(ActiveModifier): class Module(ActiveModifier):
def __init__(self) -> None: def __init__(self) -> None:

View File

@@ -10,10 +10,10 @@ from . import PlaylistAdvisor, log95, Path
import os, datetime import os, datetime
from typing import TextIO from typing import TextIO
_log_file: TextIO _log_out: TextIO
assert _log_file # pyright: ignore[reportUnboundVariable] assert _log_out # pyright: ignore[reportUnboundVariable]
logger = log95.log95("ADVISOR", output=_log_file) logger = log95.log95("ADVISOR", output=_log_out)
playlist_dir = Path("/home/user/playlists") playlist_dir = Path("/home/user/playlists")

View File

@@ -11,11 +11,11 @@ rds_default_artist = "radio95"
udp_host = ("127.0.0.1", 5000) udp_host = ("127.0.0.1", 5000)
from typing import TextIO from typing import TextIO
_log_file: TextIO _log_out: TextIO
logger_level = log95.log95Levels.DEBUG if DEBUG else log95.log95Levels.CRITICAL_ERROR logger_level = log95.log95Levels.DEBUG if DEBUG else log95.log95Levels.CRITICAL_ERROR
assert _log_file # pyright: ignore[reportUnboundVariable] assert _log_out # pyright: ignore[reportUnboundVariable]
logger = log95.log95("RDS-MODULE", logger_level, output=_log_file) logger = log95.log95("RDS-MODULE", logger_level, output=_log_out)
def load_dict_from_custom_format(file_path: str) -> dict[str, str]: def load_dict_from_custom_format(file_path: str) -> dict[str, str]:
try: try:

View File

@@ -2,10 +2,10 @@ from . import PlayerModule, log95, Track
import os import os
from typing import TextIO from typing import TextIO
_log_file: TextIO _log_out: TextIO
assert _log_file # pyright: ignore[reportUnboundVariable] assert _log_out # pyright: ignore[reportUnboundVariable]
logger = log95.log95("PlayView", output=_log_file) logger = log95.log95("PlayView", output=_log_out)
class Module(PlayerModule): class Module(PlayerModule):
def __init__(self) -> None: def __init__(self) -> None:

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import time import time
import os, subprocess, importlib.util, types import os, subprocess, importlib.util, types
import sys, signal, threading, glob import sys, signal, threading, glob
import libcache, traceback import libcache, traceback, atexit
from modules import * from modules import *
def prefetch(path): def prefetch(path):
@@ -13,25 +14,9 @@ def prefetch(path):
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_NOREUSE) os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_NOREUSE)
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_WILLNEED) os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_WILLNEED)
simple_modules: list[PlayerModule] = []
playlist_modifier_modules: list[PlaylistModifierModule] = []
playlist_advisor: PlaylistAdvisor | None = None
active_modifier: ActiveModifier | None = None
MODULES_PACKAGE = "modules" MODULES_PACKAGE = "modules"
MODULES_DIR = Path(__file__, "..", MODULES_PACKAGE).resolve() MODULES_DIR = Path(__file__, "..", MODULES_PACKAGE).resolve()
log_file_path = Path("/tmp/radioPlayer_log")
if log_file_path.exists(): log_file_path.unlink()
log_file_path.touch()
log_file = open(log_file_path, "w")
logger = log95.log95("CORE", output=log_file)
exit_pending = False
exit_status_code = 0
intr_time = 0
exit_lock = threading.Lock()
class ProcessManager(Skeleton_ProcessManager): class ProcessManager(Skeleton_ProcessManager):
def __init__(self) -> None: def __init__(self) -> None:
self.lock = threading.Lock() self.lock = threading.Lock()
@@ -83,49 +68,33 @@ class ProcessManager(Skeleton_ProcessManager):
except subprocess.TimeoutExpired: process.process.terminate() except subprocess.TimeoutExpired: process.process.terminate()
self.processes.clear() self.processes.clear()
procman = ProcessManager() class PlaylistParser:
def __init__(self, output: log95.TextIO) -> types.NoneType: self.logger = log95.log95("PARSER", output=output)
def handle_sigint(signum, frame): def load_filelines(self, path: Path):
global exit_pending, intr_time, exit_status_code
with exit_lock:
logger.info("Received SIGINT")
if (time.monotonic() - intr_time) > 5:
intr_time = time.monotonic()
logger.info("Will quit on song end.")
exit_pending = True
exit_status_code = 130
else:
logger.warning("Force-Quit pending")
procman.stop_all()
raise SystemExit(130)
signal.signal(signal.SIGINT, handle_sigint)
def load_filelines(path: Path):
try: try:
return [line.strip() for line in path.read_text().splitlines() if line.strip()] return [line.strip() for line in path.read_text().splitlines() if line.strip()]
except FileNotFoundError: except FileNotFoundError:
logger.error(f"Playlist not found: {path.name}") self.logger.error(f"Playlist not found: {path.name}")
return [] return []
def parse_playlistfile(playlist_path: Path) -> tuple[dict[str, str], list[tuple[list[str], dict[str, str]]]]: def _check_for_imports(self, path: Path, seen=None) -> list[str]:
lines = load_filelines(playlist_path)
def check_for_imports(lines: list[str], seen=None) -> list[str]:
if seen is None: seen = set() if seen is None: seen = set()
lines = self.load_filelines(path)
out = [] out = []
for line in lines: for line in lines:
line = line.strip()
if line.startswith("@"): if line.startswith("@"):
target = Path(line.removeprefix("@")) target = Path(line.removeprefix("@"))
if target not in seen: if target not in seen:
if not target.exists(): if not target.exists():
logger.error(f"Target {target.name} of {playlist_path.name} does not exist") self.logger.error(f"Target {target.name} of {path.name} does not exist")
continue continue
seen.add(target) seen.add(target)
out.extend(check_for_imports(load_filelines(target), seen)) out.extend(self._check_for_imports(target, seen))
else: out.append(line) else: out.append(line)
return out return out
lines = check_for_imports(lines) # First, import everything
def parse_playlistfile(self, playlist_path: Path) -> tuple[dict[str, str], list[tuple[list[str], dict[str, str]]]]:
lines = self._check_for_imports(playlist_path) # First, import everything
out = [] out = []
global_arguments = {} global_arguments = {}
for line in lines: for line in lines:
@@ -147,23 +116,108 @@ def parse_playlistfile(playlist_path: Path) -> tuple[dict[str, str], list[tuple[
out.append(([f for f in glob.glob(line) if os.path.isfile(f)], arguments)) out.append(([f for f in glob.glob(line) if os.path.isfile(f)], arguments))
return global_arguments, out return global_arguments, out
def play_playlist(playlist_path: Path, starting_index: int = 0): class RadioPlayer:
assert playlist_advisor def __init__(self, output: log95.TextIO):
self.simple_modules: list[PlayerModule] = []
self.playlist_modifier_modules: list[PlaylistModifierModule] = []
self.playlist_advisor: PlaylistAdvisor | None = None
self.active_modifier: ActiveModifier | None = None
self.logger = log95.log95("CORE", output=output)
self.exit_pending = False
self.exit_status_code = 0
self.intr_time = 0
self.exit_lock = threading.Lock()
self.procman = ProcessManager()
self.modules: list[tuple] = []
self.parser = PlaylistParser(output)
def shutdown(self): self.procman.stop_all()
def handle_sigint(self, signum, frame):
with self.exit_lock:
self.logger.info("Received CTRL+C (SIGINT)")
if (time.monotonic() - self.intr_time) > 5:
self.intr_time = time.monotonic()
self.logger.info("Will quit on song end.")
self.exit_pending = True
self.exit_status_code = 130
else:
self.logger.warning("Force-Quit pending")
raise SystemExit(130)
try: global_args, parsed = parse_playlistfile(playlist_path) def load_modules(self):
for file in MODULES_DIR.glob("*"):
if file.name.endswith(".py") and file.name != "__init__.py":
module_name = file.name[:-3]
full_module_name = f"{MODULES_PACKAGE}.{module_name}"
spec = importlib.util.spec_from_file_location(full_module_name, Path(MODULES_DIR, file))
assert spec
module = importlib.util.module_from_spec(spec)
sys.modules[full_module_name] = module
if MODULES_PACKAGE not in sys.modules:
parent = types.ModuleType(MODULES_PACKAGE)
parent.__path__ = [str(MODULES_DIR)]
parent.__package__ = MODULES_PACKAGE
sys.modules[MODULES_PACKAGE] = parent
module.__package__ = MODULES_PACKAGE
module._log_out = self.logger.output # type: ignore
module.__dict__['_log_out'] = self.logger.output
self.modules.append((spec, module, module_name))
def start_modules(self):
for (spec, module, module_name) in self.modules:
assert spec.loader
try:
start = time.monotonic()
time.perf_counter()
spec.loader.exec_module(module)
time_took = time.monotonic() - start
if time_took > 0.2: self.logger.warning(f"{module_name} took {time_took:.2f}s to start")
except Exception as e: except Exception as e:
logger.info(f"Exception ({e}) while parsing playlist, retrying in 15 seconds...") traceback.print_exc(file=self.logger.output)
self.logger.error(f"Failed loading {module_name} due to {e}, continuing")
continue
if md := getattr(module, "module", None):
if isinstance(md, list): self.simple_modules.extend(md)
else: self.simple_modules.append(md)
if md := getattr(module, "playlistmod", None):
if isinstance(md, tuple):
md, index = md
if isinstance(md, list): self.playlist_modifier_modules[index:index] = md
else: self.playlist_modifier_modules.insert(index, md)
elif isinstance(md, list): self.playlist_modifier_modules.extend(md)
else: self.playlist_modifier_modules.append(md)
if md := getattr(module, "advisor", None):
if self.playlist_advisor: raise Exception("Multiple playlist advisors")
self.playlist_advisor = md
if md := getattr(module, "activemod", None):
if self.active_modifier: raise Exception("Multiple active modifiers")
self.active_modifier = md
InterModuleCommunication(self.simple_modules + [self.playlist_advisor, ProcmanCommunicator(self.procman), self.active_modifier])
def start(self):
self.logger.info("Core starting, loading modules")
self.load_modules();self.start_modules()
if not self.playlist_advisor:
self.logger.critical_error("Playlist advisor was not found")
raise SystemExit(1)
def play_playlist(self, playlist_path: Path, starting_index: int = 0):
assert self.playlist_advisor
try: global_args, parsed = self.parser.parse_playlistfile(playlist_path)
except Exception as e:
self.logger.info(f"Exception ({e}) while parsing playlist, retrying in 15 seconds...")
time.sleep(15) time.sleep(15)
return return
playlist: list[Track] = [] playlist: list[Track] = []
[playlist.extend(Track(Path(line).absolute(), True, True, True, args) for line in lns) for (lns, args) in parsed] # i can read this, i think [playlist.extend(Track(Path(line).absolute(), True, True, True, args) for line in lns) for (lns, args) in parsed] # i can read this, i think
for module in playlist_modifier_modules: playlist = module.modify(global_args, playlist) or playlist # id one liner this but the assignement is stopping me for module in self.playlist_modifier_modules: playlist = module.modify(global_args, playlist) or playlist # id one liner this but the assignement is stopping me
prefetch(playlist[0].path) prefetch(playlist[0].path)
[mod.on_new_playlist(playlist) for mod in self.simple_modules + [self.active_modifier] if mod] # one liner'd everything
[mod.on_new_playlist(playlist) for mod in simple_modules + [active_modifier] if mod] # one liner'd everything
return_pending = False return_pending = False
@@ -173,24 +227,23 @@ def play_playlist(playlist_path: Path, starting_index: int = 0):
song_i = i = starting_index song_i = i = starting_index
while i < max_iterator: while i < max_iterator:
if exit_pending: if self.exit_pending:
logger.info("Quit received, waiting for song end.") self.logger.info("Quit received, waiting for song end.")
procman.wait_all() self.procman.wait_all()
raise SystemExit(exit_status_code) raise SystemExit(self.exit_status_code)
elif return_pending: elif return_pending:
logger.info("Return reached, next song will reload the playlist.") self.logger.info("Return reached, next song will reload the playlist.")
procman.wait_all() self.procman.wait_all()
return return
if self.playlist_advisor.new_playlist():
if playlist_advisor.new_playlist(): self.logger.info("Reloading now...")
logger.info("Reloading now...")
return_pending = True return_pending = True
continue continue
track = playlist[song_i % len(playlist)] track = playlist[song_i % len(playlist)]
next_track = playlist[song_i + 1] if song_i + 1 < len(playlist) else None next_track = playlist[song_i + 1] if song_i + 1 < len(playlist) else None
if active_modifier: if self.active_modifier:
(track, next_track), extend = active_modifier.play(song_i, track, next_track) (track, next_track), extend = self.active_modifier.play(song_i, track, next_track)
if track is None: if track is None:
song_i += 1 song_i += 1
continue continue
@@ -198,104 +251,49 @@ def play_playlist(playlist_path: Path, starting_index: int = 0):
else: extend = False else: extend = False
prefetch(track.path) prefetch(track.path)
self.logger.info(f"Now playing: {track.path.name}")
logger.info(f"Now playing: {track.path.name}") for module in self.simple_modules: module.on_new_track(song_i, track, next_track)
for module in simple_modules: module.on_new_track(song_i, track, next_track)
pr = procman.play(track, cross_fade)
pr = self.procman.play(track, cross_fade)
ttw = pr.duration ttw = pr.duration
if track.fade_out: ttw -= cross_fade if track.fade_out: ttw -= cross_fade
end_time = pr.started_at + ttw end_time = pr.started_at + ttw
if next_track: prefetch(next_track.path) if next_track: prefetch(next_track.path)
while end_time >= time.monotonic() and pr.process.poll() is None: while end_time >= time.monotonic() and pr.process.poll() is None:
start = time.monotonic() start = time.monotonic()
[module.progress(song_i, track, time.monotonic() - pr.started_at, pr.duration, ttw) for module in self.simple_modules if module]
[module.progress(song_i, track, time.monotonic() - pr.started_at, pr.duration, ttw) for module in simple_modules if module]
elapsed = time.monotonic() - start elapsed = time.monotonic() - start
remaining_until_end = end_time - time.monotonic() remaining_until_end = end_time - time.monotonic()
if elapsed < 1 and remaining_until_end > 0: time.sleep(min(1 - elapsed, remaining_until_end)) if elapsed < 1 and remaining_until_end > 0: time.sleep(min(1 - elapsed, remaining_until_end))
if next_track: prefetch(next_track.path) if next_track: prefetch(next_track.path)
i += 1 i += 1
if not extend: song_i += 1 if not extend: song_i += 1
def main(): def loop(self):
logger.info("Core is starting, loading modules") assert self.playlist_advisor
global playlist_advisor, active_modifier self.logger.info("Starting playback.")
modules: list[tuple] = []
for file in MODULES_DIR.glob("*"):
if file.name.endswith(".py") and file.name != "__init__.py":
module_name = file.name[:-3]
full_module_name = f"{MODULES_PACKAGE}.{module_name}"
spec = importlib.util.spec_from_file_location(full_module_name, Path(MODULES_DIR, file))
if not spec: continue
module = importlib.util.module_from_spec(spec)
sys.modules[full_module_name] = module
if MODULES_PACKAGE not in sys.modules:
parent = types.ModuleType(MODULES_PACKAGE)
parent.__path__ = [str(MODULES_DIR)]
parent.__package__ = MODULES_PACKAGE
sys.modules[MODULES_PACKAGE] = parent
module.__package__ = MODULES_PACKAGE
module._log_file = log_file # type: ignore
module.__dict__['_log_file'] = log_file
modules.append((spec, module, module_name))
for (spec, module, module_name) in modules:
if not spec.loader: continue
try: spec.loader.exec_module(module)
except Exception as e:
traceback.print_exc()
logger.error(f"Failed loading {module_name} due to {e}")
continue
if md := getattr(module, "module", None):
if isinstance(md, list): simple_modules.extend(md)
else: simple_modules.append(md)
if md := getattr(module, "playlistmod", None):
if isinstance(md, tuple):
md, index = md
if isinstance(md, list): playlist_modifier_modules[index:index] = md
else: playlist_modifier_modules.insert(index, md)
elif isinstance(md, list): playlist_modifier_modules.extend(md)
else: playlist_modifier_modules.append(md)
if md := getattr(module, "advisor", None):
if playlist_advisor: raise Exception("Multiple playlist advisors")
playlist_advisor = md
if md := getattr(module, "activemod", None):
if active_modifier: raise Exception("Multiple active modifiers")
active_modifier = md
if not playlist_advisor:
logger.critical_error("Playlist advisor was not found")
raise SystemExit(1)
InterModuleCommunication(simple_modules + [playlist_advisor, ProcmanCommunicator(procman), active_modifier])
logger.info("Starting playback.")
try: try:
arg = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else None arg = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else None
if active_modifier: active_modifier.arguments(arg) if self.active_modifier: self.active_modifier.arguments(arg)
while True: while True:
if playlist := playlist_advisor.advise(arg): if playlist := self.playlist_advisor.advise(arg): self.play_playlist(playlist)
logger.info(f"Advisor picked '{playlist}' to play") if self.exit_pending: raise SystemExit(self.exit_status_code)
play_playlist(playlist)
if exit_pending: raise SystemExit(exit_status_code)
except Exception as e: except Exception as e:
logger.critical_error(f"Unexpected error: {e}") self.logger.critical_error(f"Unexpected error: {e}")
raise raise
finally:
procman.stop_all() def main():
log_file.close() log_file_path = Path("/tmp/radioPlayer_log")
log_file_path.touch()
log_file = open(log_file_path, "w")
core = RadioPlayer(log_file)
atexit.register(core.shutdown)
core.start()
signal.signal(signal.SIGINT, core.handle_sigint)
try: core.loop()
finally: log_file.close()