diff --git a/modules/__init__.py b/modules/__init__.py index 54fad9f..d01c372 100644 --- a/modules/__init__.py +++ b/modules/__init__.py @@ -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 {}, [] \ No newline at end of file + 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 \ No newline at end of file diff --git a/modules/crossfade_init.py b/modules/crossfade_init.py index fb5c56c..4246b77 100644 --- a/modules/crossfade_init.py +++ b/modules/crossfade_init.py @@ -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) \ No newline at end of file +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 \ No newline at end of file diff --git a/modules/jingle.py b/modules/jingle.py index 428633f..dce438a 100644 --- a/modules/jingle.py +++ b/modules/jingle.py @@ -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) diff --git a/modules/play_counter.py b/modules/play_counter.py index 46ce128..8413713 100644 --- a/modules/play_counter.py +++ b/modules/play_counter.py @@ -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(): diff --git a/modules/play_sort.py b/modules/play_sort.py index f7ecfd5..a66cb33 100644 --- a/modules/play_sort.py +++ b/modules/play_sort.py @@ -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: diff --git a/modules/playlist_parser.py b/modules/playlist_parser.py index fed97aa..d2bb188 100644 --- a/modules/playlist_parser.py +++ b/modules/playlist_parser.py @@ -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 diff --git a/modules/skipper.py b/modules/skipper.py index 5b7ea7b..9f1630b 100644 --- a/modules/skipper.py +++ b/modules/skipper.py @@ -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 \ No newline at end of file diff --git a/modules/web.py b/modules/web.py index 512ce16..f8062f1 100644 --- a/modules/web.py +++ b/modules/web.py @@ -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()