mirror of
https://github.com/radio95-rnt/RadioPlayer.git
synced 2026-07-29 15:29:14 +02:00
tests in procman, and rejected tracks
This commit is contained in:
+8
-2
@@ -1,5 +1,6 @@
|
||||
import log95, abc
|
||||
from collections.abc import Sequence
|
||||
from typing import Literal
|
||||
from subprocess import Popen
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -22,15 +23,18 @@ class Process:
|
||||
started_at: float
|
||||
duration: float
|
||||
|
||||
class RejectedTrack(Exception): ...
|
||||
class ABC_ProcessManager(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def play(self, track: Track) -> Process: ...
|
||||
def play(self, track: Track) -> Process: """RejectedTrack may be raised by this function if the track cannot be played, but other can"""
|
||||
@abc.abstractmethod
|
||||
def anything_playing(self) -> bool: ...
|
||||
@abc.abstractmethod
|
||||
def stop_all(self, timeout: float | None = None) -> None: ...
|
||||
@abc.abstractmethod
|
||||
def wait_all(self, timeout: float | None = None) -> None: ...
|
||||
@abc.abstractmethod
|
||||
def test(self) -> bool: """Ran on startup. This should return false if this process manager can't play any track"""
|
||||
class BaseIMCModule:
|
||||
"""This is not a module to be used but rather a placeholder IMC api to be used in other modules"""
|
||||
def imc(self, imc: 'InterModuleCommunication') -> None:
|
||||
@@ -136,7 +140,9 @@ class PlaylistParser:
|
||||
and a dictionary of str:str consistent of the arguments which affect the files given
|
||||
"""
|
||||
return {}, []
|
||||
|
||||
|
||||
# This file is a part of RadioPlayer
|
||||
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
|
||||
@@ -2,7 +2,7 @@ from modules import BaseIMCModule, InterModuleCommunication
|
||||
from . import ActiveModifier, log95, Track, Path
|
||||
import os, glob, datetime
|
||||
from threading import Lock
|
||||
DEFAULT_CROSSFADE = 5
|
||||
DEFAULT_CROSSFADE = 6
|
||||
|
||||
from typing import TextIO
|
||||
_log_out: TextIO
|
||||
@@ -95,9 +95,6 @@ class Module(ActiveModifier):
|
||||
logger.info("Skipping...")
|
||||
self.skip_next -= 1
|
||||
return self.play(index, track, next_track)
|
||||
if index in self.skip_indexes:
|
||||
logger.info("Skipping...")
|
||||
return self.play(index, track, next_track)
|
||||
return (self.last_track, next_track), True
|
||||
elif len(self.originals):
|
||||
self.last_track = self.originals.pop(0)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from . import ABC_ProcessManager, Process, Track, Popen, tinytag
|
||||
from . import ABC_ProcessManager, Process, Track, Popen, tinytag, RejectedTrack
|
||||
from threading import Lock
|
||||
import subprocess, time
|
||||
|
||||
@@ -7,8 +7,9 @@ class ProcessManager(ABC_ProcessManager):
|
||||
self.lock = Lock()
|
||||
self.processes: list[Process] = []
|
||||
self.tinytag = tinytag.TinyTag()
|
||||
|
||||
def play(self, track: Track) -> Process:
|
||||
assert track.path.exists()
|
||||
if track.path.suffix not in self.tinytag.SUPPORTED_FILE_EXTENSIONS or not track.path.exists(): raise RejectedTrack
|
||||
cmd = ['ffplay', '-nodisp', '-hide_banner', '-autoexit', '-loglevel', 'quiet']
|
||||
|
||||
duration = self.tinytag.get(track.path.absolute(), tags=False).duration
|
||||
@@ -48,6 +49,14 @@ class ProcessManager(ABC_ProcessManager):
|
||||
try: process.process.wait(timeout)
|
||||
except subprocess.TimeoutExpired: process.process.terminate()
|
||||
self.processes.clear()
|
||||
def test(self) -> bool:
|
||||
proc = Popen(["ffplay"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
start = time.monotonic()
|
||||
while proc.poll() is None and (time.monotonic() - start) < 1: time.sleep(0.01)
|
||||
|
||||
if proc.poll() is None: proc.kill()
|
||||
return proc.poll() not in (None, 127)
|
||||
|
||||
procman = ProcessManager()
|
||||
|
||||
|
||||
@@ -59,3 +59,28 @@ class Module2(PlayerModule):
|
||||
|
||||
module = Module2()
|
||||
playlistmod = Module(), 2
|
||||
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
# distribute this software, either in source code form or as a compiled
|
||||
# binary, for any purpose, commercial or non-commercial, and by any
|
||||
# means.
|
||||
|
||||
# In jurisdictions that recognize copyright laws, the author or authors
|
||||
# of this software dedicate any and all copyright interest in the
|
||||
# software to the public domain. We make this dedication for the benefit
|
||||
# of the public at large and to the detriment of our heirs and
|
||||
# successors. We intend this dedication to be an overt act of
|
||||
# relinquishment in perpetuity of all present and future rights to this
|
||||
# software under copyright law.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# For more information, please refer to <https://unlicense.org>
|
||||
@@ -1,16 +0,0 @@
|
||||
Modules in the radioPlayer are quite simple.
|
||||
|
||||
First of all, ther are in total only 4 modules:
|
||||
- Observer (PlayerModule), this module is a passive observer, you can use this as a status api or to send the song metadata to your RDS encoder
|
||||
- Modifier (PlaylistModifierModule), module which intercepts the playlist before playing and replaced it, this module can shuffle the playlist, insert jingles or intros and outros
|
||||
- (!) Advisor (PlaylistAdvisor), this module is very important and is required to run and there can be only one of these in a core session. It is responsible for picking the playlist file itself as in a file path. This can be a scheduler, or just a constant
|
||||
- Active modifier (ActiveModifier), this module is optional, but there can still be only one. This module can replace the track while playing, allowing you to skip tracks or play tracks on demand, it can also extend the playlist
|
||||
|
||||
Each module shall have a python script in the modules directory. Each of the modules need to define one or more global variables in order to be seen by the core:
|
||||
- module (list['PlayerModule'] or 'PlayerModule'), this shall be just the list or one passive observer class
|
||||
- playlistmod ('PlaylistModifierModule', list['PlaylistModifierModule'], tuple['PlaylistModifierModule' | list['PlaylistModifierModule'], int]), module itself, list of modules or the module itself and list of them with an index integer which sets the order of modifiers (0 is first)
|
||||
- advisor ('PlaylistAdvisor')
|
||||
- activemod ('ActiveModifier')
|
||||
|
||||
NEW! The procman communicator allows you to get the track duration, but also STOP WHATEVER IS PLAYING! That means we can skip tracks WHILE THEY ARE PLAYING
|
||||
Newer! You can run advisor-less, this however also remvoes the usage of any and all playlist modifiers, because there is no playlist in such case
|
||||
@@ -6,3 +6,28 @@ class Module(PlaylistModifierModule):
|
||||
return playlist
|
||||
|
||||
playlistmod = Module(), -1
|
||||
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
# distribute this software, either in source code form or as a compiled
|
||||
# binary, for any purpose, commercial or non-commercial, and by any
|
||||
# means.
|
||||
|
||||
# In jurisdictions that recognize copyright laws, the author or authors
|
||||
# of this software dedicate any and all copyright interest in the
|
||||
# software to the public domain. We make this dedication for the benefit
|
||||
# of the public at large and to the detriment of our heirs and
|
||||
# successors. We intend this dedication to be an overt act of
|
||||
# relinquishment in perpetuity of all present and future rights to this
|
||||
# software under copyright law.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# For more information, please refer to <https://unlicense.org>
|
||||
+26
-1
@@ -37,4 +37,29 @@ class Module(PlayerModule):
|
||||
self._save_counts()
|
||||
def shutdown(self): self._save_counts()
|
||||
|
||||
module = Module()
|
||||
module = Module()
|
||||
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
# distribute this software, either in source code form or as a compiled
|
||||
# binary, for any purpose, commercial or non-commercial, and by any
|
||||
# means.
|
||||
|
||||
# In jurisdictions that recognize copyright laws, the author or authors
|
||||
# of this software dedicate any and all copyright interest in the
|
||||
# software to the public domain. We make this dedication for the benefit
|
||||
# of the public at large and to the detriment of our heirs and
|
||||
# successors. We intend this dedication to be an overt act of
|
||||
# relinquishment in perpetuity of all present and future rights to this
|
||||
# software under copyright law.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# For more information, please refer to <https://unlicense.org>
|
||||
+26
-1
@@ -66,4 +66,29 @@ class PopularitySorterModule(PlaylistModifierModule):
|
||||
self.logger.info("Popularity sorting complete.")
|
||||
return playlist
|
||||
|
||||
playlistmod = PopularitySorterModule(), 2
|
||||
playlistmod = PopularitySorterModule(), 2
|
||||
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
# distribute this software, either in source code form or as a compiled
|
||||
# binary, for any purpose, commercial or non-commercial, and by any
|
||||
# means.
|
||||
|
||||
# In jurisdictions that recognize copyright laws, the author or authors
|
||||
# of this software dedicate any and all copyright interest in the
|
||||
# software to the public domain. We make this dedication for the benefit
|
||||
# of the public at large and to the detriment of our heirs and
|
||||
# successors. We intend this dedication to be an overt act of
|
||||
# relinquishment in perpetuity of all present and future rights to this
|
||||
# software under copyright law.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# For more information, please refer to <https://unlicense.org>
|
||||
+4
-5
@@ -9,10 +9,8 @@ import mimetypes
|
||||
import shutil
|
||||
|
||||
def get_content_type(filename: str) -> str:
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
mime_type, _ = mimetypes.guess_type(filename, False)
|
||||
if mime_type: return mime_type
|
||||
|
||||
# Final fallback
|
||||
return "application/octet-stream"
|
||||
|
||||
from modules import InterModuleCommunication
|
||||
@@ -249,8 +247,9 @@ class Module(PlayerModule):
|
||||
"offset": track.offset,
|
||||
"focus_time_offset": track.focus_time_offset
|
||||
})
|
||||
self.data["playlist"] = json.dumps(api_data)
|
||||
try: self.ws_q.put({"event": "playlist", "data": api_data})
|
||||
output_data = {"playlist": api_data, "global_args": global_args}
|
||||
self.data["playlist"] = json.dumps(output_data)
|
||||
try: self.ws_q.put({"event": "playlist", "data": output_data})
|
||||
except Exception: pass
|
||||
|
||||
def on_new_track(self, index: int, track: Track, next_track: Track | None) -> None:
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ function handleMessage(msg) {
|
||||
document.getElementById("rds-text").textContent = msg.data?.rt ?? "";
|
||||
break;
|
||||
case "playlist":
|
||||
playlist = msg.data || [];
|
||||
playlist = msg.data?.playlist || [];
|
||||
renderAll();
|
||||
break;
|
||||
case "new_track":
|
||||
|
||||
+17
-11
@@ -11,7 +11,6 @@ def prefetch(path):
|
||||
with open(path, "rb") as f:
|
||||
fd = f.fileno()
|
||||
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_SEQUENTIAL)
|
||||
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_NOREUSE)
|
||||
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_WILLNEED)
|
||||
except Exception: pass
|
||||
|
||||
@@ -122,6 +121,9 @@ class ModuleManager:
|
||||
if not procman:
|
||||
self.logger.critical_error("Missing process mananger.")
|
||||
raise SystemExit("Missing process mananger.")
|
||||
if not procman.test():
|
||||
self.logger.critical_error("Process manager has failed its test.")
|
||||
raise SystemExit("Process manager has failed its test.")
|
||||
if not parser: self.logger.warning("Missing parser, advisor-less will be forced.")
|
||||
self.playlist_modifier_modules += neg_modifiers
|
||||
InterModuleCommunication(self.simple_modules + [self.playlist_advisor, ProcmanCommunicator(procman), self.active_modifier])
|
||||
@@ -158,7 +160,7 @@ class RadioPlayer:
|
||||
raise SystemExit(130)
|
||||
|
||||
def start(self):
|
||||
"""Single functon for starting the core, returns but might exit raising an SystemExit"""
|
||||
"""Single functon for starting the core, returns but might exit raising a SystemExit"""
|
||||
self.logger.info("Core starting, loading modules")
|
||||
self.modman.load_modules()
|
||||
self.procman, self.parser = self.modman.start_modules(self.arg)
|
||||
@@ -243,15 +245,18 @@ class RadioPlayer:
|
||||
self.logger.info("Now playing:", track.path.name)
|
||||
prefetch(track.path)
|
||||
|
||||
pr = self.procman.play(track)
|
||||
[module.on_new_track(song_i, pr.track, next_track) for module in self.modman.simple_modules if module]
|
||||
end_time = pr.started_at + pr.duration + pr.track.focus_time_offset
|
||||
self.procman.anything_playing()
|
||||
try:
|
||||
pr = self.procman.play(track)
|
||||
[module.on_new_track(song_i, pr.track, next_track) for module in self.modman.simple_modules if module]
|
||||
end_time = pr.started_at + pr.duration + pr.track.focus_time_offset
|
||||
self.procman.anything_playing()
|
||||
|
||||
while end_time >= time.monotonic() and pr.process.poll() is None:
|
||||
start = time.monotonic()
|
||||
[module.progress(song_i, track, time.monotonic() - pr.started_at, pr.duration, end_time - pr.started_at) for module in self.modman.simple_modules if module]
|
||||
if (elapsed := time.monotonic() - start) < 1 and (remaining_until_end := end_time - time.monotonic()) > 0: time.sleep(min(1 - elapsed, remaining_until_end))
|
||||
while end_time >= time.monotonic() and pr.process.poll() is None:
|
||||
start = time.monotonic()
|
||||
[module.progress(song_i, track, time.monotonic() - pr.started_at, pr.duration, end_time - pr.started_at) for module in self.modman.simple_modules if module]
|
||||
if (elapsed := time.monotonic() - start) < 1 and (remaining_until_end := end_time - time.monotonic()) > 0: time.sleep(min(1 - elapsed, remaining_until_end))
|
||||
except RejectedTrack: pass
|
||||
except BaseException: raise
|
||||
|
||||
self.procman.anything_playing()
|
||||
if next_track: prefetch(next_track.path)
|
||||
@@ -276,8 +281,9 @@ class RadioPlayer:
|
||||
class RotatingLog(io.TextIOWrapper):
|
||||
def write(self, *args, **kwargs) -> int:
|
||||
if self.tell() > 2_000_000:
|
||||
self.truncate(0)
|
||||
self.flush()
|
||||
self.seek(0)
|
||||
self.truncate(0)
|
||||
return super().write(*args, **kwargs)
|
||||
|
||||
def main():
|
||||
|
||||
Reference in New Issue
Block a user