mirror of
https://github.com/radio95-rnt/RadioPlayer.git
synced 2026-07-29 15:29:14 +02:00
move stuff to .playlist
This commit is contained in:
+44
-49
@@ -32,9 +32,7 @@ class ABC_ProcessManager(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def wait_all(self, timeout: float | None = None) -> None: ...
|
||||
class BaseIMCModule:
|
||||
"""
|
||||
This is not a module to be used but rather a placeholder IMC api to be used in other modules
|
||||
"""
|
||||
"""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:
|
||||
"""
|
||||
Receive an IMC object
|
||||
@@ -64,8 +62,7 @@ class ProcmanCommunicator(BaseIMCModule):
|
||||
elif int(op) == 2:
|
||||
self.procman.stop_all(data.get("timeout", None))
|
||||
return {"op": 2}
|
||||
elif int(op) == 3:
|
||||
raise NotImplementedError("This feature was removed.")
|
||||
elif int(op) == 3: raise NotImplementedError("This feature was removed.")
|
||||
elif int(op) == 4:
|
||||
return {"op": 4, "arg": self.procman.anything_playing()}
|
||||
elif int(op) == 5:
|
||||
@@ -73,16 +70,12 @@ class ProcmanCommunicator(BaseIMCModule):
|
||||
else: return
|
||||
|
||||
class PlayerModule(BaseIMCModule):
|
||||
"""
|
||||
Simple passive observer, this allows you to send the current track the your RDS encoder, or to your website
|
||||
"""
|
||||
"""Simple passive observer, this allows you to send the current track the your RDS encoder, or to your website"""
|
||||
def on_new_playlist(self, playlist: list[Track], global_args: dict[str, str]) -> None:
|
||||
"""This is called every new playlist"""
|
||||
pass
|
||||
def on_new_track(self, index: int, track: Track, next_track: Track | None) -> None:
|
||||
"""
|
||||
Called on every track including the ones added by the active modifier, you can check for that comparing the playlists[index] and the track
|
||||
"""
|
||||
"""Called on every track including the ones added by the active modifier, you can check for that comparing the playlists[index] and the track"""
|
||||
pass
|
||||
def progress(self, index: int, track: Track, elapsed: float, total: float, real_total: float) -> None:
|
||||
"""
|
||||
@@ -92,14 +85,10 @@ class PlayerModule(BaseIMCModule):
|
||||
"""
|
||||
pass
|
||||
def shutdown(self):
|
||||
"""
|
||||
Ran while shutting down
|
||||
"""
|
||||
"""Ran while shutting down"""
|
||||
pass
|
||||
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"""
|
||||
def modify(self, global_args: dict, playlist: list[Track]) -> list[Track] | None:
|
||||
"""
|
||||
global_args are playlist global args (see radioPlayer_playlist_file.txt)
|
||||
@@ -107,27 +96,17 @@ class PlaylistModifierModule:
|
||||
return playlist
|
||||
# No IMC, as we only run on new playlists
|
||||
class PlaylistAdvisor(BaseIMCModule):
|
||||
"""
|
||||
Only one of a playlist advisor can be loaded. This module picks the playlist file to play, this can be a scheduler or just a static file
|
||||
"""
|
||||
"""Only one of a playlist advisor can be loaded. This module picks the playlist file to play, this can be a scheduler or just a static file"""
|
||||
def advise(self, arguments: str | None) -> Path | None:
|
||||
"""
|
||||
Arguments are the arguments passed to the program on startup
|
||||
"""
|
||||
"""Arguments are the arguments passed to the program on startup"""
|
||||
return Path("/path/to/playlist.txt")
|
||||
def new_playlist(self) -> bool:
|
||||
"""
|
||||
Whether to play a new playlist, if this is True, then the player will refresh and fetch a new playlist, calling advise
|
||||
"""
|
||||
"""Whether to play a new playlist, if this is True, then the player will refresh and fetch a new playlist, calling advise"""
|
||||
return False
|
||||
class ActiveModifier(BaseIMCModule):
|
||||
"""
|
||||
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"""
|
||||
def arguments(self, arguments: str | None) -> None:
|
||||
"""
|
||||
Called at start up with the program arguments
|
||||
"""
|
||||
"""Called at start up with the program arguments"""
|
||||
pass
|
||||
def play(self, index: int, track: Track | None, next_track: Track | None) -> tuple[tuple[Track | None, Track | None], bool | None]:
|
||||
"""
|
||||
@@ -137,9 +116,7 @@ class ActiveModifier(BaseIMCModule):
|
||||
"""
|
||||
return (track, None), False
|
||||
def on_new_playlist(self, playlist: list[Track], global_args: dict[str, str]) -> None:
|
||||
"""
|
||||
Same behaviour as the basic module function
|
||||
"""
|
||||
"""Same behaviour as the basic module function"""
|
||||
pass
|
||||
class InterModuleCommunication:
|
||||
def __init__(self, modules: Sequence[BaseIMCModule | None]) -> None:
|
||||
@@ -147,31 +124,24 @@ class InterModuleCommunication:
|
||||
self.names_modules: dict[str, BaseIMCModule] = {}
|
||||
[module.imc(self) for module in modules if module]
|
||||
def broadcast(self, source: BaseIMCModule, data: object) -> None:
|
||||
"""
|
||||
Send data to all modules, other than ourself
|
||||
"""
|
||||
"""Send data to all modules, other than ourself"""
|
||||
source_name = next((k for k, v in self.names_modules.items() if v is source), None)
|
||||
for module in [f for f in self.modules if (f is not source) and f]: module.imc_data(source, source_name, data, True)
|
||||
def register(self, module: BaseIMCModule, name: str) -> bool:
|
||||
"""
|
||||
Register our module with a name, so we can be sent data via the send function
|
||||
"""
|
||||
"""Register our module with a name, so we can be sent data via the send function"""
|
||||
if name in self.names_modules.keys(): return False
|
||||
self.names_modules[name] = module
|
||||
return True
|
||||
def send(self, source: BaseIMCModule, name: str, data: object, aggresive: bool = True) -> object:
|
||||
"""
|
||||
Sends the data to a named module, and return its response
|
||||
"""
|
||||
def send(self, source: BaseIMCModule, name: str, data: object, aggressive: bool = True) -> object:
|
||||
"""Sends the data to a named module, and return its response"""
|
||||
if not name in self.names_modules.keys():
|
||||
if aggresive: raise ModuleNotFoundError("No such module")
|
||||
if aggressive: raise ModuleNotFoundError("No such module")
|
||||
return None
|
||||
|
||||
return self.names_modules[name].imc_data(source, next((k for k, v in self.names_modules.items() if v is source), None), data, False)
|
||||
|
||||
class PlaylistParser:
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
def __init__(self) -> None: pass
|
||||
def parse(self, playlist_path: Path) -> tuple[dict[str, str], list[tuple[list[str], dict[str, str]]]]:
|
||||
"""
|
||||
This should return the following information:
|
||||
@@ -180,4 +150,29 @@ class PlaylistParser:
|
||||
a entry is just a tuple of a list of strings (file paths)
|
||||
and a dictionary of str:str consistent of the arguments which affect the files given
|
||||
"""
|
||||
return {}, []
|
||||
return {}, []
|
||||
|
||||
# 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>
|
||||
@@ -10,4 +10,29 @@ class Module(PlaylistModifierModule):
|
||||
out.append(Track(track.path, track_crossfade, track_crossfade, do_cross_fade, track.args, focus_time_offset=-track_crossfade))
|
||||
else: out.append(track)
|
||||
return out
|
||||
playlistmod = (Module(),1)
|
||||
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>
|
||||
+13
-3
@@ -49,6 +49,16 @@ class Module2(PlayerModule):
|
||||
if self.secondary and (random.randint(1,3) == 1): jingle = random.choice(self.secondary)
|
||||
return self._imc.send(self, "activemod", {"action": "add_to_toplay", "songs": [f"!{jingle}"], "top": bool(data)})
|
||||
|
||||
options = Path("/home/user/Jingiel.mp3"), [Path("/home/user/jing2.opus"), Path("/home/user/Jing3.opus"), Path("/home/user/jing4.opus")]
|
||||
module = Module2(*options)
|
||||
playlistmod = Module(*options)
|
||||
master: Path | None = None
|
||||
jingles: list[Path] = []
|
||||
for file in Path("/home/user/mixes/.playlist/jingle").iterdir():
|
||||
if not file.is_file(): continue
|
||||
name, ext = file.name.rsplit('.', 1)
|
||||
if name.lower() == "master":
|
||||
master = file
|
||||
continue
|
||||
jingles.append(file)
|
||||
if not master: master = jingles.pop(0)
|
||||
|
||||
module = Module2(master, jingles)
|
||||
playlistmod = Module(master, jingles)
|
||||
|
||||
@@ -15,8 +15,8 @@ def load_dict_from_custom_format(file_path: str | Path) -> dict[str, str]:
|
||||
|
||||
class Module(PlayerModule):
|
||||
def __init__(self) -> None:
|
||||
self.logger = log95.log95("PlayCnt", output=_log_out)
|
||||
self.file = Path(__file__, "..", "..", "play_counter").resolve()
|
||||
self.logger = log95.log95("PlayCnt", output=_log_out) # That sounds bad...
|
||||
self.file = Path("/home/user/mixes/.playlist/count.txt").resolve()
|
||||
self.counts: dict[str, int] = {}
|
||||
loaded = load_dict_from_custom_format(self.file)
|
||||
for k, v in loaded.items():
|
||||
|
||||
+16
-18
@@ -29,7 +29,7 @@ class PopularitySorterModule(PlaylistModifierModule):
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
self.logger = log95.log95("PopSort", output=_log_out)
|
||||
self.play_counts_file = Path(__file__, "..", "..", "play_counter").resolve()
|
||||
self.play_counts_file = Path("/home/user/mixes/.playlist/count.txt").resolve()
|
||||
|
||||
def modify(self, global_args: dict, playlist: list[Track]) -> list[Track]:
|
||||
self.logger.info("Applying popularity-based sorting to the playlist...")
|
||||
@@ -41,23 +41,21 @@ class PopularitySorterModule(PlaylistModifierModule):
|
||||
|
||||
sorted_by_play_count = sorted(play_counts.items(), key=lambda item: item[1], reverse=True)
|
||||
|
||||
SORT_LEN = 100
|
||||
if len(playlist) >= SORT_LEN:
|
||||
top_paths = {path for path, count in sorted_by_play_count[:SORT_LEN]}
|
||||
least_top_paths = {path for path, count in sorted_by_play_count[-SORT_LEN:]}
|
||||
for a,b in zip(top_paths, least_top_paths):
|
||||
a_track = b_track = None
|
||||
a_i = b_i = 0
|
||||
for a_i, a_track in enumerate(playlist):
|
||||
if not a_track.official: continue
|
||||
if a_track.path == a: break
|
||||
if not a_track: continue
|
||||
for b_i, b_track in enumerate(playlist):
|
||||
if not b_track.official: continue
|
||||
if b_track.path == b: break
|
||||
if not b_track: continue
|
||||
if a_i < b_i:
|
||||
playlist[a_i], playlist[b_i] = playlist[b_i], playlist[a_i]
|
||||
SORT_LEN = len(playlist) // 3
|
||||
top_paths = {path for path, count in sorted_by_play_count[:SORT_LEN]}
|
||||
least_top_paths = {path for path, count in sorted_by_play_count[-SORT_LEN:]}
|
||||
for a,b in zip(top_paths, least_top_paths):
|
||||
a_track = b_track = None
|
||||
a_i = b_i = 0
|
||||
for a_i, a_track in enumerate(playlist):
|
||||
if not a_track.official: continue
|
||||
if a_track.path == a: break
|
||||
if not a_track: continue
|
||||
for b_i, b_track in enumerate(playlist):
|
||||
if not b_track.official: continue
|
||||
if b_track.path == b: break
|
||||
if not b_track: continue
|
||||
if a_i < b_i: playlist[a_i], playlist[b_i] = playlist[b_i], playlist[a_i]
|
||||
|
||||
i = 0
|
||||
while i < len(playlist) - 1:
|
||||
|
||||
@@ -40,8 +40,7 @@ class PlaintextParser(PlaylistParser):
|
||||
if "=" in arg:
|
||||
key, val = arg.split("=", 1)
|
||||
arguments[key] = val
|
||||
else:
|
||||
arguments[arg] = True
|
||||
else: arguments[arg] = True
|
||||
else:
|
||||
line, args = line.split("|", 1)
|
||||
args = args.split(";")
|
||||
@@ -49,8 +48,7 @@ class PlaintextParser(PlaylistParser):
|
||||
if "=" in arg:
|
||||
key, val = arg.split("=", 1)
|
||||
arguments[key] = val
|
||||
else:
|
||||
arguments[arg] = True
|
||||
else: arguments[arg] = True
|
||||
out.append(([f for f in glob.glob(line) if Path(f).is_file()], arguments))
|
||||
return global_arguments, out
|
||||
|
||||
|
||||
@@ -16,3 +16,28 @@ class Module(PlayerModule):
|
||||
if next_track: logger.info("Next up:", next_track.path.name)
|
||||
|
||||
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>
|
||||
+5
-11
@@ -20,7 +20,7 @@ async def ws_handler(websocket: ServerConnection, shared_data: dict, imc_q: mult
|
||||
"progress": json.loads(shared_data.get("progress", "{}")),
|
||||
"dirs": {"files": [i.name for i in list(MAIN_PATH_DIR.iterdir()) if i.is_file()], "dirs": [i.name for i in list(MAIN_PATH_DIR.iterdir()) if i.is_dir()], "base": str(MAIN_PATH_DIR)}
|
||||
}
|
||||
except Exception: initial = {"playlist": [], "track": {}, "progress": {}}
|
||||
except Exception: initial = {"playlist": [], "track": {}, "progress": {}, "dirs": {"files": [], "dirs": [], "base": ""}}
|
||||
await websocket.send(json.dumps({"event": "state", "data": initial}))
|
||||
|
||||
async for raw in websocket:
|
||||
@@ -66,13 +66,7 @@ async def ws_handler(websocket: ServerConnection, shared_data: dict, imc_q: mult
|
||||
else:
|
||||
await websocket.send(json.dumps({"data": result, "event": "toplay"})) # Yes, this is not an accident
|
||||
await asyncio.get_event_loop().run_in_executor(None, ws_q.put, {"data": result, "event": "toplay"})
|
||||
elif action == "skip_next":
|
||||
result = await get_imc("activemod", msg)
|
||||
if result is None: await websocket.send(json.dumps({"error": "timeout", "code": 504}))
|
||||
else:
|
||||
await websocket.send(json.dumps({"data": result, "event": action}))
|
||||
await asyncio.get_event_loop().run_in_executor(None, ws_q.put, {"data": result, "event": action}) # broadcast
|
||||
elif action == "skipc":
|
||||
elif action == "skip_next" or action == "skipc":
|
||||
result = await get_imc("activemod", msg)
|
||||
if result is None: await websocket.send(json.dumps({"error": "timeout", "code": 504}))
|
||||
else:
|
||||
@@ -261,12 +255,12 @@ class Module(PlayerModule):
|
||||
try: self.ws_q.put(None)
|
||||
except: pass
|
||||
|
||||
self.ipc_thread.join(timeout=2)
|
||||
self.ws_process.join(timeout=3)
|
||||
self.ipc_thread.join(timeout=1)
|
||||
self.ws_process.join(timeout=1)
|
||||
|
||||
if self.ws_process.is_alive():
|
||||
self.ws_process.terminate()
|
||||
self.ws_process.join(timeout=2)
|
||||
self.ws_process.join(timeout=1)
|
||||
|
||||
if self.ws_process.is_alive():
|
||||
self.ws_process.kill()
|
||||
|
||||
Reference in New Issue
Block a user