diff --git a/modules/__init__.py b/modules/__init__.py index 9c111f9..e725b0c 100644 --- a/modules/__init__.py +++ b/modules/__init__.py @@ -8,6 +8,7 @@ class Track: fade_in: bool official: bool args: dict[str, str] | None + offset: float = 0.0 class PlayerModule: """ @@ -27,6 +28,8 @@ class PlayerModule: """ pass 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 class PlaylistModifierModule: """ @@ -58,7 +61,7 @@ class PlaylistAdvisor: """ pass def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object: - pass + return None 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 @@ -85,7 +88,7 @@ class ActiveModifier: """ pass def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object: - pass + return None class InterModuleCommunication: def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None: self.advisor = advisor diff --git a/modules/cli_progress.py b/modules/cli_progress.py new file mode 100644 index 0000000..ff8ab65 --- /dev/null +++ b/modules/cli_progress.py @@ -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) diff --git a/modules/modules.txt b/modules/modules.txt index edf9e7c..913d292 100644 --- a/modules/modules.txt +++ b/modules/modules.txt @@ -14,6 +14,7 @@ class Track: fade_in: bool official: bool args: dict[str, str] | None + offset: float = 0.0 class PlayerModule: """ @@ -33,7 +34,7 @@ class PlayerModule: """ pass def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object: - pass + return None class PlaylistModifierModule: """ Playlist modifier, this type of module allows you to shuffle, or put jingles into your playlist @@ -64,7 +65,7 @@ class PlaylistAdvisor: """ pass def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object: - pass + return None 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 @@ -91,7 +92,7 @@ class ActiveModifier: """ pass def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object: - pass + return None class InterModuleCommunication: def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None: self.advisor = advisor diff --git a/radioPlayer.py b/radioPlayer.py index f07d4ff..b7f77ff 100644 --- a/radioPlayer.py +++ b/radioPlayer.py @@ -35,8 +35,7 @@ class ProcessManager: self.processes: list[Process] = [] self.duration_cache = libcache.Cache() def _get_audio_duration(self, file_path): - if result := self.duration_cache.getElement(file_path, False): - return result + if result := self.duration_cache.getElement(file_path, False): 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) if result.returncode == 0: @@ -44,20 +43,22 @@ class ProcessManager: self.duration_cache.saveElement(file_path, result, (60*60)) return result 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'] + duration = self._get_audio_duration(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 = [] 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)]) - cmd.append(track_path) 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) return pr 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.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]: nonlocal parser_log if seen is None: seen = set() out = [] for line in lines: + line = line.strip() if line.startswith("@"): target = line.removeprefix("@") if target not in seen: parser_log.debug("Importing", 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)) else: out.append(line) return out @@ -127,7 +129,8 @@ def parse_playlistfile(playlist_path: str) -> tuple[dict[str, str], list[tuple[l global_arguments = {} for line in lines: 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 line.startswith("|"): # No file name, we're defining global arguments args = line.removeprefix("|").split(";") @@ -148,14 +151,14 @@ def play_playlist(playlist_path): if not playlist_advisor: raise Exception("No playlist advisor") try: global_args, parsed = parse_playlistfile(playlist_path) - except Exception: - logger.info(f"Exception while parsing playlist, retrying in 15 seconds...") + except Exception as e: + logger.info(f"Exception ({e}) while parsing playlist, retrying in 15 seconds...") time.sleep(15) return playlist: list[Track] = [] 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 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) - 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 if track.fade_out: ttw -= cross_fade @@ -209,18 +212,11 @@ def play_playlist(playlist_path): end_time = time.time() + ttw 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(): start = time.time() - # do some module callback - + 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 remaining_until_end = end_time - time.time() @@ -233,6 +229,7 @@ def play_playlist(playlist_path): if not extend: song_i += 1 def main(): + logger.info("Core is starting, loading modules") global playlist_advisor, active_modifier for filename in os.listdir(MODULES_DIR): if filename.endswith(".py") and filename != "__init__.py": @@ -280,12 +277,16 @@ def main(): if not playlist_advisor: logger.critical_error("Playlist advisor was not found") exit(1) + + logger.info("Modules initialized, starting the IMC") imc = InterModuleCommunication(playlist_advisor, active_modifier, simple_modules) playlist_advisor.imc(imc) if active_modifier: active_modifier.imc(imc) for module in simple_modules: module.imc(imc) + logger.info("Starting playback.") + try: arg = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else None if active_modifier: active_modifier.arguments(arg)