From 88b44ddf32e08e73bf4ce11f5219208b8df6772a Mon Sep 17 00:00:00 2001 From: KubaPro010 Date: Tue, 2 Dec 2025 17:42:11 +0100 Subject: [PATCH] some changes, also change licence to the UNLICENCE! --- .gitignore | 12 ++++----- LICENSE | 37 +++++++++++++++------------- modules/__init__.py | 4 +-- modules/active_modifier.py | 9 +++---- modules/advisor.py | 4 +-- modules/progress.py | 2 +- modules/rds.py | 8 +++--- modules/write_playlists.py | 4 +-- radioPlayer.py | 50 ++++++++++++++++++++------------------ 9 files changed, 67 insertions(+), 63 deletions(-) diff --git a/.gitignore b/.gitignore index 0a19790..cc3d2f1 100644 --- a/.gitignore +++ b/.gitignore @@ -85,31 +85,31 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. -#Pipfile.lock +Pipfile.lock # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. -#uv.lock +uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock +poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock +pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control @@ -165,7 +165,7 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ # Ruff stuff: .ruff_cache/ diff --git a/LICENSE b/LICENSE index 3d0942a..3c577b0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,24 @@ -MIT License +This is free and unencumbered software released into the public domain. -Copyright (c) 2025 Kuba +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. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +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 above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +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. -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 OR COPYRIGHT HOLDERS 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 \ No newline at end of file diff --git a/modules/__init__.py b/modules/__init__.py index fef4dea..ce60604 100644 --- a/modules/__init__.py +++ b/modules/__init__.py @@ -140,14 +140,14 @@ class InterModuleCommunication: def __init__(self, modules: Sequence[BaseIMCModule | None]) -> None: self.modules = modules self.names_modules: dict[str, BaseIMCModule] = {} - for module in modules: + 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) - for module in [f for f in self.modules if f is not source]: + 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: """ diff --git a/modules/active_modifier.py b/modules/active_modifier.py index 13c56d4..f5d31a3 100644 --- a/modules/active_modifier.py +++ b/modules/active_modifier.py @@ -38,7 +38,6 @@ class Module(ActiveModifier): if song.startswith("!"): song = song[1:] official = False - return Path(song).absolute(), official if len(songs): @@ -48,7 +47,7 @@ class Module(ActiveModifier): else: if (index - 1) >= 0: last_track_to_fade_out = self.playlist[index - 1].fade_out else: last_track_to_fade_out = False - + if len(songs) != 0: next_track_to_fade_in = True else: if index + 1 < len(self.playlist) and next_track: next_track_to_fade_in = next_track.fade_in @@ -57,7 +56,7 @@ class Module(ActiveModifier): if not self.originals or self.originals[-1] != track: self.originals.append(track) - with open("/tmp/radioPlayer_toplay", "w") as f: + with open("/tmp/radioPlayer_toplay", "w") as f: f.write('\n'.join(songs)) f.write("\n") @@ -73,14 +72,14 @@ class Module(ActiveModifier): next_track = track self.limit_tracks = False return (self.last_track, next_track), True - elif len(self.originals): + elif len(self.originals): self.last_track = self.originals.pop(0) if len(self.originals): next_track = self.originals[0] else: self.last_track = track self.limit_tracks = self.can_limit_tracks if self.limit_tracks: - last_track_duration = self._imc.send(self, "procman", {"op": 1, "arg": self.last_track.path}) + last_track_duration = self._imc.send(self, "procman", {"op": 1, "arg": self.last_track.path}) # Ask procman for the duration of this file assert isinstance(last_track_duration, dict) last_track_duration = last_track_duration.get("arg") if last_track_duration: diff --git a/modules/advisor.py b/modules/advisor.py index c1b0dc4..4103bb3 100644 --- a/modules/advisor.py +++ b/modules/advisor.py @@ -65,7 +65,7 @@ class Module(PlaylistAdvisor): self.custom_playlist_last_mod = Time.get_playlist_modification_time(self.custom_playlist) return self.custom_playlist elif self.custom_playlist: self.custom_playlist = None - + current_day, current_hour = (time := datetime.datetime.now()).strftime('%A').lower(), time.hour morning_playlist = Path(playlist_dir, current_day, "morning").absolute() @@ -106,7 +106,7 @@ class Module(PlaylistAdvisor): self.last_playlist = night_playlist return self.last_playlist def new_playlist(self) -> bool: - if self.custom_playlist and self.custom_playlist_path.exists(): + if self.custom_playlist and self.custom_playlist_path.exists(): if Time.get_playlist_modification_time(self.custom_playlist) > self.custom_playlist_last_mod: logger.info("Custom playlist changed on disc, reloading...") self.custom_playlist = None diff --git a/modules/progress.py b/modules/progress.py index 108a224..854a8c4 100644 --- a/modules/progress.py +++ b/modules/progress.py @@ -8,7 +8,7 @@ def format_time(seconds) -> str: class Module(PlayerModule): def progress(self, index: int, track: Track, elapsed: float, total: float, real_total: float) -> None: - if track.official: + if track.official: data = f"{track.path.name}: {format_time(elapsed)} / {format_time(total)}\n" Path("/tmp/radioPlayer_progress").write_text(data) diff --git a/modules/rds.py b/modules/rds.py index 57d5e5e..6801780 100644 --- a/modules/rds.py +++ b/modules/rds.py @@ -38,7 +38,7 @@ def update_rds(track_name: str): except KeyError: has_name = False name = track_name.rsplit(".", 1)[0] - + name = re.sub(r'^\s*\d+\s*[-.]?\s*', '', name) if " - " in name: @@ -52,9 +52,9 @@ def update_rds(track_name: str): artist = rds_default_artist title = name if not has_name: logger.warning(f"File does not have a alias in the name table ({track_name})") - + # title = re.sub(r'\s*[\(\[][^\(\)\[\]]*[\)\]]', '', title) # there might be junk - + prt = rds_base.format(artist, title) rtp = [4] # type 1 rtp.append(prt.find(artist)) # start 1 @@ -63,7 +63,7 @@ def update_rds(track_name: str): rtp.append(prt.find(title)) # start 2 rtp.append(len(title) - 1) # len 2 - try: + try: f = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) f.settimeout(1.0) data = f"TEXT={prt}\r\nRTP={rtp}\r\n".encode() diff --git a/modules/write_playlists.py b/modules/write_playlists.py index 51a08fc..d22b7c4 100644 --- a/modules/write_playlists.py +++ b/modules/write_playlists.py @@ -12,7 +12,7 @@ class Module(PlayerModule): def on_new_playlist(self, playlist: list[Track]): 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}) + 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) @@ -22,7 +22,7 @@ class Module(PlayerModule): lines = self.playlist[:index] + [f"> ({track.path})"] + [self.playlist[index]] + self.playlist[index+1:] else: lines = self.playlist[:index] + [f"> {self.playlist[index]}"] + self.playlist[index+1:] with open("/tmp/radioPlayer_playlist", "w") as f: - for line in lines: + for line in lines: try: f.write(line + "\n") except UnicodeEncodeError: print(line.encode('utf-8', errors='ignore').decode('utf-8')) diff --git a/radioPlayer.py b/radioPlayer.py index e528bef..e6cebae 100644 --- a/radioPlayer.py +++ b/radioPlayer.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 import time, types -import os, subprocess, importlib.util +import os, subprocess, importlib.util, importlib.machinery import sys, signal, glob import libcache, traceback, atexit from modules import * from threading import Lock def prefetch(path): - if os.name != "posix": return - with open(path, "rb") as f: - fd = f.fileno() - os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_SEQUENTIAL) - os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_NOREUSE) - os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_WILLNEED) + if os.name == "posix": + with open(path, "rb") as f: + fd = f.fileno() + os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_SEQUENTIAL) + os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_NOREUSE) + os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_WILLNEED) MODULES_PACKAGE = "modules" MODULES_DIR = Path(__file__, "..", MODULES_PACKAGE).resolve() @@ -30,8 +30,8 @@ class ProcessManager(Skeleton_ProcessManager): self.duration_cache.saveElement(file_path.as_posix(), result, (60*60*2), False, True) return result def play(self, track: Track, fade_time: int=5) -> Process: - cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet'] assert track.path.exists() + cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet'] duration = self._get_audio_duration(track.path.absolute()) if not duration: raise Exception("Failed to get file duration for", track.path) @@ -70,7 +70,6 @@ class PlaylistParser: 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}") return [] @@ -123,7 +122,7 @@ class RadioPlayer: self.intr_time = 0 self.exit_lock = Lock() self.procman = ProcessManager() - self.modules: list[tuple] = [] + self.modules: list[tuple[importlib.machinery.ModuleSpec, types.ModuleType, str]] = [] self.parser = PlaylistParser(output) self.arg = arg @@ -132,8 +131,9 @@ class RadioPlayer: def shutdown(self): self.procman.stop_all() [module.shutdown() for module in self.simple_modules if module] + self.logger.output.close() - def handle_sigint(self, signum, frame): + def handle_sigint(self, signum: int, frame: types.FrameType | None): with self.exit_lock: self.logger.info("Received CTRL+C (SIGINT)") if (time.monotonic() - self.intr_time) > 5: @@ -146,14 +146,15 @@ class RadioPlayer: raise SystemExit(130) def load_modules(self): + """Loads the modules into memory""" 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) + module = importlib.util.module_from_spec(spec) if spec else None + assert spec and module sys.modules[full_module_name] = module if MODULES_PACKAGE not in sys.modules: @@ -167,13 +168,14 @@ class RadioPlayer: module.__dict__['_log_out'] = self.logger.output self.modules.append((spec, module, module_name)) def start_modules(self): + """Executes the module by the python interpreter""" for (spec, module, module_name) in self.modules: assert spec.loader try: start = time.perf_counter() spec.loader.exec_module(module) time_took = time.perf_counter() - start - if time_took > 0.2: self.logger.warning(f"{module_name} took {time_took:.1f}s to start") + if time_took > 0.15: self.logger.warning(f"{module_name} took {time_took:.1f}s to start") except Exception as e: traceback.print_exc(file=self.logger.output) self.logger.error(f"Failed loading {module_name} due to {e}, continuing") @@ -199,6 +201,7 @@ class RadioPlayer: if self.active_modifier: self.active_modifier.arguments(self.arg) def start(self): + """Single functon for starting the core, returns but might exit raising an SystemExit""" self.logger.info("Core starting, loading modules") self.load_modules();self.start_modules() if not self.playlist_advisor: @@ -206,11 +209,11 @@ class RadioPlayer: raise SystemExit(1) def play_once(self): + """Plays a single playlist""" if not self.playlist_advisor or not (playlist_path := self.playlist_advisor.advise(self.arg)): return try: global_args, parsed = self.parser.parse(playlist_path) except Exception as e: - self.logger.info(f"Exception ({e}) while parsing playlist, retrying in 15 seconds...") - traceback.print_exc(file=self.logger.output) + self.logger.info(f"Exception ({e}) while parsing playlist, retrying in 15 seconds...");traceback.print_exc(file=self.logger.output) time.sleep(15) return @@ -218,6 +221,7 @@ class RadioPlayer: [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 := module.modify(global_args, playlist) or playlist) for module in self.playlist_modifier_modules if module] # yep + assert len(playlist) prefetch(playlist[0].path) [mod.on_new_playlist(playlist) for mod in self.simple_modules + [self.active_modifier] if mod] # one liner'd everything @@ -260,12 +264,12 @@ class RadioPlayer: return True return False + track, next_track, extend = get_track() while i < max_iterator: if check_conditions(): return - if not track: track, next_track, extend = get_track() - prefetch(track.path) self.logger.info(f"Now playing: {track.path.name}") + prefetch(track.path) [module.on_new_track(song_i, track, next_track) for module in self.simple_modules if module] @@ -288,23 +292,21 @@ class RadioPlayer: prefetch(track.path) def loop(self): - self.logger.info("Starting playback.") + """Main loop of the player. This does not return and may or not raise an SystemExit""" try: while True: self.play_once() if self.exit_pending: raise SystemExit(self.exit_status_code) - except Exception as e: + except Exception: traceback.print_exc(file=self.logger.output) raise def main(): log_file_path = Path("/tmp/radioPlayer_log") log_file_path.touch() - log_file = open(log_file_path, "w") - core = RadioPlayer((" ".join(sys.argv[1:]) if len(sys.argv) > 1 else None), log_file) + core = RadioPlayer((" ".join(sys.argv[1:]) if len(sys.argv) > 1 else None), open(log_file_path, "w")) atexit.register(core.shutdown) core.start() signal.signal(signal.SIGINT, core.handle_sigint) - try: core.loop() - finally: log_file.close() + core.loop()