You've already forked RadioPlayer
mirror of
https://github.com/radio95-rnt/RadioPlayer.git
synced 2026-02-26 13:52:00 +01:00
some changes, also change licence to the UNLICENCE!
This commit is contained in:
12
.gitignore
vendored
12
.gitignore
vendored
@@ -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
37
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
|
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.
|
|
||||||
@@ -140,14 +140,14 @@ class InterModuleCommunication:
|
|||||||
def __init__(self, modules: Sequence[BaseIMCModule | None]) -> None:
|
def __init__(self, modules: Sequence[BaseIMCModule | None]) -> None:
|
||||||
self.modules = modules
|
self.modules = modules
|
||||||
self.names_modules: dict[str, BaseIMCModule] = {}
|
self.names_modules: dict[str, BaseIMCModule] = {}
|
||||||
for module in modules:
|
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)
|
||||||
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)
|
if module: module.imc_data(source, source_name, data, True)
|
||||||
def register(self, module: BaseIMCModule, name: str) -> bool:
|
def register(self, module: BaseIMCModule, name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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):
|
||||||
@@ -48,7 +47,7 @@ class Module(ActiveModifier):
|
|||||||
else:
|
else:
|
||||||
if (index - 1) >= 0: last_track_to_fade_out = self.playlist[index - 1].fade_out
|
if (index - 1) >= 0: last_track_to_fade_out = self.playlist[index - 1].fade_out
|
||||||
else: last_track_to_fade_out = False
|
else: last_track_to_fade_out = False
|
||||||
|
|
||||||
if len(songs) != 0: next_track_to_fade_in = True
|
if len(songs) != 0: next_track_to_fade_in = True
|
||||||
else:
|
else:
|
||||||
if index + 1 < len(self.playlist) and next_track: next_track_to_fade_in = next_track.fade_in
|
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)
|
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'.join(songs))
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
@@ -73,14 +72,14 @@ class Module(ActiveModifier):
|
|||||||
next_track = track
|
next_track = track
|
||||||
self.limit_tracks = False
|
self.limit_tracks = False
|
||||||
return (self.last_track, next_track), True
|
return (self.last_track, next_track), True
|
||||||
elif len(self.originals):
|
elif len(self.originals):
|
||||||
self.last_track = self.originals.pop(0)
|
self.last_track = self.originals.pop(0)
|
||||||
if len(self.originals): next_track = self.originals[0]
|
if len(self.originals): next_track = self.originals[0]
|
||||||
else: self.last_track = track
|
else: self.last_track = track
|
||||||
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:
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class Module(PlaylistAdvisor):
|
|||||||
self.custom_playlist_last_mod = Time.get_playlist_modification_time(self.custom_playlist)
|
self.custom_playlist_last_mod = Time.get_playlist_modification_time(self.custom_playlist)
|
||||||
return self.custom_playlist
|
return self.custom_playlist
|
||||||
elif self.custom_playlist: self.custom_playlist = None
|
elif self.custom_playlist: self.custom_playlist = None
|
||||||
|
|
||||||
current_day, current_hour = (time := datetime.datetime.now()).strftime('%A').lower(), time.hour
|
current_day, current_hour = (time := datetime.datetime.now()).strftime('%A').lower(), time.hour
|
||||||
|
|
||||||
morning_playlist = Path(playlist_dir, current_day, "morning").absolute()
|
morning_playlist = Path(playlist_dir, current_day, "morning").absolute()
|
||||||
@@ -106,7 +106,7 @@ class Module(PlaylistAdvisor):
|
|||||||
self.last_playlist = night_playlist
|
self.last_playlist = night_playlist
|
||||||
return self.last_playlist
|
return self.last_playlist
|
||||||
def new_playlist(self) -> bool:
|
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:
|
if Time.get_playlist_modification_time(self.custom_playlist) > self.custom_playlist_last_mod:
|
||||||
logger.info("Custom playlist changed on disc, reloading...")
|
logger.info("Custom playlist changed on disc, reloading...")
|
||||||
self.custom_playlist = None
|
self.custom_playlist = None
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ def format_time(seconds) -> str:
|
|||||||
|
|
||||||
class Module(PlayerModule):
|
class Module(PlayerModule):
|
||||||
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 track.official:
|
if track.official:
|
||||||
data = f"{track.path.name}: {format_time(elapsed)} / {format_time(total)}\n"
|
data = f"{track.path.name}: {format_time(elapsed)} / {format_time(total)}\n"
|
||||||
Path("/tmp/radioPlayer_progress").write_text(data)
|
Path("/tmp/radioPlayer_progress").write_text(data)
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ def update_rds(track_name: str):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
has_name = False
|
has_name = False
|
||||||
name = track_name.rsplit(".", 1)[0]
|
name = track_name.rsplit(".", 1)[0]
|
||||||
|
|
||||||
name = re.sub(r'^\s*\d+\s*[-.]?\s*', '', name)
|
name = re.sub(r'^\s*\d+\s*[-.]?\s*', '', name)
|
||||||
|
|
||||||
if " - " in name:
|
if " - " in name:
|
||||||
@@ -52,9 +52,9 @@ def update_rds(track_name: str):
|
|||||||
artist = rds_default_artist
|
artist = rds_default_artist
|
||||||
title = name
|
title = name
|
||||||
if not has_name: logger.warning(f"File does not have a alias in the name table ({track_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
|
# title = re.sub(r'\s*[\(\[][^\(\)\[\]]*[\)\]]', '', title) # there might be junk
|
||||||
|
|
||||||
prt = rds_base.format(artist, title)
|
prt = rds_base.format(artist, title)
|
||||||
rtp = [4] # type 1
|
rtp = [4] # type 1
|
||||||
rtp.append(prt.find(artist)) # start 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(prt.find(title)) # start 2
|
||||||
rtp.append(len(title) - 1) # len 2
|
rtp.append(len(title) - 1) # len 2
|
||||||
|
|
||||||
try:
|
try:
|
||||||
f = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
f = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
f.settimeout(1.0)
|
f.settimeout(1.0)
|
||||||
data = f"TEXT={prt}\r\nRTP={rtp}\r\n".encode()
|
data = f"TEXT={prt}\r\nRTP={rtp}\r\n".encode()
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -22,7 +22,7 @@ class Module(PlayerModule):
|
|||||||
lines = self.playlist[:index] + [f"> ({track.path})"] + [self.playlist[index]] + self.playlist[index+1:]
|
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:]
|
else: lines = self.playlist[:index] + [f"> {self.playlist[index]}"] + self.playlist[index+1:]
|
||||||
with open("/tmp/radioPlayer_playlist", "w") as f:
|
with open("/tmp/radioPlayer_playlist", "w") as f:
|
||||||
for line in lines:
|
for line in lines:
|
||||||
try: f.write(line + "\n")
|
try: f.write(line + "\n")
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
print(line.encode('utf-8', errors='ignore').decode('utf-8'))
|
print(line.encode('utf-8', errors='ignore').decode('utf-8'))
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
#!/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)
|
||||||
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)
|
||||||
|
|
||||||
MODULES_PACKAGE = "modules"
|
MODULES_PACKAGE = "modules"
|
||||||
MODULES_DIR = Path(__file__, "..", MODULES_PACKAGE).resolve()
|
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)
|
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()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user