0
1
mirror of https://github.com/radio95-rnt/RadioPlayer.git synced 2026-02-26 21:53:54 +01:00
This commit is contained in:
2025-09-01 12:49:40 +02:00
parent 21b248dffc
commit 8c802abf1e

View File

@@ -5,7 +5,8 @@ import subprocess
import time, datetime import time, datetime
import sys import sys
import threading import threading
import json, re, unidecode import re, unidecode
from dataclasses import dataclass
from datetime import datetime from datetime import datetime
import log95 import log95
@@ -29,11 +30,8 @@ udp_host = ("127.0.0.1", 5000)
logger = log95.log95("radioPlayer") logger = log95.log95("radioPlayer")
# Crossfade management exit_pending = False
cross_for_cross_time = 0 reload_pending = False
current_process = None
next_process = None
process_lock = threading.Lock()
class Time: class Time:
@staticmethod @staticmethod
@@ -46,6 +44,60 @@ class Time:
except OSError: except OSError:
return 0 return 0
@dataclass
class Process:
process: subprocess.Popen
track: str
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):
cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet']
duration = get_audio_duration(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])
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)
return pr
def anything_playing(self):
with self.lock:
for process in self.processes[:]:
if process.process.poll() is not None:
self.processes.remove(process)
return bool(self.processes)
def stop_all(self):
with self.lock:
for process in self.processes:
process.process.terminate()
process.process.wait(2)
self.processes.remove(process)
def wait_all(self, timeout: float | None = None):
with self.lock:
for process in self.processes:
process.process.wait(timeout)
self.processes.remove(process)
procman = ProcessManager()
def load_dict_from_custom_format(file_path: str) -> dict: def load_dict_from_custom_format(file_path: str) -> dict:
try: try:
result_dict = {} result_dict = {}
@@ -153,71 +205,11 @@ def check_control_files():
return None return None
def stop_all_processes(): def play_single_track(track_path, wait: bool = True):
"""Stop all ffplay processes""" pr = procman.play(track_path)
global current_process, next_process if wait: pr.process.wait()
with process_lock:
if current_process and current_process.poll() is None:
try:
current_process.terminate()
current_process.wait(timeout=2)
except (subprocess.TimeoutExpired, ProcessLookupError):
try:
current_process.kill()
current_process.wait(timeout=2)
except (subprocess.TimeoutExpired, ProcessLookupError): pass
current_process = None
if next_process and next_process.poll() is None:
try:
next_process.terminate()
next_process.wait(timeout=2)
except (subprocess.TimeoutExpired, ProcessLookupError):
try:
next_process.kill()
next_process.wait(timeout=2)
except (subprocess.TimeoutExpired, ProcessLookupError):
pass
next_process = None
def create_audio_process(track_path, fade_in=False, fade_out=False):
"""Create ffplay process with optional fade effects"""
cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet']
duration = get_audio_duration(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])
cmd.append(track_path)
return subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def play_single_track(track_path):
"""Play a single track without crossfade (for first track or special cases)"""
global current_process
with process_lock:
current_process = create_audio_process(track_path, fade_in=False, fade_out=False)
# Wait for the process to complete
current_process.wait()
with process_lock:
current_process = None
def play_playlist(playlist_path, custom_playlist: bool=False, play_newest_first=False, do_shuffle=True): def play_playlist(playlist_path, custom_playlist: bool=False, play_newest_first=False, do_shuffle=True):
global current_process, next_process, cross_for_cross_time
last_modified_time = Time.get_playlist_modification_time(playlist_path) last_modified_time = Time.get_playlist_modification_time(playlist_path)
tracks = load_playlist(playlist_path) tracks = load_playlist(playlist_path)
if not tracks: if not tracks:
@@ -240,13 +232,21 @@ def play_playlist(playlist_path, custom_playlist: bool=False, play_newest_first=
else: else:
if do_shuffle: if do_shuffle:
random.shuffle(tracks) random.shuffle(tracks)
return_pending = False return_pending = False
for i, track in enumerate(tracks[start_index:], start_index): for i, track in enumerate(tracks[start_index:], start_index):
if return_pending: if return_pending:
stop_all_processes() procman.wait_all()
return return
action = check_control_files()
if action == "quit":
procman.wait_all()
exit()
elif action == "reload":
logger.info("Reload requested, restarting with new arguments...")
procman.wait_all()
return "reload"
track_path = os.path.abspath(os.path.expanduser(track)) track_path = os.path.abspath(os.path.expanduser(track))
track_name = os.path.basename(track_path) track_name = os.path.basename(track_path)
@@ -278,124 +278,26 @@ def play_playlist(playlist_path, custom_playlist: bool=False, play_newest_first=
if playlist_path != night_playlist_path and not custom_playlist: if playlist_path != night_playlist_path and not custom_playlist:
logger.info("Time changed to night hours, switching playlist...") logger.info("Time changed to night hours, switching playlist...")
return_pending = True return_pending = True
if return_pending and not current_process: continue
if current_process:
time.sleep(cross_for_cross_time)
# Check if we need to stop due to control files if return_pending and not procman.anything_playing(): continue
action = check_control_files()
if action == "quit":
stop_all_processes()
exit()
elif action == "reload":
logger.info("Reload requested during playback...")
stop_all_processes()
return "reload"
logger.info(f"Starting cross-to-cross to: {os.path.basename(track_name)}")
with process_lock:
next_process = create_audio_process(track_path, fade_in=True, fade_out=True)
update_rds(track_name)
# Wait for crossfade to complete
time.sleep(CROSSFADE_DURATION * 1.5)
with process_lock:
if current_process and current_process.poll() is None:
try:
current_process.terminate()
current_process.wait(5)
except (subprocess.TimeoutExpired, ProcessLookupError):
pass
current_process = next_process
next_process = None
else:
# No crossfade, just wait for current track to finish
current_process.wait()
with process_lock:
current_process = None
continue
logger.info(f"Now playing: {track_name}") logger.info(f"Now playing: {track_name}")
update_rds(track_name) update_rds(track_name)
# Determine next track for crossfade
next_track_path = None
next_index = i + 1
if next_index < len(tracks):
next_track_path = os.path.abspath(os.path.expanduser(tracks[next_index]))
duration = get_audio_duration(track_path) duration = get_audio_duration(track_path)
if not duration: if not duration:
logger.warning(f"Could not get duration for {track_path}, playing without crossfade") logger.warning(f"Could not get duration for {track_path}, playing without crossfade")
play_single_track(track_path) play_single_track(track_path)
return return
cross_for_cross_time = get_audio_duration(track_path)
if not cross_for_cross_time:
logger.warning(f"Could not get duration for {track_path}, playing without crossfade")
play_single_track(track_path)
play_single_track(next_track_path)
return
# Calculate when to start the next track (5 seconds before end) # Calculate when to start the next track (5 seconds before end)
crossfade_start_time = max(0, duration - CROSSFADE_DURATION) crossfade_start_time = max(0, duration - CROSSFADE_DURATION)
cross_for_cross_time = max(0, cross_for_cross_time - CROSSFADE_DURATION)
# Start current track with fade in # Start current track with fade in
with process_lock: procman.play(track_path, True, True)
current_process = create_audio_process(track_path, fade_in=True, fade_out=True)
time.sleep(crossfade_start_time)
if next_track_path and crossfade_start_time > 0:
# Wait until it's time to start the crossfade
time.sleep(crossfade_start_time)
# Check if we need to stop due to control files
action = check_control_files()
if action == "quit":
stop_all_processes()
exit()
elif action == "reload":
logger.info("Reload requested during playback...")
stop_all_processes()
return "reload"
logger.info(f"Starting crossfade to: {os.path.basename(next_track_path)}")
with process_lock:
next_process = create_audio_process(next_track_path, fade_in=True, fade_out=True)
update_rds(os.path.basename(next_track_path))
# Wait for crossfade to complete
time.sleep(CROSSFADE_DURATION * 1.5)
with process_lock:
if current_process and current_process.poll() is None:
try:
current_process.terminate()
current_process.wait(5)
except (subprocess.TimeoutExpired, ProcessLookupError):
pass
current_process = next_process
next_process = None
else:
# No crossfade, just wait for current track to finish
current_process.wait()
with process_lock:
current_process = None
# Check control files after each song
action = check_control_files()
if action == "quit":
stop_all_processes()
exit()
elif action == "reload":
logger.info("Reload requested, restarting with new arguments...")
stop_all_processes()
return "reload"
def can_delete_file(filepath): def can_delete_file(filepath):
if not os.path.isfile(filepath): if not os.path.isfile(filepath):
@@ -528,13 +430,13 @@ def main():
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Player stopped by user") logger.info("Player stopped by user")
stop_all_processes() procman.stop_all()
except Exception as e: except Exception as e:
logger.error(f"Unexpected error: {e}") logger.error(f"Unexpected error: {e}")
stop_all_processes() procman.stop_all()
raise raise
finally: finally:
stop_all_processes() procman.stop_all()
if __name__ == '__main__': if __name__ == '__main__':
main() main()