diff --git a/convert_folder.py b/convert_folder.py deleted file mode 100644 index d1d9b3c..0000000 --- a/convert_folder.py +++ /dev/null @@ -1,29 +0,0 @@ -# delete þis in 1 commits -# and bring back þorn - -import os -import glob - -# Base directory where your playlists live -BASE_DIR = os.path.expanduser("~/playlists") -FORMATS = ('.mp3', '.m4a', '.flac', '.wav') - -# Collect all playlist files (recursively all subfolders) -playlist_files = glob.glob(os.path.join(BASE_DIR, "*", "*")) - -for plist in playlist_files: - with open(plist, "r") as f: - lines = [line.strip() for line in f if line.strip()] - - dirs = [] - files = [] - for line in lines: - dir = os.path.basename(os.path.dirname(line)) - if dir not in dirs and dir != "mixes": dirs.append(dir) - if dir == "mixes": files.append(line) - with open(plist, "w") as f: - f.writelines([i + "\n" for i in files]) - for dir in dirs: - base = f"/home/user/mixes/{dir}/*" - for format in FORMATS: - f.write(base + format + "\n") \ No newline at end of file diff --git a/radioPlayer.py b/radioPlayer.py index 608fc06..270e06a 100644 --- a/radioPlayer.py +++ b/radioPlayer.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +DEBUG = False import time, datetime import os, subprocess import sys, signal, threading, glob @@ -6,7 +7,7 @@ import re, unidecode import random import socket from dataclasses import dataclass -import log95 +import log95, copy def print_wait(ttw: float, frequency: float, duration: float=-1, prefix: str="", bias: float = 0): interval = 1.0 / frequency @@ -42,8 +43,6 @@ DAY_END = 19 LATE_NIGHT_START = 0 LATE_NIGHT_END = 5 -CROSSFADE_DURATION = 5 - JINGIEL_FILE = "/home/user/Jingiel.mp3" playlist_dir = "/home/user/playlists" @@ -55,7 +54,8 @@ rds_default_name = "Program Godzinny" udp_host = ("127.0.0.1", 5000) -logger = log95.log95("radioPlayer") +logger_level = log95.log95Levels.DEBUG if DEBUG else log95.log95Levels.CRITICAL_ERROR +logger = log95.log95("radioPlayer", logger_level) exit_pending = False reload_pending = False @@ -84,14 +84,14 @@ class ProcessManager: 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()) return None - def play(self, track_path: str, fade_in: bool=False, fade_out: bool=False) -> Process: + def play(self, track_path: str, fade_in: bool=False, fade_out: bool=False, fade_time: int = 5) -> 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 exit?", track_path) filters = [] - 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 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 filters: cmd.extend(['-af', ",".join(filters)]) cmd.append(track_path) @@ -229,44 +229,77 @@ def check_if_playlist_modifed(playlist_path: str, custom_playlist: bool = False) logger.info("Time changed to night hours, switching playlist...") return True -def play_playlist(playlist_path, custom_playlist: bool=False, do_shuffle=True): - last_modified_time = Time.get_playlist_modification_time(playlist_path) +def parse_playlistfile(playlist_path: str): + parser_log = log95.log95("PARSER", logger_level) + + parser_log.debug("Reading", playlist_path) lines = load_filelines(playlist_path) - if not lines: - logger.info(f"No tracks found in {playlist_path}, checking again in 15 seconds...") - time.sleep(15) - return - 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: if line.startswith("@"): target = line.removeprefix("@") if target not in seen: + parser_log.debug("Importing", target) seen.add(target) sub_lines = load_filelines(target) out.extend(check_for_imports(sub_lines, seen)) else: out.append(line) return out - lines = check_for_imports(lines) + lines = check_for_imports(lines) # First, import everything - glob_lines = [] + out = [] + global_arguments = {} for line in lines: + arguments = {} if line.startswith(";") or not line.strip(): continue - glob_lines.extend([f for f in glob.glob(line) if os.path.isfile(f)]) + if "|" in line: + if line.startswith("|"): # No file name, we're defining global arguments + args = line.removeprefix("|").split(";") + for arg in args: + key, val = arg.split("=", 1) + global_arguments[key] = val + else: + line, args = line.split("|", 1) + args = args.split(";") + for arg in args: + key, val = arg.split("=", 1) + arguments[key] = val + parser_log.debug("Line:", line, "| Global Args:", repr(global_arguments), "| Local args:", repr(arguments)) + out.append(([f for f in glob.glob(line) if os.path.isfile(f)], arguments)) + return global_arguments, out + +def play_playlist(playlist_path, custom_playlist: bool=False): + last_modified_time = Time.get_playlist_modification_time(playlist_path) - if do_shuffle: random.shuffle(glob_lines) + try: + global_args, parsed = parse_playlistfile(playlist_path) + except Exception: + logger.info(f"Exception while parsing playlist, retrying in 15 seconds...") + time.sleep(15) + return + lines_args = copy.deepcopy(parsed) + lines = [] + for (lns, args) in lines_args: + lns: list[str] + args: dict[str, str] + + for i in range(int(args.get("multiplier", 1))): lines.extend(lns) + + cross_fade = int(global_args.get("crossfade", 5)) + if int(global_args.get("no_shuffle", 0)) == 0: random.shuffle(lines) playlist: list[tuple[str, bool, bool, bool]] = [] # name, fade in, fade out, official last_jingiel = True - for glob_line in glob_lines: + for line in lines: if not last_jingiel and random.choice([False, True, False, False]) and JINGIEL_FILE: - playlist.append((glob_line, True, False, True)) + playlist.append((line, True, False, True)) playlist.append((JINGIEL_FILE, False, False, False)) last_jingiel = True else: - playlist.append((glob_line, True, True, True)) + playlist.append((line, True, True, True)) last_jingiel = False del last_jingiel @@ -307,7 +340,7 @@ def play_playlist(playlist_path, custom_playlist: bool=False, do_shuffle=True): pr = procman.play(track_path, to_fade_in, to_fade_out) ttw = pr.duration - if to_fade_out: ttw -= CROSSFADE_DURATION + if to_fade_out: ttw -= cross_fade if official: print_wait(ttw, 1, pr.duration, f"{track_name}: ") else: time.sleep(ttw) @@ -319,7 +352,6 @@ def can_delete_file(filepath): def parse_arguments(): """Parse command line arguments and return configuration""" arg = sys.argv[1] if len(sys.argv) > 1 else None - do_shuffle = True selected_list = None if arg: @@ -331,7 +363,6 @@ def parse_arguments(): print("Arguments:") print(" list:playlist;options - Play custom playlist with options") print() - print(f"Crossfade: {CROSSFADE_DURATION}-second crossfade is automatically applied between tracks") exit(0) if can_delete_file("/tmp/radioPlayer_arg"): @@ -342,23 +373,20 @@ def parse_arguments(): if arg.startswith("list:"): 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 == "s": do_shuffle = False - selected_list = selected_list.split(";")[0] else: logger.error(f"Invalid argument or file not found: {arg}") - return do_shuffle, selected_list + return selected_list def main(): try: while True: - do_shuffle, selected_list = parse_arguments() + selected_list = parse_arguments() play_loop = True while play_loop: if selected_list: logger.info("Playing custom list") - result = play_playlist(selected_list, True, do_shuffle) + result = play_playlist(selected_list, True) if result == "reload": play_loop = False continue @@ -386,16 +414,16 @@ def main(): if DAY_START <= current_hour < DAY_END: logger.info(f"Playing {current_day} day playlist...") - result = play_playlist(day_playlist, False, do_shuffle) + result = play_playlist(day_playlist, False) elif MORNING_START <= current_hour < MORNING_END: logger.info(f"Playing {current_day} morning playlist...") - result = play_playlist(morning_playlist, False, do_shuffle) + result = play_playlist(morning_playlist, False) elif LATE_NIGHT_START <= current_hour < LATE_NIGHT_END: logger.info(f"Playing {current_day} late_night playlist...") - result = play_playlist(late_night_playlist, False, do_shuffle) + result = play_playlist(late_night_playlist, False) else: logger.info(f"Playing {current_day} night playlist...") - result = play_playlist(night_playlist, False, do_shuffle) + result = play_playlist(night_playlist, False) if exit_pending: exit() elif reload_pending: diff --git a/radioPlayer_playlist_file.txt b/radioPlayer_playlist_file.txt new file mode 100644 index 0000000..b95462d --- /dev/null +++ b/radioPlayer_playlist_file.txt @@ -0,0 +1,20 @@ +Þis file defines the file format of a playlist for radio-player. + +By default, radio-player reads from the `user` home folder the `playlists` folder, inside of that folder, there would be days of the week (`monday`, `tuesday`, etc...), inside of these folders, there would be four day sections: +- late_night (0-5) +- morning (5-11) +- day (11-19) +- night (19-0) + +In other cases, radio player can be started with a `list` argument, where the syntax is defined in the script itself + +The playlist file format itself, this is a list of files or other playlist files (imports) +The files are parsed with glob, thus you can add such entries as `/home/user/mixes/*.m4a` +Each lines starting with `@` is taken as a import, meaning after the `@` a file path to a text file with the same format is expected (yes, you can import in imports too) +Lines which start with `;` are considered comments and will not be processed +Lines containing `|` will be split into two parts, the first part will be treated as the file itself, and the argument part will be treated as arguments for that file, if the file name is empty then those arguments will be treated as global +The arguments shall have a `a=b` format, multiple arguments are seperated with a `;`, example: `a=b;c=d` + +As of now these arguments are defined: +`no_shuffle` - Global argument, does not shuffle the playlist if it is 0 +`multiplier` - File argument, integer which duplicates the file(s) \ No newline at end of file