From 887e7741931b44abec457474faacf985c043d1c9 Mon Sep 17 00:00:00 2001 From: Romanin <60302782+romanin-rf@users.noreply.github.com> Date: Fri, 19 May 2023 22:18:02 +0200 Subject: [PATCH] Update 0.4a3 - Fixed cleanup in builf.py - Added new Nofy object (notifications) - Slight fixes in exceptions.py - Added some functions and parameters to LogMenu for convenience - Disabling LogMenu now works fine - Added more logging --- build.py | 21 ++++----- pyproject.toml | 2 +- requirements.txt | 4 +- seaplayer/css/objects.css | 11 +++++ seaplayer/exceptions.py | 4 +- seaplayer/objects.py | 32 ++++++++++--- seaplayer/screens.py | 25 ++++++++++- seaplayer/seaplayer.py | 94 ++++++++++++++++++++++++++------------- 8 files changed, 138 insertions(+), 55 deletions(-) diff --git a/build.py b/build.py index 171a4de..ffcfc0c 100644 --- a/build.py +++ b/build.py @@ -1,6 +1,6 @@ import os +import shutil import platform -import glob from pathlib import Path from typing import Dict, List @@ -52,15 +52,12 @@ def add_datas(data: Dict[str, str]) -> List[str]: return [f"--add-data \"{locali print(COMMAND, end="\n\n") os.system(COMMAND) -for path in TRASH_FILES: +for i in TRASH_FILES: + path = Path(localize(i)) try: - path = Path(localize(path)) - if path.is_file(): os.remove(path.name) - elif path.is_dir(): - files = glob.glob(os.path.join(path.name, "**", "*")) - for filepath in files: - try: os.remove(filepath) - except: pass - try: os.removedirs(path.name) - except: pass - except: pass \ No newline at end of file + if path.is_dir(): + shutil.rmtree(path.name, ignore_errors=True) + else: + os.remove(path.name) + except: + pass \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 78a5cb3..1d8ae1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "SeaPlayer" -version = "0.4a2" +version = "0.4a3" description = "SeaPlayer is a player that works in the terminal." repository = "https://github.com/romanin-rf/SeaPlayer" authors = ["Romanin "] diff --git a/requirements.txt b/requirements.txt index 9e694cf..c866f14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,6 @@ properties.py==1.1.0 playsoundsimple.py==0.6.3 pillow aiofiles -pyinstaller platformdirs -typing_inspect \ No newline at end of file +typing_inspect +pyinstaller \ No newline at end of file diff --git a/seaplayer/css/objects.css b/seaplayer/css/objects.css index 8a33cf1..ff81c09 100644 --- a/seaplayer/css/objects.css +++ b/seaplayer/css/objects.css @@ -72,4 +72,15 @@ .log-menu.-hidden { offset-y: 100%; +} + +Nofy { + dock: top; + layer: nofys; + width: auto; + margin: 2 4; + padding: 1 2; + background: $background; + color: $text; + height: auto; } \ No newline at end of file diff --git a/seaplayer/exceptions.py b/seaplayer/exceptions.py index 7969bb8..f235315 100644 --- a/seaplayer/exceptions.py +++ b/seaplayer/exceptions.py @@ -1,9 +1,9 @@ class PathNotExistsError(Exception): def __init__(self, path: str) -> None: super().__init__() - self.args = (f"The element along the path '{path}' does not exist.",) + self.args = (f"The element along the path {repr(path)} does not exist.",) class NotBooleanError(Exception): def __init__(self, data: str) -> None: super().__init__() - self.args = (f"The data '{data}' cannot be represented as a bool.",) \ No newline at end of file + self.args = (f"The data {repr(data)} cannot be represented as a bool.",) \ No newline at end of file diff --git a/seaplayer/objects.py b/seaplayer/objects.py index 639bb55..a484671 100644 --- a/seaplayer/objects.py +++ b/seaplayer/objects.py @@ -5,7 +5,7 @@ from rich.progress import Progress, BarColumn, TextColumn from textual.widgets import Static, Label, ListItem, ListView, Input, TextLog # > Typing -from typing import Optional, Tuple, TypeVar, Union, Any, Dict +from typing import Optional, Tuple, TypeVar, Union, Any, Literal # > Local Import's from .types import MusicList from .codeсbase import CodecBase @@ -297,14 +297,36 @@ def __init__(self, *children, **kwargs): # ! Log Menu class LogMenu(TextLog): - def __init__(self, enable_logging: bool=True, **kwargs): + def __init__(self, chap_max_width: int=8, enable_logging: bool=True, **kwargs): self.enable_logging = enable_logging + self.chap_max_width = chap_max_width + if kwargs.get("classes", None) is not None: kwargs["classes"] = kwargs["classes"] + " log-menu -hidden" else: kwargs["classes"] = "log-menu -hidden" + super().__init__(**kwargs) - def wlog(self, chap: str, msg: str, *, cc: str="green") -> None: - if self.enable_logging: - self.write(f"[[{cc}]{chap.center(8)}[/]]: {msg}\n") + def write_log(self, chap: str, msg: str, *, chap_color: str="green") -> None: + if self.enable_logging: self.write(f"[[{chap_color}]{chap.center(self.chap_max_width)}[/]]: {msg}", shrink=False) + + def info(self, msg: str) -> None: self.write_log("INFO", msg, chap_color="green") + def error(self, msg: str) -> None: self.write_log("ERROR", msg, chap_color="red") + def warn(self, msg: str) -> None: self.write_log("WARN", msg, chap_color="orange") + +# ! Nofy +class Nofy(Static): + def __init__( + self, + text: str, + life_time: float=3, + dosk: Literal["bottom", "left", "right", "top"]="top", + **kwargs + ) -> None: + super().__init__(text, **kwargs) + self.life_time = life_time + self.styles.dock = dosk + + def on_mount(self) -> None: self.set_timer(self.life_time, self.remove) + async def on_click(self) -> None: await self.remove() diff --git a/seaplayer/screens.py b/seaplayer/screens.py index 1d4be02..4c8fc24 100644 --- a/seaplayer/screens.py +++ b/seaplayer/screens.py @@ -9,7 +9,12 @@ # > Local Imports from .types import Converter from .modules.colorizer import richefication -from .objects import ConfigurateListView, ConfigurateListItem, InputField +from .objects import ( + Nofy, + InputField, + ConfigurateListView, + ConfigurateListItem +) # ! Constants TEXT = b'CkFuIGVycm9yIGhhcyBvY2N1cnJlZC4gVG8gY29udGludWU6CgpQcmVzcyBFbnRlciB0byByZXR1cm4gdG8ge3N5c3RlbX0sIG9yCgpQcmVzcyBDVFJMK0FMVCtERUwgdG8gcmVzdGFydCB5b3VyIGNvbXB1dGVyLiBJZiB5b3UgZG8gdGhpcywKeW91IHdpbGwgbG9zZSBhbnkgdW5zYXZlZCBpbmZvcm1hdGlvbiBpbiBhbGwgb3BlbiBhcHBsaWNhdGlvbnMuCgpFcnJvcjogMEUgOiAwMTZGIDogQkZGOUIzRDQK' @@ -34,6 +39,23 @@ class Configurate(Screen): """This Configurate Menu screen.""" BINDINGS = [("escape", "app.pop_screen", "Back")] + # ! Nofy Functions + def nofy( + self, + text: str, + life_time: float=3, + dosk: Literal["bottom", "left", "right", "top"]="top" + ) -> None: + self.screen.mount(Nofy(text, life_time, dosk)) + + async def aio_nofy( + self, + text: str, + life_time: float=3, + dosk: Literal["bottom", "left", "right", "top"]="top" + ) -> None: + await self.screen.mount(Nofy(text, life_time, dosk)) + # ! Update Placeholder from InputField def _upfif(self, attr_name: str) -> str: return "Currect: " + str(eval(f"self.{attr_name}")) @@ -43,6 +65,7 @@ def gupfif(self, attr_name: str): # ! Update App Config async def _uac(self, attr_name: str, input: InputField, value: str) -> None: exec(f"self.{attr_name} = value") + await self.aio_nofy("Saved!") def guac(self, attr_name: str): async def an_uac(input: InputField, value: str) -> None: await self._uac(attr_name, input, value) diff --git a/seaplayer/seaplayer.py b/seaplayer/seaplayer.py index 79ae69d..809079b 100644 --- a/seaplayer/seaplayer.py +++ b/seaplayer/seaplayer.py @@ -26,6 +26,7 @@ get_sound_basename ) from .objects import ( + Nofy, LogMenu, InputField, MusicListView, @@ -37,7 +38,7 @@ # ! Metadata __title__ = "SeaPlayer" -__version__ = "0.4a2" +__version__ = "0.4a3" __author__ = "Romanin" __email__ = "semina054@gmail.com" __url__ = "https://github.com/romanin-rf/SeaPlayer" @@ -51,12 +52,9 @@ CSS_LOCALDIR = os.path.join(os.path.dirname(__file__), "css") ASSETS_DIRPATH = os.path.join(os.path.dirname(__file__), "assets") -CONFIG_DIRPATH = os.path.abspath(user_config_dir(__title__, __author__)) +CONFIG_DIRPATH = user_config_dir(__title__, __author__, ensure_exists=True) CONFIG_FILEPATH = os.path.join(CONFIG_DIRPATH, "config.properties") -# ! Initialized Directoryes -os.makedirs(CONFIG_DIRPATH, mode=644, exist_ok=True) - # ! Assets Paths IMGPATH_IMAGE_NOT_FOUND = os.path.join(ASSETS_DIRPATH, "image-not-found.png") @@ -70,6 +68,19 @@ "box": Resampling.BOX } +# ! Main Functions +def build_bindings(config: SeaPlayerConfig): + yield Binding(config.key_quit, "quit", "Quit") + yield Binding("c,с", "push_screen('configurate')", "Configurate") + if config.log_menu_enable: + yield Binding("l,д", "app.toggle_class('.log-menu', '-hidden')", 'Logs') + yield Binding(config.key_rewind_back, "minus_rewind", f"Rewind -{config.rewind_count_seconds} sec") + yield Binding(config.key_rewind_forward, "plus_rewind", f"Rewind +{config.rewind_count_seconds} sec") + yield Binding(config.key_volume_down, "minus_volume", f"Volume -{round(config.volume_change_percent*100)}%") + yield Binding(config.key_volume_up, "plus_volume", f"Volume +{round(config.volume_change_percent*100)}%") + yield Binding("ctrl+s", "screenshot", "Screenshot") + yield Binding(UNKNOWN_OPEN_KEY, "push_screen('unknown')", "None", show=False) + # ! Main class SeaPlayer(App): # ! Textual Configuration @@ -90,19 +101,7 @@ class SeaPlayer(App): max_volume_percent: float = config.max_volume_percent # ! Textual Keys Configuration - BINDINGS = [ - Binding(config.key_quit, "quit", "Quit", priority=True), - Binding("c,с", "push_screen('configurate')", "Configurate", priority=True), - #Binding("l,д", "app.toggle_class('.log-menu', '-hidden')", 'Logs'), - Binding(config.key_rewind_back, "minus_rewind", f"Rewind -{config.rewind_count_seconds} sec"), - Binding(config.key_rewind_forward, "plus_rewind", f"Rewind +{config.rewind_count_seconds} sec"), - Binding(config.key_volume_down, "minus_volume", f"Volume -{round(config.volume_change_percent*100)}%"), - Binding(config.key_volume_up, "plus_volume", f"Volume +{round(config.volume_change_percent*100)}%"), - Binding("ctrl+s", "screenshot", "Screenshot"), - Binding(UNKNOWN_OPEN_KEY, "push_screen('unknown')", "None", show=False) - ] - if config.log_menu_enable: - BINDINGS.append(Binding("l,д", "app.toggle_class('.log-menu', '-hidden')", 'Logs', priority=True)) + BINDINGS = list(build_bindings(config)) # ! Template Configuration currect_sound_uuid: Optional[str] = None @@ -118,12 +117,36 @@ class SeaPlayer(App): CODECS: List[Type[CodecBase]] = [ MP3Codec, WAVECodec, OGGCodec, MIDICodec ] CODECS_KWARGS: Dict[str, Any] = {"sound_font_path": config.sound_font_path} + # ! Init Objects + log_menu = LogMenu(enable_logging=config.log_menu_enable, wrap=True, highlight=True, markup=True) + + # ! Log Functions + info = log_menu.info + error = log_menu.error + warn = log_menu.warn + # ! Inherited Functions async def action_push_screen(self, screen: str) -> None: if self.SCREENS[screen].id != self.screen.id: await super().action_push_screen(screen) # ! Functions, Workers and other... + def nofy( + self, + text: str, + life_time: float=3, + dosk: Literal["bottom", "left", "right", "top"]="top" + ) -> None: + self.screen.mount(Nofy(text, life_time, dosk)) + + async def aio_nofy( + self, + text: str, + life_time: float=3, + dosk: Literal["bottom", "left", "right", "top"]="top" + ) -> None: + await self.screen.mount(Nofy(text, life_time, dosk)) + def gcs(self) -> Optional[CodecBase]: if (self.currect_sound is None) and (self.currect_sound_uuid is not None): self.currect_sound = self.music_list_view.music_list.get(self.currect_sound_uuid) @@ -166,37 +189,39 @@ async def update_loop_playback(self) -> None: if (status == "Stoped") and (self.last_playback_status == "Playing"): if self.playback_mode == 1: sound.play() - self.log_menu.wlog("INFO", f"Replay sound: {repr(sound)}") + self.info(f"Replay sound: {repr(sound)}") elif self.playback_mode == 2: if (sound:=await self.set_sound_for_playback(sound_uuid:=await self.music_list_view.aio_get_next_sound_uuid(self.currect_sound_uuid), True)) is not None: self.playback_mode_blocked = True await self.music_list_view.aio_select_list_item_from_sound_uuid(sound_uuid) sound.play() - self.log_menu.wlog("INFO", f"Playing the next sound: {repr(sound)}") + self.info(f"Playing the next sound: {repr(sound)}") self.last_playback_status = status await asyncio.sleep(0.33) def compose(self) -> ComposeResult: # * Other - self.log_menu = LogMenu(enable_logging=(self.config.log_menu_enable), wrap=True, highlight=True, markup=True) - self.log_menu.wlog("INFO", f"Codecs: {repr(self.CODECS)}") + self.info(f"{__title__} v{__version__} from {__author__} ({__email__})") + self.info(f"Source : {__url__}") + self.info(f"Codecs : {repr(self.CODECS)}") + self.info(f"Config Path : {repr(self.config.filepath)}") + self.info(f"CSS Dirpath : {repr(CSS_LOCALDIR)}") + self.info(f"Assets Dirpath : {repr(ASSETS_DIRPATH)}") # * Play Screen self.music_play_screen = Static(classes="screen-box") self.music_play_screen.border_title = "Player" - img_image_not_found = Image.open(IMGPATH_IMAGE_NOT_FOUND) - + # * Image Object Init self.music_selected_label = Label(self.get_sound_selected_label_text(), classes="music-selected-label") if self.config.image_update_method == "sync": - self.music_image = StandartImageLabel(img_image_not_found, resample=RESAMPLING_SAFE[self.config.image_resample_method]) + self.music_image = StandartImageLabel(Image.open(IMGPATH_IMAGE_NOT_FOUND), resample=RESAMPLING_SAFE[self.config.image_resample_method]) elif self.config.image_update_method == "async": - self.music_image = AsyncImageLabel(img_image_not_found, resample=RESAMPLING_SAFE[self.config.image_resample_method]) + self.music_image = AsyncImageLabel(Image.open(IMGPATH_IMAGE_NOT_FOUND), resample=RESAMPLING_SAFE[self.config.image_resample_method]) else: raise RuntimeError("The configuration 'image_update_method' is incorrect.") - - self.log_menu.wlog("INFO", f"The picture from the media file is rendered using the {repr(self.config.image_update_method)} method.") + self.info(f"The picture from the media file is rendered using the {repr(self.config.image_update_method)} method.") # * Compositions Screen self.music_list_screen = Static(classes="screen-box") @@ -237,6 +262,7 @@ async def _spm(input: InputField, value: Any) -> None: await self.submit_plus_so ) async def add_sounds_to_list(self) -> None: + added_oks = 0 async for path in aiter(self.last_paths_globalized): async for codec in aiter(self.CODECS): if await codec.aio_is_this_codec(path): @@ -245,10 +271,13 @@ async def add_sounds_to_list(self) -> None: if sound is not None: if not await self.music_list_view.music_list.aio_exists_sha1(sound): await self.music_list_view.aio_add_sound(sound) - self.log_menu.wlog("INFO", f"Song added: {repr(sound)}") + self.info(f"Song added: {repr(sound)}") + added_oks += 1 break if sound is None: - self.log_menu.wlog("ERROR", f"The sound could not be loaded: {repr(path)}", cc="red") + self.error(f"The sound could not be loaded: {repr(path)}") + self.info(f"Added [cyan]{added_oks}[/cyan] songs!") + await self.aio_nofy(f"Added [cyan]{added_oks}[/cyan] songs!") async def currect_sound_stop(self, sound: Optional[CodecBase]=None): if sound is None: sound = await self.aio_gcs() @@ -324,7 +353,7 @@ async def set_sound_for_playback( if (sound:=self.music_list_view.get_sound(self.currect_sound_uuid)) is not None: sound.set_volume(self.currect_volume) await self.music_image.update_image(image_from_bytes(sound.icon_data)) - self.log_menu.wlog("INFO", f"A new sound has been selected: {repr(sound)}") + self.info(f"A new sound has been selected: {repr(sound)}") self.currect_sound = sound self.music_selected_label.update(await self.aio_get_sound_selected_label_text()) return sound @@ -355,7 +384,8 @@ async def action_minus_volume(self) -> None: async def action_screenshot(self) -> None: path = self.save_screenshot(path=LOCALDIR) - self.log_menu.wlog("INFO", f"Screenshot saved: {repr(path)}") + self.info(f"Screenshot saved to: {repr(path)}") + await self.aio_nofy(f"Screenshot saved to: [green]{repr(path)}[/]") async def action_quit(self): self.started = False