diff --git a/CUIAudioPlayer/CompatibilityPatch.py b/CUIAudioPlayer/CompatibilityPatch.py deleted file mode 100644 index d8952c7..0000000 --- a/CUIAudioPlayer/CompatibilityPatch.py +++ /dev/null @@ -1,77 +0,0 @@ -from typing import Union, Iterable -from collections import OrderedDict -import GetModuleReference - - -# Target 3.7 to 3.8, only listing what I'm using. -# will run these in order, so adding suffix as sorting key. - - -def a_suffix_prefix_patch(): - try: - getattr(str, "endswith") - - except AttributeError: - # < Python 3.8 - - def mock_suffix(self: str, suffix: Union[str, Iterable]): - if isinstance(suffix, str): - suffix = [suffix] - - for suffix_candidate in suffix: - # Python's `in` is faster than manual search. - # We first check if sub-string is in string. - if suffix_candidate in self: - - # if it contains, then check again with last char off. - # If it still finds string, it's not suffix. - if suffix_candidate not in self[:-1]: - return True - # If it can't find string, then it's suffix. - - return False - - def mock_prefix(self: str, prefix: Union[str, Iterable]): - if isinstance(prefix, str): - prefix = [prefix] - - for prefix_candidate in prefix: - - if prefix_candidate in self: - if prefix_candidate not in self[1:]: - return True - - return False - - str.endswith = mock_suffix - str.startswith = mock_prefix - - -def b_remove_suffix_prefix_patch(): - """Only applies for str. No bytes.""" - try: - getattr(str, "removesuffix") - except AttributeError: - # < Python 3.9 - - def mock_remove_suffix(self: str, suffix: str): - if self.endswith(suffix): - return self[:-len(suffix)] - - def mock_remove_prefix(self: str, prefix: str): - if self.startswith(prefix): - return self[len(prefix):] - - str.removesuffix = mock_remove_suffix - str.removeprefix = mock_remove_prefix - - -def fetch_and_run_patches(): - blacklist = ["fetch_and_run_patches"] - function_dict = GetModuleReference.list_function(__name__, blacklist, return_dict=True) - - for _, patch in OrderedDict(sorted(function_dict.items())).items(): - patch() - - -fetch_and_run_patches() diff --git a/CUIAudioPlayer/FileWalker.py b/CUIAudioPlayer/FileWalker.py index abe5afe..e6e625a 100644 --- a/CUIAudioPlayer/FileWalker.py +++ b/CUIAudioPlayer/FileWalker.py @@ -17,7 +17,7 @@ # TODO: separate audio related logic from pathwrapper via subclass class PathWrapper: primary_formats = set("." + key.lower() for key in sf.available_formats().keys()) - secondary_formats = {".m4a", ".mp3"} if PY_DUB_ENABLED else {} + secondary_formats = {".m4a", ".mp3"} if PY_DUB_ENABLED else set() supported_formats = primary_formats | secondary_formats supported_formats = supported_formats | set(key.upper() for key in supported_formats) @@ -33,42 +33,43 @@ def __init__(self, path: str = "./"): self.folder_list: List[pathlib.Path] = [] def list_audio(self) -> Generator[pathlib.Path, None, None]: - return (path_obj for path_obj in self.list_file() if path_obj.suffix in self.supported_formats) + yield from (path_obj for path_obj in self.list_file() if path_obj.suffix in self.supported_formats) def list_folder(self) -> Generator[pathlib.Path, None, None]: """ First element will be current folder location. either use next() or list()[1:] to skip it. """ - def generator(): - yield self.current_path.parent - for item in self.current_path.glob("*/"): - if item.is_dir(): - yield item - - return generator() + yield self.current_path.parent + yield from (path_ for path_ in self.current_path.iterdir() if path_.is_dir()) def list_file(self) -> Generator[pathlib.Path, None, None]: """ Can't use glob as it match folders such as .git, using pathlib.Path object instead. """ - return (item for item in self.current_path.glob("*/") if item.is_file()) + yield from (item for item in self.current_path.iterdir() if item.is_file()) - def step_in(self, directory: Union[str, pathlib.Path]): + def step_in(self, directory_idx: int): """ Relative / Absolute paths supported. """ - self.current_path = self.current_path.joinpath(directory) + try: + self.current_path = self.folder_list[directory_idx] + except IndexError as err: + raise NotADirectoryError(f"Directory index {directory_idx} does not exist!") from err + self.refresh_list() return self.current_path - def step_out(self, depth=1): - if depth <= 0: + def step_out(self): + + if self.current_path == self.current_path.parent: return self.current_path - self.current_path = self.current_path.parents[depth - 1] + self.current_path = self.current_path.parent + self.refresh_list() return self.current_path @@ -81,18 +82,12 @@ def refresh_list(self): def fetch_meta(self): # This might have to deal the cases such as path changing before generator fires up. - def generator(): - for file_dir in self.list_audio(): - yield TinyTag.get(file_dir) - - return generator() + for file_dir in self.list_audio(): + yield TinyTag.get(file_dir) def fetch_tag_data(self): - def generator(): - for file_dir in self.list_audio(): - yield TinyTag.get(file_dir) - - return generator() + for file_dir in self.list_audio(): + yield TinyTag.get(file_dir) def __len__(self): return len(self.folder_list) + len(self.audio_file_list) @@ -108,9 +103,9 @@ def __getitem__(self, item: int): return self.audio_file_list[item - len(self.folder_list)] def index(self, target: Union[str, pathlib.Path]): + path_ = pathlib.Path(target) try: - return len(self.folder_list) + self.audio_file_list.index(target) - except ValueError: - # assuming it's pure string directory. - path_converted = pathlib.Path(target) - return len(self.folder_list) + self.audio_file_list.index(path_converted) + return len(self.folder_list) + self.audio_file_list.index(path_) + except ValueError as err: + raise IndexError(f"Cannot find given target '{path_.as_posix()}'!") from err + diff --git a/CUIAudioPlayer/LoggingConfigurator.py b/CUIAudioPlayer/LoggingConfigurator.py index b257927..c2be575 100644 --- a/CUIAudioPlayer/LoggingConfigurator.py +++ b/CUIAudioPlayer/LoggingConfigurator.py @@ -1,6 +1,9 @@ import logging import inspect +from loguru import logger +assert logger +# all below are now deprecated LOG_DETAILED_CALLER = True # This will log where the function is from. @@ -23,6 +26,7 @@ def get_caller_stack_name(depth=1): """ Gets the name of caller. + :param depth: determine which scope to inspect, for nested usage. """ return inspect.stack()[depth][3] @@ -94,7 +98,8 @@ def inner(msg, *args, **kwargs): return inner -logger = CallerLoggedLogger() +# logger = CallerLoggedLogger() + # def logging_decorator(func): # async def inner() diff --git a/CUIAudioPlayer/Player/PlayerLogic.py b/CUIAudioPlayer/Player/PlayerLogic.py index eeb0bce..15a8e0a 100644 --- a/CUIAudioPlayer/Player/PlayerLogic.py +++ b/CUIAudioPlayer/Player/PlayerLogic.py @@ -4,17 +4,16 @@ from __future__ import annotations -import array import itertools from contextlib import contextmanager -from typing import TYPE_CHECKING, Iterable, Callable, Type, Tuple +from typing import TYPE_CHECKING, Iterable, Callable, Type, Tuple, Union import py_cui from FileWalker import PathWrapper -from SDManager.StreamManager import StreamManager +from SDManager.StreamManager import StreamManager, NoAudioPlayingError from LoggingConfigurator import logger from .TUI import AudioPlayerTUI -from .PlayerStates import PlayerStates, AudioUnloaded +from .PlayerStates import PlayerStates, AudioUnloaded, AudioRunning from . import ( add_callback_patch, fit_to_actual_width, @@ -37,23 +36,23 @@ class PlayerLogicMixin: # Excl. Border, Spacing of widget from abs size. usable_offset_y, usable_offset_x = 2, 6 - def _init_playlist(self: AudioPlayer): + def init_playlist(self: AudioPlayer): """ Create itertools.cycle generator that acts as a playlist """ # Shuffling is harder than imagined! # https://engineering.atspotify.com/2014/02/28/how-to-shuffle-songs/ - + copy = [i for i in self.path_wrapper.audio_file_list] cycle_gen = itertools.cycle( - array.array("i", (n for n in range(len(self.path_wrapper.audio_file_list)))) + copy ) - for _ in range(self.currently_playing + 1): - next(cycle_gen) + while next(cycle_gen) != self.current_playing_file: + pass self._current_play_generator = cycle_gen - logger.debug("Initialized playlist generator.") + logger.debug(f"Initialized playlist generator from directory '{self.path_wrapper.current_path.as_posix()}'") def playlist_next(self: AudioPlayer): """ @@ -62,13 +61,11 @@ def playlist_next(self: AudioPlayer): :return: Index of next soundtrack on PathWrapper """ - try: - return next(self._current_play_generator) - except TypeError: - self._init_playlist() - return next(self._current_play_generator) + return next(self._current_play_generator) - def get_absolute_size(self: AudioPlayer, widget: py_cui.widgets.Widget) -> Tuple[int, int]: + def get_absolute_size( + self: AudioPlayer, widget: py_cui.widgets.Widget + ) -> Tuple[int, int]: """ Get absolute dimensions of widget including borders. @@ -80,15 +77,14 @@ def get_absolute_size(self: AudioPlayer, widget: py_cui.widgets.Widget) -> Tuple return abs_y - self.usable_offset_y, abs_x - self.usable_offset_x @property - def currently_playing(self: AudioPlayer) -> int: + def current_playing_idx(self: AudioPlayer) -> int: """ - Returns index of currently played track. Currently using slow method for simplicity. + Returns index of currently played track. :return: index of played file """ - file_name = self.stream.audio_info.loaded_data.name - return self.path_wrapper.index(file_name) + return self.path_wrapper.index(self.current_playing_file) class AudioPlayer(AudioPlayerTUI, PlayerLogicMixin): @@ -114,7 +110,9 @@ def __init__(self, root: py_cui.PyCUI): # -- Key binds self.audio_list.add_key_command(py_cui.keys.KEY_ENTER, self._play_cb_enter) - self.volume_slider.add_key_command(py_cui.keys.KEY_SPACE, self._play_cb_space_bar) + self.volume_slider.add_key_command( + py_cui.keys.KEY_SPACE, self._play_cb_space_bar + ) for widget in ( self.audio_list, @@ -122,15 +120,25 @@ def __init__(self, root: py_cui.PyCUI): self.meta_list, ): widget.add_key_command(py_cui.keys.KEY_SPACE, self._play_cb_space_bar) - widget.add_key_command(py_cui.keys.KEY_LEFT_ARROW, self._adjust_playback_left) - widget.add_key_command(py_cui.keys.KEY_RIGHT_ARROW, self._adjust_playback_right) + widget.add_key_command( + py_cui.keys.KEY_LEFT_ARROW, self._adjust_playback_left + ) + widget.add_key_command( + py_cui.keys.KEY_RIGHT_ARROW, self._adjust_playback_right + ) # -- Color rules self.info_box.add_text_color_rule("ERR:", py_cui.WHITE_ON_RED, "startswith") - self.audio_list.add_text_color_rule(self.symbols["play"], py_cui.WHITE_ON_YELLOW, "contains") - self.audio_list.add_text_color_rule(self.symbols["pause"], py_cui.WHITE_ON_YELLOW, "contains") - self.audio_list.add_text_color_rule(self.symbols["stop"], py_cui.WHITE_ON_YELLOW, "contains") + self.audio_list.add_text_color_rule( + self.symbols["play"], py_cui.WHITE_ON_YELLOW, "contains" + ) + self.audio_list.add_text_color_rule( + self.symbols["pause"], py_cui.WHITE_ON_YELLOW, "contains" + ) + self.audio_list.add_text_color_rule( + self.symbols["stop"], py_cui.WHITE_ON_YELLOW, "contains" + ) self.audio_list.add_text_color_rule( r"DIR", py_cui.CYAN_ON_BLACK, "startswith", include_whitespace=False ) @@ -138,11 +146,14 @@ def __init__(self, root: py_cui.PyCUI): # -- State self.player_state: Type[PlayerStates] = AudioUnloaded self.initial_volume_position = self.volume_slider.get_slider_value() + self.global_volume_multiplier = 0.4 self._digit: int = 0 # -- Path and stream instance self.stream = StreamManager(self.show_progress_wrapper(), self.play_next) + self.volume_callback() self.path_wrapper = PathWrapper() + self.current_playing_file: Union[pathlib.Path, None] = None # -- Generator instance and states self._current_play_generator = None @@ -229,22 +240,25 @@ def volume_callback(self): Callback for volume slider that adjust multiplier inside StreamManager. """ - self.stream.multiplier = self.volume_slider.get_slider_value() / self.initial_volume_position + self.stream.multiplier = self.global_volume_multiplier * ( + self.volume_slider.get_slider_value() / self.initial_volume_position + ) - def play_stream(self, audio_idx=None) -> int: + def play_stream(self, audio_file: Union[None, pathlib.Path] = None, next_=False) -> int: """ Load audio and starts audio stream. Returns True if successful. - :param audio_idx: If not None, will play given index of track instead + :param audio_file: If not None, will play given index of track instead + :param next_: Used to not refresh playlist when playing next. """ - if not audio_idx: - audio_idx = self.selected_idx + if not audio_file: + audio_file = self.selected_idx_path try: - self.stream.load_stream(self.path_wrapper[audio_idx]) + self.stream.load_stream(audio_file) except IndexError: - logger.debug(f"Invalid idx: {audio_idx} / {len(self.path_wrapper)}") + logger.debug(f"Invalid idx: {audio_file} / {len(self.path_wrapper)}") return False except RuntimeError as err: @@ -253,15 +267,18 @@ def play_stream(self, audio_idx=None) -> int: self.write_info(msg) return False - self._refresh_list(search_files=False) + self.current_playing_file = audio_file + self.refresh_list(search_files=False) self.stream.start_stream() - self._init_playlist() + + if not next_: + self.init_playlist() return True # End of primary callback - def _refresh_list(self, search_files=True): + def refresh_list(self, search_files=True): """ Refresh directory contents. If search_files is True, will also update cached files list. Will separate this after changing list generating method to use internal item list of ScrollWidget. @@ -319,7 +336,9 @@ def write_info(self, text: str): if text: # Will have hard time to implement cycling texts. - fit_text = fit_to_actual_width(str(text), self.get_absolute_size(self.info_box)[-1]) + fit_text = fit_to_actual_width( + str(text), self.get_absolute_size(self.info_box)[-1] + ) self.info_box.set_text(fit_text) else: self.info_box.clear() @@ -343,7 +362,9 @@ def _write_to_scroll_widget( def wrapper_gen(): for source_line in lines: - yield from fit_to_actual_width_multiline(source_line, usable_x + offset) + yield from fit_to_actual_width_multiline( + source_line, usable_x + offset + ) for line in wrapper_gen(): widget.add_item(line) @@ -381,7 +402,9 @@ def _mark_target(self, track_idx, replace_target: str): source = self.audio_list.get_item_list() string = source[track_idx] - source[track_idx] = (string[: self._digit] + replace_target + string[self._digit + 1:]) + source[track_idx] = ( + string[: self._digit] + replace_target + string[self._digit + 1:] + ) self.write_audio_list(source) def reset_marking(self, track_idx): @@ -420,7 +443,9 @@ def mark_as_stopped(self, track_idx): self._mark_target(track_idx, self.symbols["stop"]) - def show_progress_wrapper(self, paused=False) -> Callable[[AudioObject.AudioInfo, int], None]: + def show_progress_wrapper( + self, paused=False + ) -> Callable[[AudioObject.AudioInfo, int], None]: """ Wrapper for function that handles progress. Returning callable that is meant to run in sounddevice callback. @@ -443,7 +468,9 @@ def show_progress(audio_info: AudioObject.AudioInfo, current_frame): duration_digit = digit(duration) format_specifier = f"0{duration_digit}.1f" - time_string = f"|{current_frame * duration / max_frame:{format_specifier}}/{duration}" + time_string = ( + f"|{current_frame * duration / max_frame:{format_specifier}}/{duration}" + ) _, x_width = self.info_box.get_absolute_dimensions() @@ -463,7 +490,7 @@ def play_next(self): Play next track. Called by finished callback of sounddevice when conditions are met. """ - logger.debug(f"Proceed: {self.stream.stop_flag}") + logger.debug(f"Stop Flag: {self.stream.stop_flag}") if not self.stream.stop_flag: next_ = self.playlist_next() @@ -471,11 +498,19 @@ def play_next(self): logger.debug(f"Playing Next - {next_}") with self.maintain_current_view(): - if not self.play_stream(next_): + if not self.play_stream(next_, True): + # TODO: add mark_as_error + # TODO: add total player length with playlist gen so it can find infinite fail loop logger.warning("Error playing next track. Moving on.") self.play_next() else: - self.mark_as_playing(self.currently_playing) + # update state + self.player_state = AudioRunning + logger.debug(f"Next track started, state: {self.player_state}") + try: + self.mark_as_playing(self.current_playing_idx) + except IndexError: + pass # Helper functions ----------------------------------------- @@ -517,5 +552,5 @@ def maintain_current_view(self): def _tui_destroy_callback(self): try: self.stream.stop_stream() - except RuntimeError: + except (RuntimeError, NoAudioPlayingError): pass diff --git a/CUIAudioPlayer/Player/PlayerStates.py b/CUIAudioPlayer/Player/PlayerStates.py index 923f847..20a513f 100644 --- a/CUIAudioPlayer/Player/PlayerStates.py +++ b/CUIAudioPlayer/Player/PlayerStates.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING from LoggingConfigurator import logger -from SDManager.StreamManager import StreamManager +from SDManager.StreamManager import StreamManager, NoAudioPlayingError if TYPE_CHECKING: from .PlayerLogic import AudioPlayer @@ -48,13 +48,17 @@ def on_stop_click(audio_player: AudioPlayer): try: audio_player.stream.stop_stream() - except (RuntimeError, FileNotFoundError): + except (RuntimeError, FileNotFoundError, NoAudioPlayingError): return with audio_player.maintain_current_view(): # revert texts - audio_player._refresh_list(search_files=False) - audio_player.mark_as_stopped(audio_player.currently_playing) + audio_player.refresh_list(search_files=False) + try: + audio_player.mark_as_stopped(audio_player.current_playing_idx) + except IndexError: + # playing audio is not in current dir, ignore + pass audio_player.write_info("") audio_player.player_state = AudioStopped @@ -79,7 +83,7 @@ def on_reload_click(audio_player: AudioPlayer): widget.clear() audio_player.stream = StreamManager(audio_player.show_progress_wrapper(), audio_player.play_next) - audio_player._refresh_list(search_files=True) + audio_player.refresh_list(search_files=True) audio_player.volume_callback() audio_player.player_state = AudioUnloaded @@ -87,24 +91,26 @@ def on_reload_click(audio_player: AudioPlayer): @staticmethod def on_audio_list_enter_press(audio_player: AudioPlayer): """ - Enters directory if selected item is one of them. Else will stop current track and play selected track. + Enters if selected item is directory. Else will stop current track and play selected track. """ - if audio_player.selected_idx_path in audio_player.path_wrapper.folder_list: - audio_player.path_wrapper.step_in(audio_player.selected_idx_path) - PlayerStates.on_reload_click(audio_player) + if audio_player.selected_idx_path.is_dir(): + audio_player.path_wrapper.step_in(audio_player.selected_idx) + # PlayerStates.on_reload_click(audio_player) + audio_player.refresh_list(search_files=True) else: # force play audio with audio_player.maintain_current_view(): try: audio_player.stream.stop_stream() - except RuntimeError as err: - logger.warning(str(err)) - except FileNotFoundError as err: + except (RuntimeError, FileNotFoundError, NoAudioPlayingError) as err: logger.warning(str(err)) if audio_player.play_stream(): - audio_player.mark_as_playing(audio_player.currently_playing) + try: + audio_player.mark_as_playing(audio_player.current_playing_idx) + except IndexError: + pass audio_player.player_state = AudioRunning @staticmethod @@ -157,7 +163,10 @@ def on_audio_list_space_press(audio_player: AudioPlayer): with audio_player.maintain_current_view(): audio_player.stream.start_stream() - audio_player.mark_as_playing(audio_player.currently_playing) + try: + audio_player.mark_as_playing(audio_player.current_playing_idx) + except IndexError: + pass audio_player.player_state = AudioRunning @@ -190,7 +199,6 @@ def on_next_track_click(audio_player: AudioPlayer): """ audio_player.stream.stop_stream(run_finished_callback=False) - audio_player.player_state = AudioStopped @staticmethod def on_audio_list_space_press(audio_player: AudioPlayer): @@ -200,7 +208,10 @@ def on_audio_list_space_press(audio_player: AudioPlayer): with audio_player.maintain_current_view(): audio_player.stream.pause_stream() - audio_player.mark_as_paused(audio_player.currently_playing) + try: + audio_player.mark_as_paused(audio_player.current_playing_idx) + except IndexError: + pass audio_player.player_state = AudioPaused @@ -250,7 +261,10 @@ def on_audio_list_space_press(audio_player: AudioPlayer): with audio_player.maintain_current_view(): audio_player.stream.pause_stream() - audio_player.mark_as_playing(audio_player.currently_playing) + try: + audio_player.mark_as_playing(audio_player.current_playing_idx) + except IndexError: + pass audio_player.player_state = AudioRunning diff --git a/CUIAudioPlayer/Player/TUI.py b/CUIAudioPlayer/Player/TUI.py index fcb730a..99fb147 100644 --- a/CUIAudioPlayer/Player/TUI.py +++ b/CUIAudioPlayer/Player/TUI.py @@ -18,7 +18,7 @@ def __init__(self, root: py_cui.PyCUI): self.meta_list = self._root.add_scroll_menu("Meta", 0, 5, column_span=2, row_span=5) self.info_box = self._root.add_text_box("Info", 3, 0, column_span=4) - self.volume_slider = self._root.add_slider("Volume", 3, 4, column_span=1, min_val=0, max_val=10, init_val=8) + self.volume_slider = self._root.add_slider("Volume", 3, 4, column_span=1, min_val=0, max_val=10, init_val=6) self.play_btn = self._root.add_button("Play", 4, 0) self.stop_btn = self._root.add_button("Stop", 4, 1) @@ -32,7 +32,7 @@ def __init__(self, root: py_cui.PyCUI): # Theme changes self.volume_slider.toggle_value() - self.volume_slider.align_to_bottom() + self.volume_slider.align_to_middle() self.volume_slider.set_bar_char("█") self.play_btn.set_color(py_cui.YELLOW_ON_BLACK) diff --git a/CUIAudioPlayer/Player/__init__.py b/CUIAudioPlayer/Player/__init__.py index 2308ba4..fd27b0d 100644 --- a/CUIAudioPlayer/Player/__init__.py +++ b/CUIAudioPlayer/Player/__init__.py @@ -12,13 +12,14 @@ def add_callback_patch(widget_: py_cui.widgets.Widget, callback: Callable, keypr """ Adding callback support for widget that lacks such as ScrollMenu. - :param widget_: Any widget you want to add callback on each input events. - :param callback: Any callables - :param keypress_only: Decides whether to replace mouse input handler alongside with key input one. + Args: + widget_: Any widget you want to add callback on each input events. + callback: Any callables + keypress_only: Decides whether to replace mouse input handler alongside with key input one. """ # Sequence is _draw -> _handle_mouse_press, so patching on _draw results 1 update behind. - # Therefore we need to patch both _handle_mouse_press and _handle_keyboard_press. + # Therefore, we need to patch both _handle_mouse_press and _handle_keyboard_press. def patch_factory(old_func): # fix for late binding issue: stackoverflow.com/questions/3431676 @@ -109,7 +110,7 @@ def fit_to_actual_width(text: str, length_lim: int) -> str: limited = source[:length_lim] # if last character was 2-width, padding unicode wore off, so last 2-width character can't fit. - # instead pad with space for consistent ellipsis position. + # instead, pad with space for consistent ellipsis position. if wcwidth(limited[-1]) == 2: limited = limited[:-1] + " " else: diff --git a/CUIAudioPlayer/SDManager/StreamManager.py b/CUIAudioPlayer/SDManager/StreamManager.py index ab3f8d0..74bbac0 100644 --- a/CUIAudioPlayer/SDManager/StreamManager.py +++ b/CUIAudioPlayer/SDManager/StreamManager.py @@ -4,7 +4,7 @@ from .AudioObject import AudioInfo import sounddevice as sd -from .StreamStates import StreamState, AudioUnloadedState +from .StreamStates import StreamState, AudioUnloadedState, NoAudioPlayingError from LoggingConfigurator import logger @@ -48,5 +48,5 @@ def pause_stream(self): def __del__(self): try: self.stream_state.stop_stream(self) - except (RuntimeError, FileNotFoundError): + except (RuntimeError, FileNotFoundError, NoAudioPlayingError): pass diff --git a/CUIAudioPlayer/SDManager/StreamStates.py b/CUIAudioPlayer/SDManager/StreamStates.py index 1fdcde5..7e31d63 100644 --- a/CUIAudioPlayer/SDManager/StreamStates.py +++ b/CUIAudioPlayer/SDManager/StreamStates.py @@ -14,6 +14,10 @@ """ +class NoAudioPlayingError(Exception): + pass + + class StreamState: @staticmethod def start_stream(stream_manager: StreamManager): @@ -39,11 +43,11 @@ def start_stream(stream_manager: StreamManager): @staticmethod def stop_stream(stream_manager: StreamManager): - raise FileNotFoundError("No audio file is loaded.") + raise NoAudioPlayingError("No audio file is loaded.") @staticmethod def pause_stream(stream_manager: StreamManager): - raise FileNotFoundError("No audio file is loaded.") + raise NoAudioPlayingError("No audio file is loaded.") @staticmethod def load_stream(stream_manager: StreamManager, audio_dir: str): @@ -68,12 +72,12 @@ def load_stream(stream_manager: StreamManager, audio_dir: str): class StreamStoppedState(StreamState): @staticmethod def stop_stream(stream_manager: StreamManager): - raise RuntimeError("Stream is not active.") + raise NoAudioPlayingError("Stream is not active.") @staticmethod def pause_stream(stream_manager: StreamManager): stream_manager.stream.stop() - raise RuntimeError("Stream is not active.") + raise NoAudioPlayingError("Stream is not active.") @staticmethod def start_stream(stream_manager: StreamManager): @@ -89,7 +93,7 @@ def start_stream(stream_manager: StreamManager): @staticmethod def load_stream(stream_manager: StreamManager, audio_dir: str): logger.debug("Loading new file.") - logger.debug("Delegating to: StreamPlayingState.stop_stream") + logger.debug("Delegating to: StreamPlayingState.load_stream") AudioUnloadedState.load_stream(stream_manager, audio_dir) @@ -118,6 +122,7 @@ def load_stream(stream_manager: StreamManager, audio_dir: str): logger.debug("Stopping and loading new audio.") logger.debug("Delegating to: StreamPlayingState.stop_stream") StreamPlayingState.stop_stream(stream_manager) + logger.debug("Delegating to: AudioUnloadedState.load_stream") AudioUnloadedState.load_stream(stream_manager, audio_dir) @@ -142,4 +147,5 @@ def load_stream(stream_manager: StreamManager, audio_dir: str): logger.debug("Stopping and loading new audio.") logger.debug("Delegating to: StreamPlayingState.stop_stream") StreamPlayingState.stop_stream(stream_manager) + logger.debug("Delegating to: AudioUnloadedState.load_stream") AudioUnloadedState.load_stream(stream_manager, audio_dir) diff --git a/CUIAudioPlayer/__main__.py b/CUIAudioPlayer/__main__.py index eddf68a..ba1b490 100644 --- a/CUIAudioPlayer/__main__.py +++ b/CUIAudioPlayer/__main__.py @@ -2,7 +2,6 @@ from sys import platform import py_cui -import CompatibilityPatch from LoggingConfigurator import logger from Player.PlayerLogic import AudioPlayer @@ -13,10 +12,8 @@ except ImportError: pass -assert CompatibilityPatch - -VERSION_TAG = "0.0.3a - dev" +VERSION_TAG = "0.0.4a - dev" logger.debug(f"Platform: {platform} Version: {VERSION_TAG}") @@ -28,7 +25,7 @@ def draw_player(): TUI driver """ - root = py_cui.PyCUI(5, 7) + root = py_cui.PyCUI(5, 7, auto_focus_buttons=True) root.set_title(f"CUI Audio Player - v{VERSION_TAG}") root.set_widget_border_characters("╔", "╗", "╚", "╝", "═", "║") root.set_refresh_timeout(0.1) diff --git a/Demo/Images/Demo.png b/Demo/Images/Demo.png index 3ff7612..9e3f2cd 100644 Binary files a/Demo/Images/Demo.png and b/Demo/Images/Demo.png differ diff --git a/README.md b/README.md index 587a151..ee7411d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ # CUI Audio Player +![](Demo/Images/Demo.png) + --- -Still under heavy development, expect tons of bugs. +Still under heavy(and slow) development, expect tons of bugs. -- Python 3.8 ~ 3.9+ +- Python 3.9+ - Requires ```master``` branch of [py_cui](https://github.com/jwlodek/py_cui) - Requires CMD on Windows, changing font may be necessary depending on your codepage. -*0.0.3a - dev* +(a bit old demo) ![](Demo/Images/Demo.webp) --- @@ -104,14 +106,18 @@ Will mark those if it's implemented. - [x] Continues play - works mostly. - [x] Volume control (SW) - [x] Jump to section - - [ ] Previous / Next track + - [x] Next track + - [ ] Previous track - [ ] mp3 / m4a support - might require pydub and conversion. -- [ ] Shuffle -- [ ] Album art visualizing on some sort of ascii art. -- [ ] lrc support -- [ ] Show freq. map -- [ ] favorites -- [ ] Server - client stream + +Probably never list: + - [ ] Shuffle +~~ - [ ] Album art visualizing on some sort of ascii art. ~~ Can't get reasonable quality. + - [ ] lrc support + - [ ] Show freq. map + - [ ] favorites + - [ ] Server - client stream + - [ ] Arguments --- ## Things to overcome / overcame diff --git a/requirements.txt b/requirements.txt index 7832702..c517df2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ sounddevice>=0.4.1 SoundFile>=0.10.3.post1 tinytag>=1.5.0 wcwidth>=0.2.5 --e https://github.com/jwlodek/py_cui.git +numpy +loguru +git+https://github.com/jwlodek/py_cui.git