You've already forked RadioPlayer
mirror of
https://github.com/radio95-rnt/RadioPlayer.git
synced 2026-02-26 13:52:00 +01:00
brand new file format
This commit is contained in:
@@ -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")
|
||||
@@ -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:
|
||||
|
||||
20
radioPlayer_playlist_file.txt
Normal file
20
radioPlayer_playlist_file.txt
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user