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

some changes, also change licence to the UNLICENCE!

This commit is contained in:
2025-12-02 17:42:11 +01:00
parent 75802ccef4
commit 88b44ddf32
9 changed files with 67 additions and 63 deletions

12
.gitignore vendored
View File

@@ -85,31 +85,31 @@ ipython_config.py
# pyenv # pyenv
# For a library or package, you might want to ignore these files since the code is # 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: # intended to run in multiple environments; otherwise, check them in:
# .python-version .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # 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 # 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 # having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock Pipfile.lock
# UV # UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # 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 # This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries. # commonly ignored for libraries.
#uv.lock uv.lock
# poetry # poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # 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 # This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries. # commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock poetry.lock
# pdm # pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. # 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 # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control. # in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-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 # 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 # 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. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ .idea/
# Ruff stuff: # Ruff stuff:
.ruff_cache/ .ruff_cache/

37
LICENSE
View File

@@ -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 In jurisdictions that recognize copyright laws, the author or authors
of this software and associated documentation files (the "Software"), to deal of this software dedicate any and all copyright interest in the
in the Software without restriction, including without limitation the rights software to the public domain. We make this dedication for the benefit
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell of the public at large and to the detriment of our heirs and
copies of the Software, and to permit persons to whom the Software is successors. We intend this dedication to be an overt act of
furnished to do so, subject to the following conditions: 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 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
copies or substantial portions of the Software. 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 For more information, please refer to <https://unlicense.org>
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.

View File

@@ -38,7 +38,6 @@ class Module(ActiveModifier):
if song.startswith("!"): if song.startswith("!"):
song = song[1:] song = song[1:]
official = False official = False
return Path(song).absolute(), official return Path(song).absolute(), official
if len(songs): if len(songs):
@@ -80,7 +79,7 @@ class Module(ActiveModifier):
self.limit_tracks = self.can_limit_tracks self.limit_tracks = self.can_limit_tracks
if self.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) assert isinstance(last_track_duration, dict)
last_track_duration = last_track_duration.get("arg") last_track_duration = last_track_duration.get("arg")
if last_track_duration: if last_track_duration:

View File

