You've already forked RadioPlayer
mirror of
https://github.com/radio95-rnt/RadioPlayer.git
synced 2026-02-26 21:53:54 +01:00
module progress! (literally0123
This commit is contained in:
@@ -8,6 +8,7 @@ class Track:
|
|||||||
fade_in: bool
|
fade_in: bool
|
||||||
official: bool
|
official: bool
|
||||||
args: dict[str, str] | None
|
args: dict[str, str] | None
|
||||||
|
offset: float = 0.0
|
||||||
|
|
||||||
class PlayerModule:
|
class PlayerModule:
|
||||||
"""
|
"""
|
||||||
@@ -27,6 +28,8 @@ class PlayerModule:
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
|
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
|
pass
|
||||||
class PlaylistModifierModule:
|
class PlaylistModifierModule:
|
||||||
"""
|
"""
|
||||||
@@ -58,7 +61,7 @@ class PlaylistAdvisor:
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
|
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
|
||||||
pass
|
return None
|
||||||
class ActiveModifier:
|
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
|
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
|
pass
|
||||||
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
|
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
|
||||||
pass
|
return None
|
||||||
class InterModuleCommunication:
|
class InterModuleCommunication:
|
||||||
def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None:
|
def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None:
|
||||||
self.advisor = advisor
|
self.advisor = advisor
|
||||||
|
|||||||
13
modules/cli_progress.py
Normal file
13
modules/cli_progress.py
Normal file
@@ -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)
|
||||||
@@ -14,6 +14,7 @@ class Track:
|
|||||||
fade_in: bool
|
fade_in: bool
|
||||||
official: bool
|
official: bool
|
||||||
args: dict[str, str] | None
|
args: dict[str, str] | None
|
||||||
|
offset: float = 0.0
|
||||||
|
|
||||||
class PlayerModule:
|
class PlayerModule:
|
||||||
"""
|
"""
|
||||||
@@ -33,7 +34,7 @@ class PlayerModule:
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
|
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
|
||||||
pass
|
return None
|
||||||
class PlaylistModifierModule:
|
class PlaylistModifierModule:
|
||||||
"""
|
"""
|
||||||
Playlist modifier, this type of module allows you to shuffle, or put jingles into your playlist
|
Playlist modifier, this type of module allows you to shuffle, or put jingles into your playlist
|
||||||
@@ -64,7 +65,7 @@ class PlaylistAdvisor:
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
|
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
|
||||||
pass
|
return None
|
||||||
class ActiveModifier:
|
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
|
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
|
pass
|
||||||
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
|
def imc_data(self, source: 'PlayerModule | ActiveModifier | PlaylistAdvisor', data: object, broadcast: bool) -> object:
|
||||||
pass
|
return None
|
||||||
class InterModuleCommunication:
|
class InterModuleCommunication:
|
||||||
def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None:
|
def __init__(self, advisor: PlaylistAdvisor, active_modifier: ActiveModifier | None, simple_modules: list[PlayerModule]) -> None:
|
||||||
self.advisor = advisor
|
self.advisor = advisor
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ class ProcessManager:
|
|||||||
self.processes: list[Process] = []
|
self.processes: list[Process] = []
|
||||||
self.duration_cache = libcache.Cache()
|
self.duration_cache = libcache.Cache()
|
||||||
def _get_audio_duration(self, file_path):
|
def _get_audio_duration(self, file_path):
|
||||||
if result := self.duration_cache.getElement(file_path, False):
|
if result := self.duration_cache.getElement(file_path, False): return result
|
||||||
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)
|
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:
|
if result.returncode == 0:
|
||||||
@@ -44,20 +43,22 @@ class ProcessManager:
|
|||||||
self.duration_cache.saveElement(file_path, result, (60*60))
|
self.duration_cache.saveElement(file_path, result, (60*60))
|
||||||
return result
|
return result
|
||||||
return None
|
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']
|
cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet']
|
||||||
|
|
||||||
duration = self._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 exist?", 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 = []
|
filters = []
|
||||||
if fade_in: filters.append(f"afade=t=in:st=0:d={fade_time}")
|
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)])
|
if filters: cmd.extend(['-af', ",".join(filters)])
|
||||||
|
|
||||||
cmd.append(track_path)
|
cmd.append(track_path)
|
||||||
|
|
||||||
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
|
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)
|
with self.lock: self.processes.append(pr)
|
||||||
return pr
|
return pr
|
||||||
def anything_playing(self) -> bool:
|
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 = log95.log95("PARSER", logger_level)
|
||||||
|
|
||||||
parser_log.debug("Reading", playlist_path)
|
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]:
|
def check_for_imports(lines: list[str], seen=None) -> list[str]:
|
||||||
nonlocal parser_log
|
nonlocal parser_log
|
||||||
if seen is None: seen = set()
|
if seen is None: seen = set()
|
||||||
out = []
|
out = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
if line.startswith("@"):
|
if line.startswith("@"):
|
||||||
target = line.removeprefix("@")
|
target = line.removeprefix("@")
|
||||||
if target not in seen:
|
if target not in seen:
|
||||||
parser_log.debug("Importing", target)
|
parser_log.debug("Importing", target)
|
||||||
seen.add(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))
|
out.extend(check_for_imports(sub_lines, seen))
|
||||||
else: out.append(line)
|
else: out.append(line)
|
||||||
return out
|
return out
|
||||||
@@ -127,7 +129,8 @@ def parse_playlistfile(playlist_path: str) -> tuple[dict[str, str], list[tuple[l
|
|||||||
global_arguments = {}
|
global_arguments = {}
|
||||||
for line in lines:
|
for line in lines:
|
||||||
arguments = {}
|
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 "|" in line:
|
||||||
if line.startswith("|"): # No file name, we're defining global arguments
|
if line.startswith("|"): # No file name, we're defining global arguments
|
||||||
args = line.removeprefix("|").split(";")
|
args = line.removeprefix("|").split(";")
|
||||||
@@ -148,14 +151,14 @@ def play_playlist(playlist_path):
|
|||||||
if not playlist_advisor: raise Exception("No playlist advisor")
|
if not playlist_advisor: raise Exception("No playlist advisor")
|
||||||
|
|
||||||
try: global_args, parsed = parse_playlistfile(playlist_path)
|
try: global_args, parsed = parse_playlistfile(playlist_path)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
logger.info(f"Exception while parsing playlist, retrying in 15 seconds...")
|
logger.info(f"Exception ({e}) while parsing playlist, retrying in 15 seconds...")
|
||||||
time.sleep(15)
|
time.sleep(15)
|
||||||
return
|
return
|
||||||
|
|
||||||
playlist: list[Track] = []
|
playlist: list[Track] = []
|
||||||
for (lns, args) in parsed:
|
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 playlist_modifier_modules: playlist = module.modify(global_args, playlist)
|
||||||
for module in simple_modules: module.on_new_playlist(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)
|
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
|
ttw = pr.duration
|
||||||
if track.fade_out: ttw -= cross_fade
|
if track.fade_out: ttw -= cross_fade
|
||||||
@@ -209,18 +212,11 @@ def play_playlist(playlist_path):
|
|||||||
end_time = time.time() + ttw
|
end_time = time.time() + ttw
|
||||||
loop_start = time.time() # Outside the loop
|
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():
|
while end_time >= time.time():
|
||||||
start = time.time()
|
start = time.time()
|
||||||
# do some module callback
|
|
||||||
|
|
||||||
total_uptime = time.time() - loop_start
|
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
|
elapsed = time.time() - start
|
||||||
remaining_until_end = end_time - time.time()
|
remaining_until_end = end_time - time.time()
|
||||||
@@ -233,6 +229,7 @@ def play_playlist(playlist_path):
|
|||||||
if not extend: song_i += 1
|
if not extend: song_i += 1
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
logger.info("Core is starting, loading modules")
|
||||||
global playlist_advisor, active_modifier
|
global playlist_advisor, active_modifier
|
||||||
for filename in os.listdir(MODULES_DIR):
|
for filename in os.listdir(MODULES_DIR):
|
||||||
if filename.endswith(".py") and filename != "__init__.py":
|
if filename.endswith(".py") and filename != "__init__.py":
|
||||||
@@ -280,12 +277,16 @@ def main():
|
|||||||
if not playlist_advisor:
|
if not playlist_advisor:
|
||||||
logger.critical_error("Playlist advisor was not found")
|
logger.critical_error("Playlist advisor was not found")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
logger.info("Modules initialized, starting the IMC")
|
||||||
|
|
||||||
imc = InterModuleCommunication(playlist_advisor, active_modifier, simple_modules)
|
imc = InterModuleCommunication(playlist_advisor, active_modifier, simple_modules)
|
||||||
playlist_advisor.imc(imc)
|
playlist_advisor.imc(imc)
|
||||||
if active_modifier: active_modifier.imc(imc)
|
if active_modifier: active_modifier.imc(imc)
|
||||||
for module in simple_modules: module.imc(imc)
|
for module in simple_modules: module.imc(imc)
|
||||||
|
|
||||||
|
logger.info("Starting playback.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
arg = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else None
|
arg = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else None
|
||||||
if active_modifier: active_modifier.arguments(arg)
|
if active_modifier: active_modifier.arguments(arg)
|
||||||
|
|||||||
Reference in New Issue
Block a user