diff --git a/radioPlayer.py b/radioPlayer.py index c6be308..872d79e 100644 --- a/radioPlayer.py +++ b/radioPlayer.py @@ -48,49 +48,56 @@ class Time: class Process: process: subprocess.Popen track: str + started_at: float + duration: float class ProcessManager: def __init__(self) -> None: self.lock = threading.Lock() self.processes: list[Process] = [] - def play(self, track_path, fade_in=False, fade_out=False): + def _get_audio_duration(self, file_path): + try: + 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: return float(result.stdout.strip()) + except Exception as e: logger.warning(f"Exception while reading audio duration: {e}") + return None + def play(self, track_path: str, fade_in: bool=False, fade_out: bool=False) -> Process: cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet'] - duration = 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 exit?", track_path) - # Build filter chain filters = [] - - # Add fade in if requested - if fade_in: - filters.append(f"afade=t=in:st=0:d={CROSSFADE_DURATION}") - if fade_out and duration: - filters.append(f"afade=t=out:st={duration-CROSSFADE_DURATION}:d={CROSSFADE_DURATION}") - - # Apply filters if any exist - if filters: - filter_chain = ",".join(filters) - cmd.extend(['-af', filter_chain]) + if fade_in: filters.append(f"afade=t=in:st=0:d={CROSSFADE_DURATION}") + if fade_out: filters.append(f"afade=t=out:st={duration-CROSSFADE_DURATION}:d={CROSSFADE_DURATION}") + if filters: cmd.extend(['-af', ",".join(filters)]) cmd.append(track_path) proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - pr = Process(proc, track_path) - with self.lock: - self.processes.append(pr) + pr = Process(proc, track_path, time.time(), duration) + with self.lock: self.processes.append(pr) return pr - def anything_playing(self): + def anything_playing(self) -> bool: with self.lock: for process in self.processes[:]: - if process.process.poll() is not None: - self.processes.remove(process) + if process.process.poll() is not None: self.processes.remove(process) return bool(self.processes) - def stop_all(self, timeout: float | None = 2): + def stop_all(self, timeout: float | None = 2) -> bool: + success = True with self.lock: for process in self.processes: process.process.terminate() - process.process.wait(timeout) + try: process.process.wait(timeout) + except: + success = False + continue self.processes.remove(process) - def wait_all(self, timeout: float | None = None): + return success + def wait_all(self, timeout: float | None = None) -> None: with self.lock: for process in self.processes: process.process.wait(timeout) @@ -103,8 +110,7 @@ def load_dict_from_custom_format(file_path: str) -> dict: result_dict = {} with open(file_path, 'r') as file: for line in file: - if line.strip() == "" or line.startswith(";"): - continue + if line.strip() == "" or line.startswith(";"): continue key, value = line.split(':', 1) result_dict[key.strip()] = value.strip() return result_dict @@ -112,17 +118,6 @@ def load_dict_from_custom_format(file_path: str) -> dict: logger.error(f"{name_table_path} does not exist, or could not be accesed") return {} -def get_audio_duration(file_path): - try: - 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: return float(result.stdout.strip()) - except Exception as e: logger.warning(f"Exception while reading audio duration: {e}") - return None - def update_rds(track_name: str): try: name_table: dict[str, str] = load_dict_from_custom_format(name_table_path) @@ -205,9 +200,29 @@ def check_control_files(): return None -def play_single_track(track_path, wait: bool = True): - pr = procman.play(track_path) - if wait: pr.process.wait() +def check_if_playlist_modifed(playlist_path: str, custom_playlist: bool = False): + current_day, current_hour = Time.get_day_hour() + morning_playlist_path = os.path.join(playlist_dir, current_day, 'morning') + day_playlist_path = os.path.join(playlist_dir, current_day, 'day') + night_playlist_path = os.path.join(playlist_dir, current_day, 'night') + late_night_playlist_path = os.path.join(playlist_dir, current_day, 'late_night') + + if DAY_START <= current_hour < DAY_END and not custom_playlist: + if playlist_path != day_playlist_path: + logger.info("Time changed to day hours, switching playlist...") + return True + elif MORNING_START <= current_hour < MORNING_END and not custom_playlist: + if playlist_path != morning_playlist_path: + logger.info("Time changed to morning hours, switching playlist...") + return True + elif LATE_NIGHT_START <= current_hour < LATE_NIGHT_END and not custom_playlist: + if playlist_path != late_night_playlist_path: + logger.info("Time changed to late night hours, switching playlist...") + return True + else: + if playlist_path != night_playlist_path and not custom_playlist: + logger.info("Time changed to night hours, switching playlist...") + return True def play_playlist(playlist_path, custom_playlist: bool=False, play_newest_first=False, do_shuffle=True): last_modified_time = Time.get_playlist_modification_time(playlist_path) @@ -230,17 +245,18 @@ def play_playlist(playlist_path, custom_playlist: bool=False, play_newest_first= if do_shuffle: random.shuffle(tracks) tracks.insert(0, newest_track) else: - if do_shuffle: - random.shuffle(tracks) + if do_shuffle: random.shuffle(tracks) return_pending = False for i, track in enumerate(tracks[start_index:], start_index): if return_pending: + logger.info("Return reached, next song will reload the playlist.") procman.wait_all() return action = check_control_files() if action == "quit": + logger.info("Quit received, waiting for song end.") procman.wait_all() exit() elif action == "reload": @@ -256,52 +272,17 @@ def play_playlist(playlist_path, custom_playlist: bool=False, play_newest_first= return_pending = True continue - current_day, current_hour = Time.get_day_hour() - morning_playlist_path = os.path.join(playlist_dir, current_day, 'morning') - day_playlist_path = os.path.join(playlist_dir, current_day, 'day') - night_playlist_path = os.path.join(playlist_dir, current_day, 'night') - late_night_playlist_path = os.path.join(playlist_dir, current_day, 'late_night') - - if DAY_START <= current_hour < DAY_END and not custom_playlist: - if playlist_path != day_playlist_path: - logger.info("Time changed to day hours, switching playlist...") - return_pending = True - elif MORNING_START <= current_hour < MORNING_END and not custom_playlist: - if playlist_path != morning_playlist_path: - logger.info("Time changed to morning hours, switching playlist...") - return_pending = True - elif LATE_NIGHT_START <= current_hour < LATE_NIGHT_END and not custom_playlist: - if playlist_path != late_night_playlist_path: - logger.info("Time changed to late night hours, switching playlist...") - return_pending = True - else: - if playlist_path != night_playlist_path and not custom_playlist: - logger.info("Time changed to night hours, switching playlist...") - return_pending = True - + return_pending = check_if_playlist_modifed(playlist_path, custom_playlist) if return_pending and not procman.anything_playing(): continue logger.info(f"Now playing: {track_name}") - update_rds(track_name) - - duration = get_audio_duration(track_path) - if not duration: - logger.warning(f"Could not get duration for {track_path}, playing without crossfade") - play_single_track(track_path) - return - - # Calculate when to start the next track (5 seconds before end) - crossfade_start_time = max(0, duration - CROSSFADE_DURATION) - - # Start current track with fade in - procman.play(track_path, True, True) - time.sleep(crossfade_start_time) + pr = procman.play(track_path, True, True) + time.sleep(pr.duration - CROSSFADE_DURATION) def can_delete_file(filepath): - if not os.path.isfile(filepath): - return False + if not os.path.isfile(filepath): return False directory = os.path.dirname(os.path.abspath(filepath)) or '.' return os.access(directory, os.W_OK | os.X_OK) @@ -330,8 +311,7 @@ def parse_arguments(): exit(0) if can_delete_file("/tmp/radioPlayer_arg"): - with open("/tmp/radioPlayer_arg", "r") as f: - arg = f.read().strip() + with open("/tmp/radioPlayer_arg", "r") as f: arg = f.read().strip() os.remove("/tmp/radioPlayer_arg") if arg: @@ -342,29 +322,26 @@ def parse_arguments(): selected_list = arg.removeprefix("list:") logger.info(f"The list {selected_list.split(';')[0]} will be played instead of the daily section lists.") for option in selected_list.split(";"): - if option == "n": - play_newest_first = True - elif option == "ns": - do_shuffle = False + if option == "n": play_newest_first = True + elif option == "ns": do_shuffle = False selected_list = selected_list.split(";")[0] elif os.path.isfile(arg): pre_track_path = arg logger.info(f"Will play requested song first: {arg}") - else: - logger.error(f"Invalid argument or file not found: {arg}") + else: logger.error(f"Invalid argument or file not found: {arg}") return play_newest_first, do_shuffle, pre_track_path, selected_list def main(): try: - while True: # Main reload loop + while True: play_newest_first, do_shuffle, pre_track_path, selected_list = parse_arguments() if pre_track_path: track_name = os.path.basename(pre_track_path) logger.info(f"Now playing: {track_name}") update_rds(track_name) - play_single_track(pre_track_path) + procman.play(pre_track_path).process.wait() action = check_control_files() if action == "quit": @@ -373,13 +350,12 @@ def main(): logger.info("Reload requested, restarting with new arguments...") continue # Restart the main loop - playlist_loop_active = True - while playlist_loop_active: + play_loop = True + while play_loop: if selected_list: logger.info("Playing custom list") result = play_playlist(selected_list, True, play_newest_first, do_shuffle) - if result == "reload": - playlist_loop_active = False # Break out to reload + if result == "reload": play_loop = False continue current_day, current_hour = Time.get_day_hour() @@ -402,8 +378,7 @@ def main(): for playlist_path in [morning_playlist, day_playlist, night_playlist, late_night_playlist]: if not os.path.exists(playlist_path): logger.info(f"Creating empty playlist: {playlist_path}") - with open(playlist_path, 'w') as f: - pass + with open(playlist_path, 'w'): pass if DAY_START <= current_hour < DAY_END: logger.info(f"Playing {current_day} day playlist...") @@ -419,14 +394,12 @@ def main(): result = play_playlist(night_playlist, False, play_newest_first, do_shuffle) action = check_control_files() - if action == "quit": - exit() + if action == "quit": exit() elif action == "reload": logger.info("Reload requested, restarting with new arguments...") result = "reload" - if result == "reload": - playlist_loop_active = False # Break out to reload + if result == "reload": play_loop = False except KeyboardInterrupt: logger.info("Player stopped by user") @@ -435,8 +408,6 @@ def main(): logger.error(f"Unexpected error: {e}") procman.stop_all() raise - finally: - procman.stop_all() + finally: procman.stop_all() -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == '__main__': main() \ No newline at end of file