@@ -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 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: def progress(self, index: int, track: Track, elapsed: float, total: float, real_total: float) -> None:
if os.path.exists("/tmp/radioPlayer_skip"): 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") os.remove("/tmp/radioPlayer_skip")
def on_new_track(self, index: int, track: Track, next_track: Track | None): def on_new_track(self, index: int, track: Track, next_track: Track | None):
if next_track: logger.info("Next up:", next_track.path.name) if next_track: logger.info("Next up:", next_track.path.name)

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import time, types import time, types
import os, subprocess, importlib.util import os, subprocess, importlib.util, importlib.machinery
import sys, signal, glob import sys, signal, glob
import libcache, traceback, atexit import libcache, traceback, atexit
from modules import * from modules import *
from threading import Lock from threading import Lock
def prefetch(path): def prefetch(path):
if os.name != "posix": return if os.name == "posix":
with open(path, "rb") as f: with open(path, "rb") as f:
fd = f.fileno() fd = f.fileno()
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_SEQUENTIAL) os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_SEQUENTIAL)
@@ -30,8 +30,8 @@ class ProcessManager(Skeleton_ProcessManager):
self.duration_cache.saveElement(file_path.as_posix(), result, (60*60*2), False, True) self.duration_cache.saveElement(file_path.as_posix(), result, (60*60*2), False, True)
return result return result
def play(self, track: Track, fade_time: int=5) -> Process: def play(self, track: Track, fade_time: int=5) -> Process:
cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet']
assert track.path.exists() assert track.path.exists()
cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet']
duration = self._get_audio_duration(track.path.absolute()) duration = self._get_audio_duration(track.path.absolute())
if not duration: raise Exception("Failed to get file duration for", track.path) 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]: def _check_for_imports(self, path: Path, seen=None) -> list[str]:
if seen is None: seen = set() if seen is None: seen = set()
if not path.exists(): if not path.exists():
self.logger.error(f"Playlist not found: {path.name}") self.logger.error(f"Playlist not found: {path.name}")
return [] return []
@@ -123,7 +122,7 @@ class RadioPlayer:
self.intr_time = 0 self.intr_time = 0
self.exit_lock = Lock() self.exit_lock = Lock()
self.procman = ProcessManager() self.procman = ProcessManager()
self.modules: list[tuple] = [] self.modules: list[tuple[importlib.machinery.ModuleSpec, types.ModuleType, str]] = []
self.parser = PlaylistParser(output) self.parser = PlaylistParser(output)
self.arg = arg self.arg = arg
@@ -132,8 +131,9 @@ class RadioPlayer:
def shutdown(self): def shutdown(self):
self.procman.stop_all() self.procman.stop_all()
[module.shutdown() for module in self.simple_modules if module] [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: with self.exit_lock:
self.logger.info("Received CTRL+C (SIGINT)") self.logger.info("Received CTRL+C (SIGINT)")
if (time.monotonic() - self.intr_time) > 5: if (time.monotonic() - self.intr_time) > 5:
@@ -146,14 +146,15 @@ class RadioPlayer:
raise SystemExit(130) raise SystemExit(130)
def load_modules(self): def load_modules(self):
"""Loads the modules into memory"""
for file in MODULES_DIR.glob("*"): for file in MODULES_DIR.glob("*"):
if file.name.endswith(".py") and file.name != "__init__.py": if file.name.endswith(".py") and file.name != "__init__.py":
module_name = file.name[:-3] module_name = file.name[:-3]
full_module_name = f"{MODULES_PACKAGE}.{module_name}" full_module_name = f"{MODULES_PACKAGE}.{module_name}"
spec = importlib.util.spec_from_file_location(full_module_name, Path(MODULES_DIR, file)) spec = importlib.util.spec_from_file_location(full_module_name, Path(MODULES_DIR, file))
assert spec module = importlib.util.module_from_spec(spec) if spec else None
module = importlib.util.module_from_spec(spec) assert spec and module
sys.modules[full_module_name] = module sys.modules[full_module_name] = module
if MODULES_PACKAGE not in sys.modules: if MODULES_PACKAGE not in sys.modules:
@@ -167,13 +168,14 @@ class RadioPlayer:
module.__dict__['_log_out'] = self.logger.output module.__dict__['_log_out'] = self.logger.output
self.modules.append((spec, module, module_name)) self.modules.append((spec, module, module_name))
def start_modules(self): def start_modules(self):
"""Executes the module by the python interpreter"""
for (spec, module, module_name) in self.modules: for (spec, module, module_name) in self.modules:
assert spec.loader assert spec.loader
try: try:
start = time.perf_counter() start = time.perf_counter()
spec.loader.exec_module(module) spec.loader.exec_module(module)
time_took = time.perf_counter() - start 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: except Exception as e:
traceback.print_exc(file=self.logger.output) traceback.print_exc(file=self.logger.output)
self.logger.error(f"Failed loading {module_name} due to {e}, continuing") 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) if self.active_modifier: self.active_modifier.arguments(self.arg)
def start(self): def start(self):
"""Single functon for starting the core, returns but might exit raising an SystemExit"""
self.logger.info("Core starting, loading modules") self.logger.info("Core starting, loading modules")
self.load_modules();self.start_modules() self.load_modules();self.start_modules()
if not self.playlist_advisor: if not self.playlist_advisor:
@@ -206,11 +209,11 @@ class RadioPlayer:
raise SystemExit(1) raise SystemExit(1)
def play_once(self): def play_once(self):
"""Plays a single playlist"""
if not self.playlist_advisor or not (playlist_path := self.playlist_advisor.advise(self.arg)): return 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) try: global_args, parsed = self.parser.parse(playlist_path)
except Exception as e: except Exception as e:
self.logger.info(f"Exception ({e}) while parsing playlist, retrying in 15 seconds...") self.logger.info(f"Exception ({e}) while parsing playlist, retrying in 15 seconds...");traceback.print_exc(file=self.logger.output)
traceback.print_exc(file=self.logger.output)
time.sleep(15) time.sleep(15)
return 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.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 [(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) 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 self.simple_modules + [self.active_modifier] if mod] # one liner'd everything
@@ -260,12 +264,12 @@ class RadioPlayer:
return True return True
return False return False
track, next_track, extend = get_track()
while i < max_iterator: while i < max_iterator:
if check_conditions(): return 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}") 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] [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) prefetch(track.path)
def loop(self): 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: try:
while True: while True:
self.play_once() self.play_once()
if self.exit_pending: raise SystemExit(self.exit_status_code) if self.exit_pending: raise SystemExit(self.exit_status_code)
except Exception as e: except Exception:
traceback.print_exc(file=self.logger.output) traceback.print_exc(file=self.logger.output)
raise raise
def main(): def main():
log_file_path = Path("/tmp/radioPlayer_log") log_file_path = Path("/tmp/radioPlayer_log")
log_file_path.touch() 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) atexit.register(core.shutdown)
core.start() core.start()
signal.signal(signal.SIGINT, core.handle_sigint) signal.signal(signal.SIGINT, core.handle_sigint)
try: core.loop() core.loop()
finally: log_file.close()