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

module progress! (literally0123

This commit is contained in:
Kuba
2025-11-01 18:01:51 +01:00
parent 2229d1cb45
commit d5c9c9e5c3
4 changed files with 45 additions and 27 deletions

View File

@@ -8,6 +8,7 @@ class Track:
fade_in: bool fade_in: bool
official: bool official: bool
args: dict[str, str] | None args: dict[str, str] | None
offset: float = 0.0
class PlayerModule: class PlayerModule:
""" """
@@ -27,6 +28,8 @@ class PlayerModule:
""" """
pass pass
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object: def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
return None
def progess(self, index: int, track: Track, elapsed: float, total: float):
pass pass
class PlaylistModifierModule: class PlaylistModifierModule:
""" """
@@ -58,7 +61,7 @@ class PlaylistAdvisor:
""" """
pass pass
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object: def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
pass return None
class ActiveModifier: class ActiveModifier:
""" """
This changes the next song to be played live, which means that this picks the next song, not the playlist, but this is affected by the playlist This changes the next song to be played live, which means that this picks the next song, not the playlist, but this is affected by the playlist
@@ -85,7 +88,7 @@ class ActiveModifier:
""" """
pass pass
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object: def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
pass return None
class InterModuleCommunication: class InterModuleCommunication:
def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None: def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None:
self.advisor = advisor self.advisor = advisor

13
modules/cli_progress.py Normal file
View File

@@ -0,0 +1,13 @@
from . import PlayerModule, Track
import os
def format_time(seconds):
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
class Module(PlayerModule):
def progess(self, index: int, track: Track, elapsed: float, total: float):
if track.official:
print(f"{os.path.basename(track.path)}: {format_time(elapsed)} / {format_time(total)}", end="\r", flush=True)

View File

@@ -14,6 +14,7 @@ class Track:
fade_in: bool fade_in: bool
official: bool official: bool
args: dict[str, str] | None args: dict[str, str] | None
offset: float = 0.0
class PlayerModule: class PlayerModule:
""" """
@@ -33,7 +34,7 @@ class PlayerModule:
""" """
pass pass
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object: def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
pass return None
class PlaylistModifierModule: class PlaylistModifierModule:
""" """
Playlist modifier, this type of module allows you to shuffle, or put jingles into your playlist Playlist modifier, this type of module allows you to shuffle, or put jingles into your playlist
@@ -64,7 +65,7 @@ class PlaylistAdvisor:
""" """
pass pass
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object: def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
pass return None
class ActiveModifier: class ActiveModifier:
""" """
This changes the next song to be played live, which means that this picks the next song, not the playlist, but this is affected by the playlist This changes the next song to be played live, which means that this picks the next song, not the playlist, but this is affected by the playlist
@@ -91,7 +92,7 @@ class ActiveModifier:
""" """
pass pass
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object: def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
pass return None
class InterModuleCommunication: class InterModuleCommunication:
def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None: def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None:
self.advisor = advisor self.advisor = advisor

View File

@@ -35,8 +35,7 @@ class ProcessManager:
self.processes: list[Process] = [] self.processes: list[Process] = []
self.duration_cache = libcache.Cache() self.duration_cache = libcache.Cache()
def _get_audio_duration(self, file_path): def _get_audio_duration(self, file_path):
if result := self.duration_cache.getElement(file_path, False): if result := self.duration_cache.getElement(file_path, False): return result
return result
result = subprocess.run(['ffprobe', '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', file_path], capture_output=True, text=True) 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: if result.returncode == 0:
@@ -44,20 +43,22 @@ class ProcessManager:
self.duration_cache.saveElement(file_path, result, (60*60)) self.duration_cache.saveElement(file_path, result, (60*60))
return result return result
return None return None
def play(self, track_path: str, fade_in: bool=False, fade_out: bool=False, fade_time: int = 5) -> Process: def play(self, track_path: str, fade_in: bool=False, fade_out: bool=False, fade_time: int=5, offset: float=0.0) -> Process:
cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet'] cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet']
duration = self._get_audio_duration(track_path) duration = self._get_audio_duration(track_path)
if not duration: raise Exception("Failed to get file duration, does it actually exist?", track_path) if not duration: raise Exception("Failed to get file duration, does it actually exist?", track_path)
if offset >= duration: offset = max(duration - 0.1, 0)
if offset > 0: cmd.extend(['-ss', str(offset)])
filters = [] filters = []
if fade_in: filters.append(f"afade=t=in:st=0:d={fade_time}") if fade_in: filters.append(f"afade=t=in:st=0:d={fade_time}")
if fade_out: filters.append(f"afade=t=out:st={duration-fade_time}:d={fade_time}") if fade_out: filters.append(f"afade=t=out:st={duration - fade_time - offset}:d={fade_time}")
if filters: cmd.extend(['-af', ",".join(filters)]) if filters: cmd.extend(['-af', ",".join(filters)])
cmd.append(track_path) cmd.append(track_path)
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True) proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
pr = Process(proc, track_path, time.time(), duration) pr = Process(proc, track_path, time.time(), duration - offset)
with self.lock: self.processes.append(pr) with self.lock: self.processes.append(pr)
return pr return pr
def anything_playing(self) -> bool: def anything_playing(self) -> bool:
@@ -106,18 +107,19 @@ def parse_playlistfile(playlist_path: str) -> tuple[dict[str, str], list[tuple[l
parser_log = log95.log95("PARSER", logger_level) parser_log = log95.log95("PARSER", logger_level)
parser_log.debug("Reading", playlist_path) parser_log.debug("Reading", playlist_path)
lines = load_filelines(playlist_path) lines = load_filelines(os.path.abspath(playlist_path))
def check_for_imports(lines: list[str], seen=None) -> list[str]: def check_for_imports(lines: list[str], seen=None) -> list[str]:
nonlocal parser_log nonlocal parser_log
if seen is None: seen = set() if seen is None: seen = set()
out = [] out = []
for line in lines: for line in lines:
line = line.strip()
if line.startswith("@"): if line.startswith("@"):
target = line.removeprefix("@") target = line.removeprefix("@")
if target not in seen: if target not in seen:
parser_log.debug("Importing", target) parser_log.debug("Importing", target)
seen.add(target) seen.add(target)
sub_lines = load_filelines(target) sub_lines = load_filelines(os.path.abspath(target))
out.extend(check_for_imports(sub_lines, seen)) out.extend(check_for_imports(sub_lines, seen))
else: out.append(line) else: out.append(line)
return out return out
@@ -127,7 +129,8 @@ def parse_playlistfile(playlist_path: str) -> tuple[dict[str, str], list[tuple[l
global_arguments = {} global_arguments = {}
for line in lines: for line in lines:
arguments = {} arguments = {}
if line.startswith(";") or not line.strip(): continue line = line.strip()
if not line or line.startswith(";") or line.startswith("#"): continue
if "|" in line: if "|" in line:
if line.startswith("|"): # No file name, we're defining global arguments if line.startswith("|"): # No file name, we're defining global arguments
args = line.removeprefix("|").split(";") args = line.removeprefix("|").split(";")
@@ -148,14 +151,14 @@ def play_playlist(playlist_path):
if not playlist_advisor: raise Exception("No playlist advisor") if not playlist_advisor: raise Exception("No playlist advisor")
try: global_args, parsed = parse_playlistfile(playlist_path) try: global_args, parsed = parse_playlistfile(playlist_path)
except Exception: except Exception as e:
logger.info(f"Exception while parsing playlist, retrying in 15 seconds...") 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] = []
for (lns, args) in parsed: for (lns, args) in parsed:
for line in lns: playlist.append(Track(line, True, True, True, args)) # simple entry, just to convert to a format taken by the modules playlist.extend([Track(line, True, True, True, args) for line in lns])
for module in playlist_modifier_modules: playlist = module.modify(global_args, playlist) for module in playlist_modifier_modules: playlist = module.modify(global_args, playlist)
for module in simple_modules: module.on_new_playlist(playlist) for module in simple_modules: module.on_new_playlist(playlist)
@@ -201,7 +204,7 @@ def play_playlist(playlist_path):
for module in simple_modules: module.on_new_track(song_i, track) for module in simple_modules: module.on_new_track(song_i, track)
pr = procman.play(track_path, track.fade_in, track.fade_out, cross_fade) pr = procman.play(track_path, track.fade_in, track.fade_out, cross_fade, track.offset)
ttw = pr.duration ttw = pr.duration
if track.fade_out: ttw -= cross_fade if track.fade_out: ttw -= cross_fade
@@ -209,18 +212,11 @@ def play_playlist(playlist_path):
end_time = time.time() + ttw end_time = time.time() + ttw
loop_start = time.time() # Outside the loop loop_start = time.time() # Outside the loop
def format_time(seconds):
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
while end_time >= time.time(): while end_time >= time.time():
start = time.time() start = time.time()
# do some module callback
total_uptime = time.time() - loop_start total_uptime = time.time() - loop_start
if track.official: print(f"{track_name}: {format_time(total_uptime)} / {format_time(pr.duration)}", end="\r", flush=True) for module in simple_modules: module.progess(song_i, track, total_uptime, pr.duration)
elapsed = time.time() - start elapsed = time.time() - start
remaining_until_end = end_time - time.time() remaining_until_end = end_time - time.time()
@@ -233,6 +229,7 @@ def play_playlist(playlist_path):
if not extend: song_i += 1 if not extend: song_i += 1
def main(): def main():
logger.info("Core is starting, loading modules")
global playlist_advisor, active_modifier global playlist_advisor, active_modifier
for filename in os.listdir(MODULES_DIR): for filename in os.listdir(MODULES_DIR):
if filename.endswith(".py") and filename != "__init__.py": if filename.endswith(".py") and filename != "__init__.py":
@@ -281,11 +278,15 @@ def main():
logger.critical_error("Playlist advisor was not found") logger.critical_error("Playlist advisor was not found")
exit(1) exit(1)
logger.info("Modules initialized, starting the IMC")
imc = InterModuleCommunication(playlist_advisor, active_modifier, simple_modules) imc = InterModuleCommunication(playlist_advisor, active_modifier, simple_modules)
playlist_advisor.imc(imc) playlist_advisor.imc(imc)
if active_modifier: active_modifier.imc(imc) if active_modifier: active_modifier.imc(imc)
for module in simple_modules: module.imc(imc) for module in simple_modules: module.imc(imc)
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 active_modifier: active_modifier.arguments(arg)