diff --git a/src/tauon/__main__.py b/src/tauon/__main__.py index b3ff0ff01..3f415a415 100755 --- a/src/tauon/__main__.py +++ b/src/tauon/__main__.py @@ -67,7 +67,7 @@ from tauon.t_modules.logging import CustomLoggingFormatter, LogHistoryHandler -from tauon.t_modules import t_bootstrap +from tauon.t_modules.t_bootstrap import Holder log = LogHistoryHandler() formatter = logging.Formatter('[%(levelname)s] %(message)s') @@ -427,32 +427,33 @@ def transfer_args_and_exit() -> None: SDL_FreeSurface(raw_image) SDL_DestroyTexture(sdl_texture) -holder = t_bootstrap.holder -holder.t_window = t_window -holder.renderer = renderer -holder.logical_size = logical_size -holder.window_size = window_size -holder.window_default_size = window_default_size -holder.scale = scale -holder.maximized = maximized -holder.transfer_args_and_exit = transfer_args_and_exit -holder.draw_border = draw_border -holder.window_opacity = window_opacity -holder.old_window_position = old_window_position -holder.install_directory = install_directory -holder.user_directory = user_directory -holder.pyinstaller_mode = pyinstaller_mode -holder.phone = phone -holder.window_title = window_title -holder.fs_mode = fs_mode -holder.t_title = t_title -holder.n_version = n_version -holder.t_version = t_version -holder.t_id = t_id -holder.t_agent = t_agent -holder.dev_mode = dev_mode -holder.instance_lock = fp -holder.log = log +holder = Holder( + t_window=t_window, + renderer=renderer, + logical_size=logical_size, + window_size=window_size, + window_default_size=window_default_size, + scale=scale, + maximized=maximized, + transfer_args_and_exit=transfer_args_and_exit, + draw_border=draw_border, + window_opacity=window_opacity, + old_window_position=old_window_position, + install_directory=install_directory, + user_directory=user_directory, + pyinstaller_mode=pyinstaller_mode, + phone=phone, + window_title=window_title, + fs_mode=fs_mode, + t_title=t_title, + n_version=n_version, + t_version=t_version, + t_id=t_id, + t_agent=t_agent, + dev_mode=dev_mode, + instance_lock=fp, + log=log, +) del raw_image del sdl_texture @@ -464,7 +465,8 @@ def transfer_args_and_exit() -> None: def main() -> None: """Launch Tauon by means of importing t_main.py""" - from tauon.t_modules import t_main + from tauon.t_modules.t_main import main as t_main + t_main(holder) if __name__ == "__main__": main() diff --git a/src/tauon/t_modules/t_bootstrap.py b/src/tauon/t_modules/t_bootstrap.py index c818d74bf..92217d92c 100644 --- a/src/tauon/t_modules/t_bootstrap.py +++ b/src/tauon/t_modules/t_bootstrap.py @@ -13,32 +13,38 @@ #@dataclass class Holder: - """Class that holds variables for forwarding them from tauon.py to t_main.py""" + """Class that holds variables for forwarding them from __main__.py to t_main.py""" - t_window: Any # SDL_CreateWindow() return type (???) - renderer: Any # SDL_CreateRenderer() return type (???) - logical_size: list[int] # X Y res - window_size: list[int] # X Y res - maximized: bool - scale: float - window_opacity: float - draw_border: bool - transfer_args_and_exit: Callable[[]] # transfer_args_and_exit() - TODO(Martin): This should probably be moved to extra module - old_window_position: tuple [int, int] | None # X Y res - install_directory: Path - user_directory: Path - pyinstaller_mode: bool - phone: bool - window_default_size: tuple[int, int] # X Y res - window_title: bytes # t_title.encode("utf-8") - fs_mode: bool - t_title: str # "Tauon" - n_version: str # "7.9.0" - t_version: str # "v" + n_version - t_id: str # "tauonmb" | "com.github.taiko2k.tauonmb" - t_agent: str # "TauonMusicBox/7.9.0" - dev_mode: bool - instance_lock: TextIOWrapper | None - log: LogHistoryHandler - -holder = Holder() + def __init__( + self, t_window: Any, renderer: Any, logical_size: list[int], window_size: list[int], maximized: bool, + scale: float, window_opacity: float, draw_border: bool, transfer_args_and_exit: Callable[[]], + old_window_position: tuple [int, int] | None, install_directory: Path, user_directory: Path, + pyinstaller_mode: bool, phone: bool, window_default_size: tuple[int, int], window_title: bytes, + fs_mode: bool, t_title: str, n_version: str, t_version: str, t_id: str, t_agent: str, dev_mode: bool, + instance_lock: TextIOWrapper | None, log: LogHistoryHandler, + ) -> None: + self.t_window = t_window # SDL_CreateWindow() return type (???) + self.renderer = renderer # SDL_CreateRenderer() return type (???) + self.logical_size = logical_size # X Y res + self.window_size = window_size # X Y res + self.maximized = maximized + self.scale = scale + self.window_opacity = window_opacity + self.draw_border = draw_border + self.transfer_args_and_exit = transfer_args_and_exit # transfer_args_and_exit() - TODO(Martin): This should probably be moved to extra module + self.old_window_position = old_window_position # X Y res + self.install_directory = install_directory + self.user_directory = user_directory + self.pyinstaller_mode = pyinstaller_mode + self.phone = phone + self.window_default_size = window_default_size # X Y res + self.window_title = window_title # t_title.encode("utf-8") + self.fs_mode = fs_mode + self.t_title = t_title # "Tauon" + self.n_version = n_version # "7.9.0" + self.t_version = t_version # "v" + n_version + self.t_id = t_id # "tauonmb" | "com.github.taiko2k.tauonmb" + self.t_agent = t_agent # "TauonMusicBox/7.9.0" + self.dev_mode = dev_mode + self.instance_lock = instance_lock + self.log = log diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index f740706bf..91eaa0fc2 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -261,7 +261,6 @@ builtins._ = lambda x: x -from tauon.t_modules import t_bootstrap from tauon.t_modules.t_config import Config from tauon.t_modules.t_db_migrate import database_migrate from tauon.t_modules.t_dbus import Gnome @@ -338,6 +337,95 @@ from tauon.t_modules.t_themeload import Deco, load_theme from tauon.t_modules.t_tidal import Tidal from tauon.t_modules.t_webserve import authserve, controller, stream_proxy, webserve, webserve2 +from tauon.t_modules.t_main_rework import ( + Directories, + LoadImageAsset, + WhiteModImageAsset, + DConsole, + GuiVar, + StarStore, + AlbumStarStore, + Fonts, + Input, + KeyMap, + ColoursClass, + TrackClass, + LoadClass, + GetSDLInput, + MOD, + GMETrackInfo, + PlayerCtl, + LastFMapi, + ListenBrainz, + LastScrob, + Strings, + Chunker, + MenuIcon, + MenuItem, + ThreadManager, + Menu, + GallClass, + ThumbTracks, + Tauon, + PlexService, + SubsonicService, + STray, + GStats, + Drawing, + DropShadow, + LyricsRenMini, + LyricsRen, + TimedLyricsToStatic, + TimedLyricsRen, + TextBox2, + TextBox, + ImageObject, + AlbumArt, + StyleOverlay, + ToolTip, + ToolTip3, + SubLyricsBox, + RenameTrackBox, + TransEditBox, + TransEditBox, + ExportPlaylistBox, + KoelService, + TauService, + SearchOverlay, + MessageBox, + NagBox, + PowerTag, + Over, + Fields, + TopPanel, + BottomBarType1, + BottomBarType_ao1, + MiniMode, + MiniMode2, + MiniMode3, + StandardPlaylist, + ArtBox, + ScrollBox, + RadioBox, + RenamePlaylistBox, + PlaylistBox, + ArtistList, + TreeView, + QueueBox, + MetaBox, + PictureRender, + PictureRender, + RadioThumbGen, + RadioThumbGen, + Showcase, + ColourPulse2, + ViewBox, + DLMon, + Fader, + EdgePulse, + EdgePulse2, + Undo, +) #from tauon.t_modules.guitar_chords import GuitarChords if TYPE_CHECKING: @@ -345,187 +433,30 @@ from io import BufferedReader, BytesIO from pylast import Artist, LibreFMNetwork from PIL.ImageFile import ImageFile + from tauon.t_modules.t_bootstrap import Holder -# Log to debug as we don't care at all when user does not have this -try: - import colored_traceback.always - logging.debug("Found colored_traceback for colored crash tracebacks") -except ModuleNotFoundError: - logging.debug("Unable to import colored_traceback, tracebacks will be dull.") -except Exception: - logging.exception("Unknown error trying to import colored_traceback, tracebacks will be dull.") - -try: - from jxlpy import JXLImagePlugin - # We've already logged this once to INFO from t_draw, so just log to DEBUG - logging.debug("Found jxlpy for JPEG XL support") -except ModuleNotFoundError: - logging.warning("Unable to import jxlpy, JPEG XL support will be disabled.") -except Exception: - logging.exception("Unknown error trying to import jxlpy, JPEG XL support will be disabled.") - -try: - import setproctitle -except ModuleNotFoundError: - logging.warning("Unable to import setproctitle, won't be setting process title.") -except Exception: - logging.exception("Unknown error trying to import setproctitle, won't be setting process title.") -else: - setproctitle.setproctitle("tauonmb") - -# try: -# import rpc -# discord_allow = True -# except Exception: -# logging.exception("Unable to import rpc, Discord Rich Presence will be disabled.") -discord_allow = False -try: - from pypresence import Presence -except ModuleNotFoundError: - logging.warning("Unable to import pypresence, Discord Rich Presence will be disabled.") -except Exception: - logging.exception("Unknown error trying to import pypresence, Discord Rich Presence will be disabled.") -else: - import asyncio - discord_allow = True - -use_cc = False -try: - import opencc -except ModuleNotFoundError: - logging.warning("Unable to import opencc, Traditional and Simplified Chinese searches will not be usable interchangeably.") -except Exception: - logging.exception("Unknown error trying to import opencc, Traditional and Simplified Chinese searches will not be usable interchangeably.") -else: - s2t = opencc.OpenCC("s2t") - t2s = opencc.OpenCC("t2s") - use_cc = True - -use_natsort = False -try: - import natsort -except ModuleNotFoundError: - logging.warning("Unable to import natsort, playlists may not sort as intended!") -except Exception: - logging.exception("Unknown error trying to import natsort, playlists may not sort as intended!") -else: - use_natsort = True - -# Detect platform -windows_native = False -macos = False -msys = False -system = "Linux" -arch = platform.machine() -platform_release = platform.release() -platform_system = platform.system() -win_ver = 0 -if platform_system == "Windows": - try: - win_ver = int(platform_release) - except Exception: - logging.exception("Failed getting Windows version from platform.release()") - -if sys.platform == "win32": - # system = 'Windows' - # windows_native = False - system = "Linux" - msys = True -else: - system = "Linux" - import fcntl - -if sys.platform == "darwin": - macos = True - -if system == "Windows": - import win32con - import win32api - import win32gui - import win32ui - import comtypes - import atexit - -if system == "Linux": - from tauon.t_modules import t_topchart - -if system == "Linux" and not macos and not msys: - from tauon.t_modules.t_dbus import Gnome - -holder = t_bootstrap.holder -t_window = holder.t_window -renderer = holder.renderer -logical_size = holder.logical_size -window_size = holder.window_size -maximized = holder.maximized -scale = holder.scale -window_opacity = holder.window_opacity -draw_border = holder.draw_border -transfer_args_and_exit = holder.transfer_args_and_exit -old_window_position = holder.old_window_position -install_directory = holder.install_directory -user_directory = holder.user_directory -pyinstaller_mode = holder.pyinstaller_mode -phone = holder.phone -window_default_size = holder.window_default_size -window_title = holder.window_title -fs_mode = holder.fs_mode -t_title = holder.t_title -n_version = holder.n_version -t_version = holder.t_version -t_id = holder.t_id -t_agent = holder.t_agent -dev_mode = holder.dev_mode -instance_lock = holder.instance_lock -log = holder.log -logging.info(f"Window size: {window_size}") - -should_save_state = True - -try: - import pylast - last_fm_enable = True -except Exception: - logging.exception("PyLast module not found, last fm will be disabled.") - last_fm_enable = False - -if not windows_native: - import gi - from gi.repository import GLib - - font_folder = str(install_directory / "fonts") - if os.path.isdir(font_folder): - logging.info(f"Fonts directory: {font_folder}") - import ctypes - - fc = ctypes.cdll.LoadLibrary("libfontconfig-1.dll") - fc.FcConfigReference.restype = ctypes.c_void_p - fc.FcConfigReference.argtypes = (ctypes.c_void_p,) - fc.FcConfigAppFontAddDir.argtypes = (ctypes.c_void_p, ctypes.c_char_p) - config = ctypes.c_void_p() - config.contents = fc.FcConfigGetCurrent() - fc.FcConfigAppFontAddDir(config.value, font_folder.encode()) +# 1REWORK # TLS setup (needed for frozen installs) -def get_cert_path() -> str: - if pyinstaller_mode: +def get_cert_path(holder: Holder) -> str: + if holder.pyinstaller_mode: return os.path.join(sys._MEIPASS, 'certifi', 'cacert.pem') # Running as script return certifi.where() -def setup_ssl() -> ssl.SSLContext: +def setup_ssl(holder: Holder) -> ssl.SSLContext: # Set the SSL certificate path environment variable - cert_path = get_cert_path() + cert_path = get_cert_path(holder) logging.debug(f"Found TLS cert file at: {cert_path}") os.environ['SSL_CERT_FILE'] = cert_path os.environ['REQUESTS_CA_BUNDLE'] = cert_path # Create default TLS context - ssl_context = ssl.create_default_context(cafile=get_cert_path()) + ssl_context = ssl.create_default_context(cafile=get_cert_path(holder)) return ssl_context -ssl_context = setup_ssl() +# 2REWORK @@ -569,179 +500,10 @@ def setup_ssl() -> ssl.SSLContext: except Exception: logging.exception("Error accessing GTK settings") -# Set data folders (portable mode) -config_directory = user_directory -cache_directory = user_directory / "cache" -home_directory = os.path.join(os.path.expanduser("~")) - -asset_directory = install_directory / "assets" -svg_directory = install_directory / "assets" / "svg" -scaled_asset_directory = asset_directory - -music_directory = Path("~").expanduser() / "Music" -if not music_directory.is_dir(): - music_directory = Path("~").expanduser() / "music" - -download_directory = Path("~").expanduser() / "Downloads" - -# Detect if we are installed or running portable -install_mode = False -flatpak_mode = False -snap_mode = False -if str(install_directory).startswith(("/opt/", "/usr/", "/app/", "/snap/")): - install_mode = True - if str(install_directory)[:6] == "/snap/": - snap_mode = True - if str(install_directory)[:5] == "/app/": - # Flatpak mode - logging.info("Detected running as Flatpak") - - # [old / no longer used] Symlink fontconfig from host system as workaround for poor font rendering - if os.path.exists(os.path.join(home_directory, ".var/app/com.github.taiko2k.tauonmb/config")): - - host_fcfg = os.path.join(home_directory, ".config/fontconfig/") - flatpak_fcfg = os.path.join(home_directory, ".var/app/com.github.taiko2k.tauonmb/config/fontconfig") - - if os.path.exists(host_fcfg): - - # if os.path.isdir(flatpak_fcfg) and not os.path.islink(flatpak_fcfg): - # shutil.rmtree(flatpak_fcfg) - if os.path.islink(flatpak_fcfg): - logging.info("-- Symlink to fonconfig exists, removing") - os.unlink(flatpak_fcfg) - # else: - # logging.info("-- Symlinking user fonconfig") - # #os.symlink(host_fcfg, flatpak_fcfg) - - flatpak_mode = True - -# If we're installed, use home data locations -if (install_mode and system == "Linux") or macos or msys: - cache_directory = Path(GLib.get_user_cache_dir()) / "TauonMusicBox" - #user_directory = Path(GLib.get_user_data_dir()) / "TauonMusicBox" - config_directory = user_directory - -# if not user_directory.is_dir(): -# os.makedirs(user_directory) - - if not config_directory.is_dir(): - os.makedirs(config_directory) - - if snap_mode: - logging.info("Installed as Snap") - elif flatpak_mode: - logging.info("Installed as Flatpak") - else: - logging.info("Running from installed location") - - if not (user_directory / "encoder").is_dir(): - os.makedirs(user_directory / "encoder") - +#3REWORK -# elif (system == 'Windows' or msys) and ( -# 'Program Files' in install_directory or -# os.path.isfile(install_directory + '\\unins000.exe')): -# -# user_directory = os.path.expanduser('~').replace("\\", '/') + "/Music/TauonMusicBox" -# config_directory = user_directory -# cache_directory = user_directory / "cache" -# logging.info(f"User Directory: {user_directory}") -# install_mode = True -# if not os.path.isdir(user_directory): -# os.makedirs(user_directory) - -else: - logging.info("Running in portable mode") - config_directory = user_directory - -if not (user_directory / "state.p").is_file() and cache_directory.is_dir(): - logging.info("Clearing old cache directory") - logging.info(cache_directory) - shutil.rmtree(str(cache_directory)) - -n_cache_dir = str(cache_directory / "network") -e_cache_dir = str(cache_directory / "export") -g_cache_dir = str(cache_directory / "gallery") -a_cache_dir = str(cache_directory / "artist") -r_cache_dir = str(cache_directory / "radio-thumbs") -b_cache_dir = str(user_directory / "artist-backgrounds") - -if not os.path.isdir(n_cache_dir): - os.makedirs(n_cache_dir) -if not os.path.isdir(e_cache_dir): - os.makedirs(e_cache_dir) -if not os.path.isdir(g_cache_dir): - os.makedirs(g_cache_dir) -if not os.path.isdir(a_cache_dir): - os.makedirs(a_cache_dir) -if not os.path.isdir(b_cache_dir): - os.makedirs(b_cache_dir) -if not os.path.isdir(r_cache_dir): - os.makedirs(r_cache_dir) - -if not (user_directory / "artist-pictures").is_dir(): - os.makedirs(user_directory / "artist-pictures") - -if not (user_directory / "theme").is_dir(): - os.makedirs(user_directory / "theme") - - -if platform_system == "Linux": - system_config_directory = Path(GLib.get_user_config_dir()) - xdg_dir_file = system_config_directory / "user-dirs.dirs" - - if xdg_dir_file.is_file(): - with xdg_dir_file.open() as f: - for line in f: - if line.startswith("XDG_MUSIC_DIR="): - music_directory = Path(os.path.expandvars(line.split("=")[1].strip().replace('"', ""))).expanduser() - logging.debug(f"Found XDG-Music: {music_directory} in {xdg_dir_file}") - if line.startswith("XDG_DOWNLOAD_DIR="): - target = Path(os.path.expandvars(line.split("=")[1].strip().replace('"', ""))).expanduser() - if Path(target).is_dir(): - download_directory = target - logging.debug(f"Found XDG-Downloads: {download_directory} in {xdg_dir_file}") - - -if os.getenv("XDG_MUSIC_DIR"): - music_directory = Path(os.getenv("XDG_MUSIC_DIR")) - logging.debug("Override music to: " + music_directory) - -if os.getenv("XDG_DOWNLOAD_DIR"): - download_directory = Path(os.getenv("XDG_DOWNLOAD_DIR")) - logging.debug("Override downloads to: " + download_directory) - -if music_directory: - music_directory = Path(os.path.expandvars(music_directory)) -if download_directory: - download_directory = Path(os.path.expandvars(download_directory)) - -if not music_directory.is_dir(): - music_directory = None - -locale_directory = install_directory / "locale" -#if flatpak_mode: -# locale_directory = Path("/app/share/locale") -#elif str(install_directory).startswith(("/opt/", "/usr/")): -# locale_directory = Path("/usr/share/locale") - -logging.info(f"Install directory: {install_directory}") -#logging.info(f"SVG directory: {svg_directory}") -logging.info(f"Asset directory: {asset_directory}") -#logging.info(f"Scaled Asset Directory: {scaled_asset_directory}") -if locale_directory.exists(): - logging.info(f"Locale directory: {locale_directory}") -else: - logging.error(f"Locale directory MISSING: {locale_directory}") -logging.info(f"Userdata directory: {user_directory}") -logging.info(f"Config directory: {config_directory}") -logging.info(f"Cache directory: {cache_directory}") -logging.info(f"Home directory: {home_directory}") -logging.info(f"Music directory: {music_directory}") -logging.info(f"Downloads directory: {download_directory}") - -# Things for detecting and launching programs outside of flatpak sandbox def whicher(target: str) -> bool | str | None: + """Detect and launch programs outside of flatpak sandbox""" try: if flatpak_mode: complete = subprocess.run( @@ -755,151 +517,7 @@ def whicher(target: str) -> bool | str | None: return False -launch_prefix = "" -if flatpak_mode: - launch_prefix = "flatpak-spawn --host " - -pid = os.getpid() - -if not macos: - icon = IMG_Load(str(asset_directory / "icon-64.png").encode()) -else: - icon = IMG_Load(str(asset_directory / "tau-mac.png").encode()) - -SDL_SetWindowIcon(t_window, icon) - -if not phone: - if window_size[0] != logical_size[0]: - SDL_SetWindowMinimumSize(t_window, 560, 330) - else: - SDL_SetWindowMinimumSize(t_window, round(560 * scale), round(330 * scale)) - -max_window_tex = 1000 -if window_size[0] > max_window_tex or window_size[1] > max_window_tex: - - while window_size[0] > max_window_tex: - max_window_tex += 1000 - while window_size[1] > max_window_tex: - max_window_tex += 1000 - -main_texture = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, max_window_tex, - max_window_tex) -main_texture_overlay_temp = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, - max_window_tex, max_window_tex) - -overlay_texture_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, 300, 300) -SDL_SetTextureBlendMode(overlay_texture_texture, SDL_BLENDMODE_BLEND) -SDL_SetRenderTarget(renderer, overlay_texture_texture) -SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) -SDL_RenderClear(renderer) -SDL_SetRenderTarget(renderer, None) - -tracklist_texture = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, max_window_tex, - max_window_tex) -tracklist_texture_rect = SDL_Rect(0, 0, max_window_tex, max_window_tex) -SDL_SetTextureBlendMode(tracklist_texture, SDL_BLENDMODE_BLEND) - -SDL_SetRenderTarget(renderer, None) - -# Paint main texture -SDL_SetRenderTarget(renderer, main_texture) -SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) - -SDL_SetRenderTarget(renderer, main_texture_overlay_temp) -SDL_SetTextureBlendMode(main_texture_overlay_temp, SDL_BLENDMODE_BLEND) -SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) -SDL_RenderClear(renderer) - - -# -# SDL_SetRenderTarget(renderer, None) -# SDL_SetRenderDrawColor(renderer, 7, 7, 7, 255) -# SDL_RenderClear(renderer) -# #SDL_RenderPresent(renderer) -# -# SDL_SetWindowOpacity(t_window, window_opacity) - - -class LoadImageAsset: - assets: list[LoadImageAsset] = [] - - def __init__(self, *, scaled_asset_directory: Path, path: str, is_full_path: bool = False, reload: bool = False, scale_name: str = "") -> None: - if not reload: - self.assets.append(self) - - self.path = path - self.scale_name = scale_name - self.scaled_asset_directory: Path = scaled_asset_directory - - raw_image = IMG_Load(self.path.encode()) - self.sdl_texture = SDL_CreateTextureFromSurface(renderer, raw_image) - - p_w = pointer(c_int(0)) - p_h = pointer(c_int(0)) - SDL_QueryTexture(self.sdl_texture, None, None, p_w, p_h) - - if is_full_path: - SDL_SetTextureAlphaMod(self.sdl_texture, prefs.custom_bg_opacity) - - self.rect = SDL_Rect(0, 0, p_w.contents.value, p_h.contents.value) - SDL_FreeSurface(raw_image) - self.w = p_w.contents.value - self.h = p_h.contents.value - - def reload(self) -> None: - SDL_DestroyTexture(self.sdl_texture) - if self.scale_name: - self.path = str(self.scaled_asset_directory / self.scale_name) - self.__init__(scaled_asset_directory=scaled_asset_directory, path=self.path, reload=True, scale_name=self.scale_name) - - def render(self, x: int, y: int, colour=None) -> None: - self.rect.x = round(x) - self.rect.y = round(y) - SDL_RenderCopy(renderer, self.sdl_texture, None, self.rect) - - -class WhiteModImageAsset: - assets: list[WhiteModImageAsset] = [] - - def __init__(self, *, scaled_asset_directory: Path, path: str, reload: bool = False, scale_name: str = ""): - if not reload: - self.assets.append(self) - self.path = path - self.scale_name = scale_name - self.scaled_asset_directory: Path = scaled_asset_directory - - raw_image = IMG_Load(path.encode()) - self.sdl_texture = SDL_CreateTextureFromSurface(renderer, raw_image) - self.colour = [255, 255, 255, 255] - p_w = pointer(c_int(0)) - p_h = pointer(c_int(0)) - SDL_QueryTexture(self.sdl_texture, None, None, p_w, p_h) - self.rect = SDL_Rect(0, 0, p_w.contents.value, p_h.contents.value) - SDL_FreeSurface(raw_image) - self.w = p_w.contents.value - self.h = p_h.contents.value - - def reload(self) -> None: - SDL_DestroyTexture(self.sdl_texture) - if self.scale_name: - self.path = str(self.scaled_asset_directory / self.scale_name) - self.__init__(scaled_asset_directory=scaled_asset_directory, path=self.path, reload=True, scale_name=self.scale_name) - - def render(self, x: int, y: int, colour) -> None: - if colour != self.colour: - SDL_SetTextureColorMod(self.sdl_texture, colour[0], colour[1], colour[2]) - SDL_SetTextureAlphaMod(self.sdl_texture, colour[3]) - self.colour = colour - self.rect.x = round(x) - self.rect.y = round(y) - SDL_RenderCopy(renderer, self.sdl_texture, None, self.rect) - - -loaded_asset_dc: dict[str, WhiteModImageAsset | LoadImageAsset] = {} - +#4REWORK def asset_loader( scaled_asset_directory: Path, loaded_asset_dc: dict[str, WhiteModImageAsset | LoadImageAsset], name: str, mod: bool = False, @@ -915,37 +533,7 @@ def asset_loader( loaded_asset_dc[name] = item return item - -# loading_image = asset_loader(scaled_asset_directory, loaded_asset_dc, "loading.png") - -if maximized: - i_x = pointer(c_int(0)) - i_y = pointer(c_int(0)) - - time.sleep(0.02) - SDL_PumpEvents() - SDL_GetWindowSize(t_window, i_x, i_y) - logical_size[0] = i_x.contents.value - logical_size[1] = i_y.contents.value - SDL_GL_GetDrawableSize(t_window, i_x, i_y) - window_size[0] = i_x.contents.value - window_size[1] = i_y.contents.value - -# loading_image.render(window_size[0] // 2 - loading_image.w // 2, window_size[1] // 2 - loading_image.h // 2) -# SDL_RenderPresent(renderer) - -if install_directory != config_directory and not (config_directory / "input.txt").is_file(): - logging.warning("Input config file is missing, first run? Copying input.txt template from templates directory") - #logging.warning(install_directory) - #logging.warning(config_directory) - shutil.copy(install_directory / "templates" / "input.txt", config_directory) - - -if snap_mode: - discord_allow = False - - -musicbrainzngs.set_useragent("TauonMusicBox", n_version, "https://github.com/Taiko2k/Tauon") +#5REWORK # logging.info(arch) # ----------------------------------------------------------- @@ -958,16 +546,7 @@ def asset_loader( # ------------------------------------------------ -if system == "Windows": - os.environ["PYSDL2_DLL_PATH"] = str(install_directory / "lib") -elif not msys and not macos: - try: - gi.require_version("Notify", "0.7") - except Exception: - logging.exception("Failed importing gi Notify 0.7, will try 0.8") - gi.require_version("Notify", "0.8") - from gi.repository import Notify - +#6REWORK def no_padding() -> int: @@ -1029,15 +608,6 @@ def no_padding() -> int: # Variables now go in the gui, pctl, input and prefs class instances. The following just haven't been moved yet -class DConsole: - """GUI console with logs""" - def __init__(self) -> None: - self.show: bool = False - - def toggle(self) -> None: - """Toggle the GUI console with logs on and off""" - self.show ^= True - console = DConsole() spot_cache_saved_albums = [] @@ -1061,11 +631,11 @@ def toggle(self) -> None: album_h_gap = 30 album_v_slide_value = 50 -album_mode_art_size = int(200 * scale) +#7REWORK time_last_save = 0 -b_info_y = int(window_size[1] * 0.7) # For future possible panel below playlist +#8REWORK volume_store = 50 # Used to save the previous volume when muted @@ -1219,7 +789,7 @@ def toggle(self) -> None: def uid_gen() -> int: return random.randrange(1, 100000000) - +#TODO(Martin): Get rid of this notify_change = lambda: None @@ -1240,6 +810,7 @@ def pl_gen( if playlist_ids == None: playlist_ids = [] + #TODO(Martin): Change to pctl.notify_change() notify_change() # return copy.deepcopy([title, playing, playlist, position, hide_title, selected, uid_gen(), [], hidden, False, parent, False]) @@ -1294,22 +865,7 @@ def queue_item_gen(track_id: int, position: int, pl_id: int, type: int = 0, albu albums = [] album_position = 0 -prefs = Prefs( - user_directory=user_directory, - music_directory=music_directory, - cache_directory=cache_directory, - macos=macos, - phone=phone, - left_window_control=left_window_control, - detect_macstyle=detect_macstyle, - gtk_settings=gtk_settings, - discord_allow=discord_allow, - flatpak_mode=flatpak_mode, - desktop=desktop, - window_opacity=window_opacity, - scale=scale, -) - +#9REWORK def open_uri(uri:str) -> None: logging.info("OPEN URI") @@ -1334,434 +890,7 @@ def open_uri(uri:str) -> None: load_orders.append(copy.deepcopy(load_order)) gui.update += 1 - -class GuiVar: - """Use to hold any variables for use in relation to UI""" - - def update_layout(self) -> None: - global update_layout - update_layout = True - - def show_message(self, line1: str, line2: str = "", line3: str = "", mode: str = "info") -> None: - show_message(line1, line2, line3, mode=mode) - - def delay_frame(self, t): - gui.frame_callback_list.append(TestTimer(t)) - - def destroy_textures(self): - SDL_DestroyTexture(self.spec4_tex) - SDL_DestroyTexture(self.spec1_tex) - SDL_DestroyTexture(self.spec2_tex) - SDL_DestroyTexture(self.spec_level_tex) - - # def test_text_input(self): - # if self.text_input_request and not self.text_input_active: - # SDL_StartTextInput() - # self.update += 1 - # if not self.text_input_request and self.text_input_active: - # SDL_StopTextInput() - # self.text_input_request = False - - def rescale(self): - self.spec_y = int(round(5 * self.scale)) - self.spec_w = int(round(80 * self.scale)) - self.spec_h = int(round(20 * self.scale)) - self.spec1_rec = SDL_Rect(0, self.spec_y, self.spec_w, self.spec_h) - - self.spec4_y = int(round(200 * self.scale)) - self.spec4_w = int(round(322 * self.scale)) - self.spec4_h = int(round(100 * self.scale)) - self.spec4_rec = SDL_Rect(0, self.spec4_y, self.spec4_w, self.spec4_h) - - self.bar = SDL_Rect(10, 10, round(3 * self.scale), 10) # spec bar bin - self.bar4 = SDL_Rect(10, 10, round(3 * self.scale), 10) # spec bar bin - self.set_height = round(25 * self.scale) - self.panelBY = round(51 * self.scale) - self.panelY = round(30 * self.scale) - self.panelY2 = round(30 * self.scale) - self.playlist_top = self.panelY + (8 * self.scale) - self.playlist_top_bk = self.playlist_top - self.scroll_hide_box = (0, self.panelY, 28, window_size[1] - self.panelBY - self.panelY) - - self.spec2_y = int(round(22 * self.scale)) - self.spec2_w = int(round(140 * self.scale)) - self.spec2 = [0] * self.spec2_y - self.spec2_phase = 0 - self.spec2_buffers = [] - self.spec2_rec = SDL_Rect(1230, round(4 * self.scale), self.spec2_w, self.spec2_y) - self.spec2_source = SDL_Rect(900, round(4 * self.scale), self.spec2_w, self.spec2_y) - self.spec2_dest = SDL_Rect(900, round(4 * self.scale), self.spec2_w, self.spec2_y) - self.spec2_position = 0 - self.spec2_timer = Timer() - self.spec2_timer.set() - - self.level_w = 5 * self.scale - self.level_y = 16 * self.scale - self.level_s = 1 * self.scale - self.level_ww = round(79 * self.scale) - self.level_hh = round(18 * self.scale) - self.spec_level_rec = SDL_Rect( - 0, round(self.level_y - 10 * self.scale), round(self.level_ww),round(self.level_hh)) - - self.spec2_tex = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, self.spec2_w, self.spec2_y) - self.spec4_tex = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, self.spec4_w, self.spec4_y) - self.spec1_tex = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, self.spec_w, self.spec_h) - self.spec_level_tex = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, self.level_ww, self.level_hh) - SDL_SetTextureBlendMode(self.spec4_tex, SDL_BLENDMODE_BLEND) - self.artist_panel_height = 320 * self.scale - self.last_artist_panel_height = self.artist_panel_height - - self.window_control_hit_area_w = 100 * self.scale - self.window_control_hit_area_h = 30 * self.scale - - def __init__(self): - - self.scale = prefs.ui_scale - - self.window_id = 0 - self.update = 2 # UPDATE - self.turbo = True - self.turbo_next = 0 - self.pl_update = 1 - self.lowered = False - self.request_raise = False - self.maximized = False - - self.message_box = False - self.message_text = "" - self.message_mode = "info" - self.message_subtext = "" - self.message_subtext2 = "" - self.message_box_confirm_reference = None - self.message_box_use_reference = True - self.message_box_confirm_callback = None - - self.save_size = [450, 310] - self.show_playlist = True - self.show_bottom_title = False - # self.show_top_title = True - self.search_error = False - - self.level_update = False - self.level_time = Timer() - self.level_peak: list[float] = [0, 0] - self.level = 0 - self.time_passed = 0 - self.level_meter_colour_mode = 3 - - self.vis = 0 # visualiser mode actual - self.vis_want = 2 # visualiser mode setting - self.spec = None - self.s_spec = [0] * 24 - self.s4_spec = [0] * 45 - self.update_spec = 0 - - # self.spec_rect = [0, 5, 80, 20] # x = 72 + 24 - 6 - 10 - - self.spec4_array = [] - - self.draw_spec4 = False - - self.combo_mode = False - self.showcase_mode = False - self.display_time_mode = 0 - - self.pl_text_real_height = 12 - self.pl_title_real_height = 11 - - self.row_extra = 0 - self.test = False - self.light_mode = False - - self.level_2_click = False - self.universal_y_text_offset = 0 - - self.star_text_y_offset = 0 - if system == "Windows": - self.star_text_y_offset = -2 - - self.set_bar = True - self.set_mode = False - self.set_hold = -1 - self.set_label_hold = -1 - self.set_label_point = (0, 0) - self.set_point = 0 - self.set_old = 0 - self.pl_st = [ - ["Artist", 156, False], ["Title", 188, False], ["T", 40, True], ["Album", 153, False], - ["P", 28, True], ["Starline", 86, True], ["Date", 48, True], ["Codec", 55, True], - ["Time", 53, True]] - - for item in self.pl_st: - item[1] = item[1] * self.scale - - self.offset_extra: int = 0 - - self.playlist_row_height: int = 16 - self.playlist_text_offset: int = 0 - self.row_font_size: int = 13 - self.compact_bar = False - self.tracklist_texture_rect = tracklist_texture_rect - self.tracklist_texture = tracklist_texture - - self.trunk_end = "..." # "…" - self.temp_themes = {} - self.theme_temp_current = -1 - - self.pl_title_y_offset = 0 - self.pl_title_font_offset = -1 - - self.playlist_box_d_click = -1 - - self.gallery_show_text = True - self.bb_show_art = False - - self.rename_folder_box = False - - self.present = False - self.drag_source_position = (0, 0) - self.drag_source_position_persist = (0, 0) - self.album_tab_mode = False - self.main_art_box = (0, 0, 10, 10) - self.gall_tab_enter = False - - self.lightning_copy = False - - self.gallery_animate_highlight_on = 0 - - self.seek_cur_show = False - self.cur_time = "0" - self.force_showcase_index = -1 - - self.frame_callback_list = [] - - self.playlist_left = None - self.image_downloading = False - self.tc_cancel = False - self.im_cancel = False - self.force_search = False - - self.pl_pulse = False - - self.view_name = "S" - self.restart_album_mode = False - - self.dtm3_index = -1 - self.dtm3_cum = 0 - self.dtm3_total = 0 - self.previous_playlist_id = "" - - self.star_mode = "line" - self.heart_fields = [] - self.show_ratings = False - - self.web_running = False - - self.rsp = True - if phone: - self.rsp = False - self.rspw = round(300 * self.scale) - self.lsp = False - self.lspw = round(220 * self.scale) - self.plw = None - - self.pref_rspw = 300 - - self.pref_gallery_w = 600 - - self.artist_info_panel = False - - self.show_hearts = True - - self.cursor_is = 0 - self.cursor_want = 0 - # 0 standard - # 1 drag horizontal - # 2 text - # 3 hand - - self.power_bar = None - self.gallery_scroll_field_left = 1 - self.combo_was_album = False - - self.gallery_positions = {} - - self.remember_library_mode = False - - self.first_in_grid = None - - self.art_aspect_ratio = 1 - self.art_drawn_rect = None - self.art_unlock_ratio = False - self.art_max_ratio_lock = 1 - self.side_bar_drag_source = 0 - self.side_bar_drag_original = 0 - - self.scroll_direction = 0 - self.add_music_folder_ready = False - - self.playlist_current_visible_tracks = 0 - self.playlist_current_visible_tracks_id = 0 - - self.theme_name = "" - self.rename_playlist_box = False - self.queue_frame_draw = None # Set when need draw frame later - - self.mode = 1 - - self.save_position = [0, 0] - - self.draw_vis4_top = False - # self.vis_4_colour = [0,0,0,255] - self.vis_4_colour = None - - self.layer_focus = 0 - self.tab_menu_pl = 0 - - self.tool_tip_lock_off_f = False - self.tool_tip_lock_off_b = False - - self.auto_play_import = False - - self.transcoding_batch_total = 0 - self.transcoding_bach_done = 0 - - self.seek_bar_rect = (0, 0, 0, 0) - self.volume_bar_rect = (0, 0, 0, 0) - - self.mini_mode_return_maximized = False - - self.opened_config_file = False - - self.notify_main_id = None - - self.halt_image_rendering = False - self.generating_chart = False - - self.top_bar_mode2 = False - self.mode_toast_text = "" - - self.rescale() - # self.smooth_scrolling = False - - self.compact_artist_list = False - - self.rsp_full_lock = False - - self.album_scroll_px = album_v_slide_value - self.queue_toast_plural = False - self.reload_theme = False - self.theme_number = 0 - self.toast_queue_object: TauonQueueItem | None = None - self.toast_love_object = None - self.toast_love_added = True - - self.force_side_on_drag = False - self.last_left_panel_mode = "playlist" - self.showing_l_panel = False - - self.downloading_bass = False - self.d_click_ref = -1 - - self.max_window_tex = max_window_tex - self.main_texture = main_texture - self.main_texture_overlay_temp = main_texture_overlay_temp - - self.preview_artist = "" - self.preview_artist_location = (0, 0) - self.preview_artist_loading = "" - self.mouse_left_window = False - - self.rendered_playlist_position = 0 - - self.console = console - self.show_album_ratings = False - self.gen_code_errors = False - - self.regen_single = -1 - self.regen_single_id = None - - self.tracklist_bg_is_light = False - self.clear_image_cache_next = 0 - - self.column_d_click_timer = Timer(10) - self.column_d_click_on = -1 - self.column_sort_ani_timer = Timer(10) - self.column_sort_down_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "sort-down.png", True) - self.column_sort_up_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "sort-up.png", True) - self.column_sort_ani_direction = 1 - self.column_sort_ani_x = 0 - - self.restore_showcase_view = False - self.restore_radio_view = False - - self.tracklist_center_mode = False - self.tracklist_inset_left = 0 - self.tracklist_inset_width = 0 - self.tracklist_highlight_width = 0 - self.tracklist_highlight_left = 0 - - self.hide_tracklist_in_gallery = False - - self.saved_prime_tab = 0 - self.saved_prime_direction = 0 - - self.stop_sync = False - self.sync_progress = "" - self.sync_speed = "" - - self.bar_hover_timer = Timer() - - self.level_decay_timer = Timer() - - self.showed_title = False - - self.to_get = 0 - self.to_got = 0 - self.switch_showcase_off = False - - self.backend_reloading = False - - self.spot_info_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "spot-info.png", True) - self.tray_active = False - self.buffering = False - self.buffering_text = "" - - self.update_on_drag = False - self.pl_update_on_drag = False - self.drop_playlist_target = 0 - self.discord_status = "Standby" - self.mouse_unknown = False - self.macstyle = prefs.macstyle - if macos or detect_macstyle: - self.macstyle = True - self.radio_view = False - self.window_size = window_size - self.box_over = False - self.suggest_clean_db = False - self.style_worker_timer = Timer() - - self.shuffle_was_showcase = False - self.shuffle_was_random = True - self.shuffle_was_repeat = False - - self.was_radio = False - self.fullscreen = False - self.mouse_in_window = True - - self.write_tag_in_progress = False - self.tag_write_count = 0 - # self.text_input_request = False - # self.text_input_active = False - self.center_blur_pixel = (0, 0, 0) - - -gui = GuiVar() - +#10REWORK def toast(text: str) -> None: gui.mode_toast_text = text @@ -1806,677 +935,101 @@ def set_drag_source(): # Functions for reading and setting play counts -class StarStore: - def __init__(self) -> None: - self.db = {} - - def key(self, track_id: int) -> tuple[str, str, str]: - track_object = pctl.master_library[track_id] - return track_object.artist, track_object.title, track_object.filename - - def object_key(self, track: TrackClass) -> tuple[str, str, str]: - return track.artist, track.title, track.filename - - def add(self, index: int, value): - """Increments the play time""" - track_object = pctl.master_library[index] +star_store = StarStore() +album_star_store = AlbumStarStore() +fonts = Fonts() +inp = Input() +keymaps = KeyMap() - if after_scan: - if track_object in after_scan: - return - key = track_object.artist, track_object.title, track_object.filename +def update_set(): + """This is used to scale columns when windows is resized or items added/removed""" + wid = gui.plw - round(16 * gui.scale) + if gui.tracklist_center_mode: + wid = gui.tracklist_highlight_width - round(16 * gui.scale) - if key in self.db: - self.db[key][0] += value - if value < 0 and self.db[key][0] < 0: - self.db[key][0] = 0 + total = 0 + for item in gui.pl_st: + if item[2] is False: + total += item[1] else: - self.db[key] = [value, "", 0, 0] # Playtime in s, flags, rating, love timestamp + wid -= item[1] - def get(self, index: int): - """Returns the track play time""" - if index < 0: - return 0 - return self.db.get(self.key(index), (0,))[0] - - def get_rating(self, index: int): - """Returns the track user rating""" - key = self.key(index) - if key in self.db: - # self.db[key] - return self.db[key][2] - return 0 + wid = max(75, wid) - def set_rating(self, index: int, value: int, write: bool = False) -> None: - """Sets the track user rating""" - key = self.key(index) - if key not in self.db: - self.db[key] = self.new_object() - self.db[key][2] = value - - tr = pctl.get_track(index) - if tr.file_ext == "SUB": - self.db[key][2] = math.ceil(value / 2) * 2 - shooter(subsonic.set_rating, (tr, value)) - - if prefs.write_ratings and write: - logging.info("Writing rating..") - assert value <= 10 - assert value >= 0 - - if tr.file_ext == "OGG" or tr.file_ext == "OPUS": - tag = mutagen.oggvorbis.OggVorbis(tr.fullpath) - if value == 0: - if "FMPS_RATING" in tag: - del tag["FMPS_RATING"] - tag.save() - else: - tag["FMPS_RATING"] = [f"{value / 10:.2f}"] - tag.save() + for i in range(len(gui.pl_st)): + if gui.pl_st[i][2] is False and total: + gui.pl_st[i][1] = int(round((gui.pl_st[i][1] / total) * wid)) # + 1 - elif tr.file_ext == "MP3": - tag = mutagen.id3.ID3(tr.fullpath) - # if True: - # if value == 0: - # tag.delall("POPM") - # else: - # p_rating = 0 - # - # tag.add(mutagen.id3.POPM(email="Windows Media Player 9 Series", rating=int)) - - if value == 0: - changed = False - frames = tag.getall("TXXX") - for i in reversed(range(len(frames))): - if frames[i].desc.lower() == "fmps_rating": - changed = True - if changed: - tag.delall("TXXX:FMPS_RATING") - tag.save() - else: - changed = False - frames = tag.getall("TXXX") - for i in reversed(range(len(frames))): - if frames[i].desc.lower() == "fmps_rating": - frames[i].text = f"{value / 10:.2f}" - changed = True - if not changed: - tag.add( - mutagen.id3.TXXX( - encoding=mutagen.id3.Encoding.UTF8, text=f"{value / 10:.2f}", - desc="FMPS_RATING")) - tag.save() - - elif tr.file_ext == "FLAC": - audio = mutagen.flac.FLAC(tr.fullpath) - tags = audio.tags - if value == 0: - if "FMPS_Rating" in tags: - del tags["FMPS_Rating"] - audio.save() - else: - tags["FMPS_Rating"] = f"{value / 10:.2f}" - audio.save() +def auto_size_columns(): + fixed_n = 0 - tr.misc["FMPS_Rating"] = float(value / 10) - if value == 0: - del tr.misc["FMPS_Rating"] + wid = gui.plw - round(16 * gui.scale) + if gui.tracklist_center_mode: + wid = gui.tracklist_highlight_width - round(16 * gui.scale) - def new_object(self): - return [0, "", 0, 0] + total = wid + for item in gui.pl_st: - def get_by_object(self, track: TrackClass): + if item[2]: + fixed_n += 1 - return self.db.get(self.object_key(track), (0,))[0] + if item[0] == "Lyrics": + item[1] = round(50 * gui.scale) + total -= round(50 * gui.scale) - def get_total(self): + if item[0] == "Rating": + item[1] = round(80 * gui.scale) + total -= round(80 * gui.scale) - return sum(item[0] for item in self.db.values()) + if item[0] == "Starline": + item[1] = round(78 * gui.scale) + total -= round(78 * gui.scale) - def full_get(self, index: int): - return self.db.get(self.key(index)) + if item[0] == "Time": + item[1] = round(58 * gui.scale) + total -= round(58 * gui.scale) - def remove(self, index: int): - key = self.key(index) - if key in self.db: - del self.db[key] + if item[0] == "Codec": + item[1] = round(58 * gui.scale) + total -= round(58 * gui.scale) - def insert(self, index: int, object): - key = self.key(index) - self.db[key] = object + if item[0] == "P" or item[0] == "S" or item[0] == "#": + item[1] = round(32 * gui.scale) + total -= round(32 * gui.scale) - def merge(self, index: int, object): - if object is None or object == self.new_object(): - return - key = self.key(index) - if key not in self.db: - self.db[key] = object - else: - self.db[key][0] += object[0] - self.db[key][2] = object[2] - for cha in object[1]: - if cha not in self.db[key][1]: - self.db[key][1] += cha + if item[0] == "Date": + item[1] = round(55 * gui.scale) + total -= round(55 * gui.scale) + if item[0] == "Bitrate": + item[1] = round(67 * gui.scale) + total -= round(67 * gui.scale) -star_store = StarStore() + if item[0] == "❤": + item[1] = round(27 * gui.scale) + total -= round(27 * gui.scale) + vr = len(gui.pl_st) - fixed_n -class AlbumStarStore: + if vr > 0 and total > 50: - def __init__(self) -> None: - self.db = {} + space = round(total / vr) - def get_key(self, track_object: TrackClass) -> str: - artist = track_object.album_artist - if not artist: - artist = track_object.artist - return artist + ":" + track_object.album + for item in gui.pl_st: + if not item[2]: + item[1] = space - def get_rating(self, track_object: TrackClass): - return self.db.get(self.get_key(track_object), 0) + gui.pl_update += 1 + update_set() - def set_rating(self, track_object: TrackClass, rating): - self.db[self.get_key(track_object)] = rating - if track_object.file_ext == "SUB": - self.db[self.get_key(track_object)] = math.ceil(rating / 2) * 2 - subsonic.set_album_rating(track_object, rating) +colours = ColoursClass() +colours.post_config() - def set_rating_artist_title(self, artist: str, album: str, rating): - self.db[artist + ":" + album] = rating - def get_rating_artist_title(self, artist: str, album: str): - return self.db.get(artist + ":" + album, 0) - - -album_star_store = AlbumStarStore() - - -class Fonts: - """Used to hold font sizes (I forget to use this)""" - - def __init__(self): - self.tabs = 211 - self.panel_title = 213 - - self.side_panel_line1 = 214 - self.side_panel_line2 = 13 - - self.bottom_panel_time = 212 - - # if system == 'Windows': - # self.bottom_panel_time = 12 # The Arial bold font is too big so just leaving this as normal. (lazy) - - -fonts = Fonts() - - -class Input: - """Used to keep track of button states (or should be)""" - - def __init__(self) -> None: - self.mouse_click = False - # self.right_click = False - self.level_2_enter = False - self.key_return_press = False - self.key_tab_press = False - self.backspace_press = 0 - - self.media_key = "" - - def m_key_play(self) -> None: - self.media_key = "Play" - gui.update += 1 - - def m_key_pause(self) -> None: - self.media_key = "Pause" - gui.update += 1 - - def m_key_stop(self) -> None: - self.media_key = "Stop" - gui.update += 1 - - def m_key_next(self) -> None: - self.media_key = "Next" - gui.update += 1 - - def m_key_previous(self) -> None: - self.media_key = "Previous" - gui.update += 1 - - -inp = Input() - - -class KeyMap: - - def __init__(self): - - self.hits = [] # The keys hit this frame - self.maps = {} # Loaded from input.txt - - def load(self): - - path = config_directory / "input.txt" - with path.open(encoding="utf_8") as f: - content = f.read().splitlines() - for p in content: - if len(p) == 0 or len(p) > 100: - continue - if p[0] == " " or p[0] == "#": - continue - - items = p.split() - if 1 < len(items) < 5: - function = items[0] - - if items[1] in ("MB4", "MB5"): - key = items[1] - else: - if prefs.use_scancodes: - key = SDL_GetScancodeFromName(items[1].encode()) - else: - key = SDL_GetKeyFromName(items[1].encode()) - if key == 0: - continue - - mod = [] - - if len(items) > 2: - mod.append(items[2].lower()) - if len(items) > 3: - mod.append(items[3].lower()) - - if function in self.maps: - self.maps[function].append((key, mod)) - else: - self.maps[function] = [(key, mod)] - - def test(self, function): - - if not self.hits: - return False - if function not in self.maps: - return False - - for code, mod in self.maps[function]: - - if code in self.hits: - - ctrl = (key_ctrl_down or key_rctrl_down) * 1 - shift = (key_shift_down or key_shiftr_down) * 10 - alt = (key_lalt or key_ralt) * 100 - - if ctrl + shift + alt == ("ctrl" in mod) * 1 + ("shift" in mod) * 10 + ("alt" in mod) * 100: - return True - - return False - - -keymaps = KeyMap() - - -def update_set(): - """This is used to scale columns when windows is resized or items added/removed""" - wid = gui.plw - round(16 * gui.scale) - if gui.tracklist_center_mode: - wid = gui.tracklist_highlight_width - round(16 * gui.scale) - - total = 0 - for item in gui.pl_st: - if item[2] is False: - total += item[1] - else: - wid -= item[1] - - wid = max(75, wid) - - for i in range(len(gui.pl_st)): - if gui.pl_st[i][2] is False and total: - gui.pl_st[i][1] = int(round((gui.pl_st[i][1] / total) * wid)) # + 1 - - -def auto_size_columns(): - fixed_n = 0 - - wid = gui.plw - round(16 * gui.scale) - if gui.tracklist_center_mode: - wid = gui.tracklist_highlight_width - round(16 * gui.scale) - - total = wid - for item in gui.pl_st: - - if item[2]: - fixed_n += 1 - - if item[0] == "Lyrics": - item[1] = round(50 * gui.scale) - total -= round(50 * gui.scale) - - if item[0] == "Rating": - item[1] = round(80 * gui.scale) - total -= round(80 * gui.scale) - - if item[0] == "Starline": - item[1] = round(78 * gui.scale) - total -= round(78 * gui.scale) - - if item[0] == "Time": - item[1] = round(58 * gui.scale) - total -= round(58 * gui.scale) - - if item[0] == "Codec": - item[1] = round(58 * gui.scale) - total -= round(58 * gui.scale) - - if item[0] == "P" or item[0] == "S" or item[0] == "#": - item[1] = round(32 * gui.scale) - total -= round(32 * gui.scale) - - if item[0] == "Date": - item[1] = round(55 * gui.scale) - total -= round(55 * gui.scale) - - if item[0] == "Bitrate": - item[1] = round(67 * gui.scale) - total -= round(67 * gui.scale) - - if item[0] == "❤": - item[1] = round(27 * gui.scale) - total -= round(27 * gui.scale) - - vr = len(gui.pl_st) - fixed_n - - if vr > 0 and total > 50: - - space = round(total / vr) - - for item in gui.pl_st: - if not item[2]: - item[1] = space - - gui.pl_update += 1 - update_set() - - -class ColoursClass: - """Used to store colour values for UI elements - - These are changed for themes - """ - - def grey(self, value: int) -> list[int]: - return [value, value, value, 255] - - def alpha_grey(self, value: int) -> list[int]: - return [255, 255, 255, value] - - def grey_blend_bg(self, value: int) -> list[int]: - return alpha_blend((255, 255, 255, value), self.box_background) - - def __init__(self) -> None: - - self.deco = None - self.column_colours = {} - self.column_colours_playing = {} - - self.last_album = "" - self.link_text = [100, 200, 252, 255] - - self.tb_line = self.grey(21) # not currently used - self.art_box = self.grey(24) - - self.volume_bar_background = self.grey(30) - self.volume_bar_fill = self.grey(125) - self.seek_bar_background = self.grey(30) - self.seek_bar_fill = self.grey(80) - - self.tab_text_active = self.grey(230) - self.tab_text = self.grey(215) - self.tab_background = self.grey(25) - self.tab_highlight = self.grey(40) - self.tab_background_active = self.grey(45) - - self.title_text = [190, 190, 190, 255] - self.index_text = self.grey(70) - self.time_text = self.grey(180) - self.artist_text = [195, 255, 104, 255] - self.album_text = [245, 240, 90, 255] - - self.index_playing = self.grey(190) - self.artist_playing = [195, 255, 104, 255] - self.album_playing = [245, 240, 90, 255] - self.title_playing = self.grey(230) - - self.time_playing = [180, 194, 107, 255] - - self.playlist_text_missing = self.grey(85) - self.bar_time = self.grey(70) - - self.top_panel_background = self.grey(15) - self.status_text_over = rgb_add_hls(self.top_panel_background, 0, 0.83, 0) - self.status_text_normal = rgb_add_hls(self.top_panel_background, 0, 0.30, -0.15) - - self.side_panel_background = self.grey(18) - self.gallery_background = self.side_panel_background - self.playlist_panel_background = self.grey(21) - self.bottom_panel_colour = self.grey(15) - - self.row_playing_highlight = [255, 255, 255, 4] - self.row_select_highlight = [255, 255, 255, 5] - - self.side_bar_line1 = self.grey(230) - self.side_bar_line2 = self.grey(210) - - self.mode_button_off = self.grey(50) - self.mode_button_over = self.grey(200) - self.mode_button_active = self.grey(190) - - self.media_buttons_over = self.grey(220) - self.media_buttons_active = self.grey(220) - self.media_buttons_off = self.grey(55) - - self.star_line = [100, 100, 100, 255] - self.star_line_playing = None - self.folder_title = [130, 130, 130, 255] - self.folder_line = [40, 40, 40, 255] - - self.scroll_colour = [45, 45, 45, 255] - - self.level_1_bg = [0, 30, 0, 255] - self.level_2_bg = [30, 30, 0, 255] - self.level_3_bg = [30, 0, 0, 255] - self.level_green = [20, 120, 20, 255] - self.level_red = [190, 30, 30, 255] - self.level_yellow = [135, 135, 30, 255] - - self.vis_colour = self.grey(200) - self.vis_bg = [0, 0, 0, 255] - - self.menu_background = None # self.grey(12) - self.menu_highlight_background = None - self.menu_text = [230, 230, 230, 255] - self.menu_text_disabled = self.grey(50) - self.menu_icons = [255, 255, 255, 25] - self.menu_tab = self.grey(30) - - self.gallery_highlight = self.artist_playing - - self.status_info_text = [245, 205, 0, 255] - self.streaming_text = [220, 75, 60, 255] - self.lyrics = self.grey(245) - - self.corner_button = [255, 255, 255, 50] # [60, 60, 60, 255] - self.corner_button_active = [255, 255, 255, 230] # [230, 230, 230, 255] - - self.window_buttons_bg = [0, 0, 0, 50] - self.window_buttons_bg_over = [255, 255, 255, 10] # [80, 80, 80, 120] - self.window_buttons_icon_over = (255, 255, 255, 60) - self.window_button_icon_off = (255, 255, 255, 40) - self.window_button_x_on = None - self.window_button_x_off = self.window_button_icon_off - - self.message_box_bg = self.grey(0) - self.message_box_text = self.grey(230) - - self.sys_title = self.grey(220) - self.sys_title_strong = self.grey(230) - self.lm = False - - self.pluse_colour = [244, 212, 66, 255] - - self.mini_mode_background = [19, 19, 19, 255] - self.mini_mode_border = [45, 45, 45, 255] - self.mini_mode_text_1 = [255, 255, 255, 240] - self.mini_mode_text_2 = [255, 255, 255, 77] - - self.queue_drag_indicator_colour = [200, 50, 240, 255] - - self.playlist_box_background: list[int] = self.side_panel_background - - self.bar_title_text = None - - self.corner_icon = [40, 40, 40, 255] - self.queue_background = None # self.side_panel_background #self.grey(18) # 18 - self.queue_card_background = self.grey(23) - - self.column_bar_background = [30, 30, 30, 255] - self.column_grip = [255, 255, 255, 14] - self.column_bar_text = [240, 240, 240, 255] - - self.window_frame = [30, 30, 30, 255] - - self.box_background: list[int] = [16, 16, 16, 255] - self.box_border = rgb_add_hls(self.box_background, 0, 0.17, 0) - self.box_text_border = rgb_add_hls(self.box_background, 0, 0.1, 0) - self.box_text_label = rgb_add_hls(self.box_background, 0, 0.32, -0.1) - self.box_sub_highlight = rgb_add_hls(self.box_background, 0, 0.07, -0.05) # 58, 47, 85 - self.box_check_border = [255, 255, 255, 18] - - self.box_title_text = self.grey(245) - self.box_text = self.grey(240) - self.box_sub_text = self.grey_blend_bg(225) - self.box_input_text = self.grey(225) - self.box_button_text_highlight = self.grey(250) - self.box_button_text = self.grey(225) - self.box_button_background = alpha_blend([255, 255, 255, 11], self.box_background) - self.box_thumb_background = None - self.box_button_background_highlight = alpha_blend([255, 255, 255, 20], self.box_background) - - self.artist_bio_background = [27, 27, 27, 255] - self.artist_bio_text = [230, 230, 230, 255] - - def post_config(self): - - if self.box_thumb_background is None: - self.box_thumb_background = alpha_mod(self.box_button_background, 175) - - # Pre calculate alpha blend for spec background - self.vis_bg[0] = int(0.05 * 255 + (1 - 0.05) * self.top_panel_background[0]) - self.vis_bg[1] = int(0.05 * 255 + (1 - 0.05) * self.top_panel_background[1]) - self.vis_bg[2] = int(0.05 * 255 + (1 - 0.05) * self.top_panel_background[2]) - - self.message_box_bg = self.box_background - self.sys_tab_bg = self.tab_background - self.sys_tab_hl = self.tab_background_active - self.toggle_box_on = self.folder_title - self.toggle_box_on = [255, 150, 100, 255] - self.toggle_box_on = self.artist_playing - if colour_value(self.toggle_box_on) < 150: - self.toggle_box_on = [160, 160, 160, 255] - # self.time_sub = [255, 255, 255, 80]#alpha_blend([255, 255, 255, 80], self.bottom_panel_colour) - - self.time_sub = rgb_add_hls(self.bottom_panel_colour, 0, 0.29, 0) - - if test_lumi(colours.bottom_panel_colour) < 0.2: - # self.time_sub = [0, 0, 0, 80] - self.time_sub = rgb_add_hls(self.bottom_panel_colour, 0, -0.15, -0.3) - elif test_lumi(colours.bottom_panel_colour) < 0.8: - self.time_sub = [255, 255, 255, 135] - # self.time_sub = self.mode_button_off - - if self.bar_title_text is None: - self.bar_title_text = self.side_bar_line1 - - self.gallery_artist_line = alpha_mod(self.side_bar_line2, 120) - - if self.menu_highlight_background is None: - self.menu_highlight_background = [40, 40, 40, 255] - - if not self.queue_background: - self.queue_background = self.side_panel_background - - if test_lumi(self.queue_background) > 0.8: - self.queue_card_background = alpha_blend([255, 255, 255, 10], self.queue_background) - - if self.menu_background is None and not self.lm: - self.menu_background = self.bottom_panel_colour - - self.message_box_text = self.box_text - self.message_box_border = self.box_border - - if self.window_button_x_on is None: - self.window_button_x_on = self.artist_playing - - if test_lumi(self.column_bar_background) < 0.4: - self.column_bar_text = [40, 40, 40, 200] - self.column_grip = [255, 255, 255, 20] - - def light_mode(self): - - self.lm = True - self.star_line_playing = [255, 255, 255, 255] - self.sys_tab_bg = self.grey(25) - self.sys_tab_hl = self.grey(45) - # self.box_background = self.grey(30) - self.toggle_box_on = self.tab_background_active - # if colour_value(self.tab_background_active) < 250: - # self.toggle_box_on = [255, 255, 255, 200] - - # self.time_sub = [0, 0, 0, 200] - self.gallery_artist_line = self.grey(40) - # self.bar_title_text = self.grey(30) - self.status_text_normal = self.grey(70) - self.status_text_over = self.grey(40) - self.status_info_text = [40, 40, 40, 255] - - # self.bar_title_text = self.grey(255) - self.vis_bg = [235, 235, 235, 255] - # self.menu_background = [240, 240, 240, 250] - # self.menu_text = self.grey(40) - # self.menu_text_disabled = self.grey(180) - # self.menu_highlight_background = [200, 200, 200, 250] - if self.menu_background is None: - self.menu_background = [15, 15, 15, 250] - if not self.menu_icons: - self.menu_icons = [0, 0, 0, 40] - - # self.menu_background = [40, 40, 40, 250] - # self.menu_text = self.grey(220) - # self.menu_text_disabled = self.grey(120) - # self.menu_highlight_background = [120, 80, 220, 250] - - self.corner_button = self.grey(160) - self.corner_button_active = self.grey(35) - # self.window_buttons_bg = [0, 0, 0, 5] - self.message_box_bg = [245, 245, 245, 255] - self.message_box_text = self.grey(20) - self.message_box_border = self.grey(40) - self.gallery_background = self.grey(230) - self.gallery_artist_line = self.grey(40) - self.pluse_colour = [212, 66, 244, 255] - - # view_box.off_colour = self.grey(200) - - -colours = ColoursClass() -colours.post_config() - - -def set_colour(colour): - SDL_SetRenderDrawColor(renderer, colour[0], colour[1], colour[2], colour[3]) +def set_colour(colour): + SDL_SetRenderDrawColor(renderer, colour[0], colour[1], colour[2], colour[3]) def get_themes(deco: bool = False): @@ -2520,53 +1073,6 @@ def scan_folders(folders: list[str]) -> None: "scroll-enable": True, } - -class TrackClass: - """This is the fundamental object/data structure of a track""" - - def __init__(self) -> None: - self.index: int = 0 - self.subtrack: int = 0 - self.fullpath: str = "" - self.filename: str = "" - self.parent_folder_path: str = "" - self.parent_folder_name: str = "" - self.file_ext: str = "" - self.size: int = 0 - self.modified_time: float = 0 - - self.is_network: bool = False - self.url_key: str = "" - self.art_url_key: str = "" - - self.artist: str = "" - self.album_artist: str = "" - self.title: str = "" - self.composer: str = "" - self.length: float = 0 - self.bitrate: int = 0 - self.samplerate: int = 0 - self.bit_depth: int = 0 - self.album: str = "" - self.date: str = "" - self.track_number: str = "" - self.track_total: str = "" - self.start_time: int = 0 - self.is_cue: bool = False - self.is_embed_cue: bool = False - self.cue_sheet: str = "" - self.genre: str = "" - self.found: bool = True - self.skips: int = 0 - self.comment: str = "" - self.disc_number: str = "" - self.disc_total: str = "" - self.lyrics: str = "" - - self.lfm_friend_likes = set() - self.lfm_scrobbles: int = 0 - self.misc: list = {} - def get_end_folder(direc): for w in range(len(direc)): if direc[-w - 1] == "\\" or direc[-w - 1] == "/": @@ -2581,19 +1087,6 @@ def set_path(nt: TrackClass, path: str) -> None: nt.parent_folder_name = get_end_folder(os.path.dirname(path)) nt.file_ext = os.path.splitext(os.path.basename(path))[1][1:].upper() -class LoadClass: - """Object for import track jobs (passed to worker thread)""" - - def __init__(self) -> None: - self.target: str = "" - self.playlist: int = 0 # Playlist UID - self.tracks: list[TrackClass] = [] - self.stage: int = 0 - self.playlist_position: int | None = None - self.replace_stem: bool = False - self.notify: bool = False - self.play: bool = False - self.force_scan: bool = False # url_saves = [] rename_files_previous = "" @@ -2624,52 +1117,7 @@ def show_message(line1: str, line2: str ="", line3: str = "", mode: str = "info" gui.update = 1 -# ----------------------------------------------------- -# STATE LOADING -# Loading of program data from previous run -gbc.disable() -ggc = 2 - -star_path1 = user_directory / "star.p" -star_path2 = user_directory / "star.p.backup" -star_size1 = 0 -star_size2 = 0 -to_load = star_path1 -if star_path1.is_file(): - star_size1 = star_path1.stat().st_size -if star_path2.is_file(): - star_size2 = star_path2.stat().st_size -if star_size2 > star_size1: - logging.warning("Loading backup star.p as it was bigger than regular file!") - to_load = star_path2 -if star_size1 == 0 and star_size2 == 0: - logging.warning("Star database file is missing, first run? Will create one anew!") -else: - try: - with to_load.open("rb") as file: - star_store.db = pickle.load(file) - except Exception: - logging.exception("Unknown error loading star.p file") - - -album_star_path = user_directory / "album-star.p" -if album_star_path.is_file(): - try: - with album_star_path.open("rb") as file: - album_star_store.db = pickle.load(file) - except Exception: - logging.exception("Unknown error loading album-star.p file") -else: - logging.warning("Album star database file is missing, first run? Will create one anew!") - -if (user_directory / "lyrics_substitutions.json").is_file(): - try: - with (user_directory / "lyrics_substitutions.json").open() as f: - prefs.lyrics_subs = json.load(f) - except FileNotFoundError: - logging.error("No existing lyrics_substitutions.json file") - except Exception: - logging.exception("Unknown error loading lyrics_substitutions.json") +#11REWORK perf_timer.set() @@ -2730,413 +1178,9 @@ def pumper(): shoot_pump.daemon = True shoot_pump.start() -state_path1 = user_directory / "state.p" -state_path2 = user_directory / "state.p.backup" -for t in range(2): - # os.path.getsize(user_directory / "state.p") < 100 - try: - if t == 0: - if not state_path1.is_file(): - continue - with state_path1.open("rb") as file: - save = pickle.load(file) - if t == 1: - if not state_path2.is_file(): - logging.warning("State database file is missing, first run? Will create one anew!") - break - logging.warning("Loading backup state.p!") - with state_path2.open("rb") as file: - save = pickle.load(file) - - # def tt(): - # while True: - # logging.info(state_file.tell()) - # time.sleep(0.01) - # shooter(tt) - - db_version = save[17] - if db_version != latest_db_version: - if db_version > latest_db_version: - logging.critical(f"Loaded DB version: '{db_version}' is newer than latest known DB version '{latest_db_version}', refusing to load!\nAre you running an out of date Tauon version using Configuration directory from a newer one?") - sys.exit(42) - logging.warning(f"Loaded older DB version: {db_version}") - if save[63] is not None: - prefs.ui_scale = save[63] - # prefs.ui_scale = 1.3 - # gui.__init__() - - if save[0] is not None: - master_library = save[0] - master_count = save[1] - playlist_playing = save[2] - playlist_active = save[3] - playlist_view_position = save[4] - if save[5] is not None: - if db_version > 68: - multi_playlist = [] - tauonplaylist_jar = save[5] - for d in tauonplaylist_jar: - nt = TauonPlaylist(**d) - multi_playlist.append(nt) - else: - multi_playlist = save[5] - volume = save[6] - track_queue = save[7] - playing_in_queue = save[8] - default_playlist = save[9] - # playlist_playing = save[10] - # cue_list = save[11] - # radio_field_text = save[12] - theme = save[13] - folder_image_offsets = save[14] - # lfm_username = save[15] - # lfm_hash = save[16] - view_prefs = save[18] - # window_size = save[19] - gui.save_size = copy.copy(save[19]) - gui.rspw = save[20] - # savetime = save[21] - gui.vis_want = save[22] - selected_in_playlist = save[23] - if save[24] is not None: - album_mode_art_size = save[24] - if save[25] is not None: - draw_border = save[25] - if save[26] is not None: - prefs.enable_web = save[26] - if save[27] is not None: - prefs.allow_remote = save[27] - if save[28] is not None: - prefs.expose_web = save[28] - if save[29] is not None: - prefs.enable_transcode = save[29] - if save[30] is not None: - prefs.show_rym = save[30] - # if save[31] is not None: - # combo_mode_art_size = save[31] - if save[32] is not None: - gui.maximized = save[32] - if save[33] is not None: - prefs.prefer_bottom_title = save[33] - if save[34] is not None: - gui.display_time_mode = save[34] - # if save[35] is not None: - # prefs.transcode_mode = save[35] - if save[36] is not None: - prefs.transcode_codec = save[36] - if save[37] is not None: - prefs.transcode_bitrate = save[37] - # if save[38] is not None: - # prefs.line_style = save[38] - # if save[39] is not None: - # prefs.cache_gallery = save[39] - if save[40] is not None: - prefs.playlist_font_size = save[40] - if save[41] is not None: - prefs.use_title = save[41] - if save[42] is not None: - gui.pl_st = save[42] - # if save[43] is not None: - # gui.set_mode = save[43] - # gui.set_bar = gui.set_mode - if save[45] is not None: - prefs.playlist_row_height = save[45] - if save[46] is not None: - prefs.show_wiki = save[46] - if save[47] is not None: - prefs.auto_extract = save[47] - if save[48] is not None: - prefs.colour_from_image = save[48] - if save[49] is not None: - gui.set_bar = save[49] - if save[50] is not None: - gui.gallery_show_text = save[50] - if save[51] is not None: - gui.bb_show_art = save[51] - # if save[52] is not None: - # gui.show_stars = save[52] - if save[53] is not None: - prefs.auto_lfm = save[53] - if save[54] is not None: - prefs.scrobble_mark = save[54] - if save[55] is not None: - prefs.replay_gain = save[55] - # if save[56] is not None: - # prefs.radio_page_lyrics = save[56] - if save[57] is not None: - prefs.show_gimage = save[57] - if save[58] is not None: - prefs.end_setting = save[58] - if save[59] is not None: - prefs.show_gen = save[59] - # if save[60] is not None: - # url_saves = save[60] - if save[61] is not None: - prefs.auto_del_zip = save[61] - if save[62] is not None: - gui.level_meter_colour_mode = save[62] - if save[64] is not None: - prefs.show_lyrics_side = save[64] - # if save[65] is not None: - # prefs.last_device = save[65] - if save[66] is not None: - gui.restart_album_mode = save[66] - if save[67] is not None: - album_playlist_width = save[67] - if save[68] is not None: - prefs.transcode_opus_as = save[68] - if save[69] is not None: - gui.star_mode = save[69] - if save[70] is not None: - gui.rsp = save[70] - if save[71] is not None: - gui.lsp = save[71] - if save[72] is not None: - gui.rspw = save[72] - if save[73] is not None: - gui.pref_gallery_w = save[73] - if save[74] is not None: - gui.pref_rspw = save[74] - if save[75] is not None: - gui.show_hearts = save[75] - if save[76] is not None: - prefs.monitor_downloads = save[76] - if save[77] is not None: - gui.artist_info_panel = save[77] - if save[78] is not None: - prefs.extract_to_music = save[78] - if save[79] is not None: - prefs.enable_lb = save[79] - # if save[80] is not None: - # prefs.lb_token = save[80] - # if prefs.lb_token is None: - # prefs.lb_token = "" - if save[81] is not None: - rename_files_previous = save[81] - if save[82] is not None: - rename_folder_previous = save[82] - if save[83] is not None: - prefs.use_jump_crossfade = save[83] - if save[84] is not None: - prefs.use_transition_crossfade = save[84] - if save[85] is not None: - prefs.show_notifications = save[85] - # if save[86] is not None: - # prefs.true_shuffle = save[86] - if save[87] is not None: - gui.remember_library_mode = save[87] - # if save[88] is not None: - # prefs.show_queue = save[88] - # if save[89] is not None: - # prefs.show_transfer = save[89] - if save[90] is not None: - if db_version > 68: - tauonqueueitem_jar = save[90] - for d in tauonqueueitem_jar: - nt = TauonQueueItem(**d) - p_force_queue.append(nt) - else: - p_force_queue = save[90] - if save[91] is not None: - prefs.use_pause_fade = save[91] - if save[92] is not None: - prefs.append_total_time = save[92] - if save[93] is not None: - prefs.backend = save[93] # moved to config file - if save[94] is not None: - prefs.album_shuffle_mode = save[94] - if save[95] is not None: - prefs.album_repeat_mode = save[95] - # if save[96] is not None: - # prefs.finish_current = save[96] - if save[97] is not None: - reload_state = save[97] - # if save[98] is not None: - # prefs.reload_play_state = save[98] - if save[99] is not None: - prefs.last_fm_token = save[99] - if save[100] is not None: - prefs.last_fm_username = save[100] - # if save[101] is not None: - # prefs.use_card_style = save[101] - # if save[102] is not None: - # prefs.auto_lyrics = save[102] - if save[103] is not None: - prefs.auto_lyrics_checked = save[103] - if save[104] is not None: - prefs.show_side_art = save[104] - if save[105] is not None: - prefs.window_opacity = save[105] - if save[106] is not None: - prefs.gallery_single_click = save[106] - if save[107] is not None: - prefs.tabs_on_top = save[107] - if save[108] is not None: - prefs.showcase_vis = save[108] - if save[109] is not None: - prefs.spec2_colour_mode = save[109] - # if save[110] is not None: - # prefs.device_buffer = save[110] - if save[111] is not None: - prefs.use_eq = save[111] - if save[112] is not None: - prefs.eq = save[112] - if save[113] is not None: - prefs.bio_large = save[113] - if save[114] is not None: - prefs.discord_show = save[114] - if save[115] is not None: - prefs.min_to_tray = save[115] - if save[116] is not None: - prefs.guitar_chords = save[116] - if save[117] is not None: - prefs.playback_follow_cursor = save[117] - if save[118] is not None: - prefs.art_bg = save[118] - if save[119] is not None: - prefs.random_mode = save[119] - if save[120] is not None: - prefs.repeat_mode = save[120] - if save[121] is not None: - prefs.art_bg_stronger = save[121] - if save[122] is not None: - prefs.art_bg_always_blur = save[122] - if save[123] is not None: - prefs.failed_artists = save[123] - if save[124] is not None: - prefs.artist_list = save[124] - if save[125] is not None: - prefs.auto_sort = save[125] - if save[126] is not None: - prefs.lyrics_enables = save[126] - if save[127] is not None: - prefs.fanart_notify = save[127] - if save[128] is not None: - prefs.bg_showcase_only = save[128] - if save[129] is not None: - prefs.discogs_pat = save[129] - if save[130] is not None: - prefs.mini_mode_mode = save[130] - if save[131] is not None: - after_scan = save[131] - if save[132] is not None: - gui.gallery_positions = save[132] - if save[133] is not None: - prefs.chart_bg = save[133] - if save[134] is not None: - prefs.left_panel_mode = save[134] - if save[135] is not None: - gui.last_left_panel_mode = save[135] - # if save[136] is not None: - # prefs.gst_device = save[136] - if save[137] is not None: - search_string_cache = save[137] - if save[138] is not None: - search_dia_string_cache = save[138] - if save[139] is not None: - gen_codes = save[139] - if save[140] is not None: - gui.show_ratings = save[140] - if save[141] is not None: - gui.show_album_ratings = save[141] - if save[142] is not None: - prefs.radio_urls = save[142] - if save[143] is not None: - gui.restore_showcase_view = save[143] - if save[144] is not None: - gui.saved_prime_tab = save[144] - if save[145] is not None: - gui.saved_prime_direction = save[145] - if save[146] is not None: - prefs.sync_playlist = save[146] - if save[147] is not None: - prefs.spot_client = save[147] - if save[148] is not None: - prefs.spot_secret = save[148] - if save[149] is not None: - prefs.show_band = save[149] - if save[150] is not None: - prefs.download_playlist = save[150] - if save[151] is not None: - spot_cache_saved_albums = save[151] - if save[152] is not None: - prefs.auto_rec = save[152] - if save[153] is not None: - prefs.spotify_token = save[153] - if save[154] is not None: - prefs.use_libre_fm = save[154] - if save[155] is not None: - prefs.old_playlist_box_position = save[155] - if save[156] is not None: - prefs.artist_list_sort_mode = save[156] - if save[157] is not None: - prefs.phazor_device_selected = save[157] - if save[158] is not None: - prefs.failed_background_artists = save[158] - if save[159] is not None: - prefs.bg_flips = save[159] - if save[160] is not None: - prefs.tray_show_title = save[160] - if save[161] is not None: - prefs.artist_list_style = save[161] - if save[162] is not None: - trackclass_jar = save[162] - for d in trackclass_jar: - nt = TrackClass() - nt.__dict__.update(d) - master_library[d["index"]] = nt - if save[163] is not None: - prefs.premium = save[163] - if save[164] is not None: - gui.restore_radio_view = save[164] - if save[165] is not None: - radio_playlists = save[165] - if save[166] is not None: - radio_playlist_viewing = save[166] - if save[167] is not None: - prefs.radio_thumb_bans = save[167] - if save[168] is not None: - prefs.playlist_exports = save[168] - if save[169] is not None: - prefs.show_chromecast = save[169] - if save[170] is not None: - prefs.cache_list = save[170] - if save[171] is not None: - prefs.shuffle_lock = save[171] - if save[172] is not None: - prefs.album_shuffle_lock_mode = save[172] - if save[173] is not None: - gui.was_radio = save[173] - if save[174] is not None: - prefs.spot_username = save[174] - # if save[175] is not None: - # prefs.spot_password = save[175] - if save[176] is not None: - prefs.artist_list_threshold = save[176] - if save[177] is not None: - prefs.tray_theme = save[177] - if save[178] is not None: - prefs.row_title_format = save[178] - if save[179] is not None: - prefs.row_title_genre = save[179] - if save[180] is not None: - prefs.row_title_separator_type = save[180] - if save[181] is not None: - prefs.replay_preamp = save[181] - if save[182] is not None: - prefs.gallery_combine_disc = save[182] - - del save - break - - except IndexError: - logging.exception("Index error") - break - except Exception: - logging.exception("Failed to load save file") +#12REWORK core_timer.set() -logging.info(f"Database loaded in {round(perf_timer.get(), 3)} seconds.") perf_timer.set() keys = set(master_library.keys()) @@ -3152,11 +1196,7 @@ def pumper(): pump = False shoot_pump.join() -# temporary -if window_size is None: - window_size = window_default_size - gui.rspw = 200 - +#13REWORK def track_number_process(line: str) -> str: line = str(line).split("/", 1)[0].lstrip("0") @@ -3229,12 +1269,7 @@ def get_theme_name(number: int) -> str: download_directories: list[str] = [] -if download_directory.is_dir(): - download_directories.append(str(download_directory)) - -if music_directory is not None and music_directory.is_dir(): - download_directories.append(str(music_directory)) - +#14REWORK cf = Config() @@ -3837,80 +1872,9 @@ def load_prefs(): "Format is fontname + size. Default is Monospace 10") -load_prefs() -save_prefs() - -# Temporary -if 0 < db_version <= 34: - prefs.theme_name = get_theme_name(theme) -if 0 < db_version <= 66: - prefs.device_buffer = 80 -if 0 < db_version <= 53: - logging.info("Resetting fonts to defaults") - prefs.linux_font = "Noto Sans" - prefs.linux_font_semibold = "Noto Sans Medium" - prefs.linux_font_bold = "Noto Sans Bold" - save_prefs() - -# Auto detect lang -lang: list[str] | None = None -if prefs.ui_lang != "auto" or prefs.ui_lang == "": - # Force set lang - lang = [prefs.ui_lang] - -f = gettext.find("tauon", localedir=str(locale_directory), languages=lang) -if f: - translation = gettext.translation("tauon", localedir=str(locale_directory), languages=lang) - translation.install() - builtins._ = translation.gettext - - logging.info(f"Translation file for '{lang}' loaded") -elif lang: - logging.error(f"No translation file available for '{lang}'") - -# ---- - -sss = SDL_SysWMinfo() -SDL_GetWindowWMInfo(t_window, sss) - -if prefs.use_gamepad: - SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) - -smtc = False - -if msys and win_ver >= 10: - - #logging.info(sss.info.win.window) - SMTC_path = install_directory / "lib" / "TauonSMTC.dll" - if SMTC_path.exists(): - try: - sm = ctypes.cdll.LoadLibrary(str(SMTC_path)) - - def SMTC_button_callback(button: int) -> None: - logging.debug(f"SMTC sent key ID: {button}") - if button == 1: - inp.media_key = "Play" - if button == 2: - inp.media_key = "Pause" - if button == 3: - inp.media_key = "Next" - if button == 4: - inp.media_key = "Previous" - if button == 5: - inp.media_key = "Stop" - gui.update += 1 - tauon.wake() - - close_callback = ctypes.WINFUNCTYPE(ctypes.c_void_p, ctypes.c_int)(SMTC_button_callback) - smtc = sm.init(close_callback) == 0 - except Exception: - logging.exception("Failed to load TauonSMTC.dll - Media keys will not work!") - else: - logging.warning("Failed to load TauonSMTC.dll - Media keys will not work!") - - -def auto_scale() -> None: +#15REWORK +def auto_scale(prefs: Prefs) -> None: old = prefs.scale_want if prefs.x_scale: @@ -3946,7 +1910,7 @@ def auto_scale() -> None: show_message(_("Detected unsuitable UI scaling."), _("Scaling setting reset to 1x")) prefs.scale_want = 1.0 -auto_scale() +#16REWORK def scale_assets(scale_want: int, force: bool = False) -> None: @@ -3994,36 +1958,7 @@ def scale_assets(scale_want: int, force: bool = False) -> None: global album_mode_art_size album_mode_art_size = int(album_mode_art_size * diff_ratio) - -scale_assets(scale_want=prefs.scale_want) - -try: - #star_lines = view_prefs['star-lines'] - update_title = view_prefs["update-title"] - prefs.prefer_side = view_prefs["side-panel"] - prefs.dim_art = False # view_prefs['dim-art'] - #gui.turbo = view_prefs['level-meter'] - #pl_follow = view_prefs['pl-follow'] - scroll_enable = view_prefs["scroll-enable"] - if "break-enable" in view_prefs: - break_enable = view_prefs["break-enable"] - else: - logging.warning("break-enable not found in view_prefs[] when trying to load settings! First run?") - #dd_index = view_prefs['dd-index'] - #custom_line_mode = view_prefs['custom-line'] - #thick_lines = view_prefs['thick-lines'] - if "append-date" in view_prefs: - prefs.append_date = view_prefs["append-date"] - else: - logging.warning("append-date not found in view_prefs[] when trying to load settings! First run?") -except KeyError: - logging.exception("Failed to load settings - pref not found!") -except Exception: - logging.exception("Failed to load settings!") - -if prefs.prefer_side is False: - gui.rsp = False - +#17REWORK def get_global_mouse(): i_y = pointer(c_int(0)) @@ -4039,9 +1974,6 @@ def get_window_position(): return i_x.contents.value, i_y.contents.value -# Access functions from libopenmpt for scanning tracker files -class MOD(Structure): - _fields_ = [("ctl", c_char_p), ("value", c_char_p)] mpt = None @@ -4062,41 +1994,6 @@ class MOD(Structure): -class GMETrackInfo(Structure): - _fields_ = [ - ("length", c_int), - ("intro_length", c_int), - ("loop_length", c_int), - ("play_length", c_int), - ("fade_length", c_int), - ("i5", c_int), - ("i6", c_int), - ("i7", c_int), - ("i8", c_int), - ("i9", c_int), - ("i10", c_int), - ("i11", c_int), - ("i12", c_int), - ("i13", c_int), - ("i14", c_int), - ("i15", c_int), - ("system", c_char_p), - ("game", c_char_p), - ("song", c_char_p), - ("author", c_char_p), - ("copyright", c_char_p), - ("comment", c_char_p), - ("dumper", c_char_p), - ("s7", c_char_p), - ("s8", c_char_p), - ("s9", c_char_p), - ("s10", c_char_p), - ("s11", c_char_p), - ("s12", c_char_p), - ("s13", c_char_p), - ("s14", c_char_p), - ("s15", c_char_p), - ] gme = None @@ -4745,42246 +2642,20706 @@ def get_radio_art() -> None: gui.clear_image_cache_next += 1 -class PlayerCtl: - """Main class that controls playback (play, pause, stepping, playlists, queue etc). Sends commands to backend.""" - - # C-PC - def __init__(self): - - self.running: bool = True - self.prefs: Prefs = prefs - self.install_directory: Path = install_directory - - # Database - - self.master_count = master_count - self.total_playtime: float = 0 - self.master_library = master_library - # Lets clients know when to invalidate cache - self.db_inc = random.randint(0, 10000) - # self.star_library = star_library - self.LoadClass = LoadClass - - self.gen_codes = gen_codes - - self.shuffle_pools = {} - self.after_import_flag = False - self.quick_add_target = None - - self.album_mbid_release_cache = {} - self.album_mbid_release_group_cache = {} - self.mbid_image_url_cache = {} - - # Misc player control - - self.url: str = "" - # self.save_urls = url_saves - self.tag_meta: str = "" - self.found_tags = {} - self.encoder_pause = 0 - - # Playback - - self.track_queue = track_queue - self.queue_step = playing_in_queue - self.playing_time = 0 - self.playlist_playing_position = playlist_playing # track in playlist that is playing - if self.playlist_playing_position is None: - self.playlist_playing_position = -1 - self.playlist_view_position = playlist_view_position - self.selected_in_playlist = selected_in_playlist - self.target_open = "" - self.target_object = None - self.start_time = 0 - self.b_start_time = 0 - self.playerCommand = "" - self.playerSubCommand = "" - self.playerCommandReady = False - self.playing_state: int = 0 - self.playing_length: float = 0 - self.jump_time = 0 - self.random_mode = prefs.random_mode - self.repeat_mode = prefs.repeat_mode - self.album_repeat_mode = prefs.album_repeat_mode - self.album_shuffle_mode = prefs.album_shuffle_mode - # self.album_shuffle_pool = [] - # self.album_shuffle_id = "" - self.last_playing_time = 0 - self.multi_playlist = multi_playlist - self.active_playlist_viewing: int = playlist_active # the playlist index that is being viewed - self.active_playlist_playing: int = playlist_active # the playlist index that is playing from - self.force_queue: list[TauonQueueItem] = p_force_queue - self.pause_queue: bool = False - self.left_time = 0 - self.left_index = 0 - self.player_volume: float = volume - self.new_time = 0 - self.time_to_get = [] - self.a_time = 0 - self.b_time = 0 - # self.playlist_backup = [] - self.active_replaygain = 0 - self.auto_stop = False - - self.record_stream = False - self.record_title = "" - - # Bass - - self.bass_devices = [] - self.set_device = 0 - - self.gst_devices = [] # Display names - self.gst_outputs = {} # Display name : (sink, device) - -# TODO(Martin) : Fix this by moving the class to root of the module - self.mpris: Gnome.main.MPRIS | None = None - self.tray_update = None - self.eq = [0] * 2 # not used - self.enable_eq = True # not used - - self.playing_time_int = 0 # playing time but with no decimel - - self.windows_progress = None - - self.finish_transition = False - # self.queue_target = 0 - self.start_time_target = 0 - - self.decode_time = 0 - self.download_time = 0 - - self.radio_meta_on = "" - - self.radio_scrobble_trip = True - self.radio_scrobble_timer = Timer() - - self.radio_image_bin = None - self.radio_rate_timer = Timer(2) - self.radio_poll_timer = Timer(2) - - self.volume_update_timer = Timer() - self.wake_past_time = 0 - - self.regen_in_progress = False - self.notify_in_progress = False - - self.radio_playlists = radio_playlists - self.radio_playlist_viewing = radio_playlist_viewing - self.tag_history = {} - - self.commit: int | None = None - self.spot_playing = False - - self.buffering_percent = 0 - - def notify_change(self) -> None: - self.db_inc += 1 - tauon.bg_save() - - def update_tag_history(self) -> None: - if prefs.auto_rec: - self.tag_history[radiobox.song_key] = { - "title": radiobox.dummy_track.title, - "artist": radiobox.dummy_track.artist, - "album": radiobox.dummy_track.album, - # "image": self.radio_image_bin - } - - def radio_progress(self) -> None: - if radiobox.loaded_url and "radio.plaza.one" in radiobox.loaded_url and self.radio_poll_timer.get() > 0: - self.radio_poll_timer.force_set(-10) - response = requests.get("https://api.plaza.one/status", timeout=10) - - if response.status_code == 200: - d = json.loads(response.text) - if "song" in d and "artist" in d["song"] and "title" in d["song"]: - self.tag_meta = d["song"]["artist"] + " - " + d["song"]["title"] - - if self.tag_meta: - if self.radio_rate_timer.get() > 7 and self.radio_meta_on != self.tag_meta: - self.radio_rate_timer.set() - self.radio_scrobble_trip = False - self.radio_meta_on = self.tag_meta - - radiobox.dummy_track.art_url_key = "" - radiobox.dummy_track.title = "" - radiobox.dummy_track.date = "" - radiobox.dummy_track.artist = "" - radiobox.dummy_track.album = "" - radiobox.dummy_track.lyrics = "" - radiobox.dummy_track.date = "" - - tags = self.found_tags - if "title" in tags: - radiobox.dummy_track.title = tags["title"] - if "artist" in tags: - radiobox.dummy_track.artist = tags["artist"] - if "year" in tags: - radiobox.dummy_track.date = tags["year"] - if "album" in tags: - radiobox.dummy_track.album = tags["album"] - - elif self.tag_meta.count( - "-") == 1 and ":" not in self.tag_meta and "advert" not in self.tag_meta.lower(): - artist, title = self.tag_meta.split("-") - radiobox.dummy_track.title = title.strip() - radiobox.dummy_track.artist = artist.strip() - - if self.tag_meta: - radiobox.song_key = self.tag_meta - else: - radiobox.song_key = radiobox.dummy_track.artist + " - " + radiobox.dummy_track.title - - self.update_tag_history() - if radiobox.loaded_url not in radiobox.websocket_source_urls: - self.radio_image_bin = None - logging.info("NEXT RADIO TRACK") - - try: - get_radio_art() - except Exception: - logging.exception("Get art error") - - self.notify_update(mpris=False) - if self.mpris: - self.mpris.update(force=True) - - lfm_scrobbler.listen_track(radiobox.dummy_track) - lfm_scrobbler.start_queue() - - if self.radio_scrobble_trip is False and self.radio_scrobble_timer.get() > 45: - self.radio_scrobble_trip = True - lfm_scrobbler.scrob_full_track(copy.deepcopy(radiobox.dummy_track)) - - def update_shuffle_pool(self, pl_id: int) -> None: - new_pool = copy.deepcopy(self.multi_playlist[id_to_pl(pl_id)].playlist_ids) - random.shuffle(new_pool) - self.shuffle_pools[pl_id] = new_pool - logging.info("Refill shuffle pool") - - def notify_update_fire(self) -> None: - if self.mpris is not None: - self.mpris.update() - if tauon.update_play_lock is not None: - tauon.update_play_lock() - # if self.tray_update is not None: - # self.tray_update() - self.notify_in_progress = False - - def notify_update(self, mpris: bool = True) -> None: - tauon.tray_releases += 1 - if tauon.tray_lock.locked(): - try: - tauon.tray_lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked tray_lock") - else: - logging.exception("Unknown RuntimeError trying to release tray_lock") - except Exception: - logging.exception("Failed to release tray_lock") - - if mpris and smtc: - tr = self.playing_object() - if tr: - state = 0 - if self.playing_state == 1: - state = 1 - if self.playing_state == 2: - state = 2 - image_path = "" - try: - image_path = tauon.thumb_tracks.path(tr) - except Exception: - logging.exception("Failed to set image_path from thumb_tracks.path") +#18REWORK - if image_path is None: - image_path = "" +def auto_name_pl(target_pl: int) -> None: + if not pctl.multi_playlist[target_pl].playlist_ids: + return - image_path = image_path.replace("/", "\\") - #logging.info(image_path) + albums = [] + artists = [] + parents = [] - sm.update( - state, tr.title.encode("utf-16"), len(tr.title), tr.artist.encode("utf-16"), len(tr.artist), - image_path.encode("utf-16"), len(image_path)) + track = None + for index in pctl.multi_playlist[target_pl].playlist_ids: + track = pctl.get_track(index) + albums.append(track.album) + if track.album_artist: + artists.append(track.album_artist) + else: + artists.append(track.artist) + parents.append(track.parent_folder_path) - if self.mpris is not None and mpris is True: - while self.notify_in_progress: - time.sleep(0.01) - self.notify_in_progress = True - shoot = threading.Thread(target=self.notify_update_fire) - shoot.daemon = True - shoot.start() - if prefs.art_bg or (gui.mode == 3 and prefs.mini_mode_mode == 5): - tauon.thread_manager.ready("style") + nt = "" + artist = "" - def get_url(self, track_object: TrackClass) -> tuple[str | None, dict | None] | None: - if track_object.file_ext == "TIDAL": - return tauon.tidal.resolve_stream(track_object), None - if track_object.file_ext == "PLEX": - return plex.resolve_stream(track_object.url_key), None + if track: + artist = track.artist + if track.album_artist: + artist = track.album_artist - if track_object.file_ext == "JELY": - return jellyfin.resolve_stream(track_object.url_key) + if track and albums and albums[0] and albums.count(albums[0]) == len(albums): + nt = artist + " - " + track.album - if track_object.file_ext == "KOEL": - return koel.resolve_stream(track_object.url_key) + elif track and artists and artists[0] and artists.count(artists[0]) == len(artists): + nt = artists[0] - if track_object.file_ext == "SUB": - return subsonic.resolve_stream(track_object.url_key) + else: + nt = os.path.basename(commonprefix(parents)) - if track_object.file_ext == "TAU": - return tau.resolve_stream(track_object.url_key), None + pctl.multi_playlist[target_pl].title = nt - return None, None - def playing_playlist(self) -> list[int] | None: - return self.multi_playlist[self.active_playlist_playing].playlist_ids +def get_object(index: int) -> TrackClass: + return pctl.master_library[index] - def playing_ready(self) -> bool: - return len(self.track_queue) > 0 - def selected_ready(self) -> bool: - return default_playlist and self.selected_in_playlist < len(default_playlist) +def update_title_do() -> None: + if pctl.playing_state > 0: + if len(pctl.track_queue) > 0: + line = pctl.master_library[pctl.track_queue[pctl.queue_step]].artist + " - " + \ + pctl.master_library[pctl.track_queue[pctl.queue_step]].title + # line += " : : Tauon Music Box" + line = line.encode("utf-8") + SDL_SetWindowTitle(t_window, line) + else: + line = "Tauon Music Box" + line = line.encode("utf-8") + SDL_SetWindowTitle(t_window, line) - def render_playlist(self) -> None: - if taskbar_progress and msys and self.windows_progress: - self.windows_progress.update(True) - gui.pl_update = 1 - def show_selected(self) -> int: - if gui.playlist_view_length < 1: - return 0 +def open_encode_out() -> None: + if not prefs.encoder_output.exists(): + prefs.encoder_output.mkdir() + if system == "Windows" or msys: + line = r"explorer " + prefs.encoder_output.replace("/", "\\") + subprocess.Popen(line) + else: + if macos: + subprocess.Popen(["open", prefs.encoder_output]) + else: + subprocess.Popen(["xdg-open", prefs.encoder_output]) - global shift_selection - - for i in range(len(self.multi_playlist[self.active_playlist_viewing].playlist_ids)): - - if i == self.selected_in_playlist: - - if i < self.playlist_view_position: - self.playlist_view_position = i - random.randint(2, int((gui.playlist_view_length / 3) * 2) + int(gui.playlist_view_length / 6)) - logging.debug("Position changed show selected (a)") - elif abs(self.playlist_view_position - i) > gui.playlist_view_length: - self.playlist_view_position = i - logging.debug("Position changed show selected (b)") - if i > 6: - self.playlist_view_position -= 5 - logging.debug("Position changed show selected (c)") - if i > gui.playlist_view_length * 1 and i + (gui.playlist_view_length * 2) < len( - self.multi_playlist[self.active_playlist_viewing].playlist_ids) and i > 10: - self.playlist_view_position = i - random.randint(2, int(gui.playlist_view_length / 3) * 2) - logging.debug("Position changed show selected (d)") - break - self.render_playlist() +def g_open_encode_out(a, b, c) -> None: + open_encode_out() - return 0 +def notify_song_fire(notification, delay, id) -> None: + time.sleep(delay) + notification.show() + if id is None: + return - def get_track(self, track_index: int) -> TrackClass: - """Get track object by track_index""" - return self.master_library[track_index] + time.sleep(8) + if id == gui.notify_main_id: + notification.close() - def get_track_in_playlist(self, track_index: int, playlist_index: int) -> TrackClass: - """Get track object by playlist_index and track_index""" - if playlist_index == -1: - playlist_index = self.active_playlist_viewing - try: - playlist = self.multi_playlist[playlist_index].playlist - return self.get_track(playlist[track_index]) - except IndexError: - logging.exception("Failed getting track object by playlist_index and track_index!") - except Exception: - logging.exception("Unknown error getting track object by playlist_index and track_index!") - return None +def notify_song(notify_of_end: bool = False, delay: float = 0.0) -> None: + if not de_notify_support: + return - def show_object(self) -> None: - """The track to show in the metadata side panel""" - target_track = None + if notify_of_end and prefs.end_setting != "stop": + return - if self.playing_state == 3: - return radiobox.dummy_track + if prefs.show_notifications and pctl.playing_object() is not None and not window_is_focused(): + if prefs.stop_notifications_mini_mode and gui.mode == 3: + return - if 3 > self.playing_state > 0: - target_track = self.playing_object() + track = pctl.playing_object() - elif self.playing_state == 0 and prefs.meta_shows_selected: - if -1 < self.selected_in_playlist < len(self.multi_playlist[self.active_playlist_viewing].playlist_ids): - target_track = self.get_track(self.multi_playlist[self.active_playlist_viewing].playlist_ids[self.selected_in_playlist]) + if not track or not (track.title or track.artist or track.album or track.filename): + return # only display if we have at least one piece of metadata avaliable - elif self.playing_state == 0 and prefs.meta_persists_stop: - target_track = self.master_library[self.track_queue[self.queue_step]] + i_path = "" + try: + if not notify_of_end: + i_path = tauon.thumb_tracks.path(track) + except Exception: + logging.exception(track.fullpath.encode("utf-8", "replace").decode("utf-8")) + logging.error("Thumbnail error") - if prefs.meta_shows_selected_always: - if -1 < self.selected_in_playlist < len(self.multi_playlist[self.active_playlist_viewing].playlist_ids): - target_track = self.get_track(self.multi_playlist[self.active_playlist_viewing].playlist_ids[self.selected_in_playlist]) + top_line = track.title - return target_track + if prefs.notify_include_album: + bottom_line = (track.artist + " | " + track.album).strip("| ") + else: + bottom_line = track.artist - def playing_object(self) -> TrackClass | None: + if not track.title: + a, t = filename_to_metadata(clean_string(track.filename)) + if not track.artist: + bottom_line = a + top_line = t - if self.playing_state == 3: - return radiobox.dummy_track + gui.notify_main_id = uid_gen() + id = gui.notify_main_id - if len(self.track_queue) > 0: - return self.master_library[self.track_queue[self.queue_step]] - return None + if notify_of_end: + bottom_line = "Tauon Music Box" + top_line = (_("End of playlist")) + id = None - def title_text(self) -> str: - line = "" - track = self.playing_object() - if track: - title = track.title - artist = track.artist + song_notification.update(top_line, bottom_line, i_path) - if not title: - line = clean_string(track.filename) - else: - if artist != "": - line += artist - if title != "": - if line != "": - line += " - " - line += title + shoot_dl = threading.Thread(target=notify_song_fire, args=([song_notification, delay, id])) + shoot_dl.daemon = True + shoot_dl.start() - if self.playing_state == 3 and not title and not artist: - return self.tag_meta +# Last.FM ----------------------------------------------------------------- - return line +def get_backend_time(path): + pctl.time_to_get = path - def show(self) -> int | None: - global shift_selection + pctl.playerCommand = "time" + pctl.playerCommandReady = True - if not self.track_queue: - return 0 - return None + while pctl.playerCommand != "done": + time.sleep(0.005) - def show_current( - self, select: bool = True, playing: bool = True, quiet: bool = False, this_only: bool = False, highlight: bool = False, - index: int | None = None, no_switch: bool = False, folder_list: bool = True, - ) -> int | None: - - # logging.info("show------") - # logging.info(select) - # logging.info(playing) - # logging.info(quiet) - # logging.info(this_only) - # logging.info(highlight) - # logging.info("--------") - logging.debug("Position set by show playing") - - global shift_selection - - if tauon.spot_ctl.coasting: - sptr = tauon.dummy_track.misc.get("spotify-track-url") - if sptr: - - for p in default_playlist: - tr = self.get_track(p) - if tr.misc.get("spotify-track-url") == sptr: - index = tr.index - break - else: - for i, pl in enumerate(self.multi_playlist): - for p in pl.playlist_ids: - tr = self.get_track(p) - if tr.misc.get("spotify-track-url") == sptr: - index = tr.index - switch_playlist(i) - break - else: - continue - break - else: - return None + return pctl.time_to_get - if not self.track_queue: - return 0 +#19REWORK - track_index = self.track_queue[self.queue_step] - if index is not None: - track_index = index +def get_love(track_object: TrackClass) -> bool: + star = star_store.full_get(track_object.index) + if star is None: + return False - # Switch to source playlist - if not no_switch: - if self.active_playlist_viewing != self.active_playlist_playing and ( - track_index not in self.multi_playlist[self.active_playlist_viewing].playlist_ids): - switch_playlist(self.active_playlist_playing) + if "L" in star[1]: + return True + return False - if gui.playlist_view_length < 1: - return 0 - for i in range(len(self.multi_playlist[self.active_playlist_viewing].playlist_ids)): - if self.multi_playlist[self.active_playlist_viewing].playlist_ids[i] == track_index: +def get_love_index(index: int) -> bool: + star = star_store.full_get(index) + if star is None: + return False - if self.playlist_playing_position < len(self.multi_playlist[self.active_playlist_viewing].playlist_ids) and \ - self.active_playlist_viewing == self.active_playlist_playing and track_index == \ - self.multi_playlist[self.active_playlist_viewing].playlist_ids[self.playlist_playing_position] and \ - i != self.playlist_playing_position: - # continue - i = self.playlist_playing_position + if "L" in star[1]: + return True + return False - if select: - self.selected_in_playlist = i +def get_love_timestamp_index(index: int): + star = star_store.full_get(index) + if star is None: + return 0 + return star[3] - if playing: - # Make the found track the playing track - self.playlist_playing_position = i - self.active_playlist_playing = self.active_playlist_viewing +def love(set=True, track_id=None, no_delay=False, notify=False, sync=True): + if len(pctl.track_queue) < 1: + return False - vl = gui.playlist_view_length - if self.multi_playlist[self.active_playlist_viewing].uuid_int == gui.playlist_current_visible_tracks_id: - vl = gui.playlist_current_visible_tracks + if track_id is not None and track_id < 0: + return False - if not ( - quiet and self.playing_object().length < 15): # or (abs(self.playlist_view_position - i) < vl - 1)): + if track_id is None: + track_id = pctl.track_queue[pctl.queue_step] - # Align to album if in view range (and folder titles are active) - ap = get_album_info(i)[1][0] + loved = False + star = star_store.full_get(track_id) - if not (quiet and self.playlist_view_position <= i <= self.playlist_view_position + vl) and ( - not abs(i - ap) > vl - 2) and not self.multi_playlist[self.active_playlist_viewing].hide_title: - self.playlist_view_position = ap + if star is not None: + if "L" in star[1]: + loved = True - # Move to a random offset --- + if set is False: + return loved - elif i == self.playlist_view_position - 1 and self.playlist_view_position > 1: - self.playlist_view_position -= 1 + # global lfm_username + # if len(lfm_username) > 0 and not lastfm.connected and not prefs.auto_lfm: + # show_message("You have a last.fm account ready but it is not enabled.", 'info', + # 'Either connect, enable auto connect, or remove the account.') + # return - # Move a bit if its just out of range - elif self.playlist_view_position + vl - 2 == i and i < len( - self.multi_playlist[self.active_playlist_viewing].playlist_ids) - 5: - self.playlist_view_position += 3 + if star is None: + star = star_store.new_object() - # We know its out of range if above view postion - elif i < self.playlist_view_position: - self.playlist_view_position = i - random.randint(2, int(( - gui.playlist_view_length / 3) * 2) + int(gui.playlist_view_length / 6)) + loved ^= True - # If its below we need to test if its in view. If playing track in view, don't jump - elif abs(self.playlist_view_position - i) >= vl: - self.playlist_view_position = i - if i > 6: - self.playlist_view_position -= 5 - if i > gui.playlist_view_length and i + (gui.playlist_view_length * 2) < len( - self.multi_playlist[self.active_playlist_viewing].playlist_ids) and i > 10: - self.playlist_view_position = i - random.randint(2, - int(gui.playlist_view_length / 3) * 2) + if notify: + gui.toast_love_object = pctl.get_track(track_id) + gui.toast_love_added = loved + toast_love_timer.set() + gui.delay_frame(1.81) - break + delay = 0.3 + if no_delay or not sync or not lastfm.details_ready(): + delay = 0 - else: # Search other all other playlists - if not this_only: - for i, playlist in enumerate(self.multi_playlist): - if track_index in playlist.playlist_ids: - switch_playlist(i, quiet=True) - self.show_current(select, playing, quiet, this_only=True, index=track_index) - break + star[3] = time.time() - self.playlist_view_position = max(self.playlist_view_position, 0) + if loved: + time.sleep(delay) + gui.update += 1 + gui.pl_update += 1 + star[1] = star[1] + "L" # = [star[0], star[1] + "L", star[2]] + star_store.insert(track_id, star) + if sync: + if prefs.last_fm_token: + try: + lastfm.love(pctl.master_library[track_id].artist, pctl.master_library[track_id].title) + except Exception: + logging.exception("Failed updating last.fm love status") + show_message(_("Failed updating last.fm love status"), mode="warning") + star[1] = star[1].replace("L", "") # = [star[0], star[1].strip("L"), star[2]] + star_store.insert(track_id, star) + show_message( + _("Error updating love to last.fm!"), + _("Maybe check your internet connection and try again?"), mode="error") - # if self.playlist_view_position > len(self.multi_playlist[self.active_playlist_viewing].playlist_ids) - 1: - # logging.info("Run Over") + if pctl.master_library[track_id].file_ext == "JELY": + jellyfin.favorite(pctl.master_library[track_id]) - if select: - shift_selection = [] + else: + time.sleep(delay) + gui.update += 1 + gui.pl_update += 1 + star[1] = star[1].replace("L", "") + star_store.insert(track_id, star) + if sync: + if prefs.last_fm_token: + try: + lastfm.unlove(pctl.master_library[track_id].artist, pctl.master_library[track_id].title) + except Exception: + logging.exception("Failed updating last.fm love status") + show_message(_("Failed updating last.fm love status"), mode="warning") + star[1] = star[1] + "L" + star_store.insert(track_id, star) + if pctl.master_library[track_id].file_ext == "JELY": + jellyfin.favorite(pctl.master_library[track_id], un=True) - self.render_playlist() + gui.pl_update = 2 + gui.update += 1 + if sync and pctl.mpris is not None: + pctl.mpris.update(force=True) - if album_mode and not quiet: - if highlight: - gui.gallery_animate_highlight_on = goto_album(self.selected_in_playlist) - gallery_select_animate_timer.set() - else: - goto_album(self.selected_in_playlist) - if prefs.left_panel_mode == "artist list" and gui.lsp and not quiet: - artist_list_box.locate_artist(self.playing_object()) +def maloja_get_scrobble_counts(): + if lastfm.scanning_scrobbles is True or not prefs.maloja_url: + return - if folder_list and prefs.left_panel_mode == "folder view" and gui.lsp and not quiet and not tree_view_box.lock_pl: - tree_view_box.show_track(self.playing_object()) + url = prefs.maloja_url + if not url.endswith("/"): + url += "/" + url += "apis/mlj_1/scrobbles" + lastfm.scanning_scrobbles = True + try: + r = requests.get(url, timeout=10) - return 0 + if r.status_code != 200: + show_message(_("There was an error with the Maloja server"), r.text, mode="warning") + lastfm.scanning_scrobbles = False + return + except Exception: + logging.exception("There was an error reaching the Maloja server") + show_message(_("There was an error reaching the Maloja server"), mode="warning") + lastfm.scanning_scrobbles = False + return - def toggle_mute(self) -> None: - global volume_store - if self.player_volume > 0: - volume_store = self.player_volume - self.player_volume = 0 - else: - self.player_volume = volume_store + try: + data = json.loads(r.text) + l = data["list"] - self.set_volume() + counts = {} - def set_volume(self, notify: bool = True) -> None: + for item in l: + artists = item.get("artists") + title = item.get("title") + if title and artists: + key = (title, tuple(artists)) + c = counts.get(key, 0) + counts[key] = c + 1 - if (tauon.spot_ctl.coasting or tauon.spot_ctl.playing) and not tauon.spot_ctl.local and mouse_down: - # Rate limit network volume change - t = self.volume_update_timer.get() - if t < 0.3: - return + touched = [] - self.volume_update_timer.set() - self.playerCommand = "volume" - self.playerCommandReady = True - if notify: - self.notify_update() - - def revert(self) -> None: + for key, value in counts.items(): + title, artists = key + artists = [x.lower() for x in artists] + title = title.lower() + for track in pctl.master_library.values(): + if track.artist.lower() in artists and track.title.lower() == title: + if track.index in touched: + track.lfm_scrobbles += value + else: + track.lfm_scrobbles = value + touched.append(track.index) + show_message(_("Scanning scrobbles complete"), mode="done") - if self.queue_step == 0: - return + except Exception: + logging.exception("There was an error parsing the data") + show_message(_("There was an error parsing the data"), mode="warning") - prev = 0 - while len(self.track_queue) > prev + 1 and prev < 5: - if self.track_queue[len(self.track_queue) - 1 - prev] == self.left_index: - self.queue_step = len(self.track_queue) - 1 - prev - self.jump_time = self.left_time - self.playing_time = self.left_time - self.decode_time = self.left_time - break - prev += 1 - else: - self.queue_step -= 1 - self.jump_time = 0 - self.playing_time = 0 - self.decode_time = 0 + gui.pl_update += 1 + lastfm.scanning_scrobbles = False + tauon.bg_save() - if not len(self.track_queue) > self.queue_step >= 0: - logging.error("There is no previous track?") - return - self.target_open = self.master_library[self.track_queue[self.queue_step]].fullpath - self.target_object = self.master_library[self.track_queue[self.queue_step]] - self.start_time = self.master_library[self.track_queue[self.queue_step]].start_time - self.start_time_target = self.start_time - self.playing_length = self.master_library[self.track_queue[self.queue_step]].length - self.playerCommand = "open" - self.playerCommandReady = True - self.playing_state = 1 +def maloja_scrobble(track: TrackClass, timestamp: int = int(time.time())) -> bool | None: + url = prefs.maloja_url - if tauon.stream_proxy.download_running: - tauon.stream_proxy.stop() + if not track.artist or not track.title: + return None - self.show_current() - self.render_playlist() + if not url.endswith("/newscrobble"): + if not url.endswith("/"): + url += "/" + url += "apis/mlj_1/newscrobble" - def deduct_shuffle(self, track_id: int) -> None: - if self.multi_playlist and self.random_mode: - pl = self.multi_playlist[self.active_playlist_playing] - id = pl.uuid_int + d = {} + d["artists"] = [track.artist] # let Maloja parse/fix artists + d["title"] = track.title - if id not in self.shuffle_pools: - self.update_shuffle_pool(pl.uuid_int) + if track.album: + d["album"] = track.album + if track.album_artist: + d["albumartists"] = [track.album_artist] # let Maloja parse/fix artists - pool = self.shuffle_pools[id] - if not pool: - del self.shuffle_pools[id] - self.update_shuffle_pool(pl.uuid_int) - pool = self.shuffle_pools[id] + d["length"] = int(track.length) + d["time"] = timestamp + d["key"] = prefs.maloja_key - if track_id in pool: - pool.remove(track_id) + try: + r = requests.post(url, json=d, timeout=10) + if r.status_code != 200: + show_message(_("There was an error submitting data to Maloja server"), r.text, mode="warning") + return False + except Exception: + logging.exception("There was an error submitting data to Maloja server") + show_message(_("There was an error submitting data to Maloja server"), mode="warning") + return False + return True +#20REWORK - def play_target_rr(self) -> None: - tauon.thread_manager.ready_playback() - self.playing_length = self.master_library[self.track_queue[self.queue_step]].length +def id_to_pl(id: int): + for i, item in enumerate(pctl.multi_playlist): + if item.uuid_int == id: + return i + return None - if self.playing_length > 2: - random_start = random.randrange(1, int(self.playing_length) - 45 if self.playing_length > 50 else int( - self.playing_length)) - else: - random_start = 0 - - self.playing_time = random_start - self.target_open = self.master_library[self.track_queue[self.queue_step]].fullpath - self.target_object = self.master_library[self.track_queue[self.queue_step]] - self.start_time = self.master_library[self.track_queue[self.queue_step]].start_time - self.start_time_target = self.start_time - self.jump_time = random_start - self.playerCommand = "open" - if not prefs.use_jump_crossfade: - self.playerSubCommand = "now" - self.playerCommandReady = True - self.playing_state = 1 - radiobox.loaded_station = None - - if tauon.stream_proxy.download_running: - tauon.stream_proxy.stop() - - if update_title: - update_title_do() - - self.deduct_shuffle(self.target_object.index) - - def play_target(self, gapless: bool = False, jump: bool = False) -> None: - - tauon.thread_manager.ready_playback() - - #logging.info(self.track_queue) - self.playing_time = 0 - self.decode_time = 0 - target = self.master_library[self.track_queue[self.queue_step]] - self.target_open = target.fullpath - self.target_object = target - self.start_time = target.start_time - self.start_time_target = self.start_time - self.playing_length = target.length - self.last_playing_time = 0 - self.commit = None - radiobox.loaded_station = None - - if tauon.stream_proxy and tauon.stream_proxy.download_running: - tauon.stream_proxy.stop() - - if self.multi_playlist[self.active_playlist_playing].persist_time_positioning: - t = target.misc.get("position", 0) - if t: - self.playing_time = 0 - self.decode_time = 0 - self.jump_time = t - - self.playerCommand = "open" - if jump: # and not prefs.use_jump_crossfade: - self.playerSubCommand = "now" - - self.playerCommandReady = True - - self.playing_state = 1 - self.update_change() - self.deduct_shuffle(target.index) - - def update_change(self) -> None: - if update_title: - update_title_do() - self.notify_update() - hit_discord() - self.render_playlist() - - if lfm_scrobbler.a_sc: - lfm_scrobbler.a_sc = False - self.a_time = 0 +def pl_to_id(pl: int) -> int: + return pctl.multi_playlist[pl].uuid_int - lfm_scrobbler.start_queue() +def encode_track_name(track_object: TrackClass) -> str: + if track_object.is_cue or not track_object.filename: + out_line = str(track_object.track_number) + ". " + out_line += track_object.artist + " - " + track_object.title + return filename_safe(out_line) + return os.path.splitext(track_object.filename)[0] - if (album_mode or not gui.rsp) and (gui.theme_name == "Carbon" or prefs.colour_from_image): - target = self.playing_object() - if target and prefs.colour_from_image and target.parent_folder_path == colours.last_album: - return - album_art_gen.display(target, (0, 0), (50, 50), theme_only=True) +def encode_folder_name(track_object: TrackClass) -> str: + folder_name = track_object.artist + " - " + track_object.album - def jump(self, index: int, pl_position: int = None, jump: bool = True) -> None: - lfm_scrobbler.start_queue() - self.auto_stop = False + if folder_name == " - ": + folder_name = track_object.parent_folder_name - if self.force_queue and not self.pause_queue: - if self.force_queue[0].uuid_int == 1: # TODO(Martin): How can the UUID be 1 when we're doing a random on 1-1m except for massive chance? Is that the point? - if self.get_track(self.force_queue[0].track_id).parent_folder_path != self.get_track(index).parent_folder_path: - del self.force_queue[0] + folder_name = filename_safe(folder_name).strip() - if len(self.track_queue) > 0: - self.left_time = self.playing_time - self.left_index = self.track_queue[self.queue_step] + if not folder_name: + folder_name = str(track_object.index) - if self.playing_state == 1 and self.left_time > 5 and self.playing_length - self.left_time > 15: - self.master_library[self.left_index].skips += 1 + if "cd" not in folder_name.lower() or "disc" not in folder_name.lower(): + if track_object.disc_total not in ("", "0", 0, "1", 1) or ( + str(track_object.disc_number).isdigit() and int(track_object.disc_number) > 1): + folder_name += " CD" + str(track_object.disc_number) - global playlist_hold - gui.update_spec = 0 - self.active_playlist_playing = self.active_playlist_viewing - self.track_queue.append(index) - self.queue_step = len(self.track_queue) - 1 - playlist_hold = False - self.play_target(jump=jump) + return folder_name - if pl_position is not None: - self.playlist_playing_position = pl_position +#21REWORK - gui.pl_update = 1 +def signal_handler(signum, frame): + signal.signal(signum, signal.SIG_IGN) # ignore additional signals + tauon.exit(reason="SIGINT recieved") - def back(self) -> None: - if self.playing_state < 3 and prefs.back_restarts and self.playing_time > 6: - self.seek_time(0) - self.render_playlist() - return +#22REWORK - if tauon.spot_ctl.coasting: - tauon.spot_ctl.control("previous") - tauon.spot_ctl.update_timer.set() - self.playing_time = -2 - self.decode_time = -2 - return +def get_network_thumbnail_url(track_object: TrackClass): + if track_object.file_ext == "TIDAL": + return track_object.art_url_key + if track_object.file_ext == "SPTY": + return track_object.art_url_key + if track_object.file_ext == "PLEX": + url = plex.resolve_thumbnail(track_object.art_url_key) + assert url is not None + return url +# if track_object.file_ext == "JELY": +# url = jellyfin.resolve_thumbnail(track_object.art_url_key) +# assert url is not None +# assert url != "" +# return url + if track_object.file_ext == "KOEL": + url = track_object.art_url_key + assert url + return url + if track_object.file_ext == "TAU": + url = tau.resolve_picture(track_object.art_url_key) + assert url + return url - if len(self.track_queue) > 0: - self.left_time = self.playing_time - self.left_index = self.track_queue[self.queue_step] + return None - gui.update_spec = 0 - # Move up - if self.random_mode is False and len(self.playing_playlist()) > self.playlist_playing_position > 0: +def jellyfin_get_playlists_thread() -> None: + if jellyfin.scanning: + inp.mouse_click = False + show_message(_("Job already in progress!")) + return + jellyfin.scanning = True + shoot_dl = threading.Thread(target=jellyfin.get_playlists) + shoot_dl.daemon = True + shoot_dl.start() - if len(self.track_queue) > 0 and self.playing_playlist()[self.playlist_playing_position] != \ - self.track_queue[ - self.queue_step]: +def jellyfin_get_library_thread() -> None: + pref_box.close() + save_prefs() + if jellyfin.scanning: + inp.mouse_click = False + show_message(_("Job already in progress!")) + return - try: - p = self.playing_playlist().index(self.track_queue[self.queue_step]) - except Exception: - logging.exception("Failed to change playing_playlist") - p = random.randrange(len(self.playing_playlist())) - if p is not None: - self.playlist_playing_position = p - - self.playlist_playing_position -= 1 - self.track_queue.append(self.playing_playlist()[self.playlist_playing_position]) - self.queue_step = len(self.track_queue) - 1 - self.play_target(jump=True) - - elif self.random_mode is True and self.queue_step > 0: - self.queue_step -= 1 - self.play_target(jump=True) - else: - logging.info("BACK: NO CASE!") - self.show_current() + jellyfin.scanning = True + shoot_dl = threading.Thread(target=jellyfin.ingest_library) + shoot_dl.daemon = True + shoot_dl.start() - if self.active_playlist_viewing == self.active_playlist_playing: - self.show_current(False, True) +def plex_get_album_thread() -> None: + pref_box.close() + save_prefs() + if plex.scanning: + inp.mouse_click = False + show_message(_("Already scanning!")) + return + plex.scanning = True - if album_mode: - goto_album(self.playlist_playing_position) - if gui.combo_mode and self.active_playlist_viewing == self.active_playlist_playing: - self.show_current() + shoot_dl = threading.Thread(target=plex.get_albums) + shoot_dl.daemon = True + shoot_dl.start() - self.render_playlist() - self.notify_update() - notify_song() - lfm_scrobbler.start_queue() - gui.pl_update += 1 +def sub_get_album_thread() -> None: + # if prefs.backend != 1: + # show_message("This feature is currently only available with the BASS backend") + # return - def stop(self, block: bool = False, run : bool = False) -> None: + pref_box.close() + save_prefs() + if subsonic.scanning: + inp.mouse_click = False + show_message(_("Already scanning!")) + return + subsonic.scanning = True - self.playerCommand = "stop" - if run: - self.playerCommand = "runstop" - if block: - self.playerSubCommand = "return" + shoot_dl = threading.Thread(target=subsonic.get_music3) + shoot_dl.daemon = True + shoot_dl.start() - self.playerCommandReady = True +def koel_get_album_thread() -> None: + # if prefs.backend != 1: + # show_message("This feature is currently only available with the BASS backend") + # return - if tauon.thread_manager.player_lock.locked(): - try: - tauon.thread_manager.player_lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked player_lock") - else: - logging.exception("Unknown RuntimeError trying to release player_lock") - except Exception: - logging.exception("Unknown exception trying to release player_lock") - - self.record_stream = False - if len(self.track_queue) > 0: - self.left_time = self.playing_time - self.left_index = self.track_queue[self.queue_step] - previous_state = self.playing_state - self.playing_time = 0 - self.decode_time = 0 - self.playing_state = 0 - self.render_playlist() - - gui.update_spec = 0 - # gui.update_level = True # Allows visualiser to enter decay sequence - gui.update = True - if update_title: - update_title_do() # Update title bar text - - if tauon.stream_proxy and tauon.stream_proxy.download_running: - tauon.stream_proxy.stop() - - if block: - loop = 0 - sleep_timeout(lambda: self.playerSubCommand != "stopped", 2) - if tauon.stream_proxy.download_running: - sleep_timeout(lambda: tauon.stream_proxy.download_running, 2) - - if tauon.spot_ctl.playing or tauon.spot_ctl.coasting: - logging.info("Spotify stop") - tauon.spot_ctl.control("stop") - - self.notify_update() - lfm_scrobbler.start_queue() - return previous_state - - def pause(self) -> None: - - if tauon.spotc and tauon.spotc.running and tauon.spot_ctl.playing: - if self.playing_state == 1: - self.playerCommand = "pauseon" - self.playerCommandReady = True - elif self.playing_state == 2: - self.playerCommand = "pauseoff" - self.playerCommandReady = True - - if self.playing_state == 3: - if tauon.spot_ctl.coasting: - if tauon.spot_ctl.paused: - tauon.spot_ctl.control("resume") - else: - tauon.spot_ctl.control("pause") - return + pref_box.close() + save_prefs() + if koel.scanning: + inp.mouse_click = False + show_message(_("Already scanning!")) + return + koel.scanning = True - if tauon.spot_ctl.playing: - if self.playing_state == 2: - tauon.spot_ctl.control("resume") - self.playing_state = 1 - elif self.playing_state == 1: - tauon.spot_ctl.control("pause") - self.playing_state = 2 - self.render_playlist() - return + shoot_dl = threading.Thread(target=koel.get_albums) + shoot_dl.daemon = True + shoot_dl.start() - if self.playing_state == 1: - self.playerCommand = "pauseon" - self.playing_state = 2 - elif self.playing_state == 2: - self.playerCommand = "pauseoff" - self.playing_state = 1 - notify_song() - - self.playerCommandReady = True - - self.render_playlist() - self.notify_update() - - def pause_only(self) -> None: - if self.playing_state == 1: - self.playerCommand = "pauseon" - self.playing_state = 2 - - self.playerCommandReady = True - self.render_playlist() - self.notify_update() - - def play_pause(self) -> None: - if self.playing_state == 3: - self.stop() - elif self.playing_state > 0: - self.pause() +def do_exit_button() -> None: + if mouse_up or ab_click: + if gui.tray_active and prefs.min_to_tray: + if key_shift_down: + tauon.exit("User clicked X button with shift key") + return + tauon.min_to_tray() + elif gui.sync_progress and not gui.stop_sync: + show_message(_("Stop the sync before exiting!")) else: - self.play() - - def seek_decimal(self, decimal: int) -> None: - # if self.commit: - # return - if self.playing_state in (1, 2) or (self.playing_state == 3 and tauon.spot_ctl.coasting): - if decimal > 1: - decimal = 1 - elif decimal < 0: - decimal = 0 - self.new_time = self.playing_length * decimal - #logging.info('seek to:' + str(self.new_time)) - self.playerCommand = "seek" - self.playerCommandReady = True - self.playing_time = self.new_time - - if msys and taskbar_progress and self.windows_progress: - self.windows_progress.update(True) - - if self.mpris is not None: - self.mpris.seek_do(self.playing_time) - - def seek_time(self, new: float) -> None: - # if self.commit: - # return - if self.playing_state in (1, 2) or (self.playing_state == 3 and tauon.spot_ctl.coasting): + tauon.exit("User clicked X button") - if new > self.playing_length - 0.5: - self.advance() - return +def do_maximize_button() -> None: + global mouse_down + global drag_mode + if gui.fullscreen: + gui.fullscreen = False + SDL_SetWindowFullscreen(t_window, 0) + elif gui.maximized: + gui.maximized = False + SDL_RestoreWindow(t_window) + else: + gui.maximized = True + SDL_MaximizeWindow(t_window) - if new < 0.4: - new = 0 + mouse_down = False + inp.mouse_click = False + drag_mode = False - self.new_time = new - self.playing_time = new +def do_minimize_button(): - self.playerCommand = "seek" - self.playerCommandReady = True + global mouse_down + global drag_mode + if macos: + # hack + SDL_SetWindowBordered(t_window, True) + SDL_MinimizeWindow(t_window) + SDL_SetWindowBordered(t_window, False) + else: + SDL_MinimizeWindow(t_window) - if self.mpris is not None: - self.mpris.seek_do(self.playing_time) + mouse_down = False + inp.mouse_click = False + drag_mode = False - def play(self) -> None: +def draw_window_tools(): + global mouse_down + global drag_mode - if tauon.spot_ctl.playing: - if self.playing_state == 2: - self.play_pause() - return + # rect = (window_size[0] - 55 * gui.scale, window_size[1] - 35 * gui.scale, 53 * gui.scale, 33 * gui.scale) + # fields.add(rect) + # prefs.left_window_control = not key_shift_down + macstyle = gui.macstyle - # Unpause if paused - if self.playing_state == 2: - self.playerCommand = "pauseoff" - self.playerCommandReady = True - self.playing_state = 1 - self.notify_update() + bg_off = colours.window_buttons_bg + bg_on = colours.window_buttons_bg_over + fg_off = colours.window_button_icon_off + fg_on = colours.window_buttons_icon_over + x_on = colours.window_button_x_on + x_off = colours.window_button_x_off - # If stopped - elif self.playing_state == 0: + h = round(28 * gui.scale) + y = round(1 * gui.scale) + if macstyle: + y = round(9 * gui.scale) - if radiobox.loaded_station: - radiobox.start(radiobox.loaded_station) - return + x_width = round(26 * gui.scale) + ma_width = round(33 * gui.scale) + mi_width = round(35 * gui.scale) + re_width = round(30 * gui.scale) + last_width = 0 - # If the queue is empty - if self.track_queue == [] and len(self.multi_playlist[self.active_playlist_playing].playlist_ids) > 0: - self.track_queue.append(self.multi_playlist[self.active_playlist_playing].playlist_ids[0]) - self.queue_step = 0 - self.playlist_playing_position = 0 - self.active_playlist_playing = 0 - - self.play_target() - - # If the queue is not empty, play? - elif len(self.track_queue) > 0: - self.play_target() - - self.render_playlist() - - def spot_test_progress(self) -> None: - if self.playing_state in (1, 2) and tauon.spot_ctl.playing: - th = 5 # the rate to poll the spotify API - if self.playing_time > self.playing_length: - th = 1 - if not tauon.spot_ctl.paused: - if tauon.spot_ctl.start_timer.get() < 0.5: - tauon.spot_ctl.progress_timer.set() - return - add_time = tauon.spot_ctl.progress_timer.get() - if add_time > 5: - add_time = 0 - self.playing_time += add_time - self.decode_time = self.playing_time - # self.test_progress() - tauon.spot_ctl.progress_timer.set() - if len(self.track_queue) > 0 and 2 > add_time > 0: - star_store.add(self.track_queue[self.queue_step], add_time) - if tauon.spot_ctl.update_timer.get() > th: - tauon.spot_ctl.update_timer.set() - shooter(tauon.spot_ctl.monitor) - else: - self.test_progress() - - elif self.playing_state == 3 and tauon.spot_ctl.coasting: - th = 7 - if self.playing_time > self.playing_length or self.playing_time < 2.5: - th = 1 - if tauon.spot_ctl.update_timer.get() < th: - if not tauon.spot_ctl.paused: - self.playing_time += tauon.spot_ctl.progress_timer.get() - self.decode_time = self.playing_time - tauon.spot_ctl.progress_timer.set() + xx = 0 + l = prefs.left_window_control + r = not l + focused = window_is_focused() - else: - tauon.spot_ctl.update_timer.set() - tauon.spot_ctl.update() - - def purge_track(self, track_id: int, fast: bool = False) -> None: - """Remove a track from the database""" - # Remove from all playlists - if not fast: - for playlist in self.multi_playlist: - while track_id in playlist.playlist: - album_dex.clear() - playlist.playlist.remove(track_id) - # Stop if track is playing track - if self.track_queue and self.track_queue[self.queue_step] == track_id and self.playing_state != 0: - self.stop(block=True) - # Remove from playback history - while track_id in self.track_queue: - self.track_queue.remove(track_id) - self.queue_step -= 1 - # Remove track from force queue - for i in reversed(range(len(self.force_queue))): - if self.force_queue[i].track_id == track_id: - del self.force_queue[i] - del self.master_library[track_id] - - def test_progress(self) -> None: - # Fuzzy reload lastfm for rescrobble - if lfm_scrobbler.a_sc and self.playing_time < 1: - lfm_scrobbler.a_sc = False - self.a_time = 0 - - # Update the UI if playing time changes a whole number - # next_round = int(self.playing_time) - # if self.playing_time_int != next_round: - # #if not prefs.power_save: - # #gui.update += 1 - # self.playing_time_int = next_round - - gap_extra = 2 # 2 - - if tauon.spot_ctl.playing or tauon.chrome_mode: - gap_extra = 3 - - if msys and taskbar_progress and self.windows_progress: - self.windows_progress.update(True) - - if self.commit is not None: - return - - if self.playing_state == 1 and self.multi_playlist[self.active_playlist_playing].persist_time_positioning: - tr = self.playing_object() - if tr: - tr.misc["position"] = self.decode_time - - if self.playing_state == 1 and self.decode_time + gap_extra >= self.playing_length and self.decode_time > 0.2: - - # Allow some time for spotify playing time to update? - if tauon.spot_ctl.playing and tauon.spot_ctl.start_timer.get() < 3: - return - - # Allow some time for backend to provide a length - if self.playing_time < 6 and self.playing_length == 0: - return - if not tauon.spot_ctl.playing and self.a_time < 2: - return - - self.decode_time = 0 - - pp = self.playing_playlist() - - if self.auto_stop: # and not self.force_queue and not (self.force_queue and self.pause_queue): - self.stop(run=True) - if self.force_queue or (not self.force_queue and not self.random_mode and not self.repeat_mode): - self.advance(play=False) - gui.update += 2 - self.auto_stop = False - - elif self.force_queue and not self.pause_queue: - id = self.advance(end=True, quiet=True, dry=True) - if id is not None: - self.start_commit(id) - return - self.advance(end=True, quiet=True) - - - - elif self.repeat_mode is True: - - if self.album_repeat_mode: - - if self.playlist_playing_position > len(pp) - 1: - self.playlist_playing_position = 0 # Hack fix, race condition bug? - - ti = self.get_track(pp[self.playlist_playing_position]) - - i = self.playlist_playing_position - - # Test if next track is in same folder - if i + 1 < len(pp): - nt = self.get_track(pp[i + 1]) - if ti.parent_folder_path == nt.parent_folder_path: - # The next track is in the same folder - # so advance normally - self.advance(quiet=True, end=True) - return - - # We need to backtrack to see where the folder begins - i -= 1 - while i >= 0: - nt = self.get_track(pp[i]) - if ti.parent_folder_path != nt.parent_folder_path: - i += 1 - break - i -= 1 - i = max(i, 0) - - self.selected_in_playlist = i - shift_selection = [i] - - self.jump(pp[i], i, jump=False) - - elif prefs.playback_follow_cursor and self.playing_ready() \ - and self.multi_playlist[self.active_playlist_viewing].playlist[ - self.selected_in_playlist] != self.playing_object().index \ - and -1 < self.selected_in_playlist < len(default_playlist): + # Close + if r: + xx = window_size[0] - x_width + xx -= round(2 * gui.scale) - logging.info("Repeat follow cursor") + if macstyle: + xx = window_size[0] - 27 * gui.scale + if l: + xx = round(4 * gui.scale) + rect = (xx + 5, y - 1, 14 * gui.scale, 14 * gui.scale) + fields.add(rect) + colour = mac_close + if not focused: + colour = (86, 85, 86, 255) + mac_circle.render(xx + 6 * gui.scale, y, colour) + if coll(rect) and not gui.mouse_unknown: + if coll_point(last_click_location, rect): + do_exit_button() + else: + rect = (xx, y, x_width, h) + last_width = x_width + ddt.rect((rect[0], rect[1], rect[2], rect[3]), bg_off) + fields.add(rect) + if coll(rect) and not gui.mouse_unknown: + ddt.rect((rect[0], rect[1], rect[2], rect[3]), bg_on) + top_panel.exit_button.render(rect[0] + 8 * gui.scale, rect[1] + 8 * gui.scale, x_on) + if coll_point(last_click_location, rect): + do_exit_button() + else: + top_panel.exit_button.render(rect[0] + 8 * gui.scale, rect[1] + 8 * gui.scale, x_off) - self.playing_time = 0 - self.decode_time = 0 - self.active_playlist_playing = self.active_playlist_viewing - self.playlist_playing_position = self.selected_in_playlist + # Macstyle restore + if gui.mode == 3: + if macstyle: + if r: + xx -= round(20 * gui.scale) + if l: + xx += round(20 * gui.scale) + rect = (xx + 5, y - 1, 14 * gui.scale, 14 * gui.scale) - self.track_queue.append(default_playlist[self.selected_in_playlist]) - self.queue_step = len(self.track_queue) - 1 - self.play_target(jump=False) - self.render_playlist() - lfm_scrobbler.start_queue() + fields.add(rect) + colour = (160, 55, 225, 255) + if not focused: + colour = (86, 85, 86, 255) + mac_circle.render(xx + 6 * gui.scale, y, colour) + if coll(rect) and not gui.mouse_unknown: + if (mouse_up or ab_click) and coll_point(last_click_location, rect): + restore_full_mode() + gui.update += 2 - else: - id = self.track_queue[self.queue_step] - self.commit = id - target = self.get_track(id) - self.target_open = target.fullpath - self.target_object = target - self.start_time = target.start_time - self.start_time_target = self.start_time - self.playerCommand = "open" - self.playerSubCommand = "repeat" - self.playerCommandReady = True - - #self.render_playlist() - lfm_scrobbler.start_queue() - - # Reload lastfm for rescrobble - if lfm_scrobbler.a_sc: - lfm_scrobbler.a_sc = False - self.a_time = 0 - - elif self.random_mode is False and len(pp) > self.playlist_playing_position + 1 and \ - self.master_library[pp[self.playlist_playing_position]].is_cue is True \ - and self.master_library[pp[self.playlist_playing_position + 1]].filename == \ - self.master_library[pp[self.playlist_playing_position]].filename and int( - self.master_library[pp[self.playlist_playing_position]].track_number) == int( - self.master_library[pp[self.playlist_playing_position + 1]].track_number) - 1: - - # not (self.force_queue and not self.pause_queue) and \ - - # We can shave it closer - if not self.playing_time + 0.1 >= self.playing_length: - return + # maximize - logging.info("Do transition CUE") - self.playlist_playing_position += 1 - self.queue_step += 1 - self.track_queue.append(pp[self.playlist_playing_position]) - self.playing_state = 1 - self.playing_time = 0 - self.decode_time = 0 - self.playing_length = self.master_library[self.track_queue[self.queue_step]].length - self.start_time = self.master_library[self.track_queue[self.queue_step]].start_time - self.start_time_target = self.start_time - lfm_scrobbler.start_queue() + if draw_max_button and gui.mode != 3: + if macstyle: + if r: + xx -= round(20 * gui.scale) + if l: + xx += round(20 * gui.scale) + rect = (xx + 5, y - 1, 14 * gui.scale, 14 * gui.scale) - gui.update += 1 - gui.pl_update = 1 + fields.add(rect) + colour = mac_maximize + if not focused: + colour = (86, 85, 86, 255) + mac_circle.render(xx + 6 * gui.scale, y, colour) + if coll(rect) and not gui.mouse_unknown: + if (mouse_up or ab_click) and coll_point(last_click_location, rect): + do_minimize_button() - if update_title: - update_title_do() - self.notify_update() + else: + if r: + xx -= ma_width + if l: + xx += last_width + rect = (xx, y, ma_width, h) + last_width = ma_width + ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_off) + fields.add(rect) + if coll(rect): + ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_on) + top_panel.maximize_button.render(rect[0] + 10 * gui.scale, rect[1] + 10 * gui.scale, fg_on) + if (mouse_up or ab_click) and coll_point(last_click_location, rect): + do_maximize_button() else: - # self.advance(quiet=True, end=True) - - id = self.advance(quiet=True, end=True, dry=True) - if id is not None and not tauon.spot_ctl.playing: - #logging.info("Commit") - self.start_commit(id) - return + top_panel.maximize_button.render(rect[0] + 10 * gui.scale, rect[1] + 10 * gui.scale, fg_off) - self.advance(quiet=True, end=True) - self.playing_time = 0 - self.decode_time = 0 - - def start_commit(self, commit_id: int, repeat: bool = False) -> None: - self.commit = commit_id - target = self.get_track(commit_id) - self.target_open = target.fullpath - self.target_object = target - self.start_time = target.start_time - self.start_time_target = self.start_time - self.playerCommand = "open" - if repeat: - self.playerSubCommand = "repeat" - self.playerCommandReady = True - - def advance( - self, rr: bool = False, quiet: bool = False, inplace: bool = False, end: bool = False, - force: bool = False, play: bool = True, dry: bool = False, - ) -> int | None: - # Spotify remote control mode - if not dry and tauon.spot_ctl.coasting: - tauon.spot_ctl.control("next") - tauon.spot_ctl.update_timer.set() - self.playing_time = -2 - self.decode_time = -2 - return None + # minimize - # Temporary Workaround for UI block causing unwanted dragging - if not dry: - quick_d_timer.set() + if draw_min_button: - if prefs.show_current_on_transition: - quiet = False + # x = window_size[0] - round(65 * gui.scale) + # if draw_max_button and not gui.mode == 3: + # x -= round(34 * gui.scale) + if macstyle: + if r: + xx -= round(20 * gui.scale) + if l: + xx += round(20 * gui.scale) + rect = (xx + 5, y - 1, 14 * gui.scale, 14 * gui.scale) - # Trim the history if it gets too long - while len(self.track_queue) > 250: - self.queue_step -= 1 - del self.track_queue[0] + fields.add(rect) + colour = mac_minimize + if not focused: + colour = (86, 85, 86, 255) + mac_circle.render(xx + 6 * gui.scale, y, colour) + if coll(rect) and not gui.mouse_unknown: + if (mouse_up or ab_click) and coll_point(last_click_location, rect): + do_maximize_button() - # Save info about the track we are leaving - if not dry and len(self.track_queue) > 0: - self.left_time = self.playing_time - self.left_index = self.track_queue[self.queue_step] + else: + if r: + xx -= mi_width + if l: + xx += last_width - # Test to register skip (not currently used for anything) - if not dry and self.playing_state == 1 and 1 < self.left_time < 45: - self.master_library[self.left_index].skips += 1 - #logging.info('skip registered') + rect = (xx, y, mi_width, h) + last_width = mi_width + ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_off) + fields.add(rect) + if coll(rect): + ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_on) + ddt.rect_a((rect[0] + 11 * gui.scale, rect[1] + 16 * gui.scale), (14 * gui.scale, 3 * gui.scale), fg_on) + if (mouse_up or ab_click) and coll_point(last_click_location, rect): + do_minimize_button() + else: + ddt.rect_a( + (rect[0] + 11 * gui.scale, rect[1] + 16 * gui.scale), (14 * gui.scale, 3 * gui.scale), fg_off) - if not dry: - self.playing_time = 0 - self.decode_time = 0 - self.playing_length = 100 - gui.update_spec = 0 + # restore - old = self.queue_step - end_of_playlist = False + if gui.mode == 3: - # Force queue (middle click on track) - if len(self.force_queue) > 0 and not self.pause_queue: + # bg_off = [0, 0, 0, 50] + # bg_on = [255, 255, 255, 10] + # fg_off =(255, 255, 255, 40) + # fg_on = (255, 255, 255, 60) + if macstyle: + pass + else: + if r: + xx -= re_width + if l: + xx += last_width - q = self.force_queue[0] - target_index = q.track_id + rect = (xx, y, re_width, h) + ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_off) + fields.add(rect) + if coll(rect): + ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_on) + top_panel.restore_button.render(rect[0] + 8 * gui.scale, rect[1] + 9 * gui.scale, fg_on) + if (inp.mouse_click or ab_click) and coll_point(click_location, rect): + restore_full_mode() + gui.update += 2 + else: + top_panel.restore_button.render(rect[0] + 8 * gui.scale, rect[1] + 9 * gui.scale, fg_off) - if q.type == 1: - # This is an album type +def draw_window_border(): + corner_icon.render(window_size[0] - corner_icon.w, window_size[1] - corner_icon.h, colours.corner_icon) - if q.album_stage == 0: - # We have not started playing the album yet - # So we go to that track - # (This is a copy of the track code, but we don't delete the item) + corner_rect = (window_size[0] - 20 * gui.scale, window_size[1] - 20 * gui.scale, 20, 20) + fields.add(corner_rect) - if not dry: + right_rect = (window_size[0] - 3 * gui.scale, 20 * gui.scale, 10, window_size[1] - 40 * gui.scale) + fields.add(right_rect) - pl = id_to_pl(q.playlist_id) - if pl is not None: - self.active_playlist_playing = pl + # top_rect = (20 * gui.scale, 0, window_size[0] - 40 * gui.scale, 2 * gui.scale) + # fields.add(top_rect) - if target_index not in self.playing_playlist(): - del self.force_queue[0] - self.advance() - return None + left_rect = (0, 10 * gui.scale, 4 * gui.scale, window_size[1] - 50 * gui.scale) + fields.add(left_rect) - if dry: - return target_index + bottom_rect = (20 * gui.scale, window_size[1] - 4, window_size[0] - 40 * gui.scale, 7 * gui.scale) + fields.add(bottom_rect) - self.playlist_playing_position = q.position - self.track_queue.append(target_index) - self.queue_step = len(self.track_queue) - 1 - # self.queue_target = len(self.track_queue) - 1 - if play: - self.play_target(jump=not end) + if coll(corner_rect): + gui.cursor_want = 4 + elif coll(right_rect): + gui.cursor_want = 8 + # elif coll(top_rect): + # gui.cursor_want = 9 + elif coll(left_rect): + gui.cursor_want = 10 + elif coll(bottom_rect): + gui.cursor_want = 11 - # Set the flag that we have entered the album - self.force_queue[0].album_stage = 1 + colour = colours.window_frame - # This code is mirrored below ------- - ok_continue = True + ddt.rect((0, 0, window_size[0], 1 * gui.scale), colour) + ddt.rect((0, 0, 1 * gui.scale, window_size[1]), colour) + ddt.rect((0, window_size[1] - 1 * gui.scale, window_size[0], 1 * gui.scale), colour) + ddt.rect((window_size[0] - 1 * gui.scale, 0, 1 * gui.scale, window_size[1]), colour) - # Check if we are at end of playlist - pl = self.multi_playlist[self.active_playlist_playing].playlist_ids - if self.playlist_playing_position > len(pl) - 3: - ok_continue = False +def bass_player_thread(player): + # logging.basicConfig(filename=user_directory + '/crash.log', level=logging.ERROR, + # format='%(asctime)s %(levelname)s %(name)s %(message)s') - # Check next song is in album - if ok_continue and self.get_track(pl[self.playlist_playing_position + 1]).parent_folder_path != self.get_track(target_index).parent_folder_path: - ok_continue = False + try: + player(pctl, gui, prefs, lfm_scrobbler, star_store, tauon) + except Exception: + logging.exception("Exception on player thread") + show_message(_("Playback thread has crashed. Sorry about that."), _("App will need to be restarted."), mode="error") + time.sleep(1) + show_message(_("Playback thread has crashed. Sorry about that."), _("App will need to be restarted."), mode="error") + time.sleep(1) + show_message(_("Playback thread has crashed. Sorry about that."), _("App will need to be restarted."), mode="error") + raise - # ----------- +# --------------------------------------------------------------------------------------------- +# ABSTRACT SDL DRAWING FUNCTIONS ----------------------------------------------------- - elif q.album_stage == 1: - # We have previously started playing this album +def coll_point(l, r): + # rect point collision detection + return r[0] < l[0] <= r[0] + r[2] and r[1] <= l[1] <= r[1] + r[3] - # Check to see if we still are: - ok_continue = True - if self.get_track(target_index).parent_folder_path != self.playing_object().parent_folder_path: - # Remember to set jumper check this too (leave album if we jump to some other track, i.e. double click)) - ok_continue = False +def coll(r): + return r[0] < mouse_position[0] <= r[0] + r[2] and r[1] <= mouse_position[1] <= r[1] + r[3] - pl = self.multi_playlist[self.active_playlist_playing].playlist_ids +#26REWORK - # Check next song is in album - if ok_continue: +def prime_fonts(prefs: Prefs): + standard_font = prefs.linux_font + # if msys: + # standard_font = prefs.linux_font + ", Sans" # The CJK ones dont appear to be working + ddt.prime_font(standard_font, 8, 9) + ddt.prime_font(standard_font, 8, 10) + ddt.prime_font(standard_font, 8.5, 11) + ddt.prime_font(standard_font, 8.7, 11.5) + ddt.prime_font(standard_font, 9, 12) + ddt.prime_font(standard_font, 10, 13) + ddt.prime_font(standard_font, 10, 14) + ddt.prime_font(standard_font, 10.2, 14.5) + ddt.prime_font(standard_font, 11, 15) + ddt.prime_font(standard_font, 12, 16) + ddt.prime_font(standard_font, 12, 17) + ddt.prime_font(standard_font, 12, 18) + ddt.prime_font(standard_font, 13, 19) + ddt.prime_font(standard_font, 14, 20) + ddt.prime_font(standard_font, 24, 30) - # Check if we are at end of playlist, or already at end of album - if self.playlist_playing_position >= len(pl) - 1 or (self.playlist_playing_position < len( - pl) - 1 and \ - self.get_track(pl[self.playlist_playing_position + 1]).parent_folder_path != self.get_track( - target_index).parent_folder_path): + ddt.prime_font(standard_font, 9, 412) + ddt.prime_font(standard_font, 10, 413) - if dry: - return None + standard_font = prefs.linux_font_semibold + # if msys: + # standard_font = prefs.linux_font_semibold + ", Noto Sans Med, Sans" #, Noto Sans CJK JP Medium, Noto Sans CJK Medium, Sans" - del self.force_queue[0] - self.advance() - return None + ddt.prime_font(standard_font, 8, 309) + ddt.prime_font(standard_font, 8, 310) + ddt.prime_font(standard_font, 8.5, 311) + ddt.prime_font(standard_font, 9, 312) + ddt.prime_font(standard_font, 10, 313) + ddt.prime_font(standard_font, 10.5, 314) + ddt.prime_font(standard_font, 11, 315) + ddt.prime_font(standard_font, 12, 316) + ddt.prime_font(standard_font, 12, 317) + ddt.prime_font(standard_font, 12, 318) + ddt.prime_font(standard_font, 13, 319) + ddt.prime_font(standard_font, 24, 330) + standard_font = prefs.linux_font_bold + # if msys: + # standard_font = prefs.linux_font_bold + ", Noto Sans, Sans Bold" - # Check if 2 songs down is in album, remove entry in queue if not - if self.playlist_playing_position < len(pl) - 2 and \ - self.get_track(pl[self.playlist_playing_position + 2]).parent_folder_path != self.get_track( - target_index).parent_folder_path: - ok_continue = False - - # if ok_continue: - # We seem to be still in the album. Step down one and play - if not dry: - self.playlist_playing_position += 1 - - if len(pl) <= self.playlist_playing_position: - if dry: - return None - logging.info("END OF PLAYLIST!") - del self.force_queue[0] - self.advance() - return None - - if dry: - return pl[self.playlist_playing_position + 1] - self.track_queue.append(pl[self.playlist_playing_position]) - self.queue_step = len(self.track_queue) - 1 - # self.queue_target = len(self.track_queue) - 1 - if play: - self.play_target(jump=not end) + ddt.prime_font(standard_font, 6, 209) + ddt.prime_font(standard_font, 7, 210) + ddt.prime_font(standard_font, 8, 211) + ddt.prime_font(standard_font, 9, 212) + ddt.prime_font(standard_font, 10, 213) + ddt.prime_font(standard_font, 11, 214) + ddt.prime_font(standard_font, 12, 215) + ddt.prime_font(standard_font, 13, 216) + ddt.prime_font(standard_font, 14, 217) + ddt.prime_font(standard_font, 17, 218) + ddt.prime_font(standard_font, 19, 219) + ddt.prime_font(standard_font, 20, 220) + ddt.prime_font(standard_font, 25, 228) - if not ok_continue: - # It seems this item has expired, remove it and call advance again + standard_font = prefs.linux_font_condensed + # if msys: + # standard_font = "Noto Sans ExtCond, Sans" + ddt.prime_font(standard_font, 10, 413) + ddt.prime_font(standard_font, 11, 414) + ddt.prime_font(standard_font, 12, 415) + ddt.prime_font(standard_font, 13, 416) - if dry: - return None + standard_font = prefs.linux_font_condensed_bold # "Noto Sans, ExtraCondensed Bold" + # if msys: + # standard_font = "Noto Sans ExtCond, Sans Bold" + # ddt.prime_font(standard_font, 9, 512) + ddt.prime_font(standard_font, 10, 513) + ddt.prime_font(standard_font, 11, 514) + ddt.prime_font(standard_font, 12, 515) + ddt.prime_font(standard_font, 13, 516) - logging.info("Remove expired album from queue") - del self.force_queue[0] +#27REWORK - if q.auto_stop: - self.auto_stop = True - if prefs.stop_end_queue and not self.force_queue: - self.auto_stop = True +def find_synced_lyric_data(track: TrackClass) -> list[str] | None: + if track.is_network: + return None - if queue_box.scroll_position > 0: - queue_box.scroll_position -= 1 + direc = track.parent_folder_path + name = os.path.splitext(track.filename)[0] + ".lrc" - # self.advance() - # return + if len(track.lyrics) > 20 and track.lyrics[0] == "[" and ":" in track.lyrics[:20] and "." in track.lyrics[:20]: + return track.lyrics.splitlines() - else: - # This is track type - pl = id_to_pl(q.playlist_id) - if not dry and pl is not None: - self.active_playlist_playing = pl - - if target_index not in self.playing_playlist(): - if dry: - return None - del self.force_queue[0] - self.advance() - return None - - if dry: - return target_index - - self.playlist_playing_position = q.position - self.track_queue.append(target_index) - self.queue_step = len(self.track_queue) - 1 - # self.queue_target = len(self.track_queue) - 1 - if play: - self.play_target(jump=not end) - del self.force_queue[0] - if q.auto_stop: - self.auto_stop = True - if prefs.stop_end_queue and not self.force_queue: - self.auto_stop = True - if queue_box.scroll_position > 0: - queue_box.scroll_position -= 1 - - # Stop if playlist is empty - elif len(self.playing_playlist()) == 0: - if dry: - return None - self.stop() - return 0 + try: + if os.path.isfile(os.path.join(direc, name)): + with open(os.path.join(direc, name), encoding="utf-8") as f: + data = f.readlines() + else: + return None + except Exception: + logging.exception("Read lyrics file error") + return None - # Playback follow cursor - elif prefs.playback_follow_cursor and self.playing_ready() \ - and self.multi_playlist[self.active_playlist_viewing].playlist_ids[ - self.selected_in_playlist] != self.playing_object().index \ - and -1 < self.selected_in_playlist < len(default_playlist): + return data - if dry: - return default_playlist[self.selected_in_playlist] +def get_real_time(): + offset = pctl.decode_time - (prefs.sync_lyrics_time_offset / 1000) + if prefs.backend == 4: + offset -= (prefs.device_buffer - 120) / 1000 + elif prefs.backend == 2: + offset += 0.1 + return max(0, offset) - self.active_playlist_playing = self.active_playlist_viewing - self.playlist_playing_position = self.selected_in_playlist +def draw_internel_link(x, y, text, colour, font): + tweak = font + while tweak > 100: + tweak -= 100 - self.track_queue.append(default_playlist[self.selected_in_playlist]) - self.queue_step = len(self.track_queue) - 1 - if play: - self.play_target(jump=not end) + if gui.scale == 2: + tweak *= 2 + tweak += 4 + if gui.scale == 1.25: + tweak = round(tweak * 1.25) + tweak += 1 - # If random, jump to random track - elif (self.random_mode or rr) and len(self.playing_playlist()) > 0 and not ( - self.album_shuffle_mode or prefs.album_shuffle_lock_mode): - # self.queue_step += 1 - new_step = self.queue_step + 1 + sp = ddt.text((x, y), text, colour, font) - if new_step == len(self.track_queue): + rect = [x - 5 * gui.scale, y - 2 * gui.scale, sp + 11 * gui.scale, 23 * gui.scale] + fields.add(rect) - if self.album_repeat_mode and self.repeat_mode: - # Album shuffle mode - pp = self.playing_playlist() - k = self.playlist_playing_position - # ti = self.get_track(pp[k]) - ti = self.master_library[self.track_queue[self.queue_step]] + if coll(rect): + if not inp.mouse_click: + gui.cursor_want = 3 + ddt.line(x, y + tweak + 2, x + sp, y + tweak + 2, alpha_mod(colour, 180)) + if inp.mouse_click: + return True + return False - if ti.index not in pp: - if dry: - return None - logging.info("No tracks to repeat!") - return 0 +# No hit detect +def draw_linked_text(location, text, colour, font, force=False, replace=""): + base = "" + link_text = "" + rest = "" + on_base = True - matches = [] - for i, p in enumerate(pp): + if force: + on_base = False + base = "" + link_text = text + rest = "" + else: + for i in range(len(text)): + if text[i:i + 7] == "http://" or text[i:i + 4] == "www." or text[i:i + 8] == "https://": + on_base = False + if on_base: + base += text[i] + elif i == len(text) or text[i] in '\\) "\'': + rest = text[i:] + break + else: + link_text += text[i] - if self.get_track(p).parent_folder_path == ti.parent_folder_path: - matches.append((i, p)) + target_link = link_text + if replace: + link_text = replace - if matches: - # Avoid a repeat of same track - if len(matches) > 1 and (k, ti.index) in matches: - matches.remove((k, ti.index)) + left = ddt.get_text_w(base, font) + right = ddt.get_text_w(base + link_text, font) - i, p = random.choice(matches) # not used + x = location[0] + y = location[1] - if prefs.true_shuffle: + ddt.text((x, y), base, colour, font) + ddt.text((x + left, y), link_text, colours.link_text, font) + ddt.text((x + right, y), rest, colour, font) - id = ti.parent_folder_path + tweak = font + while tweak > 100: + tweak -= 100 - while True: - if id in self.shuffle_pools: + if gui.scale == 2: + tweak *= 2 + tweak += 4 + elif gui.scale != 1: + tweak = round(tweak * gui.scale) + tweak += 2 - pool = self.shuffle_pools[id] + if system == "Windows": + tweak += 1 - if not pool: - del self.shuffle_pools[id] # Trigger a refill - continue + # ddt.line(x + left, y + tweak + 2, x + right, y + tweak + 2, alpha_mod(colours.link_text, 120)) + ddt.rect((x + left, y + tweak + 2, right - left, round(1 * gui.scale)), alpha_mod(colours.link_text, 120)) - ref = pool.pop() - if dry: - pool.append(ref) - return ref[1] - # ref = random.choice(pool) - # pool.remove(ref) + return left, right - left, target_link - if ref[1] not in pp: # Check track still in the live playlist - logging.info("Track not in pool") - continue - i, p = ref # Find position of reference in playlist - break +def draw_linked_text2(x, y, text, colour, font, click=False, replace=""): + link_pa = draw_linked_text( + (x, y), text, colour, font, replace=replace) + link_rect = [x + link_pa[0], y, link_pa[1], 18 * gui.scale] + if coll(link_rect): + if not click: + gui.cursor_want = 3 + if click: + webbrowser.open(link_pa[2], new=2, autoraise=True) + fields.add(link_rect) - # Refill the pool - random.shuffle(matches) - self.shuffle_pools[id] = matches - logging.info("Refill folder shuffle pool") - self.playlist_playing_position = i - self.track_queue.append(p) +def link_activate(x, y, link_pa, click=None): + link_rect = [x + link_pa[0], y - 2 * gui.scale, link_pa[1], 20 * gui.scale] - else: - # Normal select from playlist + if click is None: + click = inp.mouse_click - if prefs.true_shuffle: - # True shuffle avoids repeats by using a pool + fields.add(link_rect) + if coll(link_rect): + if not click: + gui.cursor_want = 3 + if click: + webbrowser.open(link_pa[2], new=2, autoraise=True) + track_box = True - pl = self.multi_playlist[self.active_playlist_playing] - id = pl.uuid_int +def pixel_to_logical(x): + return round((x / window_size[0]) * logical_size[0]) - while True: +def img_slide_update_gall(value, pause: bool = True) -> None: + global album_mode_art_size + gui.halt_image_rendering = True - if id in self.shuffle_pools: + album_mode_art_size = value - pool = self.shuffle_pools[id] + clear_img_cache(False) + if pause: + gallery_load_delay.set() + gui.frame_callback_list.append(TestTimer(0.6)) + gui.halt_image_rendering = False - if not pool: - del self.shuffle_pools[id] # Trigger a refill - continue + # Update sizes + tauon.gall_ren.size = album_mode_art_size - ref = pool.pop() - if dry: - pool.append(ref) - return ref - # ref = random.choice(pool) - # pool.remove(ref) + if album_mode_art_size > 150: + prefs.thin_gallery_borders = False - if ref not in pl.playlist_ids: # Check track still in the live playlist - continue - random_jump = pl.playlist_ids.index(ref) # Find position of reference in playlist - break +def clear_img_cache(delete_disk: bool = True) -> None: + global album_art_gen + album_art_gen.clear_cache() + prefs.failed_artists.clear() + prefs.failed_background_artists.clear() + tauon.gall_ren.key_list = [] - # Refill the pool - self.update_shuffle_pool(pl.uuid_int) + i = 0 + while len(tauon.gall_ren.queue) > 0: + time.sleep(0.01) + i += 1 + if i > 5 / 0.01: + break - else: - random_jump = random.randrange(len(self.playing_playlist())) # not used + for key, value in tauon.gall_ren.gall.items(): + SDL_DestroyTexture(value[2]) + tauon.gall_ren.gall = {} - self.playlist_playing_position = random_jump - self.track_queue.append(self.playing_playlist()[random_jump]) + if delete_disk: + dirs = [g_cache_dir, n_cache_dir, e_cache_dir] + for direc in dirs: + if os.path.isdir(direc): + for item in os.listdir(direc): + path = os.path.join(direc, item) + os.remove(path) - if inplace and self.queue_step > 1: - del self.track_queue[self.queue_step] - else: - if dry: - return self.track_queue[new_step] - self.queue_step = new_step - - if rr: - if dry: - return None - self.play_target_rr() - elif play: - self.play_target(jump=not end) - - - # If not random mode, Step down 1 on the playlist - elif self.random_mode is False and len(self.playing_playlist()) > 0: - - # Stop at end of playlist - if self.playlist_playing_position == len(self.playing_playlist()) - 1: - if dry: - return None - if prefs.end_setting == "stop": - self.playing_state = 0 - self.playerCommand = "runstop" - self.playerCommandReady = True - end_of_playlist = True - - elif prefs.end_setting in ("advance", "cycle"): - - # If at end playlist and not cycle mode, stop playback - if self.active_playlist_playing == len( - self.multi_playlist) - 1 and prefs.end_setting != "cycle": - self.playing_state = 0 - self.playerCommand = "runstop" - self.playerCommandReady = True - end_of_playlist = True + prefs.failed_artists.clear() + for key, value in artist_list_box.thumb_cache.items(): + if value: + SDL_DestroyTexture(value[0]) + artist_list_box.thumb_cache.clear() + gui.update += 1 - else: - p = self.active_playlist_playing - for i in range(len(self.multi_playlist)): +def clear_track_image_cache(track: TrackClass): + gui.halt_image_rendering = True + if tauon.gall_ren.queue: + time.sleep(0.05) + if tauon.gall_ren.queue: + time.sleep(0.2) + if tauon.gall_ren.queue: + time.sleep(0.5) - k = (p + i + 1) % len(self.multi_playlist) + direc = os.path.join(g_cache_dir) + if os.path.isdir(direc): + for item in os.listdir(direc): + n = item.split("-") + if len(n) > 2 and n[2] == str(track.index): + os.remove(os.path.join(direc, item)) + logging.info("Cleared cache thumbnail: " + os.path.join(direc, item)) - # Skip a playlist if empty - if not (self.multi_playlist[k].playlist_ids): - continue + keys = set() + for key, value in tauon.gall_ren.gall.items(): + if key[0] == track: + SDL_DestroyTexture(value[2]) + if key not in keys: + keys.add(key) + for key in keys: + del tauon.gall_ren.gall[key] + if key in tauon.gall_ren.key_list: + tauon.gall_ren.key_list.remove(key) - # Skip a playlist if hidden - if self.multi_playlist[k].hidden and prefs.tabs_on_top: - continue + gui.halt_image_rendering = False + album_art_gen.clear_cache() - # Set found playlist as playing the first track - self.active_playlist_playing = k - self.playlist_playing_position = -1 - self.advance(end=end, force=True, play=play) - break +def trunc_line(line: str, font: str, px: int, dots: bool = True) -> str: + """This old function is slow and should be avoided""" + if ddt.get_text_w(line, font) < px + 10: + return line - else: - # Restart current if no other eligible playlist found - self.playlist_playing_position = -1 - self.advance(end=end, force=True, play=play) + if dots: + while ddt.get_text_w(line.rstrip(" ") + gui.trunk_end, font) > px: + if len(line) == 0: + return gui.trunk_end + line = line[:-1] + return line.rstrip(" ") + gui.trunk_end - return None + while ddt.get_text_w(line, font) > px: - elif prefs.end_setting == "repeat": - self.playlist_playing_position = -1 - self.advance(end=end, force=True, play=play) - return None + line = line[:-1] + if len(line) < 2: + break - gui.update += 3 + return line - else: - if self.playlist_playing_position > len(self.playing_playlist()) - 1: - if dry: - return None - self.playlist_playing_position = 0 - - elif not force and len(self.track_queue) > 0 and self.playing_playlist()[ - self.playlist_playing_position] != self.track_queue[ - self.queue_step]: - try: - if dry: - return None - self.playlist_playing_position = self.playing_playlist().index( - self.track_queue[self.queue_step]) - except Exception: - logging.exception("Failed to set playlist_playing_position") - if len(self.playing_playlist()) == self.playlist_playing_position + 1: - return None +def right_trunc(line: str, font: str, px: int, dots: bool = True) -> str: + if ddt.get_text_w(line, font) < px + 10: + return line - if dry: - return self.playing_playlist()[self.playlist_playing_position + 1] - self.playlist_playing_position += 1 - self.track_queue.append(self.playing_playlist()[self.playlist_playing_position]) + if dots: + while ddt.get_text_w(line.rstrip(" ") + gui.trunk_end, font) > px: + if len(line) == 0: + return gui.trunk_end + line = line[1:] + return gui.trunk_end + line.rstrip(" ") - # logging.info("standand advance") - # self.queue_target = len(self.track_queue) - 1 - # if end: - # self.play_target_gapless(jump= not end) - # else: - self.queue_step = len(self.track_queue) - 1 - if play: - self.play_target(jump=not end) - - elif self.random_mode and (self.album_shuffle_mode or prefs.album_shuffle_lock_mode): - - # Album shuffle mode - logging.info("Album shuffle mode") - - po = self.playing_object() - - redraw = False - - # Checks - if po is not None and len(self.playing_playlist()) > 0: - - # If we at end of playlist, we'll go to a new album - if len(self.playing_playlist()) == self.playlist_playing_position + 1: - redraw = True - # If the next track is a new album, go to a new album - elif po.parent_folder_path != self.get_track( - self.playing_playlist()[self.playlist_playing_position + 1]).parent_folder_path: - redraw = True - # Always redraw on press in album shuffle lockdown - if prefs.album_shuffle_lock_mode and not end: - redraw = True - - if not redraw: - if dry: - return self.playing_playlist()[self.playlist_playing_position + 1] - self.playlist_playing_position += 1 - self.track_queue.append(self.playing_playlist()[self.playlist_playing_position]) - self.queue_step = len(self.track_queue) - 1 - # self.queue_target = len(self.track_queue) - 1 - if play: - self.play_target(jump=not end) - - else: - - if dry: - return None - albums = [] - current_folder = "" - for i in range(len(self.playing_playlist())): - if i == 0: - albums.append(i) - current_folder = self.master_library[self.playing_playlist()[i]].parent_folder_path - elif self.master_library[self.playing_playlist()[i]].parent_folder_path != current_folder: - current_folder = self.master_library[self.playing_playlist()[i]].parent_folder_path - albums.append(i) - - random.shuffle(albums) - - for a in albums: - if self.get_track(self.playing_playlist()[a]).parent_folder_path != self.playing_object().parent_folder_path: - self.playlist_playing_position = a - self.track_queue.append(self.playing_playlist()[a]) - self.queue_step = len(self.track_queue) - 1 - # self.queue_target = len(self.track_queue) - 1 - if play: - self.play_target(jump=not end) - break - a = 0 - self.playlist_playing_position = a - self.track_queue.append(self.playing_playlist()[a]) - self.queue_step = len(self.track_queue) - 1 - if play: - self.play_target(jump=not end) - # logging.info("THERE IS ONLY ONE ALBUM IN THE PLAYLIST") - # self.stop() + while ddt.get_text_w(line, font) > px: + # trunk = True + line = line[1:] + if len(line) < 2: + break + # if trunk and dots: + # line = line.rstrip(" ") + gui.trunk_end + return line - else: - logging.error("ADVANCE ERROR - NO CASE!") - if dry: - return None +# def trunc_line2(line, font, px): +# trunk = False +# p = ddt.get_text_w(line, font) +# if p == 0 or p < px + 15: +# return line +# +# tl = line[0:(int(px / p * len(line)) + 3)] +# +# if ddt.get_text_w(line.rstrip(" ") + gui.trunk_end, font) > px: +# line = tl +# +# while ddt.get_text_w(line.rstrip(" ") + gui.trunk_end, font) > px + 10: +# trunk = True +# line = line[:-1] +# if len(line) < 1: +# break +# +# return line.rstrip(" ") + gui.trunk_end - if self.active_playlist_viewing == self.active_playlist_playing: - self.show_current(quiet=quiet) - elif prefs.auto_goto_playing: - self.show_current(quiet=quiet, this_only=True, playing=False, highlight=True, no_switch=True) - # if album_mode: - # goto_album(self.playlist_playing) +click_time = time.time() +scroll_hold = False +scroll_point = 0 +scroll_bpoint = 0 +sbl = 50 +sbp = 100 - self.render_playlist() +asbp = 50 +album_scroll_hold = False - if tauon.spot_ctl.playing and end_of_playlist: - tauon.spot_ctl.control("stop") - self.notify_update() - lfm_scrobbler.start_queue() - if play: - notify_song(end_of_playlist, delay=1.3) - return None +def fix_encoding(index, mode, enc): + global default_playlist + global enc_field - def reset_missing_flags(self) -> None: - for value in self.master_library.values(): - value.found = True - gui.pl_update += 1 + todo = [] -pctl = PlayerCtl() + if mode == 1: + todo = [index] + elif mode == 0: + for b in range(len(default_playlist)): + if pctl.master_library[default_playlist[b]].parent_folder_name == pctl.master_library[ + index].parent_folder_name: + todo.append(default_playlist[b]) -notify_change = pctl.notify_change + for q in range(len(todo)): + # key = pctl.master_library[todo[q]].title + pctl.master_library[todo[q]].filename + old_star = star_store.full_get(todo[q]) + if old_star != None: + star_store.remove(todo[q]) -def auto_name_pl(target_pl: int) -> None: - if not pctl.multi_playlist[target_pl].playlist_ids: - return + if enc_field == "All" or enc_field == "Artist": + line = pctl.master_library[todo[q]].artist + line = line.encode("Latin-1", "ignore") + line = line.decode(enc, "ignore") + pctl.master_library[todo[q]].artist = line - albums = [] - artists = [] - parents = [] + if enc_field == "All" or enc_field == "Album": + line = pctl.master_library[todo[q]].album + line = line.encode("Latin-1", "ignore") + line = line.decode(enc, "ignore") + pctl.master_library[todo[q]].album = line - track = None + if enc_field == "All" or enc_field == "Title": + line = pctl.master_library[todo[q]].title + line = line.encode("Latin-1", "ignore") + line = line.decode(enc, "ignore") + pctl.master_library[todo[q]].title = line - for index in pctl.multi_playlist[target_pl].playlist_ids: - track = pctl.get_track(index) - albums.append(track.album) - if track.album_artist: - artists.append(track.album_artist) - else: - artists.append(track.artist) - parents.append(track.parent_folder_path) + if old_star != None: + star_store.insert(todo[q], old_star) - nt = "" - artist = "" + # if key in pctl.star_library: + # newkey = pctl.master_library[todo[q]].title + pctl.master_library[todo[q]].filename + # if newkey not in pctl.star_library: + # pctl.star_library[newkey] = copy.deepcopy(pctl.star_library[key]) + # # del pctl.star_library[key] - if track: - artist = track.artist - if track.album_artist: - artist = track.album_artist - if track and albums and albums[0] and albums.count(albums[0]) == len(albums): - nt = artist + " - " + track.album +def transfer_tracks(index, mode, to): + todo = [] - elif track and artists and artists[0] and artists.count(artists[0]) == len(artists): - nt = artists[0] + if mode == 0: + todo = [index] + elif mode == 1: + for b in range(len(default_playlist)): + if pctl.master_library[default_playlist[b]].parent_folder_name == pctl.master_library[ + index].parent_folder_name: + todo.append(default_playlist[b]) + elif mode == 2: + todo = default_playlist - else: - nt = os.path.basename(commonprefix(parents)) + pctl.multi_playlist[to].playlist_ids += todo - pctl.multi_playlist[target_pl].title = nt +def prep_gal(): + global albums + albums = [] -def get_object(index: int) -> TrackClass: - return pctl.master_library[index] + folder = "" + for index in default_playlist: -def update_title_do() -> None: - if pctl.playing_state > 0: - if len(pctl.track_queue) > 0: - line = pctl.master_library[pctl.track_queue[pctl.queue_step]].artist + " - " + \ - pctl.master_library[pctl.track_queue[pctl.queue_step]].title - # line += " : : Tauon Music Box" - line = line.encode("utf-8") - SDL_SetWindowTitle(t_window, line) - else: - line = "Tauon Music Box" - line = line.encode("utf-8") - SDL_SetWindowTitle(t_window, line) + if folder != pctl.master_library[index].parent_folder_name: + albums.append([index, 0]) + folder = pctl.master_library[index].parent_folder_name -def open_encode_out() -> None: - if not prefs.encoder_output.exists(): - prefs.encoder_output.mkdir() - if system == "Windows" or msys: - line = r"explorer " + prefs.encoder_output.replace("/", "\\") - subprocess.Popen(line) - else: - if macos: - subprocess.Popen(["open", prefs.encoder_output]) +def add_stations(stations: list[dict[str, int | str]], name: str): + if len(stations) == 1: + for i, s in enumerate(pctl.radio_playlists): + if s["name"] == "Default": + s["items"].insert(0, stations[0]) + s["scroll"] = 0 + pctl.radio_playlist_viewing = i + break else: - subprocess.Popen(["xdg-open", prefs.encoder_output]) + r = {} + r["uid"] = uid_gen() + r["name"] = "Default" + r["items"] = stations + r["scroll"] = 0 + pctl.radio_playlists.append(r) + pctl.radio_playlist_viewing = len(pctl.radio_playlists) - 1 + else: + r = {} + r["uid"] = uid_gen() + r["name"] = name + r["items"] = stations + r["scroll"] = 0 + pctl.radio_playlists.append(r) + pctl.radio_playlist_viewing = len(pctl.radio_playlists) - 1 + if not gui.radio_view: + enter_radio_view() -def g_open_encode_out(a, b, c) -> None: - open_encode_out() +def load_m3u(path: str) -> None: + name = os.path.basename(path)[:-4] + playlist = [] + stations = [] + location_dict = {} + titles = {} + if not os.path.isfile(path): + return -if system == "Linux" and not macos and not msys: + with Path(path).open(encoding="utf-8") as file: + lines = file.readlines() - try: - Notify.init("Tauon Music Box") - g_tc_notify = Notify.Notification.new( - "Tauon Music Box", - "Transcoding has finished.") - value = GLib.Variant("s", t_id) - g_tc_notify.set_hint("desktop-entry", value) - - g_tc_notify.add_action( - "action_click", - "Open Output Folder", - g_open_encode_out, - None, - ) + for i, line in enumerate(lines): + line = line.strip("\r\n").strip() + if not line.startswith("#"): # line.startswith("http"): - de_notify_support = True + # Get title if present + line_title = "" + if i > 0: + bline = lines[i - 1] + if "," in bline and bline.startswith("#EXTINF:"): + line_title = bline.split(",", 1)[1].strip("\r\n").strip() - except Exception: - logging.exception("Failed init notifications") + if line.startswith("http"): + radio: dict[str, int | str] = {} + radio["stream_url"] = line - if de_notify_support: - song_notification = Notify.Notification.new("Next track notification") - value = GLib.Variant("s", t_id) - song_notification.set_hint("desktop-entry", value) + if line_title: + radio["title"] = line_title + else: + radio["title"] = os.path.splitext(os.path.basename(path))[0].strip() + stations.append(radio) -def notify_song_fire(notification, delay, id) -> None: - time.sleep(delay) - notification.show() - if id is None: - return + if gui.auto_play_import: + gui.auto_play_import = False + radiobox.start(radio) + else: + line = uri_parse(line) + # Join file path if possibly relative + if not line.startswith("/"): + line = os.path.join(os.path.dirname(path), line) - time.sleep(8) - if id == gui.notify_main_id: - notification.close() + # Cache datbase file paths for quick lookup + if not location_dict: + for key, value in pctl.master_library.items(): + if value.fullpath: + location_dict[value.fullpath] = value + if value.title: + titles[value.artist + " - " + value.title] = value + # Is file path already imported? + logging.info(line) + if line in location_dict: + playlist.append(location_dict[line].index) + logging.info("found imported") + # Or... does the file exist? Then import it + elif os.path.isfile(line): + nt = TrackClass() + nt.index = pctl.master_count + set_path(nt, line) + nt = tag_scan(nt) + pctl.master_library[pctl.master_count] = nt + playlist.append(pctl.master_count) + pctl.master_count += 1 + logging.info("found file") + # Last resort, guess based on title + elif line_title in titles: + playlist.append(titles[line_title].index) + logging.info("found title") + else: + logging.info("not found") -def notify_song(notify_of_end: bool = False, delay: float = 0.0) -> None: - if not de_notify_support: - return + if playlist: + pctl.multi_playlist.append( + pl_gen(title=name, playlist_ids=playlist)) + if stations: + add_stations(stations, name) - if notify_of_end and prefs.end_setting != "stop": - return + gui.update = 1 - if prefs.show_notifications and pctl.playing_object() is not None and not window_is_focused(): - if prefs.stop_notifications_mini_mode and gui.mode == 3: - return - track = pctl.playing_object() +def read_pls(lines: list[str], path: str, followed: bool = False) -> None: + ids = [] + urls = {} + titles = {} - if not track or not (track.title or track.artist or track.album or track.filename): - return # only display if we have at least one piece of metadata avaliable + for line in lines: + line = line.strip("\r\n") + if "=" in line and line.startswith("File") and "http" in line: + # Get number + n = line.split("=")[0][4:] + if n.isdigit(): + if n not in ids: + ids.append(n) + urls[n] = line.split("=", 1)[1].strip() - i_path = "" - try: - if not notify_of_end: - i_path = tauon.thumb_tracks.path(track) - except Exception: - logging.exception(track.fullpath.encode("utf-8", "replace").decode("utf-8")) - logging.error("Thumbnail error") + if "=" in line and line.startswith("Title"): + # Get number + n = line.split("=")[0][5:] + if n.isdigit(): + if n not in ids: + ids.append(n) + titles[n] = line.split("=", 1)[1].strip() - top_line = track.title + stations: list[dict[str, int | str]] = [] + for id in ids: + if id in urls: + radio: dict[str, int | str] = {} + radio["stream_url"] = urls[id] + radio["title"] = os.path.splitext(os.path.basename(path))[0] + radio["scroll"] = 0 + if id in titles: + radio["title"] = titles[id] - if prefs.notify_include_album: - bottom_line = (track.artist + " | " + track.album).strip("| ") - else: - bottom_line = track.artist + if ".pls" in radio["stream_url"]: + if not followed: + try: + logging.info("Download .pls") + response = requests.get(radio["stream_url"], stream=True, timeout=15) + if int(response.headers["Content-Length"]) < 2000: + read_pls(response.content.decode().splitlines(), path, followed=True) + except Exception: + logging.exception("Failed to retrieve .pls") + else: + stations.append(radio) + if gui.auto_play_import: + gui.auto_play_import = False + radiobox.start(radio) + if stations: + add_stations(stations, os.path.basename(path)) - if not track.title: - a, t = filename_to_metadata(clean_string(track.filename)) - if not track.artist: - bottom_line = a - top_line = t - gui.notify_main_id = uid_gen() - id = gui.notify_main_id +def load_pls(path: str) -> None: + if os.path.isfile(path): + f = open(path) + lines = f.readlines() + read_pls(lines, path) + f.close() - if notify_of_end: - bottom_line = "Tauon Music Box" - top_line = (_("End of playlist")) - id = None - song_notification.update(top_line, bottom_line, i_path) +def load_xspf(path: str) -> None: + global to_got - shoot_dl = threading.Thread(target=notify_song_fire, args=([song_notification, delay, id])) - shoot_dl.daemon = True - shoot_dl.start() + name = os.path.basename(path)[:-5] + # tauon.log("Importing XSPF playlist: " + path, title=True) + logging.info("Importing XSPF playlist: " + path) + try: + parser = ET.XMLParser(encoding="utf-8") + e = ET.parse(path, parser).getroot() -# Last.FM ----------------------------------------------------------------- -class LastFMapi: - API_SECRET = "6e433964d3ff5e817b7724d16a9cf0cc" - connected = False - API_KEY = "bfdaf6357f1dddd494e5bee1afe38254" - scanning_username = "" - - network = None - lastfm_network = None - tries = 0 - - scanning_friends = False - scanning_loves = False - scanning_scrobbles = False - - def __init__(self) -> None: - self.sg = None - self.url = None - - def get_network(self) -> LibreFMNetwork: - if prefs.use_libre_fm: - return pylast.LibreFMNetwork - return pylast.LastFMNetwork - - def auth1(self) -> None: - if not last_fm_enable: - show_message(_("Optional module python-pylast not installed"), mode="warning") - return - # This is step one where the user clicks "login" + a = [] + b = {} + info = "" - if self.network is None: - self.no_user_connect() + for top in e: - self.sg = pylast.SessionKeyGenerator(self.network) - self.url = self.sg.get_web_auth_url() - show_message(_("Web auth page opened"), _("Once authorised click the 'done' button."), mode="arrow") - webbrowser.open(self.url, new=2, autoraise=True) + if top.tag.endswith("info"): + info = top.text + if top.tag.endswith("title"): + name = top.text + if top.tag.endswith("trackList"): + for track in top: + if track.tag.endswith("track"): + for field in track: + logging.info(field.tag) + logging.info(field.text) + if "title" in field.tag and field.text: + b["title"] = field.text + if "location" in field.tag and field.text: + l = field.text + l = str(urllib.parse.unquote(l)) + if l[:5] == "file:": + l = l.replace("file:", "") + l = l.lstrip("/") + l = "/" + l - def auth2(self) -> None: + b["location"] = l + if "creator" in field.tag and field.text: + b["artist"] = field.text + if "album" in field.tag and field.text: + b["album"] = field.text + if "duration" in field.tag and field.text: + b["duration"] = field.text - # This is step 2 where the user clicks "Done" + b["info"] = info + b["name"] = name + a.append(copy.deepcopy(b)) + b = {} - if self.sg is None: - show_message(_("You need to log in first")) - return + except Exception: + logging.exception("Error importing/parsing XSPF playlist") + show_message(_("Error importing XSPF playlist."), _("Sorry about that."), mode="warning") + return - try: - # session_key = self.sg.get_web_auth_session_key(self.url) - session_key, username = self.sg.get_web_auth_session_key_username(self.url) - prefs.last_fm_token = session_key - self.network = self.get_network()(api_key=self.API_KEY, api_secret= - self.API_SECRET, session_key=prefs.last_fm_token) - # user = self.network.get_authenticated_user() - # username = user.get_name() - prefs.last_fm_username = username - - except Exception as e: - if "Unauthorized Token" in str(e): - logging.exception("Not authorized") - show_message(_("Error - Not authorized"), mode="error") - else: - logging.exception("Unknown error") - show_message(_("Error"), _("Unknown error."), mode="error") + # Extract internet streams first + stations: list[dict[str, int | str]] = [] + for i in reversed(range(len(a))): + item = a[i] + if item["location"].startswith("http"): + radio: dict[str, int | str] = {} + radio["stream_url"] = item["location"] + radio["title"] = item["name"] + radio["scroll"] = 0 + if item["info"].startswith("http"): + radio["website_url"] = item["info"] - if not toggle_lfm_auto(mode=1): - toggle_lfm_auto() + stations.append(radio) - def auth3(self) -> None: - """This is used for 'logout'""" - prefs.last_fm_token = None - prefs.last_fm_username = "" - show_message(_("Logout will complete on app restart.")) + if gui.auto_play_import: + gui.auto_play_import = False + radiobox.start(radio) - def connect(self, m_notify: bool = True) -> bool | None: + del a[i] + if stations: + add_stations(stations, os.path.basename(path)) + playlist = [] + missing = 0 - if not last_fm_enable: - return False + if len(a) > 5000: + to_got = "xspfl" - if self.connected is True: - if m_notify: - show_message(_("Already connected to Last.fm")) - return True + # Generate location dict + location_dict = {} + base_names = {} + r_base_names = {} + titles = {} + for key, value in pctl.master_library.items(): + if value.fullpath != "": + location_dict[value.fullpath] = key + if value.filename != "": + base_names[value.filename] = 0 + r_base_names[key] = value.filename + if value.title != "": + titles[value.title] = 0 - if prefs.last_fm_token is None: - show_message(_("No Last.Fm account registered"), _("Authorise an account in settings"), mode="info") - return None + for track in a: + found = False - logging.info("Attempting to connect to Last.fm network") + # Check if we already have a track with full file path in database + if not found and "location" in track: - try: + location = track["location"] + if location in location_dict: + playlist.append(location_dict[location]) + if not os.path.isfile(location): + missing += 1 + found = True - self.network = self.get_network()( - api_key=self.API_KEY, api_secret=self.API_SECRET, session_key=prefs.last_fm_token) # , username=lfm_username, password_hash=lfm_hash) + if found is True: + continue - self.connected = True - if m_notify: - show_message(_("Connection to Last.fm was successful."), mode="done") + # Then check for title, artist and filename match + if not found and "location" in track and "duration" in track and "title" in track and "artist" in track: + base = os.path.basename(track["location"]) + if base in base_names: + for index, bn in r_base_names.items(): + va = pctl.master_library[index] + if va.artist == track["artist"] and va.title == track["title"] and \ + os.path.isfile(va.fullpath) and \ + va.filename == base: + playlist.append(index) + if not os.path.isfile(va.fullpath): + missing += 1 + found = True + break + if found is True: + continue - logging.info("Connection to lastfm appears successful") - return True + # Then check for just title and artist match + if not found and "title" in track and "artist" in track and track["title"] in titles: + for key, value in pctl.master_library.items(): + if value.artist == track["artist"] and value.title == track["title"] and os.path.isfile(value.fullpath): + playlist.append(key) + if not os.path.isfile(value.fullpath): + missing += 1 + found = True + break + if found is True: + continue - except Exception as e: - logging.exception("Error connecting to Last.fm network") - show_message(_("Error connecting to Last.fm network"), str(e), mode="warning") - return False + if (not found and "location" in track) or "title" in track: + nt = TrackClass() + nt.index = pctl.master_count + nt.found = False - def toggle(self) -> None: - prefs.scrobble_hold ^= True + if "location" in track: + location = track["location"] + set_path(nt, location) + if os.path.isfile(location): + nt.found = True + elif "album" in track: + nt.parent_folder_name = track["album"] + if "artist" in track: + nt.artist = track["artist"] + if "title" in track: + nt.title = track["title"] + if "duration" in track: + nt.length = int(float(track["duration"]) / 1000) + if "album" in track: + nt.album = track["album"] + nt.is_cue = False + if nt.found: + nt = tag_scan(nt) - def details_ready(self) -> bool: - if prefs.last_fm_token: - return True - return False + pctl.master_library[pctl.master_count] = nt + playlist.append(pctl.master_count) + pctl.master_count += 1 + if nt.found: + continue - def last_fm_only_connect(self) -> bool: - if not last_fm_enable: - return False - try: - self.lastfm_network = pylast.LastFMNetwork(api_key=self.API_KEY, api_secret=self.API_SECRET) - logging.info("Connection appears successful") - return True - - except Exception as e: - logging.exception("Error communicating with Last.fm network") - show_message(_("Error communicating with Last.fm network"), str(e), mode="warning") - return False + missing += 1 + logging.error("-- Failed to locate track") + if "location" in track: + logging.error("-- -- Expected path: " + track["location"]) + if "title" in track: + logging.error("-- -- Title: " + track["title"]) + if "artist" in track: + logging.error("-- -- Artist: " + track["artist"]) + if "album" in track: + logging.error("-- -- Album: " + track["album"]) - def no_user_connect(self) -> bool: - if not last_fm_enable: - return False - try: - self.network = self.get_network()(api_key=self.API_KEY, api_secret=self.API_SECRET) - logging.info("Connection appears successful") - return True + if missing > 0: + show_message( + _("Failed to locate {N} out of {T} tracks.") + .format(N=str(missing), T=str(len(a)))) + #logging.info(playlist) + if playlist: + pctl.multi_playlist.append( + pl_gen(title=name, playlist_ids=playlist)) + gui.update = 1 - except Exception as e: - logging.exception("Error communicating with Last.fm network") - show_message(_("Error communicating with Last.fm network"), str(e), mode="warning") - return False + # tauon.log("Finished importing XSPF") - def get_all_scrobbles_estimate_time(self) -> float | None: - if not self.connected: - self.connect(False) - if not self.connected or not prefs.last_fm_username: - return None +bb_type = 0 - user = pylast.User(prefs.last_fm_username, self.network) - total = user.get_playcount() +# gui.scroll_hide_box = (0, gui.panelY, 28, window_size[1] - gui.panelBY - gui.panelY) - if total: - return 0.04364 * total - return 0 +encoding_menu = False +enc_index = 0 +enc_setting = 0 +enc_field = "All" - def get_all_scrobbles(self) -> None: +gen_menu = False - if not self.connected: - self.connect(False) - if not self.connected or not prefs.last_fm_username: - return +transfer_setting = 0 - try: - self.scanning_scrobbles = True - self.network.enable_rate_limit() - user = pylast.User(prefs.last_fm_username, self.network) - # username = user.get_name() - perf_timer.set() - tracks = user.get_recent_tracks(None) +b_panel_size = 300 +b_info_bar = False - counts = {} +#28REWORK - # Count up the unique pairs - for track in tracks: - key = (str(track.track.artist), str(track.track.title)) - c = counts.get(key, 0) - counts[key] = c + 1 +def ex_tool_tip(x, y, text1_width, text, font): + text2_width = ddt.get_text_w(text, font) + if text2_width == text1_width: + return - touched = [] - - # Add counts to matching tracks - for key, value in counts.items(): - artist, title = key - artist = artist.lower() - title = title.lower() - - for track in pctl.master_library.values(): - t_artist = track.artist.lower() - artists = [x.lower() for x in get_split_artists(track)] - if t_artist == artist or artist in artists or ( - track.album_artist and track.album_artist.lower() == artist): - if track.title.lower() == title: - if track.index in touched: - track.lfm_scrobbles += value - else: - track.lfm_scrobbles = value - touched.append(track.index) - except Exception: - logging.exception("Scanning failed. Try again?") - gui.pl_update += 1 - self.scanning_scrobbles = False - show_message(_("Scanning failed. Try again?"), mode="error") - return + y -= 10 * gui.scale - logging.info(perf_timer.get()) - gui.pl_update += 1 - self.scanning_scrobbles = False - tauon.bg_save() - show_message(_("Scanning scrobbles complete"), mode="done") + w = ddt.get_text_w(text, 312) + 24 * gui.scale + h = 24 * gui.scale - def artist_info(self, artist: str): + x -= int(w / 2) - if self.lastfm_network is None: - if self.last_fm_only_connect() is False: - return False, "", "" + border = 1 * gui.scale + ddt.rect((x - border, y - border, w + border * 2, h + border * 2), colours.grey(60)) + ddt.rect((x, y, w, h), colours.menu_background) + p = ddt.text((x + int(w / 2), y + 3 * gui.scale, 2), text, colours.menu_text, 312, bg=colours.menu_background) - try: - if artist != "": - l_artist = pylast.Artist( - artist.replace("/", "").replace("\\", "").replace(" & ", " and ").replace("&", " "), - self.lastfm_network) - bio = l_artist.get_bio_content() - # cover_link = l_artist.get_cover_image() - mbid = l_artist.get_mbid() - url = l_artist.get_url() - - return True, bio, "", mbid, url - except Exception: - logging.exception("last.fm get artist info failed") +def close_all_menus(): + for menu in Menu.instances: + menu.active = False + Menu.active = False - return False, "", "", "", "" - def artist_mbid(self, artist: str): +def menu_standard_or_grey(bool: bool): + if bool: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - if self.lastfm_network is None: - if self.last_fm_only_connect() is False: - return "" + return [line_colour, colours.menu_background, None] - try: - if artist != "": - l_artist = pylast.Artist( - artist.replace("/", "").replace("\\", "").replace(" & ", " and ").replace("&", " "), - self.lastfm_network) - mbid = l_artist.get_mbid() - return mbid - except Exception: - logging.exception("last.fm get artist mbid info failed") - return "" +def enable_artist_list(): + if prefs.left_panel_mode != "artist list": + gui.last_left_panel_mode = prefs.left_panel_mode + prefs.left_panel_mode = "artist list" + gui.lsp = True + gui.update_layout() - def sync_pull_love(self, track_object: TrackClass) -> None: - if not prefs.lastfm_pull_love or not (track_object.artist and track_object.title): - return - if not last_fm_enable: - return - if prefs.auto_lfm: - self.connect(False) - if not self.connected: - return - try: - track = self.network.get_track(track_object.artist, track_object.title) - if not track: - logging.error("Get love: track not found") - return - track.username = prefs.last_fm_username +def enable_playlist_list(): + if prefs.left_panel_mode != "playlist": + gui.last_left_panel_mode = prefs.left_panel_mode + prefs.left_panel_mode = "playlist" + gui.lsp = True + gui.update_layout() - remote_loved = track.get_userloved() - if track_object.title != track.get_correction() or track_object.artist != track.get_artist().get_correction(): - logging.warning(f"Pylast/lastfm bug workaround. API thought {track_object.artist} - {track_object.title} loved status was: {remote_loved}") - return +def enable_queue_panel(): + if prefs.left_panel_mode != "queue": + gui.last_left_panel_mode = prefs.left_panel_mode + prefs.left_panel_mode = "queue" + gui.lsp = True + gui.update_layout() - if remote_loved is None: - logging.error("Error getting loved status") - return - local_loved = love(set=False, track_id=track_object.index, notify=False, sync=False) +def enable_folder_list(): + if prefs.left_panel_mode != "folder view": + gui.last_left_panel_mode = prefs.left_panel_mode + prefs.left_panel_mode = "folder view" + gui.lsp = True + gui.update_layout() - if remote_loved != local_loved: - love(set=True, track_id=track_object.index, notify=False, sync=False) - except Exception: - logging.exception("Failed to pull love") - def scrobble(self, track_object: TrackClass, timestamp: float | None = None) -> bool: - if not last_fm_enable: - return True - if prefs.scrobble_hold: - return True - if prefs.auto_lfm: - self.connect(False) +def lsp_menu_test_queue(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "queue" - if timestamp is None: - timestamp = int(time.time()) - # lastfm_user = self.network.get_user(self.username) +def lsp_menu_test_playlist(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "playlist" - title = track_object.title - album = track_object.album - artist = get_artist_strip_feat(track_object) - album_artist = track_object.album_artist - logging.info("Submitting scrobble...") +def lsp_menu_test_tree(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "folder view" - # Act - try: - if title != "" and artist != "": - if album != "": - if album_artist and album_artist != artist: - self.network.scrobble( - artist=artist, title=title, album=album, album_artist=album_artist, timestamp=timestamp) - else: - self.network.scrobble(artist=artist, title=title, album=album, timestamp=timestamp) - else: - self.network.scrobble(artist=artist, title=title, timestamp=timestamp) - # logging.info('Scrobbled') - # Pull loved status +def lsp_menu_test_artist(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "artist list" - self.sync_pull_love(track_object) +def toggle_left_last(): + gui.lsp = True + t = prefs.left_panel_mode + if t != gui.last_left_panel_mode: + prefs.left_panel_mode = gui.last_left_panel_mode + gui.last_left_panel_mode = t - else: - logging.warning("Not sent, incomplete metadata") +def toggle_repeat() -> None: + gui.update += 1 + pctl.repeat_mode ^= True + if pctl.mpris is not None: + pctl.mpris.update_loop() - except Exception as e: - logging.exception("Failed to Scrobble!") - if "retry" in str(e): - logging.warning("Retrying in a couple seconds...") - time.sleep(7) +def menu_repeat_off() -> None: + pctl.repeat_mode = False + pctl.album_repeat_mode = False + if pctl.mpris is not None: + pctl.mpris.update_loop() - try: - self.network.scrobble(artist=artist, title=title, timestamp=timestamp) - # logging.info('Scrobbled') - return True - except Exception: - logging.exception("Failed to retry!") +def menu_set_repeat() -> None: + pctl.repeat_mode = True + pctl.album_repeat_mode = False + if pctl.mpris is not None: + pctl.mpris.update_loop() - # show_message(_("Error: Could not scrobble. ", str(e), mode='warning') - logging.error("Error connecting to last.fm") - scrobble_warning_timer.set() - gui.update += 1 - gui.delay_frame(5) +def menu_album_repeat() -> None: + pctl.repeat_mode = True + pctl.album_repeat_mode = True + if pctl.mpris is not None: + pctl.mpris.update_loop() - return False - return True +def toggle_random(): + gui.update += 1 + pctl.random_mode ^= True + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - def get_bio(self, artist: str) -> str: - - if self.lastfm_network is None: - if self.last_fm_only_connect() is False: - return "" - - artist_object = pylast.Artist(artist, self.lastfm_network) - bio = artist_object.get_bio_summary(language="en") - # logging.info(artist_object.get_cover_image()) - # logging.info("\n\n") - # logging.info(bio) - # logging.info("\n\n") - # logging.info(artist_object.get_bio_content()) - return bio - # else: - # return "" - - def love(self, artist: str, title: str): - - if not self.connected and prefs.auto_lfm: - self.connect(False) - prefs.scrobble_hold = True - if self.connected and artist != "" and title != "": - track = self.network.get_track(artist, title) - track.love() - - def unlove(self, artist: str, title: str): - if not last_fm_enable: - return - if not self.connected and prefs.auto_lfm: - self.connect(False) - prefs.scrobble_hold = True - if self.connected and artist != "" and title != "": - track = self.network.get_track(artist, title) - track.love() - track.unlove() +def toggle_random_on(): + pctl.random_mode = True + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - def clear_friends_love(self) -> None: +def toggle_random_off(): + pctl.random_mode = False + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - count = 0 - for index, tr in pctl.master_library.items(): - count += len(tr.lfm_friend_likes) - tr.lfm_friend_likes.clear() +def menu_shuffle_off(): + pctl.random_mode = False + pctl.album_shuffle_mode = False + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - show_message(_("Removed {N} loves.").format(N=count)) +def menu_set_random(): + pctl.random_mode = True + pctl.album_shuffle_mode = False + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - def get_friends_love(self): - if not last_fm_enable: - return - self.scanning_friends = True +def menu_album_random(): + pctl.random_mode = True + pctl.album_shuffle_mode = True + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - try: - username = prefs.last_fm_username - logging.info(f"Username is {username}") +def toggle_shuffle_layout(albums: bool = False): + prefs.shuffle_lock ^= True + if prefs.shuffle_lock: - if not username: - self.scanning_friends = False - show_message(_("There was an error, try re-log in")) - return + gui.shuffle_was_showcase = gui.showcase_mode + gui.shuffle_was_random = pctl.random_mode + gui.shuffle_was_repeat = pctl.repeat_mode - if self.network is None: - self.no_user_connect() + if not gui.combo_mode: + view_box.lyrics(hit=True) + pctl.random_mode = True + pctl.repeat_mode = False + if albums: + prefs.album_shuffle_lock_mode = True + if pctl.playing_state == 0: + pctl.advance() + else: + pctl.random_mode = gui.shuffle_was_random + pctl.repeat_mode = gui.shuffle_was_repeat + prefs.album_shuffle_lock_mode = False + if not gui.shuffle_was_showcase: + exit_combo() - self.network.enable_rate_limit() - lastfm_user = self.network.get_user(username) - friends = lastfm_user.get_friends(limit=None) - show_message(_("Getting friend data..."), _("This may take a very long time."), mode="info") - for friend in friends: - self.scanning_username = friend.name - logging.info("Getting friend loves: " + friend.name) +def toggle_shuffle_layout_albums(): + toggle_shuffle_layout(albums=True) - try: - loves = friend.get_loved_tracks(limit=None) - except Exception: - logging.exception("Failed to get_loved_tracks!") +def exit_shuffle_layout(_): + return prefs.shuffle_lock - for track in loves: - title = track.track.title.casefold() - artist = track.track.artist.name.casefold() - for index, tr in pctl.master_library.items(): +def bio_set_large(): + # if window_size[0] >= round(1000 * gui.scale): + # gui.artist_panel_height = 320 * gui.scale + prefs.bio_large = True + if gui.artist_info_panel: + artist_info_box.get_data(artist_info_box.artist_on) - if tr.title.casefold() == title and tr.artist.casefold() == artist: - tr.lfm_friend_likes.add(friend.name) - logging.info("MATCH") - logging.info(" " + artist + " - " + title) - logging.info(" ----- " + friend.name) +def bio_set_small(): + # gui.artist_panel_height = 200 * gui.scale + prefs.bio_large = False + update_layout_do() + if gui.artist_info_panel: + artist_info_box.get_data(artist_info_box.artist_on) - except Exception: - logging.exception("There was an error getting friends loves") - show_message(_("There was an error getting friends loves"), "", mode="warning") +def artist_info_panel_close(): + gui.artist_info_panel ^= True + gui.update_layout() - self.scanning_friends = False +def toggle_bio_size_deco(): + line = _("Make Large Size") + if prefs.bio_large: + line = _("Make Compact Size") + return [colours.menu_text, colours.menu_background, line] - def dl_love(self) -> None: - if not last_fm_enable: - return - username = prefs.last_fm_username - show_message(_("Scanning loved tracks for: {username}").format(username=username), mode="info") - self.scanning_username = username +def toggle_bio_size(): + if prefs.bio_large: + prefs.bio_large = False + update_layout_do() + # bio_set_small() + else: + prefs.bio_large = True + update_layout_do() + # bio_set_large() + # gui.update_layout() - if not username: - show_message(_("No username found"), mode="error") - return +def flush_artist_bio(artist): + if os.path.isfile(os.path.join(a_cache_dir, artist + "-lfm.txt")): + os.remove(os.path.join(a_cache_dir, artist + "-lfm.txt")) + artist_info_box.text = "" + artist_info_box.artist_on = None - if len(username) > 25: - logging.error("Aborted due to long username") - return +def test_shift(_): + return key_shift_down or key_shiftr_down - self.scanning_loves = True +def test_artist_dl(_): + return not prefs.auto_dl_artist_data - logging.info("Connect for friend scan") +def show_in_playlist(): + if album_mode and window_size[0] < 750 * gui.scale: + toggle_album_mode() - try: - if self.network is None: - self.no_user_connect() + pctl.playlist_view_position = pctl.selected_in_playlist + logging.debug("Position changed by show in playlist") + shift_selection.clear() + shift_selection.append(pctl.selected_in_playlist) + pctl.render_playlist() - self.network.enable_rate_limit() - logging.info("Get user...") - lastfm_user = self.network.get_user(username) - tracks = lastfm_user.get_loved_tracks(limit=None) +def open_folder_stem(path): + if system == "Windows" or msys: + line = r'explorer /select,"%s"' % ( + path.replace("/", "\\")) + subprocess.Popen(line) + else: + line = path + line += "/" + if macos: + subprocess.Popen(["open", line]) + else: + subprocess.Popen(["xdg-open", line]) - matches = 0 - updated = 0 +def open_folder_disable_test(index: int): + track = pctl.master_library[index] + return track.is_network and not os.path.isdir(track.parent_folder_path) - for track in tracks: - title = track.track.title.casefold() - artist = track.track.artist.name.casefold() - - for index, tr in pctl.master_library.items(): - if tr.title.casefold() == title and tr.artist.casefold() == artist: - matches += 1 - logging.info("MATCH:") - logging.info(" " + artist + " - " + title) - star = star_store.full_get(index) - if star is None: - star = star_store.new_object() - if "L" not in star[1]: - updated += 1 - logging.info(" NEW LOVE") - star[1] += "L" - - star_store.insert(index, star) - - self.scanning_loves = False - if len(tracks) == 0: - show_message(_("User has no loved tracks.")) - return - if matches > 0 and updated == 0: - show_message(_("{N} matched tracks are up to date.").format(N=str(matches))) - return - if matches > 0 and updated > 0: - show_message(_("{N} tracks matched. {T} were updated.").format(N=str(matches), T=str(updated))) - return - show_message(_("Of {N} loved tracks, no matches were found in local db").format(N=str(len(tracks)))) - return - except Exception: - logging.exception("This doesn't seem to be working :(") - show_message(_("This doesn't seem to be working :("), mode="error") - self.scanning_loves = False +def open_folder(index: int): + track = pctl.master_library[index] + if open_folder_disable_test(index): + show_message(_("Can't open folder of a network track.")) + return - def update(self, track_object: TrackClass) -> int | None: - if not last_fm_enable: - return None - if prefs.scrobble_hold: - return 0 - if prefs.auto_lfm: - if self.connect(False) is False: - prefs.auto_lfm = False + if system == "Windows" or msys: + line = r'explorer /select,"%s"' % ( + track.fullpath.replace("/", "\\")) + subprocess.Popen(line) + else: + line = track.parent_folder_path + line += "/" + if macos: + line = track.fullpath + subprocess.Popen(["open", "-R", line]) else: - return 0 - - # logging.info('Updating Now Playing') + subprocess.Popen(["xdg-open", line]) - title = track_object.title - album = track_object.album - artist = get_artist_strip_feat(track_object) +def tag_to_new_playlist(tag_item): + path_stem_to_playlist(tag_item.path, tag_item.name) - try: - if title != "" and artist != "": - self.network.update_now_playing( - artist=artist, title=title, album=album) - return 0 - logging.error("Not sent, incomplete metadata") - return 0 - except Exception as e: - logging.exception("Error connecting to last.fm.") - if "retry" in str(e): - return 2 - # show_message(_("Could not update Last.fm. ", str(e), mode='warning') - pctl.b_time -= 5000 - return 1 +def folder_to_new_playlist_by_track_id(track_id: int) -> None: + track = pctl.get_track(track_id) + path_stem_to_playlist(track.parent_folder_path, track.parent_folder_name) +def stem_to_new_playlist(path: str) -> None: + path_stem_to_playlist(path, os.path.basename(path)) -def get_backend_time(path): - pctl.time_to_get = path +def move_playing_folder_to_tree_stem(path: str) -> None: + move_playing_folder_to_stem(path, pl_id=tree_view_box.get_pl_id()) - pctl.playerCommand = "time" - pctl.playerCommandReady = True +def move_playing_folder_to_stem(path: str, pl_id: int | None = None) -> None: + if not pl_id: + pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - while pctl.playerCommand != "done": - time.sleep(0.005) + track = pctl.playing_object() - return pctl.time_to_get + if not track or pctl.playing_state == 0: + show_message(_("No item is currently playing")) + return + move_folder = track.parent_folder_path -lastfm = LastFMapi() + # Stop playing track if its in the current folder + if pctl.playing_state > 0: + if move_folder in pctl.playing_object().parent_folder_path: + pctl.stop(True) + target_base = path -class ListenBrainz: + # Determine name for artist folder + artist = track.artist + if track.album_artist: + artist = track.album_artist - def __init__(self): + # Make filename friendly + artist = filename_safe(artist) + if not artist: + artist = "unknown artist" - self.enable = prefs.enable_lb - # self.url = "https://api.listenbrainz.org/1/submit-listens" + # Sanity checks + if track.is_network: + show_message(_("This track is a networked track."), mode="error") + return - def url(self): - url = prefs.listenbrainz_url - if not url: - url = "https://api.listenbrainz.org/" - if not url.endswith("/"): - url += "/" - return url + "1/submit-listens" + if not os.path.isdir(move_folder): + show_message(_("The source folder does not exist."), mode="error") + return - def listen_full(self, track_object: TrackClass, time) -> bool: + if not os.path.isdir(target_base): + show_message(_("The destination folder does not exist."), mode="error") + return - if self.enable is False: - return True - if prefs.scrobble_hold is True: - return True - if prefs.lb_token is None: - show_message(_("ListenBrainz is enabled but there is no token."), _("How did this even happen."), mode="error") + if os.path.normpath(target_base) == os.path.normpath(move_folder): + show_message(_("The destination and source folders are the same."), mode="error") + return - title = track_object.title - album = track_object.album - artist = get_artist_strip_feat(track_object) + if len(target_base) < 4: + show_message(_("Safety interupt! The source path seems oddly short."), target_base, mode="error") + return - if title == "" or artist == "": - return True + protect = ("", "Documents", "Music", "Desktop", "Downloads") + for fo in protect: + if move_folder.strip("\\/") == os.path.join(os.path.expanduser("~"), fo).strip("\\/"): + show_message( + _("Better not do anything to that folder!"), os.path.join(os.path.expanduser("~"), fo), + mode="warning") + return - data = {"listen_type": "single", "payload": []} - metadata = {"track_name": title, "artist_name": artist} + if directory_size(move_folder) > 3000000000: + show_message(_("Folder size safety limit reached! (3GB)"), move_folder, mode="warning") + return - additional = {} + # Use target folder if it already is an artist folder + if os.path.basename(target_base).lower() == artist.lower(): + artist_folder = target_base - # MusicBrainz Artist IDs - if "musicbrainz_artistids" in track_object.misc: - additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] + # Make artist folder if it does not exist + else: + artist_folder = os.path.join(target_base, artist) + if not os.path.exists(artist_folder): + os.makedirs(artist_folder) - # MusicBrainz Release ID - if "musicbrainz_albumid" in track_object.misc: - additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] + # Remove all tracks with the old paths + for pl in pctl.multi_playlist: + for i in reversed(range(len(pl.playlist_ids))): + if pctl.get_track(pl.playlist_ids[i]).parent_folder_path == track.parent_folder_path: + del pl.playlist_ids[i] - # MusicBrainz Recording ID - if "musicbrainz_recordingid" in track_object.misc: - additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] + # Find insert location + pl = pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids - # MusicBrainz Track ID - if "musicbrainz_trackid" in track_object.misc: - additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] + matches = [] + insert = 0 - if additional: - metadata["additional_info"] = additional + for i, item in enumerate(pl): + if pctl.get_track(item).fullpath.startswith(target_base): + insert = i - # logging.info(additional) - data["payload"].append({"track_metadata": metadata}) - data["payload"][0]["listened_at"] = time + for i, item in enumerate(pl): + if pctl.get_track(item).fullpath.startswith(artist_folder): + insert = i - r = requests.post(self.url(), headers={"Authorization": "Token " + prefs.lb_token}, data=json.dumps(data), timeout=10) - if r.status_code != 200: - show_message(_("There was an error submitting data to ListenBrainz"), r.text, mode="warning") - return False - return True + logging.info("The folder to be moved is: " + move_folder) + load_order = LoadClass() + load_order.target = os.path.join(artist_folder, track.parent_folder_name) + load_order.playlist = pl_id + load_order.playlist_position = insert - def listen_playing(self, track_object: TrackClass) -> None: - if self.enable is False: - return - if prefs.scrobble_hold is True: - return - if prefs.lb_token is None: - show_message(_("ListenBrainz is enabled but there is no token."), _("How did this even happen."), mode="error") - title = track_object.title - album = track_object.album - artist = get_artist_strip_feat(track_object) + logging.info(artist_folder) + logging.info(os.path.join(artist_folder, track.parent_folder_name)) + move_jobs.append( + (move_folder, os.path.join(artist_folder, track.parent_folder_name), True, + track.parent_folder_name, load_order)) + tauon.thread_manager.ready("worker") - if title == "" or artist == "": - return +def move_playing_folder_to_tag(tag_item): + move_playing_folder_to_stem(tag_item.path) - data = {"listen_type": "playing_now", "payload": []} - metadata = {"track_name": title, "artist_name": artist} +def re_import4(id): + p = None + for i, idd in enumerate(default_playlist): + if idd == id: + p = i + break - additional = {} + load_order = LoadClass() - # MusicBrainz Artist IDs - if "musicbrainz_artistids" in track_object.misc: - additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] + if p is not None: + load_order.playlist_position = p - # MusicBrainz Release ID - if "musicbrainz_albumid" in track_object.misc: - additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] + load_order.replace_stem = True + load_order.target = pctl.get_track(id).parent_folder_path + load_order.notify = True + load_order.playlist = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + load_orders.append(copy.deepcopy(load_order)) + show_message(_("Rescanning folder..."), pctl.get_track(id).parent_folder_path, mode="info") - # MusicBrainz Recording ID - if "musicbrainz_recordingid" in track_object.misc: - additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] +def re_import3(stem): + p = None + for i, id in enumerate(default_playlist): + if pctl.get_track(id).fullpath.startswith(stem + "/"): + p = i + break - # MusicBrainz Track ID - if "musicbrainz_trackid" in track_object.misc: - additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] + load_order = LoadClass() - if track_object.track_number: - try: - additional["tracknumber"] = str(int(track_object.track_number)) - except Exception: - logging.exception("Error trying to get track_number") + if p is not None: + load_order.playlist_position = p - if track_object.length: - additional["duration"] = str(int(track_object.length)) + load_order.replace_stem = True + load_order.target = stem + load_order.notify = True + load_order.playlist = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + load_orders.append(copy.deepcopy(load_order)) + show_message(_("Rescanning folder..."), stem, mode="info") - additional["media_player"] = t_title - additional["submission_client"] = t_title - additional["media_player_version"] = str(n_version) +def collapse_tree_deco(): + pl_id = tree_view_box.get_pl_id() - metadata["additional_info"] = additional - data["payload"].append({"track_metadata": metadata}) - # data["payload"][0]["listened_at"] = int(time.time()) + if tree_view_box.opens.get(pl_id): + return [colours.menu_text, colours.menu_background, None] + return [colours.menu_text_disabled, colours.menu_background, None] - r = requests.post(self.url(), headers={"Authorization": "Token " + prefs.lb_token}, data=json.dumps(data), timeout=10) - if r.status_code != 200: - show_message(_("There was an error submitting data to ListenBrainz"), r.text, mode="warning") - logging.error("There was an error submitting data to ListenBrainz") - logging.error(r.status_code) - logging.error(r.json()) +def collapse_tree(): + tree_view_box.collapse_all() - def paste_key(self): +def lock_folder_tree(): + if tree_view_box.lock_pl: + tree_view_box.lock_pl = None + else: + tree_view_box.lock_pl = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - text = copy_from_clipboard() - if text == "": - show_message(_("There is no text in the clipboard"), mode="error") - return +def lock_folder_tree_deco(): + if tree_view_box.lock_pl: + return [colours.menu_text, colours.menu_background, _("Unlock Panel")] + return [colours.menu_text, colours.menu_background, _("Lock Panel")] - if prefs.listenbrainz_url: - prefs.lb_token = text - return +def finish_current(): + playing_object = pctl.playing_object() + if playing_object is None: + show_message("") - if len(text) == 36 and text[8] == "-": - prefs.lb_token = text - else: - show_message(_("That is not a valid token."), mode="error") + if not pctl.force_queue: + pctl.force_queue.insert( + 0, queue_item_gen(playing_object.index, + pctl.playlist_playing_position, + pl_to_id(pctl.active_playlist_playing), 1, 1)) - def clear_key(self): +def add_album_to_queue(ref, position=None, playlist_id=None): + if position is None: + position = r_menu_position + if playlist_id is None: + playlist_id = pl_to_id(pctl.active_playlist_viewing) - prefs.lb_token = "" - save_prefs() - self.enable = False + partway = 0 + playing_object = pctl.playing_object() + if not pctl.force_queue and playing_object is not None: + if pctl.get_track(ref).parent_folder_path == playing_object.parent_folder_path: + partway = 1 + queue_object = queue_item_gen(ref, position, playlist_id, 1, partway) + pctl.force_queue.append(queue_object) + queue_timer_set(queue_object=queue_object) + if prefs.stop_end_queue: + pctl.auto_stop = False -lb = ListenBrainz() +def add_album_to_queue_fc(ref): + playing_object = pctl.playing_object() + if playing_object is None: + show_message("") + queue_item = None -def get_love(track_object: TrackClass) -> bool: - star = star_store.full_get(track_object.index) - if star is None: - return False + if not pctl.force_queue: + queue_item = queue_item_gen( + playing_object.index, pctl.playlist_playing_position, pl_to_id(pctl.active_playlist_playing), 1, 1) + pctl.force_queue.insert(0, queue_item) + add_album_to_queue(ref) + return - if "L" in star[1]: - return True - return False + if pctl.force_queue[0].album_stage == 1: + queue_item = queue_item_gen(ref, pctl.playlist_playing_position, pl_to_id(pctl.active_playlist_playing), 1, 0) + pctl.force_queue.insert(1, queue_item) + else: + p = pctl.get_track(ref).parent_folder_path + p = "" + if pctl.playing_ready(): + p = pctl.playing_object().parent_folder_path + # fixme for network tracks -def get_love_index(index: int) -> bool: - star = star_store.full_get(index) - if star is None: - return False + for i, item in enumerate(pctl.force_queue): - if "L" in star[1]: - return True - return False + if p != pctl.get_track(item.track_id).parent_folder_path: + queue_item = queue_item_gen( + ref, + pctl.playlist_playing_position, + pl_to_id(pctl.active_playlist_playing), 1, 0) + pctl.force_queue.insert(i, queue_item) + break + else: + queue_item = queue_item_gen( + ref, pctl.playlist_playing_position, pl_to_id(pctl.active_playlist_playing), 1, 0) + pctl.force_queue.insert(len(pctl.force_queue), queue_item) + if queue_item: + queue_timer_set(queue_object=queue_item) + if prefs.stop_end_queue: + pctl.auto_stop = False -def get_love_timestamp_index(index: int): - star = star_store.full_get(index) - if star is None: - return 0 - return star[3] +def cancel_import(): + if transcode_list: + del transcode_list[1:] + gui.tc_cancel = True + if loading_in_progress: + gui.im_cancel = True + if gui.sync_progress: + gui.stop_sync = True + gui.sync_progress = _("Aborting Sync") -def love(set=True, track_id=None, no_delay=False, notify=False, sync=True): - if len(pctl.track_queue) < 1: - return False +def toggle_lyrics_show(a): + return not gui.combo_mode - if track_id is not None and track_id < 0: - return False +def toggle_side_art_deco(): + colour = colours.menu_text + if prefs.show_side_lyrics_art_panel: + line = _("Hide Metadata Panel") + else: + line = _("Show Metadata Panel") - if track_id is None: - track_id = pctl.track_queue[pctl.queue_step] + if gui.combo_mode: + colour = colours.menu_text_disabled - loved = False - star = star_store.full_get(track_id) + return [colour, colours.menu_background, line] - if star is not None: - if "L" in star[1]: - loved = True +def toggle_lyrics_panel_position_deco(): + colour = colours.menu_text + if prefs.lyric_metadata_panel_top: + line = _("Panel Below Lyrics") + else: + line = _("Panel Above Lyrics") - if set is False: - return loved + if gui.combo_mode or not prefs.show_side_lyrics_art_panel: + colour = colours.menu_text_disabled - # global lfm_username - # if len(lfm_username) > 0 and not lastfm.connected and not prefs.auto_lfm: - # show_message("You have a last.fm account ready but it is not enabled.", 'info', - # 'Either connect, enable auto connect, or remove the account.') - # return + return [colour, colours.menu_background, line] - if star is None: - star = star_store.new_object() +def toggle_lyrics_panel_position(): + prefs.lyric_metadata_panel_top ^= True - loved ^= True +def lyrics_in_side_show(track_object: TrackClass): + if gui.combo_mode or not prefs.show_lyrics_side: + return False + return True - if notify: - gui.toast_love_object = pctl.get_track(track_id) - gui.toast_love_added = loved - toast_love_timer.set() - gui.delay_frame(1.81) +def toggle_side_art(): + prefs.show_side_lyrics_art_panel ^= True - delay = 0.3 - if no_delay or not sync or not lastfm.details_ready(): - delay = 0 +def toggle_lyrics_deco(track_object: TrackClass): + colour = colours.menu_text - star[3] = time.time() + if gui.combo_mode: + if prefs.show_lyrics_showcase: + line = _("Hide Lyrics") + else: + line = _("Show Lyrics") + if not track_object or (track_object.lyrics == "" and not timed_lyrics_ren.generate(track_object)): + colour = colours.menu_text_disabled + return [colour, colours.menu_background, line] - if loved: - time.sleep(delay) - gui.update += 1 - gui.pl_update += 1 - star[1] = star[1] + "L" # = [star[0], star[1] + "L", star[2]] - star_store.insert(track_id, star) - if sync: - if prefs.last_fm_token: - try: - lastfm.love(pctl.master_library[track_id].artist, pctl.master_library[track_id].title) - except Exception: - logging.exception("Failed updating last.fm love status") - show_message(_("Failed updating last.fm love status"), mode="warning") - star[1] = star[1].replace("L", "") # = [star[0], star[1].strip("L"), star[2]] - star_store.insert(track_id, star) - show_message( - _("Error updating love to last.fm!"), - _("Maybe check your internet connection and try again?"), mode="error") + if prefs.side_panel_layout == 1: # and prefs.show_side_art: - if pctl.master_library[track_id].file_ext == "JELY": - jellyfin.favorite(pctl.master_library[track_id]) + if prefs.show_lyrics_side: + line = _("Hide Lyrics") + else: + line = _("Show Lyrics") + if (track_object.lyrics == "" and not timed_lyrics_ren.generate(track_object)): + colour = colours.menu_text_disabled + return [colour, colours.menu_background, line] + if prefs.show_lyrics_side: + line = _("Hide Lyrics") else: - time.sleep(delay) - gui.update += 1 - gui.pl_update += 1 - star[1] = star[1].replace("L", "") - star_store.insert(track_id, star) - if sync: - if prefs.last_fm_token: - try: - lastfm.unlove(pctl.master_library[track_id].artist, pctl.master_library[track_id].title) - except Exception: - logging.exception("Failed updating last.fm love status") - show_message(_("Failed updating last.fm love status"), mode="warning") - star[1] = star[1] + "L" - star_store.insert(track_id, star) - if pctl.master_library[track_id].file_ext == "JELY": - jellyfin.favorite(pctl.master_library[track_id], un=True) + line = _("Show Lyrics") + if (track_object.lyrics == "" and not timed_lyrics_ren.generate(track_object)): + colour = colours.menu_text_disabled + return [colour, colours.menu_background, line] - gui.pl_update = 2 - gui.update += 1 - if sync and pctl.mpris is not None: - pctl.mpris.update(force=True) +def toggle_lyrics(track_object: TrackClass): + if not track_object: + return + if gui.combo_mode: + prefs.show_lyrics_showcase ^= True + if prefs.show_lyrics_showcase and track_object.lyrics == "" and timed_lyrics_ren.generate(track_object): + prefs.prefer_synced_lyrics = True + # if prefs.show_lyrics_showcase and track_object.lyrics == "": + # show_message("No lyrics for this track") + else: + # Handling for alt panel layout + # if prefs.side_panel_layout == 1 and prefs.show_side_art: + # #prefs.show_side_art = False + # prefs.show_lyrics_side = True + # return -def maloja_get_scrobble_counts(): - if lastfm.scanning_scrobbles is True or not prefs.maloja_url: - return + prefs.show_lyrics_side ^= True + if prefs.show_lyrics_side and track_object.lyrics == "" and timed_lyrics_ren.generate(track_object): + prefs.prefer_synced_lyrics = True + # if prefs.show_lyrics_side and track_object.lyrics == "": + # show_message("No lyrics for this track") - url = prefs.maloja_url - if not url.endswith("/"): - url += "/" - url += "apis/mlj_1/scrobbles" - lastfm.scanning_scrobbles = True - try: - r = requests.get(url, timeout=10) +def get_lyric_fire(track_object: TrackClass, silent: bool = False) -> str | None: + lyrics_ren.lyrics_position = 0 - if r.status_code != 200: - show_message(_("There was an error with the Maloja server"), r.text, mode="warning") - lastfm.scanning_scrobbles = False - return - except Exception: - logging.exception("There was an error reaching the Maloja server") - show_message(_("There was an error reaching the Maloja server"), mode="warning") - lastfm.scanning_scrobbles = False - return + if not prefs.lyrics_enables: + if not silent: + show_message( + _("There are no lyric sources enabled."), + _("See 'lyrics settings' under 'functions' tab in settings."), mode="info") + return None - try: - data = json.loads(r.text) - l = data["list"] + t = lyrics_fetch_timer.get() + logging.info("Lyric rate limit timer is: " + str(t) + " / -60") + if t < -40: + logging.info("Lets try again later") + if not silent: + show_message(_("Let's be polite and try later.")) - counts = {} + if t < -65: + show_message(_("Stop requesting lyrics AAAAAA."), mode="error") - for item in l: - artists = item.get("artists") - title = item.get("title") - if title and artists: - key = (title, tuple(artists)) - c = counts.get(key, 0) - counts[key] = c + 1 + # If the user keeps pressing, lets mess with them haha + lyrics_fetch_timer.force_set(t - 5) - touched = [] + return "later" - for key, value in counts.items(): - title, artists = key - artists = [x.lower() for x in artists] - title = title.lower() - for track in pctl.master_library.values(): - if track.artist.lower() in artists and track.title.lower() == title: - if track.index in touched: - track.lfm_scrobbles += value - else: - track.lfm_scrobbles = value - touched.append(track.index) - show_message(_("Scanning scrobbles complete"), mode="done") + if t > 0: + lyrics_fetch_timer.set() + t = 0 - except Exception: - logging.exception("There was an error parsing the data") - show_message(_("There was an error parsing the data"), mode="warning") + lyrics_fetch_timer.force_set(t - 10) - gui.pl_update += 1 - lastfm.scanning_scrobbles = False - tauon.bg_save() + if not silent: + show_message(_("Searching...")) + s_artist = track_object.artist + s_title = track_object.title -def maloja_scrobble(track: TrackClass, timestamp: int = int(time.time())) -> bool | None: - url = prefs.maloja_url + if s_artist in prefs.lyrics_subs: + s_artist = prefs.lyrics_subs[s_artist] + if s_title in prefs.lyrics_subs: + s_title = prefs.lyrics_subs[s_title] - if not track.artist or not track.title: - return None + logging.info(f"Searching for lyrics: {s_artist} - {s_title}") - if not url.endswith("/newscrobble"): - if not url.endswith("/"): - url += "/" - url += "apis/mlj_1/newscrobble" + found = False + for name in prefs.lyrics_enables: - d = {} - d["artists"] = [track.artist] # let Maloja parse/fix artists - d["title"] = track.title + if name in lyric_sources.keys(): + func = lyric_sources[name] - if track.album: - d["album"] = track.album - if track.album_artist: - d["albumartists"] = [track.album_artist] # let Maloja parse/fix artists - - d["length"] = int(track.length) - d["time"] = timestamp - d["key"] = prefs.maloja_key + try: + lyrics = func(s_artist, s_title) + if lyrics: + logging.info(f"Found lyrics from {name}") + track_object.lyrics = lyrics + found = True + break + except Exception: + logging.exception("Failed to find lyrics") - try: - r = requests.post(url, json=d, timeout=10) - if r.status_code != 200: - show_message(_("There was an error submitting data to Maloja server"), r.text, mode="warning") - return False - except Exception: - logging.exception("There was an error submitting data to Maloja server") - show_message(_("There was an error submitting data to Maloja server"), mode="warning") - return False - return True + if not found: + logging.error(f"Could not find lyrics from source {name}") + if not found: + if not silent: + show_message(_("No lyrics for this track were found")) + else: + gui.message_box = False + if not gui.showcase_mode: + prefs.show_lyrics_side = True + gui.update += 1 + lyrics_ren.lyrics_position = 0 + pctl.notify_change() -class LastScrob: +def get_lyric_wiki(track_object: TrackClass): + if track_object.artist == "" or track_object.title == "": + show_message(_("Insufficient metadata to get lyrics"), mode="warning") + return - def __init__(self): + shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object])) + shoot_dl.daemon = True + shoot_dl.start() - self.a_index = -1 - self.a_sc = False - self.a_pt = False - self.queue = [] - self.running = False + logging.info("..Done") - def start_queue(self): +def get_lyric_wiki_silent(track_object: TrackClass): + logging.info("Searching for lyrics...") - self.running = True - mini_t = threading.Thread(target=self.process_queue) - mini_t.daemon = True - mini_t.start() + if track_object.artist == "" or track_object.title == "": + return - def process_queue(self): + shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object, True])) + shoot_dl.daemon = True + shoot_dl.start() - time.sleep(0.4) + logging.info("..Done") - while self.queue: +def test_auto_lyrics(track_object: TrackClass): + if not track_object: + return - try: - tr = self.queue.pop() + if prefs.auto_lyrics and not track_object.lyrics and track_object.index not in prefs.auto_lyrics_checked: + if lyrics_check_timer.get() > 5 and pctl.playing_time > 1: + result = get_lyric_wiki_silent(track_object) + if result == "later": + pass + else: + lyrics_check_timer.set() + prefs.auto_lyrics_checked.append(track_object.index) - gui.pl_update = 1 - logging.info("Submit Scrobble " + tr[0].artist + " - " + tr[0].title) - - success = True - - if tr[2] == "lfm" and prefs.auto_lfm and (lastfm.connected or lastfm.details_ready()): - success = lastfm.scrobble(tr[0], tr[1]) - elif tr[2] == "lb" and lb.enable: - success = lb.listen_full(tr[0], tr[1]) - elif tr[2] == "maloja": - success = maloja_scrobble(tr[0], tr[1]) - elif tr[2] == "air": - success = subsonic.listen(tr[0], submit=True) - elif tr[2] == "koel": - success = koel.listen(tr[0], submit=True) - - if not success: - logging.info("Re-queue scrobble") - self.queue.append(tr) - time.sleep(10) - break +def get_bio(track_object: TrackClass): + if track_object.artist != "": + lastfm.get_bio(track_object.artist) - except Exception: - logging.exception("SCROBBLE QUEUE ERROR") +def search_lyrics_deco(track_object: TrackClass): + if not track_object.lyrics: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - if not self.queue: - scrobble_warning_timer.force_set(1000) + return [line_colour, colours.menu_background, None] - self.running = False +def toggle_synced_lyrics(tr): + prefs.prefer_synced_lyrics ^= True - def update(self, add_time): +def toggle_synced_lyrics_deco(track): + if prefs.prefer_synced_lyrics: + text = _("Show static lyrics") + else: + text = _("Show synced lyrics") + if timed_lyrics_ren.generate(track) and track.lyrics: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled + if not track.lyrics: + text = _("Show static lyrics") + if not timed_lyrics_ren.generate(track): + text = _("Show synced lyrics") - if pctl.queue_step > len(pctl.track_queue) - 1: - logging.info("Queue step error 1") - return + return [line_colour, colours.menu_background, text] - if self.a_index != pctl.track_queue[pctl.queue_step]: - pctl.a_time = 0 - pctl.b_time = 0 - self.a_index = pctl.track_queue[pctl.queue_step] - self.a_pt = False - self.a_sc = False - if pctl.playing_time == 0 and self.a_sc is True: - logging.info("Reset scrobble timer") - pctl.a_time = 0 - pctl.b_time = 0 - self.a_pt = False - self.a_sc = False - - if pctl.a_time > 6 and self.a_pt is False and pctl.master_library[self.a_index].length > 30: - self.a_pt = True - self.listen_track(pctl.master_library[self.a_index]) - # if prefs.auto_lfm and (lastfm.connected or lastfm.details_ready()) and not prefs.scrobble_hold: - # mini_t = threading.Thread(target=lastfm.update, args=([pctl.master_library[self.a_index]])) - # mini_t.daemon = True - # mini_t.start() - # - # if lb.enable and not prefs.scrobble_hold: - # mini_t = threading.Thread(target=lb.listen_playing, args=([pctl.master_library[self.a_index]])) - # mini_t.daemon = True - # mini_t.start() - - if pctl.a_time > 6 and self.a_pt: - pctl.b_time += add_time - if pctl.b_time > 20: - pctl.b_time = 0 - self.listen_track(pctl.master_library[self.a_index]) - - send_full = False - if pctl.master_library[self.a_index].length > 30 and pctl.a_time > pctl.master_library[self.a_index].length \ - * 0.50 and self.a_sc is False: - self.a_sc = True - send_full = True - - if self.a_sc is False and pctl.master_library[self.a_index].length > 30 and pctl.a_time > 240: - self.a_sc = True - send_full = True - - if send_full: - self.scrob_full_track(pctl.master_library[self.a_index]) - - def listen_track(self, track_object: TrackClass): - # logging.info("LISTEN") +def paste_lyrics_deco(): + if SDL_HasClipboardText(): + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - if track_object.is_network: - if track_object.file_ext == "SUB": - subsonic.listen(track_object, submit=False) - - if not prefs.scrobble_hold: - if prefs.auto_lfm and (lastfm.connected or lastfm.details_ready()): - mini_t = threading.Thread(target=lastfm.update, args=([track_object])) - mini_t.daemon = True - mini_t.start() - - if lb.enable: - mini_t = threading.Thread(target=lb.listen_playing, args=([track_object])) - mini_t.daemon = True - mini_t.start() - - def scrob_full_track(self, track_object: TrackClass): - # logging.info("SCROBBLE") - track_object.lfm_scrobbles += 1 - gui.pl_update += 1 + return [line_colour, colours.menu_background, None] - if track_object.is_network: - if track_object.file_ext == "SUB": - self.queue.append((track_object, int(time.time()), "air")) - if track_object.file_ext == "KOEL": - self.queue.append((track_object, int(time.time()), "koel")) +def paste_lyrics(track_object: TrackClass): + if SDL_HasClipboardText(): + clip = SDL_GetClipboardText() + #logging.info(clip) + track_object.lyrics = clip.decode("utf-8") + else: + logging.warning("NO TEXT TO PASTE") - if not prefs.scrobble_hold: - if prefs.auto_lfm and (lastfm.connected or lastfm.details_ready()): - self.queue.append((track_object, int(time.time()), "lfm")) - if lb.enable: - self.queue.append((track_object, int(time.time()), "lb")) - if prefs.maloja_url and prefs.maloja_enable: - self.queue.append((track_object, int(time.time()), "maloja")) +#def chord_lyrics_paste_show_test(_) -> bool: +# return gui.combo_mode and prefs.guitar_chords +# showcase_menu.add(MenuItem(_("Search GuitarParty"), search_guitarparty, pass_ref=True, show_test=chord_lyrics_paste_show_test)) +#guitar_chords = GuitarChords(user_directory=user_directory, ddt=ddt, inp=inp, gui=gui, pctl=pctl) +#showcase_menu.add(MenuItem(_("Paste Chord Lyrics"), guitar_chords.paste_chord_lyrics, pass_ref=True, show_test=chord_lyrics_paste_show_test)) +#showcase_menu.add(MenuItem(_("Clear Chord Lyrics"), guitar_chords.clear_chord_lyrics, pass_ref=True, show_test=chord_lyrics_paste_show_test)) -lfm_scrobbler = LastScrob() +def copy_lyrics_deco(track_object: TrackClass): + if track_object.lyrics: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled -QuickThumbnail.renderer = renderer + return [line_colour, colours.menu_background, None] +def copy_lyrics(track_object: TrackClass): + copy_to_clipboard(track_object.lyrics) -class Strings: +def clear_lyrics(track_object: TrackClass): + track_object.lyrics = "" - def __init__(self): - self.spotify_likes = _("Spotify Likes") - self.spotify_albums = _("Spotify Albums") - self.spotify_un_liked = _("Track removed from liked tracks") - self.spotify_already_un_liked = _("Track was already un-liked") - self.spotify_already_liked = _("Track is already liked") - self.spotify_like_added = _("Track added to liked tracks") - self.spotify_account_connected = _("Spotify account connected") - self.spotify_not_playing = _("This Spotify account isn't currently playing anything") - self.spotify_error_starting = _("Error starting Spotify") - self.spotify_request_auth = _("Please authorise Spotify in settings!") - self.spotify_need_enable = _("Please authorise and click the enable toggle first!") - self.spotify_import_complete = _("Spotify import complete") +def clear_lyrics_deco(track_object: TrackClass): + if track_object.lyrics: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - self.day = _("day") - self.days = _("days") + return [line_colour, colours.menu_background, None] - self.scan_chrome = _("Scanning for Chromecasts...") - self.cast_to = _("Cast to: %s") - self.no_chromecasts = _("No Chromecast devices found") - self.stop_cast = _("End Cast") +def split_lyrics(track_object: TrackClass): + if track_object.lyrics != "": + track_object.lyrics = track_object.lyrics.replace(". ", ". \n") - self.web_server_stopped = _("Web server stopped.") +def show_sub_search(track_object: TrackClass): + sub_lyrics_box.activate(track_object) - self.menu_open_tauon = _("Open Tauon Music Box") - self.menu_play_pause = _("Play/Pause") - self.menu_next = _("Next Track") - self.menu_previous = _("Previous Track") - self.menu_quit = _("Quit") +def save_embed_img_disable_test(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + return track_object.is_network +def save_embed_img(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + filepath = track_object.fullpath + folder = track_object.parent_folder_path + ext = track_object.file_ext + if save_embed_img_disable_test(track_object): + show_message(_("Saving network images not implemented")) + return -strings = Strings() + try: + pic = album_art_gen.get_embed(track_object) + if not pic: + show_message(_("Image save error."), _("No embedded album art found file."), mode="warning") + return -def id_to_pl(id: int): - for i, item in enumerate(pctl.multi_playlist): - if item.uuid_int == id: - return i - return None + source_image = io.BytesIO(pic) + im = Image.open(source_image) + source_image.close() -def pl_to_id(pl: int) -> int: - return pctl.multi_playlist[pl].uuid_int + ext = "." + im.format.lower() + if im.format == "JPEG": + ext = ".jpg" + target = os.path.join(folder, "embed-" + str(im.height) + "px-" + str(track_object.index) + ext) -class Chunker: - - def __init__(self): - self.master_count = 0 - self.chunks = {} - self.header = None - self.headers = [] - self.h2 = None - - self.clients = {} - -class MenuIcon: - - def __init__(self, asset): - self.asset = asset - self.colour = [170, 170, 170, 255] - self.base_asset = None - self.base_asset_mod = None - self.colour_callback = None - self.mode_callback = None - self.xoff = 0 - self.yoff = 0 - -class MenuItem: - __slots__ = [ - "title", # 0 - "is_sub_menu", # 1 - "func", # 2 - "render_func", # 3 - "no_exit", # 4 - "pass_ref", # 5 - "hint", # 6 - "icon", # 7 - "show_test", # 8 - "pass_ref_deco", # 9 - "disable_test", # 10 - "set_ref", # 11 - "args", # 12 - "sub_menu_number", # 13 - "sub_menu_width", # 14 - ] - def __init__( - self, title, func, render_func=None, no_exit=False, pass_ref=False, hint=None, icon=None, show_test=None, - pass_ref_deco=False, disable_test=None, set_ref=None, is_sub_menu=False, args=None, sub_menu_number=None, sub_menu_width=0, - ): - self.title = title - self.is_sub_menu = is_sub_menu - self.func = func - self.render_func = render_func - self.no_exit = no_exit - self.pass_ref = pass_ref - self.hint = hint - self.icon = icon - self.show_test = show_test - self.pass_ref_deco = pass_ref_deco - self.disable_test = disable_test - self.set_ref = set_ref - self.args = args - self.sub_menu_number = sub_menu_number - self.sub_menu_width = sub_menu_width + if len(pic) > 30: + with open(target, "wb") as w: + w.write(pic) + open_folder(track_object.index) -def encode_track_name(track_object: TrackClass) -> str: - if track_object.is_cue or not track_object.filename: - out_line = str(track_object.track_number) + ". " - out_line += track_object.artist + " - " + track_object.title - return filename_safe(out_line) - return os.path.splitext(track_object.filename)[0] + except Exception: + logging.exception("Unknown error trying to save an image") + show_message(_("Image save error."), _("A mysterious error occurred"), mode="error") +def open_image_deco(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + info = album_art_gen.get_info(track_object) -def encode_folder_name(track_object: TrackClass) -> str: - folder_name = track_object.artist + " - " + track_object.album + if info is None: + return [colours.menu_text_disabled, colours.menu_background, None] - if folder_name == " - ": - folder_name = track_object.parent_folder_name + line_colour = colours.menu_text + return [line_colour, colours.menu_background, None] - folder_name = filename_safe(folder_name).strip() +def open_image_disable_test(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + return track_object.is_network - if not folder_name: - folder_name = str(track_object.index) +def open_image(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + album_art_gen.open_external(track_object) - if "cd" not in folder_name.lower() or "disc" not in folder_name.lower(): - if track_object.disc_total not in ("", "0", 0, "1", 1) or ( - str(track_object.disc_number).isdigit() and int(track_object.disc_number) > 1): - folder_name += " CD" + str(track_object.disc_number) +def extract_image_deco(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + info = album_art_gen.get_info(track_object) - return folder_name + if info is None: + return [colours.menu_text_disabled, colours.menu_background, None] -class ThreadManager: - - def __init__(self): - - self.worker1: Thread | None = None # Artist list, download monitor, folder move, importing, db cleaning, transcoding - self.worker2: Thread | None = None # Art bg, search - self.worker3: Thread | None = None # Gallery rendering - self.playback: Thread | None = None - self.player_lock: Lock = threading.Lock() - - self.d: dict = {} - - def ready(self, type): - if self.d[type][2] is None or not self.d[type][2].is_alive(): - shoot = threading.Thread(target=self.d[type][0], args=self.d[type][1]) - shoot.daemon = True - shoot.start() - self.d[type][2] = shoot - - def ready_playback(self) -> None: - if self.playback is None or not self.playback.is_alive(): - if prefs.backend == 4: - self.playback = threading.Thread(target=player4, args=[tauon]) - # elif prefs.backend == 2: - # from tauon.t_modules.t_gstreamer import player3 - # self.playback = threading.Thread(target=player3, args=[tauon]) - self.playback.daemon = True - self.playback.start() - - def check_playback_running(self) -> bool: - if self.playback is None: - return False - return self.playback.is_alive() - -class Menu: - """Right click context menu generator""" - - switch = 0 - count = switch + 1 - instances: list[Menu] = [] - active = False - - def rescale(self): - self.vertical_size = round(self.base_v_size * gui.scale) - self.h = self.vertical_size - self.w = self.request_width * gui.scale - if gui.scale == 2: - self.w += 15 - - def __init__(self, width: int, show_icons: bool = False) -> None: - - self.base_v_size = 22 - self.active = False - self.request_width: int = width - self.close_next_frame = False - self.clicked = False - self.pos = [0, 0] - self.rescale() - - self.reference = 0 - self.items: list[MenuItem] = [] - self.subs: list[list[MenuItem]] = [] - self.selected = -1 - self.up = False - self.down = False - self.font = 412 - self.show_icons: bool = show_icons - self.sub_arrow = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "sub.png", True)) - - self.id = Menu.count - self.break_height = round(4 * gui.scale) - - Menu.count += 1 - - self.sub_number = 0 - self.sub_active = -1 - self.sub_y_postion = 0 - Menu.instances.append(self) - - @staticmethod - def deco(_=_): - return [colours.menu_text, colours.menu_background, None] - - def click(self) -> None: - self.clicked = True - # cheap hack to prevent scroll bar from being activated when closing menu - global click_location - click_location = [0, 0] - - def add(self, menu_item: MenuItem) -> None: - if menu_item.render_func is None: - menu_item.render_func = self.deco - self.items.append(menu_item) - - def br(self) -> None: - self.items.append(None) - - def add_sub(self, title: str, width: int, show_test=None) -> None: - self.items.append(MenuItem(title, self.deco, sub_menu_width=width, show_test=show_test, is_sub_menu=True, sub_menu_number=self.sub_number)) - self.sub_number += 1 - self.subs.append([]) - - def add_to_sub(self, sub_menu_index: int, menu_item: MenuItem) -> None: - if menu_item.render_func is None: - menu_item.render_func = self.deco - self.subs[sub_menu_index].append(menu_item) - - def test_item_active(self, item): - if item.show_test is not None: - if item.show_test(1) is False: - return False - return True - - def is_item_disabled(self, item): - if item.disable_test is not None: - if item.pass_ref_deco: - return item.disable_test(self.reference) - return item.disable_test() - - def render_icon(self, x, y, icon, selected, fx): - - if colours.lm: - selected = True - - if icon is not None: - - x += icon.xoff * gui.scale - y += icon.yoff * gui.scale - - colour = None - - if icon.base_asset is None: - # Colourise mode + if info[0] == 1: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - if icon.colour_callback is not None: # and icon.colour_callback() is not None: - colour = icon.colour_callback() + return [line_colour, colours.menu_background, None] - elif selected and fx[0] != colours.menu_text_disabled: - colour = icon.colour +def cycle_image_deco(track_object: TrackClass): + info = album_art_gen.get_info(track_object) - if colour is None and icon.base_asset_mod: - colour = colours.menu_icons - # if colours.lm: - # colour = [160, 160, 160, 255] - icon.base_asset_mod.render(x, y, colour) - return + if pctl.playing_state != 0 and (info is not None and info[1] > 1): + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - if colour is None: - # colour = [145, 145, 145, 70] - colour = colours.menu_icons # [255, 255, 255, 35] - # colour = [50, 50, 50, 255] + return [line_colour, colours.menu_background, None] - icon.asset.render(x, y, colour) +def cycle_image_gal_deco(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + info = album_art_gen.get_info(track_object) - else: - if not is_grey(colours.menu_background): - return # Since these are currently pre-rendered greyscale, they are - # Incompatible with coloured backgrounds. Fix TODO - if selected and fx[0] == colours.menu_text_disabled: - icon.base_asset.render(x, y) - return + if info is not None and info[1] > 1: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - # Pre-rendered mode - if icon.mode_callback is not None: - if icon.mode_callback(): - icon.asset.render(x, y) - else: - icon.base_asset.render(x, y) - elif selected: - icon.asset.render(x, y) - else: - icon.base_asset.render(x, y) + return [line_colour, colours.menu_background, None] - def render(self): - if self.active: +def cycle_offset(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + album_art_gen.cycle_offset(track_object) - if Menu.switch != self.id: - self.active = False +def cycle_offset_back(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + album_art_gen.cycle_offset_reverse(track_object) - for menu in Menu.instances: - if menu.active: - break - else: - Menu.active = False +def dl_art_deco(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + if not track_object.album or not track_object.artist: + return [colours.menu_text_disabled, colours.menu_background, None] + return [colours.menu_text, colours.menu_background, None] - return +def download_art1(tr): + if tr.is_network: + show_message(_("Cannot download art for network tracks.")) + return - # ytoff = 3 - y_run = round(self.pos[1]) - to_call = None + # Determine noise of folder ---------------- + siblings = [] + parent = tr.parent_folder_path - # if window_size[1] < 250 * gui.scale: - # self.h = round(14 * gui.scale) - # ytoff = -1 * gui.scale - # else: - self.h = self.vertical_size - ytoff = round(self.h * 0.71 - 13 * gui.scale) + for pl in pctl.multi_playlist: + for ti in pl.playlist_ids: + tr = pctl.get_track(ti) + if tr.parent_folder_path == parent: + siblings.append(tr) - x_run = self.pos[0] + album_tags = [] + date_tags = [] - for i in range(len(self.items)): - #logging.info(self.items[i]) + for tr in siblings: + album_tags.append(tr.album) + date_tags.append(tr.date) - # Draw menu break - if self.items[i] is None: + album_tags = set(album_tags) + date_tags = set(date_tags) - if is_light(colours.menu_background): - break_colour = rgb_add_hls(colours.menu_background, 0, -0.1, -0.1) - else: - break_colour = rgb_add_hls(colours.menu_background, 0, 0.06, 0) + if len(album_tags) > 2 or len(date_tags) > 2: + show_message(_("It doesn't look like this folder belongs to a single album, sorry")) + return - rect = (x_run, y_run, self.w, self.break_height - 1) - if coll(rect): - self.clicked = False + # ------------------------------------------- - ddt.rect_a((x_run, y_run), (self.w, self.break_height), colours.menu_background) + if not os.path.isdir(tr.parent_folder_path): + show_message(_("Directory missing.")) + return - ddt.rect_a((x_run, y_run + 2 * gui.scale), (self.w, 2 * gui.scale), break_colour) + try: + show_message(_("Looking up MusicBrainz ID...")) - # Draw tab - ddt.rect_a((x_run, y_run), (4 * gui.scale, self.break_height), colours.menu_tab) - y_run += self.break_height + if "musicbrainz_releasegroupid" not in tr.misc or "musicbrainz_artistids" not in tr.misc or not tr.misc[ + "musicbrainz_artistids"]: - continue + logging.info("MusicBrainz ID lookup...") - if self.test_item_active(self.items[i]) is False: - continue - # if self.items[i][1] is False and self.items[i][8] is not None: - # if self.items[i][8](1) == False: - # continue - - # Get properties for menu item - if self.items[i].render_func is not None: - if self.items[i].pass_ref_deco: - fx = self.items[i].render_func(self.reference) - else: - fx = self.items[i].render_func() - else: - fx = self.deco() + artist = tr.album_artist + if not tr.album: + return + if not artist: + artist = tr.artist - if fx[2] is not None: - label = fx[2] - else: - label = self.items[i].title + s = musicbrainzngs.search_release_groups(tr.album, artist=artist, limit=1) - # Show text as disabled if disable_test() passes - if self.is_item_disabled(self.items[i]): - fx[0] = colours.menu_text_disabled + album_id = s["release-group-list"][0]["id"] + artist_id = s["release-group-list"][0]["artist-credit"][0]["artist"]["id"] - # Draw item background, black by default - ddt.rect_a((x_run, y_run), (self.w, self.h), fx[1]) - bg = fx[1] + logging.info("Found release group ID: " + album_id) + logging.info("Found artist ID: " + artist_id) - # Detect if mouse is over this item - selected = False - rect = (x_run, y_run, self.w, self.h - 1) - fields.add(rect) + else: - if coll_point(mouse_position, (x_run, y_run, self.w, self.h - 1)): - ddt.rect_a((x_run, y_run), (self.w, self.h), colours.menu_highlight_background) # [15, 15, 15, 255] - selected = True - bg = alpha_blend(colours.menu_highlight_background, bg) + album_id = tr.misc["musicbrainz_releasegroupid"] + artist_id = tr.misc["musicbrainz_artistids"][0] - # Call menu items callback if clicked - if self.clicked: + logging.info("Using tagged release group ID: " + album_id) + logging.info("Using tagged artist ID: " + artist_id) - if self.items[i].is_sub_menu is False: - to_call = i - if self.items[i].set_ref is not None: - self.reference = self.items[i].set_ref - global mouse_down - mouse_down = False + if prefs.enable_fanart_cover: + try: + show_message(_("Searching fanart.tv for cover art...")) - else: - self.clicked = False - self.sub_active = self.items[i].sub_menu_number - self.sub_y_postion = y_run + r = requests.get("https://webservice.fanart.tv/v3/music/albums/" \ + + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) - # Draw tab - ddt.rect_a((x_run, y_run), (4 * gui.scale, self.h), colours.menu_tab) + artlink = r.json()["albums"][album_id]["albumcover"][0]["url"] + id = r.json()["albums"][album_id]["albumcover"][0]["id"] - # Draw Icon - x = 12 * gui.scale - if self.items[i].is_sub_menu is False and self.show_icons: - icon = self.items[i].icon - self.render_icon(x_run + x, y_run + 5 * gui.scale, icon, selected, fx) + response = urllib.request.urlopen(artlink, context=ssl_context) + info = response.info() - if self.show_icons: - x += 25 * gui.scale + t = io.BytesIO() + t.seek(0) + t.write(response.read()) + t.seek(0, 2) + l = t.tell() + t.seek(0) - # Draw arrow icon for sub menu - if self.items[i].is_sub_menu is True: + if info.get_content_maintype() == "image" and l > 1000: - if is_light(bg) or colours.lm: - colour = rgb_add_hls(bg, 0, -0.6, -0.1) + if info.get_content_subtype() == "jpeg": + filepath = os.path.join(tr.parent_folder_path, "cover-" + id + ".jpg") + elif info.get_content_subtype() == "png": + filepath = os.path.join(tr.parent_folder_path, "cover-" + id + ".png") else: - colour = rgb_add_hls(bg, 0, 0.1, 0) - - if self.sub_active == self.items[i].func: - if is_light(bg) or colours.lm: - colour = rgb_add_hls(bg, 0, -0.8, -0.1) - else: - colour = rgb_add_hls(bg, 0, 0.40, 0) - - # colour = [50, 50, 50, 255] - # if selected: - # colour = [150, 150, 150, 255] - # if self.sub_active == self.items[i][2]: - # colour = [150, 150, 150, 255] - self.sub_arrow.asset.render(x_run + self.w - 13 * gui.scale, y_run + 7 * gui.scale, colour) + show_message(_("Could not detect downloaded filetype."), mode="error") + return - # Render the items label - ddt.text((x_run + x, y_run + ytoff), label, fx[0], self.font, max_w=self.w - (x + 9 * gui.scale), bg=bg) + f = open(filepath, "wb") + f.write(t.read()) + f.close() - # Render the items hint - if self.items[i].hint != None: + show_message(_("Cover art downloaded from fanart.tv"), mode="done") + # clear_img_cache() + for track_id in default_playlist: + if tr.parent_folder_path == pctl.get_track(track_id).parent_folder_path: + clear_track_image_cache(pctl.get_track(track_id)) + return + except Exception: + logging.exception("Failed to get from fanart.tv") - if is_light(bg) or colours.lm: - hint_colour = rgb_add_hls(bg, 0, -0.30, -0.3) - else: - hint_colour = rgb_add_hls(bg, 0, 0.15, 0) + show_message(_("Searching MusicBrainz for cover art...")) + t = io.BytesIO(musicbrainzngs.get_release_group_image_front(album_id, size=None)) + l = 0 + t.seek(0, 2) + l = t.tell() + t.seek(0) + if l > 1000: + filepath = os.path.join(tr.parent_folder_path, album_id + ".jpg") + f = open(filepath, "wb") + f.write(t.read()) + f.close() - # colo = alpha_blend([255, 255, 255, 50], bg) - ddt.text((x_run + self.w - 5, y_run + ytoff, 1), self.items[i].hint, hint_colour, self.font, bg=bg) + show_message(_("Cover art downloaded from MusicBrainz"), mode="done") + # clear_img_cache() + clear_track_image_cache(tr) - y_run += self.h + for track_id in default_playlist: + if tr.parent_folder_path == pctl.get_track(track_id).parent_folder_path: + clear_track_image_cache(pctl.get_track(track_id)) - if y_run > window_size[1] - self.h: - direc = 1 - if self.pos[0] > window_size[0] // 2: - direc = -1 - x_run += self.w * direc - y_run = self.pos[1] + return - # Render sub menu if active - if self.sub_active > -1 and self.items[i].is_sub_menu and self.sub_active == self.items[i].sub_menu_number: + except Exception: + logging.exception("Matching cover art or ID could not be found.") + show_message(_("Matching cover art or ID could not be found.")) - # sub_pos = [x_run + self.w, self.pos[1] + i * self.h] - sub_pos = [x_run + self.w, self.sub_y_postion] - sub_w = self.items[i].sub_menu_width * gui.scale +def download_art1_fire_disable_test(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + return track_object.is_network - if sub_pos[0] + sub_w > window_size[0]: - sub_pos[0] = x_run - sub_w - if view_box.active: - sub_pos[0] -= view_box.w +def download_art1_fire(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + shoot_dl = threading.Thread(target=download_art1, args=[track_object]) + shoot_dl.daemon = True + shoot_dl.start() - fx = self.deco() +def remove_embed_picture(track_object: TrackClass, dry: bool = True) -> int | None: + """Return amount of removed objects or None""" + index = track_object.index - minY = window_size[1] - self.h * len(self.subs[self.sub_active]) - 15 * gui.scale - sub_pos[1] = min(sub_pos[1], minY) + if key_shift_down or key_shiftr_down: + tracks = [index] + if track_object.is_cue or track_object.is_network: + show_message(_("Error - No handling for this kind of track"), mode="warning") + return None + else: + tracks = [] + original_parent_folder = track_object.parent_folder_name + for k in default_playlist: + tr = pctl.get_track(k) + if original_parent_folder == tr.parent_folder_name: + tracks.append(k) - xoff = 0 - for i in self.subs[self.sub_active]: - if i.icon is not None: - xoff = 24 * gui.scale - break + removed = 0 + if not dry: + pr = pctl.stop(True) + try: + for item in tracks: - for w in range(len(self.subs[self.sub_active])): + tr = pctl.get_track(item) - if self.subs[self.sub_active][w].show_test is not None: - if not self.subs[self.sub_active][w].show_test(self.reference): - continue + if tr.is_cue: + continue - # Get item colours - if self.subs[self.sub_active][w].render_func is not None: - if self.subs[self.sub_active][w].pass_ref_deco: - fx = self.subs[self.sub_active][w].render_func(self.reference) - else: - fx = self.subs[self.sub_active][w].render_func() + if tr.is_network: + continue - # Item background - ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (sub_w, self.h), fx[1]) + if dry: + removed += 1 + else: + if tr.file_ext == "MP3": + try: + tag = mutagen.id3.ID3(tr.fullpath) + tag.delall("APIC") + remove = True + tag.save(padding=no_padding) + removed += 1 + except Exception: + logging.exception("No MP3 APIC found") - # Detect if mouse is over this item - rect = (sub_pos[0], sub_pos[1] + w * self.h, sub_w, self.h - 1) - fields.add(rect) - this_select = False - bg = colours.menu_background - if coll_point(mouse_position, (sub_pos[0], sub_pos[1] + w * self.h, sub_w, self.h - 1)): - ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (sub_w, self.h), colours.menu_highlight_background) - bg = alpha_blend(colours.menu_highlight_background, bg) - this_select = True + if tr.file_ext == "M4A": + try: + tag = mutagen.mp4.MP4(tr.fullpath) + del tag.tags["covr"] + tag.save(padding=no_padding) + removed += 1 + except Exception: + logging.exception("No m4A covr tag found") - # Call Callback - if self.clicked and not self.is_item_disabled(self.subs[self.sub_active][w]): + if tr.file_ext in ("OGA", "OPUS", "OGG"): + show_message(_("Removing vorbis image not implemented")) + # try: + # tag = mutagen.File(tr.fullpath).tags + # logging.info(tag) + # removed += 1 + # except Exception: + # logging.exception("Failed to manipulate tags") - # If callback needs args - if self.subs[self.sub_active][w].args is not None: - self.subs[self.sub_active][w].func(self.reference, self.subs[self.sub_active][w].args) + if tr.file_ext == "FLAC": + try: + tag = mutagen.flac.FLAC(tr.fullpath) + tag.clear_pictures() + tag.save(padding=no_padding) + removed += 1 + except Exception: + logging.exception("Failed to save tags on FLAC") - # If callback just need ref - elif self.subs[self.sub_active][w].pass_ref: - self.subs[self.sub_active][w].func(self.reference) + clear_track_image_cache(tr) - else: - self.subs[self.sub_active][w].func() + except Exception: + logging.exception("Image remove error") + show_message(_("Image remove error"), mode="error") + return None - if fx[2] is not None: - label = fx[2] - else: - label = self.subs[self.sub_active][w].title + if dry: + return removed - # Show text as disabled if disable_test() passes - if self.is_item_disabled(self.subs[self.sub_active][w]): - fx[0] = colours.menu_text_disabled + if removed == 0: + show_message(_("Image removal failed."), mode="error") + return None + if removed == 1: + show_message(_("Deleted embedded picture from file"), mode="done") + else: + show_message(_("{N} files processed").local(N=removed), mode="done") + if pr == 1: + pctl.revert() + +def delete_file_image(track_object: TrackClass): + try: + showc = album_art_gen.get_info(track_object) + if showc is not None and showc[0] == 0: + source = album_art_gen.get_sources(track_object)[showc[2]][1] + os.remove(source) + # clear_img_cache() + clear_track_image_cache(track_object) + logging.info("Deleted file: " + source) + except Exception: + logging.exception("Failed to delete file") + show_message(_("Something went wrong"), mode="error") - # Render sub items icon - icon = self.subs[self.sub_active][w].icon - self.render_icon(sub_pos[0] + 11 * gui.scale, sub_pos[1] + w * self.h + 5 * gui.scale, icon, this_select, fx) +def delete_track_image_deco(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + info = album_art_gen.get_info(track_object) - # Render the items label - ddt.text( - (sub_pos[0] + 10 * gui.scale + xoff, sub_pos[1] + ytoff + w * self.h), label, fx[0], self.font, bg=bg) + text = _("Delete Image File") + line_colour = colours.menu_text - # Draw tab - ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (4 * gui.scale, self.h), colours.menu_tab) + if info is None or track_object.is_network: + return [colours.menu_text_disabled, colours.menu_background, None] - # Render the menu outline - # ddt.rect_a(sub_pos, (sub_w, self.h * len(self.subs[self.sub_active])), colours.grey(40)) + if info and info[0] == 0: + text = _("Delete Image File") - # Process Click Actions - if to_call is not None: + elif info and info[0] == 1: + if pctl.playing_state > 0 and track_object.file_ext in ("MP3", "FLAC", "M4A"): + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - if not self.is_item_disabled(self.items[to_call]): - if self.items[to_call].pass_ref: - self.items[to_call].func(self.reference) - else: - self.items[to_call].func() + text = _("Delete Embedded | Folder") + if key_shift_down or key_shiftr_down: + text = _("Delete Embedded | Track") + return [line_colour, colours.menu_background, text] - if self.clicked or key_esc_press or self.close_next_frame: - self.close_next_frame = False - self.active = False - self.clicked = False +def delete_track_image(track_object: TrackClass): + if type(track_object) is int: + track_object = pctl.master_library[track_object] + if track_object.is_network: + return + info = album_art_gen.get_info(track_object) + if info and info[0] == 0: + delete_file_image(track_object) + elif info and info[0] == 1: + n = remove_embed_picture(track_object, dry=True) + gui.message_box_confirm_callback = remove_embed_picture + gui.message_box_confirm_reference = (track_object, False) + show_message(_("This will erase any embedded image in {N} files. Are you sure?").format(N=n), mode="confirm") - last_click_location[0] = 0 - last_click_location[1] = 0 +def toggle_gimage(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_gimage + prefs.show_gimage ^= True + return None - for menu in Menu.instances: - if menu.active: - break - else: - Menu.active = False +def search_image_deco(track_object: TrackClass): + if track_object.artist and track_object.album: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - # Render the menu outline - # ddt.rect_a(self.pos, (self.w, self.h * len(self.items)), colours.grey(40)) + return [line_colour, colours.menu_background, None] - def activate(self, in_reference=0, position=None): +def ser_gimage(track_object: TrackClass): + if track_object.artist and track_object.album: + line = "https://www.google.com/search?tbm=isch&q=" + urllib.parse.quote( + track_object.artist + " " + track_object.album) + webbrowser.open(line, new=2, autoraise=True) - Menu.active = True +def append_here(): + global cargo + global default_playlist + default_playlist += cargo - if position != None: - self.pos = [position[0], position[1]] - else: - self.pos = [copy.deepcopy(mouse_position[0]), copy.deepcopy(mouse_position[1])] +def paste_deco(): + active = False + line = None + if len(cargo) > 0: + active = True + elif SDL_HasClipboardText(): + text = copy_from_clipboard() + if text.startswith(("/", "spotify")) or "file://" in text: + active = True + elif prefs.spot_mode and text.startswith("https://open.spotify.com/album/"): # or text.startswith("https://open.spotify.com/track/"): + active = True + line = _("Paste Spotify Album") - self.reference = in_reference - Menu.switch = self.id - self.sub_active = -1 + if active: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - # Reposition the menu if it would otherwise intersect with far edge of window - if not position: - if self.pos[0] + self.w > window_size[0]: - self.pos[0] -= round(self.w + 3 * gui.scale) + return [line_colour, colours.menu_background, line] - # Get height size of menu - full_h = 0 - shown_h = 0 - for item in self.items: - if item is None: - full_h += self.break_height - shown_h += self.break_height - else: - full_h += self.h - if self.test_item_active(item) is True: - shown_h += self.h +def lightning_move_test(discard): + return gui.lightning_copy and prefs.show_transfer - # Flip menu up if would intersect with bottom of window - if self.pos[1] + full_h > window_size[1]: - self.pos[1] -= shown_h +# def copy_deco(): +# line = "Copy" +# if key_shift_down: +# line = "Copy" #Folder From Library" +# else: +# line = "Copy" +# +# +# return [colours.menu_text, colours.menu_background, line] - # Prevent moving outside top of window - if self.pos[1] < gui.panelY: - self.pos[1] = gui.panelY - self.pos[0] += 5 * gui.scale +def unique_template(string): + return "" in string or \ + "" in string or \ + "<n>" in string or \ + "<number>" in string or \ + "<tracknumber>" in string or \ + "<tn>" in string or \ + "<sn>" in string or \ + "<singlenumber>" in string or \ + "<s>" in string or "%t" in string or "%tn" in string - self.active = True +def re_template_word(word, tr): + if word == "aa" or word == "albumartist": -class GallClass: - def __init__(self, size=250, save_out=True): - self.gall = {} - self.size = size - self.queue = [] - self.key_list = [] - self.save_out = save_out - self.i = 0 - self.lock = threading.Lock() - self.limit = 60 + if tr.album_artist: + return tr.album_artist + return tr.artist - def get_file_source(self, track_object: TrackClass): + if word == "a" or word == "artist": + return tr.artist - global album_art_gen + if word == "t" or word == "title": + return tr.title - sources = album_art_gen.get_sources(track_object) + if word == "n" or word == "number" or word == "tracknumber" or word == "tn": + if len(str(tr.track_number)) < 2: + return "0" + str(tr.track_number) + return str(tr.track_number) - if len(sources) == 0: - return False, 0 + if word == "sn" or word == "singlenumber" or word == "singletracknumber" or word == "s": + return str(tr.track_number) - offset = album_art_gen.get_offset(track_object.fullpath, sources) - return sources[offset], offset + if word == "d" or word == "date" or word == "year": + return str(tr.date) - def worker_render(self): + if word == "b" or "album" in word: + return str(tr.album) - self.lock.acquire() - # time.sleep(0.1) + if word == "g" or word == "genre": + return tr.genre - if search_over.active: - while QuickThumbnail.queue: - img = QuickThumbnail.queue.pop(0) - response = urllib.request.urlopen(img.url, context=ssl_context) - source_image = io.BytesIO(response.read()) - img.read_and_thumbnail(source_image, img.size, img.size) - source_image.close() - gui.update += 1 + if word == "x" or "ext" in word or "file" in word: + return tr.file_ext.lower() - while len(self.queue) > 0: + if word == "ux" or "upper" in word: + return tr.file_ext.upper() - source_image = None + if word == "c" or "composer" in word: + return tr.composer - if gui.halt_image_rendering: - self.queue.clear() - break + if "comment" in word: + return tr.comment.replace("\n", "").replace("\r", "") + return "" - self.i += 1 +def parse_template2(string: str, track_object: TrackClass, strict: bool = False): + temp = "" + out = "" - try: - # key = self.queue[0] - key = self.queue.pop(0) - except Exception: - logging.exception("thumb queue empty") - break + mode = 0 - if key not in self.gall: - order = [1, None, None, None] - self.gall[key] = order - else: - order = self.gall[key] + for c in string: - size = key[1] + if mode == 0: - slow_load = False - cache_load = False + if c == "<": + mode = 1 + else: + out += c - try: + else: - if True: - offset = 0 - parent_folder = key[0].parent_folder_path - if parent_folder in folder_image_offsets: - offset = folder_image_offsets[parent_folder] - img_name = str(key[2]) + "-" + str(size) + "-" + str(key[0].index) + "-" + str(offset) - if prefs.cache_gallery and os.path.isfile(os.path.join(g_cache_dir, img_name + ".jpg")): - source_image = open(os.path.join(g_cache_dir, img_name + ".jpg"), "rb") - # logging.info('load from cache') - cache_load = True - else: - slow_load = True + if c == ">": - if slow_load: + test = re_template_word(temp, track_object) + if strict: + assert test + out += test - source, c_offset = self.get_file_source(key[0]) + mode = 0 + temp = "" - if source is False: - order[0] = 0 - self.gall[key] = order - # del self.queue[0] - continue + else: - img_name = str(key[2]) + "-" + str(size) + "-" + str(key[0].index) + "-" + str(c_offset) - - # gall_render_last_timer.set() - - if prefs.cache_gallery and os.path.isfile(os.path.join(g_cache_dir, img_name + ".jpg")): - source_image = open(os.path.join(g_cache_dir, img_name + ".jpg"), "rb") - logging.info("slow load image") - cache_load = True - - # elif source[0] == 1: - # #logging.info('tag') - # source_image = io.BytesIO(album_art_gen.get_embed(key[0])) - # - # elif source[0] == 2: - # try: - # url = get_network_thumbnail_url(key[0]) - # response = urllib.request.urlopen(url) - # source_image = response - # except Exception: - # logging.exception("IMAGE NETWORK LOAD ERROR") - # else: - # source_image = open(source[1], 'rb') - source_image = album_art_gen.get_source_raw(0, 0, key[0], subsource=source) + temp += c - g = io.BytesIO() - g.seek(0) + if "<und" in string: + out = out.replace(" ", "_") + return parse_template(out, track_object, strict=strict) - if cache_load: - g.write(source_image.read()) +def parse_template(string, track_object: TrackClass, up_ext: bool = False, strict: bool = False): + set = 0 + underscore = False + output = "" + while set < len(string): + if string[set] == "%" and set < len(string) - 1: + set += 1 + if string[set] == "n": + if len(str(track_object.track_number)) < 2: + output += "0" + if strict: + assert str(track_object.track_number) + output += str(track_object.track_number) + elif string[set] == "a": + if up_ext and track_object.album_artist != "": # Context of renaming a folder + output += track_object.album_artist else: - error = False - try: - # Process image - im = Image.open(source_image) - if im.mode != "RGB": - im = im.convert("RGB") - im.thumbnail((size, size), Image.Resampling.LANCZOS) - except Exception: - logging.exception("Failed to work with thumbnail") - im = album_art_gen.get_error_img(size) - error = True + if strict: + assert track_object.artist + output += track_object.artist + elif string[set] == "t": + if strict: + assert track_object.title + output += track_object.title + elif string[set] == "c": + if strict: + assert track_object.composer + output += track_object.composer + elif string[set] == "d": + if strict: + assert track_object.date + output += track_object.date + elif string[set] == "b": + if strict: + assert track_object.album + output += track_object.album + elif string[set] == "x": + if up_ext: + output += track_object.file_ext.upper() + else: + output += "." + track_object.file_ext.lower() + elif string[set] == "u": + underscore = True + else: + output += string[set] + set += 1 - im.save(g, "BMP") + output = output.rstrip(" -").lstrip(" -") - if not error and self.save_out and prefs.cache_gallery and not os.path.isfile( - os.path.join(g_cache_dir, img_name + ".jpg")): - im.save(os.path.join(g_cache_dir, img_name + ".jpg"), "JPEG", quality=95) + if underscore: + output = output.replace(" ", "_") - g.seek(0) + # Attempt to ensure the output text is filename safe + return filename_safe(output) - # source_image.close() +def rename_playlist(index, generator: bool = False) -> None: + gui.rename_playlist_box = True + rename_playlist_box.edit_generator = False + rename_playlist_box.playlist_index = index + rename_playlist_box.x = mouse_position[0] + rename_playlist_box.y = mouse_position[1] - order = [2, g, None, None] - self.gall[key] = order + if generator: + rename_playlist_box.y = window_size[1] // 2 - round(200 * gui.scale) + rename_playlist_box.x = window_size[0] // 2 - round(250 * gui.scale) - gui.update += 1 - if source_image: - source_image.close() - source_image = None - # del self.queue[0] + rename_playlist_box.y = min(rename_playlist_box.y, round(350 * gui.scale)) - time.sleep(0.001) + if rename_playlist_box.y < gui.panelY: + rename_playlist_box.y = gui.panelY + 10 * gui.scale - except Exception: - logging.exception("Image load failed on track: " + key[0].fullpath) - order = [0, None, None, None] - self.gall[key] = order - gui.update += 1 - # del self.queue[0] + if gui.radio_view: + rename_text_area.set_text(pctl.radio_playlists[index]["name"]) + else: + rename_text_area.set_text(pctl.multi_playlist[index].title) + rename_text_area.highlight_all() + gui.gen_code_errors = False - if size < 150: - random.shuffle(self.queue) + if generator: + rename_playlist_box.toggle_edit_gen() - if self.i > 0: - self.i = 0 - return True - return False - - def render(self, track: TrackClass, location, size=None, force_offset=None) -> bool | None: - if gallery_load_delay.get() < 0.5: - return None - - x = round(location[0]) - y = round(location[1]) +def edit_generator_box(index: int) -> None: + rename_playlist(index, generator=True) - # time.sleep(0.1) - if size is None: - size = self.size +def pin_playlist_toggle(pl: int) -> None: + pctl.multi_playlist[pl].hidden ^= True - size = round(size) +def pl_pin_deco(pl: int): + # if pctl.multi_playlist[pl].hidden == True and tab_menu.pos[1] > + if pctl.multi_playlist[pl].hidden == True: + return [colours.menu_text, colours.menu_background, _("Pin")] + return [colours.menu_text, colours.menu_background, _("Unpin")] - # offset = self.get_offset(pctl.master_library[index].fullpath, self.get_sources(index)) - if track.parent_folder_path in folder_image_offsets: - offset = folder_image_offsets[track.parent_folder_path] - else: - offset = 0 +def pl_lock_deco(pl: int): + if pctl.multi_playlist[pl].locked == True: + return [colours.menu_text, colours.menu_background, _("Unlock")] + return [colours.menu_text, colours.menu_background, _("Lock")] - if force_offset is not None: - offset = force_offset +def view_pl_is_locked(_) -> bool: + return pctl.multi_playlist[pctl.active_playlist_viewing].locked - key = (track, size, offset) +def pl_is_locked(pl: int) -> bool: + if not pctl.multi_playlist: + return False + return pctl.multi_playlist[pl].locked - if key in self.gall: - #logging.info("old") +def lock_playlist_toggle(pl: int) -> None: + pctl.multi_playlist[pl].locked ^= True - order = self.gall[key] +def lock_colour_callback(): + if pctl.multi_playlist[gui.tab_menu_pl].locked: + if colours.lm: + return [230, 180, 60, 255] + return [240, 190, 10, 255] + return None - if order[0] == 0: - # broken - return False +def export_m3u(pl: int, direc: str | None = None, relative: bool = False, show: bool = True) -> int | str: + if len(pctl.multi_playlist[pl].playlist_ids) < 1: + show_message(_("There are no tracks in this playlist. Nothing to export")) + return 1 - if order[0] == 1: - # not done yet - return False + if not direc: + direc = str(user_directory / "playlists") + if not os.path.exists(direc): + os.makedirs(direc) + target = os.path.join(direc, pctl.multi_playlist[pl].title + ".m3u") - if order[0] == 2: - # finish processing - - wop = rw_from_object(order[1]) - s_image = IMG_Load_RW(wop, 0) - c = SDL_CreateTextureFromSurface(renderer, s_image) - SDL_FreeSurface(s_image) - tex_w = pointer(c_int(size)) - tex_h = pointer(c_int(size)) - SDL_QueryTexture(c, None, None, tex_w, tex_h) - dst = SDL_Rect(x, y) - dst.w = int(tex_w.contents.value) - dst.h = int(tex_h.contents.value) - - - order[0] = 3 - order[1].close() - order[1] = None - order[2] = c - order[3] = dst - self.gall[(track, size, offset)] = order - - if order[0] == 3: - # ready - - order[3].x = x - order[3].y = y - order[3].x = int((size - order[3].w) / 2) + order[3].x - order[3].y = int((size - order[3].h) / 2) + order[3].y - SDL_RenderCopy(renderer, order[2], None, order[3]) - - if (track, size, offset) in self.key_list: - self.key_list.remove((track, size, offset)) - self.key_list.append((track, size, offset)) - - # Remove old images to conserve RAM usage - if len(self.key_list) > self.limit: - gui.update += 1 - key = self.key_list[0] - # while key in self.queue: - # self.queue.remove(key) - if self.gall[key][2] is not None: - SDL_DestroyTexture(self.gall[key][2]) - del self.gall[key] - del self.key_list[0] + f = open(target, "w", encoding="utf-8") + f.write("#EXTM3U") + for number in pctl.multi_playlist[pl].playlist_ids: + track = pctl.master_library[number] + title = track.artist + if title: + title += " - " + title += track.title - return True + if not track.is_network: + f.write("\n#EXTINF:") + f.write(str(round(track.length))) + if title: + f.write(f",{title}") + path = track.fullpath + if relative: + path = os.path.relpath(path, start=direc) + f.write(f"\n{path}") + f.close() + if show: + line = direc + line += "/" + if system == "Windows" or msys: + os.startfile(line) + elif macos: + subprocess.Popen(["open", line]) else: - if key not in self.queue: - self.queue.append(key) - if self.lock.locked(): - try: - self.lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked lock") - else: - logging.exception("Unknown RuntimeError trying to release lock") - except Exception: - logging.exception("Unknown error trying to release lock") - return False + subprocess.Popen(["xdg-open", line]) + return target -class ThumbTracks: - def __init__(self) -> None: - pass +def export_xspf(pl: int, direc: str | None = None, relative: bool = False, show: bool = True) -> int | str: + if len(pctl.multi_playlist[pl].playlist_ids) < 1: + show_message(_("There are no tracks in this playlist. Nothing to export")) + return 1 - def path(self, track: TrackClass) -> str: - source, offset = tauon.gall_ren.get_file_source(track) + if not direc: + direc = str(user_directory / "playlists") + if not os.path.exists(direc): + os.makedirs(direc) - if source is False: # No art - return None + target = os.path.join(direc, pctl.multi_playlist[pl].title + ".xspf") - image_name = track.album + track.parent_folder_path + str(offset) - image_name = hashlib.md5(image_name.encode("utf-8", "replace")).hexdigest() + xspf_root = ET.Element("playlist", version="1", xmlns="http://xspf.org/ns/0/") + xspf_tracklist_tag = ET.SubElement(xspf_root, "trackList") - t_path = os.path.join(e_cache_dir, image_name + ".jpg") + for number in pctl.multi_playlist[pl].playlist_ids: + track = pctl.master_library[number] + path = track.fullpath + if relative: + path = os.path.relpath(path, start=direc) - if os.path.isfile(t_path): - return t_path + xspf_track_tag = ET.SubElement(xspf_tracklist_tag, "track") + if track.title != "": + ET.SubElement(xspf_track_tag, "title").text = track.title + if track.is_cue is False and track.fullpath != "": + ET.SubElement(xspf_track_tag, "location").text = urllib.parse.quote(path) + if track.artist != "": + ET.SubElement(xspf_track_tag, "creator").text = track.artist + if track.album != "": + ET.SubElement(xspf_track_tag, "album").text = track.album + if track.track_number != "": + ET.SubElement(xspf_track_tag, "trackNum").text = str(track.track_number) - source_image = album_art_gen.get_source_raw(0, 0, track, subsource=source) + ET.SubElement(xspf_track_tag, "duration").text = str(int(track.length * 1000)) - with Image.open(source_image) as im: - if im.mode != "RGB": - im = im.convert("RGB") - im.thumbnail((1000, 1000), Image.Resampling.LANCZOS) - im.save(t_path, "JPEG") - source_image.close() - return t_path - -class Tauon: - """Root class for everything Tauon""" - def __init__(self): - - self.t_title = t_title - self.t_version = t_version - self.t_agent = t_agent - self.t_id = t_id - self.desktop: str | None = desktop - self.device = socket.gethostname() - -# TODO(Martin) : Fix this by moving the class to root of the module - self.cachement: player4.Cachement | None = None - self.dummy_event: SDL_Event = SDL_Event() - self.translate = _ - self.strings: Strings = strings - self.pctl: PlayerCtl = pctl - self.lfm_scrobbler: LastScrob = lfm_scrobbler - self.star_store: StarStore = star_store - self.gui: GuiVar = gui - self.prefs: Prefs = prefs - self.cache_directory: Path = cache_directory - self.user_directory: Path | None = user_directory - self.music_directory: Path | None = music_directory - self.locale_directory: Path = locale_directory - self.worker_save_state: bool = False - self.launch_prefix: str = launch_prefix - self.whicher = whicher - self.load_orders: list[LoadClass] = load_orders - self.switch_playlist = None - self.open_uri = open_uri - self.love = love - self.snap_mode = snap_mode - self.console = console - self.msys = msys - self.TrackClass = TrackClass - self.pl_gen = pl_gen - self.gall_ren = GallClass(album_mode_art_size) - self.QuickThumbnail = QuickThumbnail - self.thumb_tracks = ThumbTracks() - self.pl_to_id = pl_to_id - self.id_to_pl = id_to_pl - self.chunker = Chunker() - self.thread_manager: ThreadManager = ThreadManager() - self.stream_proxy = None - self.stream_proxy = StreamEnc(self) - self.level_train: list[list[float]] = [] - self.radio_server = None - self.mod_formats = MOD_Formats - self.listen_alongers = {} - self.encode_folder_name = encode_folder_name - self.encode_track_name = encode_track_name - - self.tray_lock = threading.Lock() - self.tray_releases = 0 - - self.play_lock = None - self.update_play_lock = None - self.sleep_lock = None - self.shutdown_lock = None - self.quick_close = False - - self.copied_track = None - self.macos = macos - self.aud: CDLL | None = None - - self.recorded_songs = [] - - self.chrome_mode = False - self.web_running = False - self.web_thread = None - self.remote_limited = True - self.enable_librespot = shutil.which("librespot") - -# TODO(Martin) : Fix this by moving the class to root of the module - self.spotc: player4.LibreSpot | None = None - self.librespot_p = None - self.MenuItem = MenuItem - self.tag_scan = tag_scan - - self.gme_formats = GME_Formats - - self.spot_ctl: SpotCtl = SpotCtl(self) - self.tidal: Tidal = Tidal(self) - self.chrome: Chrome | None = None - self.chrome_menu: Menu | None = None - - self.ssl_context = ssl_context - - def start_remote(self) -> None: - - if not self.web_running: - self.web_thread = threading.Thread( - target=webserve2, args=[pctl, prefs, gui, album_art_gen, str(install_directory), strings, tauon]) - self.web_thread.daemon = True - self.web_thread.start() - self.web_running = True - - def download_ffmpeg(self, x): - def go(): - url = "https://github.com/GyanD/codexffmpeg/releases/download/5.0.1/ffmpeg-5.0.1-essentials_build.zip" - sha = "9e00da9100ae1bba22b1385705837392e8abcdfd2efc5768d447890d101451b5" - show_message(_("Starting download...")) - try: - f = io.BytesIO() - r = requests.get(url, stream=True, timeout=1800) # ffmpeg is 77MB, give it half an hour in case someone is willing to suffer it on a slow connection - - dl = 0 - for data in r.iter_content(chunk_size=4096): - dl += len(data) - f.write(data) - mb = round(dl / 1000 / 1000) - if mb > 90: - break - if mb % 5 == 0: - show_message(_("Downloading... {N}/80MB").format(N=mb)) + xspf_tree = ET.ElementTree(xspf_root) + ET.indent(xspf_tree, space=' ', level=0) + xspf_tree.write(target, encoding='UTF-8', xml_declaration=True) - except Exception as e: - logging.exception("Download failed") - show_message(_("Download failed"), str(e), mode="error") + if show: + line = direc + line += "/" + if system == "Windows" or msys: + os.startfile(line) + elif macos: + subprocess.Popen(["open", line]) + else: + subprocess.Popen(["xdg-open", line]) - f.seek(0) - if hashlib.sha256(f.read()).hexdigest() != sha: - show_message(_("Download completed but checksum failed"), mode="error") - return - show_message(_("Download completed.. extracting")) - f.seek(0) - z = zipfile.ZipFile(f, mode="r") - exe = z.open("ffmpeg-5.0.1-essentials_build/bin/ffmpeg.exe") - with (user_directory / "ffmpeg.exe").open("wb") as file: - file.write(exe.read()) + return target - exe = z.open("ffmpeg-5.0.1-essentials_build/bin/ffprobe.exe") - with (user_directory / "ffprobe.exe").open("wb") as file: - file.write(exe.read()) +def reload(): + if album_mode: + reload_albums(quiet=True) - exe.close() - show_message(_("FFMPEG fetch complete"), mode="done") + # tree_view_box.clear_all() + # elif gui.combo_mode: + # reload_albums(quiet=True) + # combo_pl_render.prep() - shooter(go) +def clear_playlist(index: int): + global default_playlist - def set_tray_icons(self, force: bool = False): + if pl_is_locked(index): + show_message(_("Playlist is locked to prevent accidental erasure")) + return - indicator_icon_play = str(pctl.install_directory / "assets/svg/tray-indicator-play.svg") - indicator_icon_pause = str(pctl.install_directory / "assets/svg/tray-indicator-pause.svg") - indicator_icon_default = str(pctl.install_directory / "assets/svg/tray-indicator-default.svg") + pctl.multi_playlist[index].last_folder.clear() # clear import folder list # TODO(Martin): This was actually a string not a list wth? - if prefs.tray_theme == "gray": - indicator_icon_play = str(pctl.install_directory / "assets/svg/tray-indicator-play-g1.svg") - indicator_icon_pause = str(pctl.install_directory / "assets/svg/tray-indicator-pause-g1.svg") - indicator_icon_default = str(pctl.install_directory / "assets/svg/tray-indicator-default-g1.svg") + if not pctl.multi_playlist[index].playlist_ids: + logging.info("Playlist is already empty") + return - user_icon_dir = self.cache_directory / "icon-export" - def install_tray_icon(src: str, name: str) -> None: - alt = user_icon_dir / f"{name}.svg" - if not alt.is_file() or force: - shutil.copy(src, str(alt)) + li = [] + for i, ref in enumerate(pctl.multi_playlist[index].playlist_ids): + li.append((i, ref)) - if not user_icon_dir.is_dir(): - os.makedirs(user_icon_dir) + undo.bk_tracks(index, list(reversed(li))) - install_tray_icon(indicator_icon_play, "tray-indicator-play") - install_tray_icon(indicator_icon_pause, "tray-indicator-pause") - install_tray_icon(indicator_icon_default, "tray-indicator-default") + del pctl.multi_playlist[index].playlist_ids[:] + if pctl.active_playlist_viewing == index: + default_playlist = pctl.multi_playlist[index].playlist_ids + reload() - def get_tray_icon(self, name: str) -> str: - return str(self.cache_directory / "icon-export" / f"{name}.svg") + # pctl.playlist_playing = 0 + pctl.multi_playlist[index].position = 0 + if index == pctl.active_playlist_viewing: + pctl.playlist_view_position = 0 - def test_ffmpeg(self) -> bool: - if self.get_ffmpeg(): - return True - if msys: - show_message(_("This feature requires FFMPEG. Shall I can download that for you? (80MB)"), mode="confirm") - gui.message_box_confirm_callback = self.download_ffmpeg - gui.message_box_confirm_reference = (None,) - else: - show_message(_("FFMPEG could not be found")) - return False + gui.pl_update = 1 - def get_ffmpeg(self) -> str | None: - logging.debug(f"Looking for ffmpeg in PATH: {os.environ.get('PATH')}") - p = shutil.which("ffmpeg") - if p: - return p - p = str(user_directory / "ffmpeg.exe") - if msys and os.path.isfile(p): - return p - return None +def convert_playlist(pl: int, get_list: bool = False) -> list[list[int]]| None: + global transcode_list - def get_ffprobe(self) -> str | None: - p = shutil.which("ffprobe") - if p: - return p - p = str(user_directory / "ffprobe.exe") - if msys and os.path.isfile(p): - return p + if not tauon.test_ffmpeg(): return None - def bg_save(self) -> None: - self.worker_save_state = True - tauon.thread_manager.ready("worker") - - def exit(self, reason: str) -> None: - logging.info("Shutting down. Reason: " + reason) - pctl.running = False - self.wake() - - def min_to_tray(self) -> None: - SDL_HideWindow(t_window) - gui.mouse_unknown = True - - def raise_window(self) -> None: - SDL_ShowWindow(t_window) - SDL_RaiseWindow(t_window) - SDL_RestoreWindow(t_window) - gui.lowered = False - gui.update += 1 - - def focus_window(self) -> None: - SDL_RaiseWindow(t_window) - - def get_playing_playlist_id(self) -> int: - return pl_to_id(pctl.active_playlist_playing) - - def wake(self) -> None: - SDL_PushEvent(ctypes.byref(self.dummy_event)) + paths: list[str] = [] + folders: list[list[int]] = [] + for track in pctl.multi_playlist[pl].playlist_ids: + if pctl.master_library[track].parent_folder_path not in paths: + paths.append(pctl.master_library[track].parent_folder_path) -tauon = Tauon() + for path in paths: + folder: list[int] = [] + for track in pctl.multi_playlist[pl].playlist_ids: + if pctl.master_library[track].parent_folder_path == path: + folder.append(track) + if prefs.transcode_codec == "flac" and pctl.master_library[track].file_ext.lower() in ( + "mp3", "opus", + "m4a", "mp4", + "ogg", "aac"): + show_message(_("This includes the conversion of a lossy codec to a lossless one!")) -def signal_handler(signum, frame): - signal.signal(signum, signal.SIG_IGN) # ignore additional signals - tauon.exit(reason="SIGINT recieved") + folders.append(folder) -signal.signal(signal.SIGINT, signal_handler) + if get_list: + return folders -deco = Deco(tauon) -deco.get_themes = get_themes -deco.renderer = renderer + transcode_list.extend(folders) -if prefs.backend != 4: - prefs.backend = 4 +def get_folder_tracks_local(pl_in: int) -> list[int]: + selection = [] + parent = os.path.normpath(pctl.master_library[default_playlist[pl_in]].parent_folder_path) + while pl_in < len(default_playlist) and parent == os.path.normpath( + pctl.master_library[default_playlist[pl_in]].parent_folder_path): + selection.append(pl_in) + pl_in += 1 + return selection -chrome = None +def test_pl_tab_locked(pl: int) -> bool: + if gui.radio_view: + return False + return pctl.multi_playlist[pl].locked -try: - from tauon.t_modules.t_chrome import Chrome - chrome = Chrome(tauon) -except ModuleNotFoundError as e: - logging.debug(f"pychromecast import error: {e}") - logging.warning("Unable to import Chrome(pychromecast), chromecast support will be disabled.") -except Exception: - logging.exception("Unknown error trying to import Chrome(pychromecast), chromecast support will be disabled.") -finally: - logging.debug("Found Chrome(pychromecast) for chromecast support") +def move_radio_playlist(source, dest): + if dest > source: + dest += 1 + try: + temp = pctl.radio_playlists[source] + pctl.radio_playlists[source] = "old" + pctl.radio_playlists.insert(dest, temp) + pctl.radio_playlists.remove("old") + pctl.radio_playlist_viewing = pctl.radio_playlists.index(temp) + except Exception: + logging.exception("Playlist move error") -tauon.chrome = chrome +def move_playlist(source, dest): + global default_playlist + if dest > source: + dest += 1 + try: + active = pctl.multi_playlist[pctl.active_playlist_playing] + view = pctl.multi_playlist[pctl.active_playlist_viewing] -class PlexService: + temp = pctl.multi_playlist[source] + pctl.multi_playlist[source] = "old" + pctl.multi_playlist.insert(dest, temp) + pctl.multi_playlist.remove("old") - def __init__(self): - self.connected = False - self.resource = None - self.scanning = False + pctl.active_playlist_playing = pctl.multi_playlist.index(active) + pctl.active_playlist_viewing = pctl.multi_playlist.index(view) + default_playlist = default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + except Exception: + logging.exception("Playlist move error") - def connect(self): +def delete_playlist(index: int, force: bool = False, check_lock: bool = False) -> None: + if gui.radio_view: + del pctl.radio_playlists[index] + if not pctl.radio_playlists: + pctl.radio_playlists = [{"uid": uid_gen(), "name": "Default", "items": []}] + return - if not prefs.plex_username or not prefs.plex_password or not prefs.plex_servername: - show_message(_("Missing username, password and/or server name"), mode="warning") - self.scanning = False - return + global default_playlist - try: - from plexapi.myplex import MyPlexAccount - except ModuleNotFoundError: - logging.warning("Unable to import python-plexapi, plex support will be disabled.") - except Exception: - logging.exception("Unknown error to import python-plexapi, plex support will be disabled.") - show_message(_("Error importing python-plexapi"), mode="error") - self.scanning = False - return + if check_lock and pl_is_locked(index): + show_message(_("Playlist is locked to prevent accidental deletion")) + return - try: - account = MyPlexAccount(prefs.plex_username, prefs.plex_password) - self.resource = account.resource(prefs.plex_servername).connect() # returns a PlexServer instance - except Exception: - logging.exception("Error connecting to PLEX server, check login credentials and server accessibility.") - show_message( - _("Error connecting to PLEX server"), - _("Try checking login credentials and that the server is accessible."), mode="error") - self.scanning = False + if not force: + if pl_is_locked(index): + show_message(_("Playlist is locked to prevent accidental deletion")) return - # from plexapi.server import PlexServer - # baseurl = 'http://localhost:32400' - # token = '' + if gui.rename_playlist_box: + return - # self.resource = PlexServer(baseurl, token) + # Set screen to be redrawn + gui.pl_update = 1 + gui.update += 1 - self.connected = True + # Backup the playlist to be deleted + # pctl.playlist_backup.append(pctl.multi_playlist[index]) + # pctl.playlist_backup.append(pctl.multi_playlist[index]) + undo.bk_playlist(index) - def resolve_stream(self, location): - logging.info("Get plex stream") - if not self.connected: - self.connect() + # If we're deleting the final playlist, delete it and create a blank one in place + if len(pctl.multi_playlist) == 1: + logging.warning("Deleting final playlist and creating a new Default one") + pctl.multi_playlist.clear() + pctl.multi_playlist.append(pl_gen()) + default_playlist = pctl.multi_playlist[0].playlist_ids + pctl.active_playlist_playing = 0 + return - # return self.resource.url(location, True) - return self.resource.library.fetchItem(location).getStreamURL() + # Take note of the id of the playing playlist + old_playing_id = pctl.multi_playlist[pctl.active_playlist_playing].uuid_int - def resolve_thumbnail(self, location): + # Take note of the id of the viewed open playlist + old_view_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - if not self.connected: - self.connect() - if self.connected: - return self.resource.url(location, True) - return None + # Delete the requested playlist + del pctl.multi_playlist[index] - def get_albums(self, return_list=False): + # Re-set the open viewed playlist number by uid + for i, pl in enumerate(pctl.multi_playlist): - gui.update += 1 - self.scanning = True + if pl.uuid_int == old_view_id: + pctl.active_playlist_viewing = i + break + else: + # logging.info("Lost the viewed playlist!") + # Try find the playing playlist and make it the viewed playlist + for i, pl in enumerate(pctl.multi_playlist): + if pl.uuid_int == old_playing_id: + pctl.active_playlist_viewing = i + break + else: + # Playing playlist was deleted, lets just move down one playlist + if pctl.active_playlist_viewing > 0: + pctl.active_playlist_viewing -= 1 - if not self.connected: - self.connect() + # Re-initiate the now viewed playlist + if old_view_id != pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int: + default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + pctl.playlist_view_position = pctl.multi_playlist[pctl.active_playlist_viewing].position + logging.debug("Position reset by playlist delete") + pctl.selected_in_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].selected + shift_selection = [pctl.selected_in_playlist] - if not self.connected: - self.scanning = False - return [] + if album_mode: + reload_albums(True) + goto_album(pctl.playlist_view_position) - playlist = [] + # Re-set the playing playlist number by uid + for i, pl in enumerate(pctl.multi_playlist): - existing = {} - for track_id, track in pctl.master_library.items(): - if track.is_network and track.file_ext == "PLEX": - existing[track.url_key] = track_id + if pl.uuid_int == old_playing_id: + pctl.active_playlist_playing = i + break + else: + logging.info("Lost the playing playlist!") + pctl.active_playlist_playing = pctl.active_playlist_viewing + pctl.playlist_playing_position = -1 - albums = self.resource.library.section("Music").albums() - gui.to_got = 0 + test_show_add_home_music() - for album in albums: - year = album.year - album_artist = album.parentTitle - album_title = album.title + # Cleanup + ids = [] + for p in pctl.multi_playlist: + ids.append(p.uuid_int) - parent = (album_artist + " - " + album_title).strip("- ") + for key in list(gui.gallery_positions.keys()): + if key not in ids: + del gui.gallery_positions[key] + for key in list(pctl.gen_codes.keys()): + if key not in ids: + del pctl.gen_codes[key] - for track in album.tracks(): + pctl.db_inc += 1 - if not track.duration: - logging.warning("Skipping track with invalid duration - " + track.title + " - " + track.grandparentTitle) - continue +def delete_playlist_force(index: int): + delete_playlist(index, force=True, check_lock=True) - id = pctl.master_count - replace_existing = False +def delete_playlist_by_id(id: int, force: bool = False, check_lock: bool = False) -> None: + delete_playlist(id_to_pl(id), force=force, check_lock=check_lock) - e = existing.get(track.key) - if e is not None: - id = e - replace_existing = True +def delete_playlist_ask(index: int): + print("ark") + if gui.radio_view: + delete_playlist_force(index) + return + gen = pctl.gen_codes.get(pl_to_id(index), "") + if (gen and not gen.startswith("self ")) or len(pctl.multi_playlist[index].playlist_ids) < 2: + delete_playlist(index) + return - title = track.title - track_artist = track.grandparentTitle - duration = track.duration / 1000 + gui.message_box_confirm_callback = delete_playlist_by_id + gui.message_box_confirm_reference = (pl_to_id(index), True, True) + show_message(_("Are you sure you want to delete playlist: {name}?").format(name=pctl.multi_playlist[index].title), mode="confirm") - nt = TrackClass() - nt.index = id - nt.track_number = track.index - nt.file_ext = "PLEX" - nt.parent_folder_path = parent - nt.parent_folder_name = parent - nt.album_artist = album_artist - nt.artist = track_artist - nt.title = title - nt.album = album_title - nt.length = duration - if hasattr(track, "locations") and track.locations: - nt.fullpath = track.locations[0] +def rescan_tags(pl: int) -> None: + for track in pctl.multi_playlist[pl].playlist_ids: + if pctl.master_library[track].is_cue is False: + to_scan.append(track) + tauon.thread_manager.ready("worker") - nt.is_network = True +# def re_import(pl: int) -> None: +# +# path = pctl.multi_playlist[pl].last_folder +# if path == "": +# return +# for i in reversed(range(len(pctl.multi_playlist[pl].playlist_ids))): +# if path.replace('\\', '/') in pctl.master_library[pctl.multi_playlist[pl].playlist_ids[i]].parent_folder_path: +# del pctl.multi_playlist[pl].playlist_ids[i] +# +# load_order = LoadClass() +# load_order.replace_stem = True +# load_order.target = path +# load_order.playlist = pctl.multi_playlist[pl].uuid_int +# load_orders.append(copy.deepcopy(load_order)) - if track.thumb: - nt.art_url_key = track.thumb +def re_import2(pl: int) -> None: + paths = pctl.multi_playlist[pl].last_folder - nt.url_key = track.key - nt.date = str(year) + reduce_paths(paths) - pctl.master_library[id] = nt + for path in paths: + if os.path.isdir(path): + load_order = LoadClass() + load_order.replace_stem = True + load_order.target = path + load_order.notify = True + load_order.playlist = pctl.multi_playlist[pl].uuid_int + load_orders.append(copy.deepcopy(load_order)) - if not replace_existing: - pctl.master_count += 1 + if paths: + show_message(_("Rescanning folders..."), mode="info") - playlist.append(nt.index) +def rescan_all_folders(): + for i, p in enumerate(pctl.multi_playlist): + re_import2(i) - gui.to_got += 1 - gui.update += 1 - gui.pl_update += 1 +def s_append(index: int): + paste(playlist_no=index) - self.scanning = False +def append_playlist(index: int): + global cargo + pctl.multi_playlist[index].playlist_ids += cargo - if return_list: - return playlist + gui.pl_update = 1 + reload() - pctl.multi_playlist.append(pl_gen(title=_("PLEX Collection"), playlist_ids=playlist)) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "plex path" - switch_playlist(len(pctl.multi_playlist) - 1) +def index_key(index: int): + tr = pctl.master_library[index] + s = str(tr.track_number) + d = str(tr.disc_number) + if "/" in d: + d = d.split("/")[0] -plex = PlexService() -tauon.plex = plex + # Make sure the value for disc number is an int, make 1 if 0, otherwise ignore + if d: + try: + dd = int(d) + if dd < 2: + dd = 1 + d = str(dd) + except Exception: + logging.exception("Failed to parse as index as int") + d = "" -jellyfin = Jellyfin(tauon) -tauon.jellyfin = jellyfin + # Add the disc number for sorting by CD, make it '1' if theres isnt one + if s or d: + if not d: + s = "1" + "d" + s + else: + s = d + "d" + s -class SubsonicService: + # Use the filename if we dont have any metadata to sort by, + # since it could likely have the track number in it + else: + s = tr.filename - def __init__(self): - self.scanning = False - self.playlists = prefs.subsonic_playlists + if (not tr.disc_number or tr.disc_number == "0") and tr.is_cue: + s = tr.filename + "-" + s - def r(self, point, p=None, binary: bool = False, get_url: bool = False): - salt = secrets.token_hex(8) - server = prefs.subsonic_server.rstrip("/") + "/" + # This splits the line by groups of numbers, causing the sorting algorithum to sort + # by those numbers. Should work for filenames, even with the disc number in the name + try: + return [tryint(c) for c in re.split("([0-9]+)", s)] + except Exception: + logging.exception("Failed to parse as int, returning 'a'") + return "a" - params = { - "u": prefs.subsonic_user, - "v": "1.13.0", - "c": t_title, - "f": "json", - } +def sort_tracK_numbers_album_only(pl: int, custom_list=None): + current_folder = "" + albums = [] + if custom_list is None: + playlist = pctl.multi_playlist[pl].playlist_ids + else: + playlist = custom_list - if prefs.subsonic_password_plain: - params["p"] = prefs.subsonic_password - else: - params["t"] = hashlib.md5((prefs.subsonic_password + salt).encode()).hexdigest() - params["s"] = salt + for i in range(len(playlist)): + if i == 0: + albums.append(i) + current_folder = pctl.master_library[playlist[i]].album + elif pctl.master_library[playlist[i]].album != current_folder: + current_folder = pctl.master_library[playlist[i]].album + albums.append(i) - if p: - params.update(p) + i = 0 + while i < len(albums) - 1: + playlist[albums[i]:albums[i + 1]] = sorted(playlist[albums[i]:albums[i + 1]], key=index_key) + i += 1 + if len(albums) > 0: + playlist[albums[i]:] = sorted(playlist[albums[i]:], key=index_key) - point = "rest/" + point + gui.pl_update += 1 - url = server + point +def sort_track_2(pl: int, custom_list: list[int] | None = None) -> None: + current_folder = "" + current_album = "" + current_date = "" + albums = [] + if custom_list is None: + playlist = pctl.multi_playlist[pl].playlist_ids + else: + playlist = custom_list - if get_url: - return url, params + for i in range(len(playlist)): + tr = pctl.master_library[playlist[i]] + if i == 0: + albums.append(i) + current_folder = tr.parent_folder_path + current_album = tr.album + current_date = tr.date + elif tr.parent_folder_path != current_folder: + if tr.album == current_album and tr.album and tr.date == current_date and tr.disc_number \ + and os.path.dirname(tr.parent_folder_path) == os.path.dirname(current_folder): + continue + current_folder = tr.parent_folder_path + current_album = tr.album + current_date = tr.date + albums.append(i) - response = requests.get(url, params=params, timeout=10) + i = 0 + while i < len(albums) - 1: + playlist[albums[i]:albums[i + 1]] = sorted(playlist[albums[i]:albums[i + 1]], key=index_key) + i += 1 + if len(albums) > 0: + playlist[albums[i]:] = sorted(playlist[albums[i]:], key=index_key) - if binary: - return response.content + gui.pl_update += 1 - d = json.loads(response.text) - # logging.info(d) +def key_filepath(index: int): + track = pctl.master_library[index] + return track.parent_folder_path.lower(), track.filename - if d["subsonic-response"]["status"] != "ok": - show_message(_("Subsonic Error: ") + response.text, mode="warning") - logging.error("Subsonic Error: " + response.text) +def key_fullpath(index: int): + return pctl.master_library[index].fullpath - return d +def key_filename(index: int): + track = pctl.master_library[index] + return track.filename - def get_cover(self, track_object: TrackClass): - response = self.r("getCoverArt", p={"id": track_object.art_url_key}, binary=True) - return io.BytesIO(response) +def sort_path_pl(pl: int, custom_list=None): + if custom_list is not None: + target = custom_list + else: + target = pctl.multi_playlist[pl].playlist_ids - def resolve_stream(self, key): + if use_natsort and False: + target[:] = natsort.os_sorted(target, key=key_fullpath) + else: + target.sort(key=key_filepath) - p = {"id": key} - if prefs.network_stream_bitrate > 0: - p["maxBitRate"] = prefs.network_stream_bitrate +def append_current_playing(index: int): + if tauon.spot_ctl.coasting: + tauon.spot_ctl.append_playing(index) + gui.pl_update = 1 + return - return self.r("stream", p={"id": key}, get_url=True) - # logging.info(response.content) + if pctl.playing_state > 0 and len(pctl.track_queue) > 0: + pctl.multi_playlist[index].playlist_ids.append(pctl.track_queue[pctl.queue_step]) + gui.pl_update = 1 - def listen(self, track_object: TrackClass, submit: bool = False): +def export_stats(pl: int) -> None: + playlist_time = 0 + play_time = 0 + total_size = 0 + tracks_in_playlist = len(pctl.multi_playlist[pl].playlist_ids) - try: - a = self.r("scrobble", p={"id": track_object.url_key, "submission": submit}) - except Exception: - logging.exception("Error connecting for scrobble on airsonic") - return True + seen_files = {} + seen_types = {} - def set_rating(self, track_object: TrackClass, rating): + mp3_bitrates = {} + ogg_bitrates = {} + m4a_bitrates = {} - try: - a = self.r("setRating", p={"id": track_object.url_key, "rating": math.ceil(rating / 2)}) - except Exception: - logging.exception("Error connect for set rating on airsonic") - return True - - def set_album_rating(self, track_object: TrackClass, rating): - id = track_object.misc.get("subsonic-folder-id") - if id is not None: - try: - a = self.r("setRating", p={"id": id, "rating": math.ceil(rating / 2)}) - except Exception: - logging.exception("Error connect for set rating on airsonic") - return True + are_cue = 0 - def get_music3(self, return_list: bool = False): + for index in pctl.multi_playlist[pl].playlist_ids: + track = pctl.get_track(index) - self.scanning = True - gui.to_got = 0 + playlist_time += int(track.length) + play_time += star_store.get(index) - existing = {} + if track.is_cue: + are_cue += 1 - for track_id, track in pctl.master_library.items(): - if track.is_network and track.file_ext == "SUB": - existing[track.url_key] = track_id + if track.file_ext == "MP3": + mp3_bitrates[track.bitrate] = mp3_bitrates.get(track.bitrate, 0) + 1 + if track.file_ext == "OGG" or track.file_ext == "OGA": + ogg_bitrates[track.bitrate] = ogg_bitrates.get(track.bitrate, 0) + 1 + if track.file_ext == "M4A": + m4a_bitrates[track.bitrate] = m4a_bitrates.get(track.bitrate, 0) + 1 - try: - a = self.r("getIndexes") - except Exception: - logging.exception("Error connecting to Airsonic server") - show_message(_("Error connecting to Airsonic server"), mode="error") - self.scanning = False - return [] + type = track.file_ext + if type == "OGA": + type = "OGG" + seen_types[type] = seen_types.get(type, 0) + 1 - b = a["subsonic-response"]["indexes"]["index"] + if track.fullpath and not track.is_network: + if track.fullpath not in seen_files: + size = track.size + if not size and os.path.isfile(track.fullpath): + size = os.path.getsize(track.fullpath) + seen_files[track.fullpath] = size - folders = [] + total_size = sum(seen_files.values()) - for letter in b: - artists = letter["artist"] - for artist in artists: - folders.append(( - artist["id"], - artist["name"], - )) + stats_gen.update(pl) + line = _("Playlist:") + "\n" + pctl.multi_playlist[pl].title + "\n\n" + line += _("Generated:") + "\n" + time.strftime("%c") + "\n\n" + line += _("Tracks in playlist:") + "\n" + str(tracks_in_playlist) + line += "\n\n" + line += _("Repeats in playlist:") + "\n" + unique = len(set(pctl.multi_playlist[pl].playlist_ids)) + line += str(tracks_in_playlist - unique) + line += "\n\n" + line += _("Total local size:") + "\n" + get_filesize_string(total_size) + "\n\n" + line += _("Playlist duration:") + "\n" + str(datetime.timedelta(seconds=int(playlist_time))) + "\n\n" + line += _("Total playtime:") + "\n" + str(datetime.timedelta(seconds=int(play_time))) + "\n\n" - playlist = [] + line += _("Track types:") + "\n" + if tracks_in_playlist: + types = sorted(seen_types, key=seen_types.get, reverse=True) + for type in types: + perc = round((seen_types.get(type) / tracks_in_playlist) * 100, 1) + if perc < 0.1: + perc = "<0.1" + if type == "SPOT": + type = "SPOTIFY" + if type == "SUB": + type = "AIRSONIC" + line += f"{type} ({perc}%); " + line = line.rstrip("; ") + line += "\n\n" - songsets = [] - for i in range(len(folders)): - songsets.append([]) - statuses = [0] * len(folders) - dupes = [] + if tracks_in_playlist: + line += _("Percent of tracks are CUE type:") + "\n" + perc = are_cue / tracks_in_playlist + if perc == 0: + perc = 0 + if 0 < perc < 0.01: + perc = "<0.01" + else: + perc = round(perc, 2) - def getsongs(index, folder_id, name: str, inner: bool = False, parent=None): + line += str(perc) + "%" + line += "\n\n" - try: - d = self.r("getMusicDirectory", p={"id": folder_id}) - if "child" not in d["subsonic-response"]["directory"]: - if not inner: - statuses[index] = 2 - return + if tracks_in_playlist and mp3_bitrates: + line += _("MP3 bitrates (kbps):") + "\n" + rates = sorted(mp3_bitrates, key=mp3_bitrates.get, reverse=True) + others = 0 + for rate in rates: + perc = round((mp3_bitrates.get(rate) / sum(mp3_bitrates.values())) * 100, 1) + if perc < 1: + others += perc + else: + line += f"{rate} ({perc}%); " - except json.decoder.JSONDecodeError: - logging.exception("Error reading Airsonic directory") - if not inner: - statuses[index] = 2 - show_message(_("Error reading Airsonic directory!"), mode="warning") - return - except Exception: - logging.exception("Unknown Error reading Airsonic directory") + if others: + others = round(others, 1) + if others < 0.1: + others = "<0.1" + line += _("Others") + f"({others}%);" + line = line.rstrip("; ") + line += "\n\n" - items = d["subsonic-response"]["directory"]["child"] + if tracks_in_playlist and ogg_bitrates: + line += _("OGG bitrates (kbps):") + "\n" + rates = sorted(ogg_bitrates, key=ogg_bitrates.get, reverse=True) + others = 0 + for rate in rates: + perc = round((ogg_bitrates.get(rate) / sum(ogg_bitrates.values())) * 100, 1) + if perc < 1: + others += perc + else: + line += f"{rate} ({perc}%); " - gui.update = 2 + if others: + others = round(others, 1) + if others < 0.1: + others = "<0.1" + line += _("Others") + f"({others}%);" + line = line.rstrip("; ") + line += "\n\n" - for item in items: + # if tracks_in_playlist and m4a_bitrates: + # line += "M4A bitrates (kbps):\n" + # rates = sorted(m4a_bitrates, key=m4a_bitrates.get, reverse=True) + # others = 0 + # for rate in rates: + # perc = round((m4a_bitrates.get(rate) / sum(m4a_bitrates.values())) * 100, 1) + # if perc < 1: + # others += perc + # else: + # line += f"{rate} ({perc}%); " + # + # if others: + # others = round(others, 1) + # if others < 0.1: + # others = "<0.1" + # line += f"Others ({others}%);" + # + # line = line.rstrip("; ") + # line += "\n\n" - if item["isDir"]: + line += "\n" + f"-------------- {_('Top Artists')} --------------------" + "\n\n" - if "userRating" in item and "artist" in item: - rating = item["userRating"] - if album_star_store.get_rating_artist_title(item["artist"], item["title"]) == 0 and rating == 0: - pass - else: - album_star_store.set_rating_artist_title(item["artist"], item["title"], int(rating * 2)) + ls = stats_gen.artist_list + for i, item in enumerate(ls[:50]): + line += str(i + 1) + ".\t" + stt2(item[1]) + "\t" + item[0] + "\n" - getsongs(index, item["id"], item["title"], inner=True, parent=item) - continue + line += "\n\n" + f"-------------- {_('Top Albums')} --------------------" + "\n\n" + ls = stats_gen.album_list + for i, item in enumerate(ls[:50]): + line += str(i + 1) + ".\t" + stt2(item[1]) + "\t" + item[0] + "\n" + line += "\n\n" + f"-------------- {_('Top Genres')} --------------------" + "\n\n" + ls = stats_gen.genre_list + for i, item in enumerate(ls[:50]): + line += str(i + 1) + ".\t" + stt2(item[1]) + "\t" + item[0] + "\n" - gui.to_got += 1 - song = item - nt = TrackClass() - - if parent and "artist" in parent: - nt.album_artist = parent["artist"] - - if "title" in song: - nt.title = song["title"] - if "artist" in song: - nt.artist = song["artist"] - if "album" in song: - nt.album = song["album"] - if "track" in song: - nt.track_number = song["track"] - if "year" in song: - nt.date = str(song["year"]) - if "duration" in song: - nt.length = song["duration"] - - nt.file_ext = "SUB" - nt.parent_folder_name = name - if "path" in song: - nt.fullpath = song["path"] - nt.parent_folder_path = os.path.dirname(song["path"]) - if "coverArt" in song: - nt.art_url_key = song["id"] - nt.url_key = song["id"] - nt.misc["subsonic-folder-id"] = folder_id - nt.is_network = True - - rating = 0 - if "userRating" in song: - rating = int(song["userRating"]) - - songsets[index].append((nt, name, song["id"], rating)) - - if inner: - return - statuses[index] = 2 + line = line.encode("utf-8") + xport = (user_directory / "stats.txt").open("wb") + xport.write(line) + xport.close() + target = str(user_directory / "stats.txt") + if system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", target]) + else: + subprocess.call(["xdg-open", target]) - i = -1 - for id, name in folders: - i += 1 - while statuses.count(1) > 3: - time.sleep(0.1) +def imported_sort(pl: int) -> None: + if pl_is_locked(pl): + show_message(_("Playlist is locked")) + return - statuses[i] = 1 - t = threading.Thread(target=getsongs, args=([i, id, name])) - t.daemon = True - t.start() + og = pctl.multi_playlist[pl].playlist_ids + og.sort(key=lambda x: pctl.get_track(x).index) - while statuses.count(2) != len(statuses): - time.sleep(0.1) + reload_albums() + tree_view_box.clear_target_pl(pl) - for sset in songsets: - for nt, name, song_id, rating in sset: +def imported_sort_folders(pl: int) -> None: + if pl_is_locked(pl): + show_message(_("Playlist is locked")) + return - id = pctl.master_count + og = pctl.multi_playlist[pl].playlist_ids + og.sort(key=lambda x: pctl.get_track(x).index) - replace_existing = False - ex = existing.get(song_id) - if ex is not None: - id = ex - replace_existing = True + first_occurrences = {} + for i, x in enumerate(og): + b = pctl.get_track(x).parent_folder_path + if b not in first_occurrences: + first_occurrences[b] = i - nt.index = id - pctl.master_library[id] = nt - if not replace_existing: - pctl.master_count += 1 + og.sort(key=lambda x: first_occurrences[pctl.get_track(x).parent_folder_path]) - playlist.append(nt.index) + reload_albums() + tree_view_box.clear_target_pl(pl) - if star_store.get_rating(nt.index) == 0 and rating == 0: - pass - else: - star_store.set_rating(nt.index, rating * 2) +def standard_sort(pl: int) -> None: + if pl_is_locked(pl): + show_message(_("Playlist is locked")) + return - self.scanning = False - if return_list: - return playlist + sort_path_pl(pl) + sort_track_2(pl) + reload_albums() + tree_view_box.clear_target_pl(pl) - pctl.multi_playlist.append(pl_gen(title=_("Airsonic Collection"), playlist_ids=playlist)) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "air" - switch_playlist(len(pctl.multi_playlist) - 1) +def year_s(plt): + sorted_temp = sorted(plt, key=lambda x: x[1]) + temp = [] - # def get_music2(self, return_list=False): - # - # self.scanning = True - # gui.to_got = 0 - # - # existing = {} - # - # for track_id, track in pctl.master_library.items(): - # if track.is_network and track.file_ext == "SUB": - # existing[track.url_key] = track_id - # - # try: - # a = self.r("getIndexes") - # except Exception: - # show_message(_("Error connecting to Airsonic server"), mode="error") - # self.scanning = False - # return [] - # - # b = a["subsonic-response"]["indexes"]["index"] - # - # folders = [] - # - # for letter in b: - # artists = letter["artist"] - # for artist in artists: - # folders.append(( - # artist["id"], - # artist["name"] - # )) - # - # playlist = [] - # - # def get(folder_id, name): - # - # try: - # d = self.r("getMusicDirectory", p={"id": folder_id}) - # if "child" not in d["subsonic-response"]["directory"]: - # return - # - # except json.decoder.JSONDecodeError: - # logging.error("Error reading Airsonic directory") - # show_message(_("Error reading Airsonic directory!)", mode="warning") - # return - # - # items = d["subsonic-response"]["directory"]["child"] - # - # gui.update = 1 - # - # for item in items: - # - # gui.to_got += 1 - # - # if item["isDir"]: - # get(item["id"], item["title"]) - # continue - # - # song = item - # id = pctl.master_count - # - # replace_existing = False - # ex = existing.get(song["id"]) - # if ex is not None: - # id = ex - # replace_existing = True - # - # nt = TrackClass() - # - # if "title" in song: - # nt.title = song["title"] - # if "artist" in song: - # nt.artist = song["artist"] - # if "album" in song: - # nt.album = song["album"] - # if "track" in song: - # nt.track_number = song["track"] - # if "year" in song: - # nt.date = str(song["year"]) - # if "duration" in song: - # nt.length = song["duration"] - # - # # if "bitRate" in song: - # # nt.bitrate = song["bitRate"] - # - # nt.file_ext = "SUB" - # - # nt.index = id - # - # nt.parent_folder_name = name - # if "path" in song: - # nt.fullpath = song["path"] - # nt.parent_folder_path = os.path.dirname(song["path"]) - # - # if "coverArt" in song: - # nt.art_url_key = song["id"] - # - # nt.url_key = song["id"] - # nt.is_network = True - # - # pctl.master_library[id] = nt - # - # if not replace_existing: - # pctl.master_count += 1 - # - # playlist.append(nt.index) - # - # for id, name in folders: - # get(id, name) - # - # self.scanning = False - # if return_list: - # return playlist - # - # pctl.multi_playlist.append(pl_gen(title="Airsonic Collection", playlist_ids=playlist)) - # pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "air" - # switch_playlist(len(pctl.multi_playlist) - 1) + for album in sorted_temp: + temp += album[0] + return temp +def year_sort(pl: int, custom_list=None): + if custom_list: + playlist = custom_list + else: + playlist = pctl.multi_playlist[pl].playlist_ids + plt = [] + pl2 = [] + artist = "" + album_artist = "" -subsonic = SubsonicService() + p = 0 + while p < len(playlist): + track = get_object(playlist[p]) -class KoelService: + if track.artist != artist: + if album_artist and track.album_artist and album_artist == track.album_artist: + pass + elif len(artist) > 5 and artist.lower() in track.parent_folder_name.lower(): + pass + else: + artist = track.artist + pl2 += year_s(plt) + plt = [] - def __init__(self) -> None: - self.connected: bool = False - self.resource = None - self.scanning: bool = False - self.server: str = "" + if track.album_artist: + album_artist = track.album_artist - self.token: str = "" + if p > len(playlist) - 1: + break - def connect(self) -> None: + album = [] + on = get_object(playlist[p]).parent_folder_path + album.append(playlist[p]) + t = 1 - logging.info("Connect to koel...") - if not prefs.koel_username or not prefs.koel_password or not prefs.koel_server_url: - show_message(_("Missing username, password and/or server URL"), mode="warning") - self.scanning = False - return + while t + p < len(playlist) - 1 and get_object(playlist[p + t]).parent_folder_path == on: + album.append(playlist[p + t]) + t += 1 - if self.token: - self.connected = True - logging.info("Already authorised") - return + date = get_object(playlist[p]).date - password = prefs.koel_password - username = prefs.koel_username - server = prefs.koel_server_url - self.server = server + # If date is xx-xx-yyyy format, just grab the year from the end + # so that the M and D don't interfere with the sorter + if len(date) > 4 and date[-4:].isnumeric(): + date = date[-4:] - target = server + "/api/me" + # If we don't have a date, see if we can grab one from the folder name + # following the format: (XXXX) + if date == "": + pfn = get_object(playlist[p]).parent_folder_name + if len(pfn) > 6 and pfn[-1] == ")" and pfn[-6] == "(": + date = pfn[-5:-1] - headers = { - "Accept": "application/json", - "Content-Type": "application/json", - } - body = { - "email": username, - "password": password, - } + plt.append((album, date, artist + " " + get_object(playlist[p]).album)) + p += len(album) + #logging.info(album) - try: - r = requests.post(target, json=body, headers=headers, timeout=10) - except Exception: - logging.exception("Could not establish connection") - gui.show_message(_("Could not establish connection"), mode="error") - return + if plt: + pl2 += year_s(plt) + plt = [] - if r.status_code == 200: - # logging.info(r.json()) - self.token = r.json()["token"] - if self.token: - logging.info("GOT KOEL TOKEN") - self.connected = True + if custom_list is not None: + return pl2 - else: - logging.info("AUTH ERROR") + # We can't just assign the playlist because it may disconnect the 'pointer' default_playlist + pctl.multi_playlist[pl].playlist_ids[:] = pl2[:] + reload_albums() + tree_view_box.clear_target_pl(pl) - else: - error = "" - j = r.json() - if "message" in j: - error = j["message"] +def pl_toggle_playlist_break(ref): + pctl.multi_playlist[ref].hide_title ^= 1 + gui.pl_update = 1 - gui.show_message(_("Could not establish connection/authorisation"), error, mode="error") +def gen_unique_pl_title(base: str, extra: str="", start: int = 1) -> str: + ex = start + title = base + while ex < 100: + for playlist in pctl.multi_playlist: + if playlist.title == title: + ex += 1 + if ex == 1: + title = base + " (" + extra.rstrip(" ") + ")" + else: + title = base + " (" + extra + str(ex) + ")" + break + else: + break + return title +def new_playlist(switch: bool = True) -> int | None: + if gui.radio_view: + r = {} + r["uid"] = uid_gen() + r["name"] = _("New Radio List") + r["items"] = [] # copy.copy(prefs.radio_urls) + r["scroll"] = 0 + pctl.radio_playlists.append(r) + return None - def resolve_stream(self, id: str) -> tuple[str, dict[str, str]]: + title = gen_unique_pl_title(_("New Playlist")) - if not self.connected: - self.connect() + top_panel.prime_side = 1 + top_panel.prime_tab = len(pctl.multi_playlist) - if prefs.network_stream_bitrate > 0: - target = f"{self.server}/api/{id}/play/1/{prefs.network_stream_bitrate}" - else: - target = f"{self.server}/api/{id}/play/0/0" - params = {"jwt-token": self.token } + pctl.multi_playlist.append(pl_gen(title=title)) # [title, 0, [], 0, 0, 0]) + if switch: + switch_playlist(len(pctl.multi_playlist) - 1) + return len(pctl.multi_playlist) - 1 - # if prefs.network_stream_bitrate > 0: - # target = f"{self.server}/api/play/{id}/1/{prefs.network_stream_bitrate}" - # else: - #target = f"{self.server}/api/play/{id}/0/0" - #target = f"{self.server}/api/{id}/play" +def append_deco(): + if pctl.playing_state > 0: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - #params = {"token": self.token, } + text = None + if tauon.spot_ctl.coasting: + text = _("Add Spotify Album") - #target = f"{self.server}/api/download/songs" - #params["songs"] = [id,] - logging.info(target) - logging.info(urllib.parse.urlencode(params)) + return [line_colour, colours.menu_background, text] - return target, params +def rescan_deco(pl: int): + if pctl.multi_playlist[pl].last_folder: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - def listen(self, track_object: TrackClass, submit: bool = False) -> None: - if submit: - try: - target = self.server + "/api/interaction/play" - headers = { - "Authorization": "Bearer " + self.token, - "Accept": "application/json", - "Content-Type": "application/json", - } - - r = requests.post(target, headers=headers, json={"song": track_object.url_key}, timeout=10) - # logging.info(r.status_code) - # logging.info(r.text) - except Exception: - logging.exception("error submitting listen to koel") + # base = os.path.basename(pctl.multi_playlist[pl].last_folder) + return [line_colour, colours.menu_background, None] - def get_albums(self, return_list: bool = False) -> list[int] | None: +def regenerate_deco(pl: int): + id = pl_to_id(pl) + value = pctl.gen_codes.get(id) - gui.update += 1 - self.scanning = True + if value: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - if not self.connected: - self.connect() + return [line_colour, colours.menu_background, None] - if not self.connected: - self.scanning = False - return [] +def parse_generator(string: str): + cmds = [] + quotes = [] + current = "" + q_string = "" + inquote = False + for cha in string: + if not inquote and cha == " ": + if current: + cmds.append(current) + quotes.append(q_string) + q_string = "" + current = "" + continue + if cha == "\"": + inquote ^= True - playlist = [] + current += cha - target = self.server + "/api/data" - headers = { - "Authorization": "Bearer " + self.token, - "Accept": "application/json", - "Content-Type": "application/json", - } + if inquote and cha != "\"": + q_string += cha - r = requests.get(target, headers=headers, timeout=10) - data = r.json() + if current: + cmds.append(current) + quotes.append(q_string) - artists = data["artists"] - albums = data["albums"] - songs = data["songs"] + return cmds, quotes, inquote - artist_ids = {} - for artist in artists: - id = artist["id"] - if id not in artist_ids: - artist_ids[id] = artist["name"] +def upload_spotify_playlist(pl: int): + p_id = pl_to_id(pl) + string = pctl.gen_codes.get(p_id) + id = None + if string: + cmds, quotes, inquote = parse_generator(string) + for i, cm in enumerate(cmds): + if cm.startswith("spl\""): + id = quotes[i] + break - album_ids = {} - covers = {} - for album in albums: - id = album["id"] - if id not in album_ids: - album_ids[id] = album["name"] - if "cover" in album: - covers[id] = album["cover"] + urls = [] + playlist = pctl.multi_playlist[pl].playlist_ids - existing = {} + warn = False + for track_id in playlist: + tr = pctl.get_track(track_id) + url = tr.misc.get("spotify-track-url") + if not url: + warn = True + continue + urls.append(url) - for track_id, track in pctl.master_library.items(): - if track.is_network and track.file_ext == "KOEL": - existing[track.url_key] = track_id + if warn: + show_message(_("Playlist contains non-Spotify tracks"), mode="error") + return - for song in songs: + new = False + if id is None: + name = pctl.multi_playlist[pl].title.split(" by ")[0] + show_message(_("Created new Spotify playlist"), name, mode="done") + id = tauon.spot_ctl.create_playlist(name) + if id: + new = True + pctl.gen_codes[p_id] = "spl\"" + id + "\"" + if id is None: + show_message(_("Error creating Spotify playlist")) + return + if not new: + show_message(_("Updated Spotify playlist"), mode="done") + tauon.spot_ctl.upload_playlist(id, urls) - id = pctl.master_count - replace_existing = False +def regenerate_playlist(pl: int = -1, silent: bool = False, id: int | None = None) -> None: + if id is None and pl == -1: + return - e = existing.get(song["id"]) - if e is not None: - id = e - replace_existing = True + if id is None: + id = pl_to_id(pl) - nt = TrackClass() + if pl == -1: + pl = id_to_pl(id) + if pl is None: + return - nt.title = song["title"] - nt.index = id - if "track" in song and song["track"] is not None: - nt.track_number = song["track"] - if "disc" in song and song["disc"] is not None: - nt.disc = song["disc"] - nt.length = float(song["length"]) + source_playlist = pctl.multi_playlist[pl].playlist_ids - nt.artist = artist_ids.get(song["artist_id"], "") - nt.album = album_ids.get(song["album_id"], "") - nt.parent_folder_name = (nt.artist + " - " + nt.album).strip("- ") - nt.parent_folder_path = nt.album + "/" + nt.parent_folder_name + string = pctl.gen_codes.get(id) + if not string: + if not silent: + show_message(_("This playlist has no generator")) + return - nt.art_url_key = covers.get(song["album_id"], "") - nt.url_key = song["id"] + cmds, quotes, inquote = parse_generator(string) - nt.is_network = True - nt.file_ext = "KOEL" + if inquote: + gui.gen_code_errors = "close" + return - pctl.master_library[id] = nt + playlist = [] + selections = [] + errors = False + selections_searched = 0 - if not replace_existing: - pctl.master_count += 1 + def is_source_type(code: str | None) -> bool: + return \ + code is None or \ + code == "" or \ + code.startswith(("self", "jelly", "plex", "koel", "tau", "air", "sal")) - playlist.append(nt.index) + #logging.info(cmds) + #logging.info(quotes) - self.scanning = False + pctl.regen_in_progress = True - if return_list: - return playlist + for i, cm in enumerate(cmds): - pctl.multi_playlist.append(pl_gen(title=_("Koel Collection"), playlist_ids=playlist)) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "koel path tn" - standard_sort(len(pctl.multi_playlist) - 1) - switch_playlist(len(pctl.multi_playlist) - 1) + quote = quotes[i] + if cm.startswith("\"") and (cm.endswith((">", "<"))): + cm_found = False -koel = KoelService() -tauon.koel = koel + for col in column_names: + if quote.lower() == col.lower() or _(quote).lower() == col.lower(): + cm_found = True -class TauService: - def __init__(self) -> None: - self.processing = False + if cm[-1] == ">": + sort_ass(0, invert=False, custom_list=playlist, custom_name=col) + elif cm[-1] == "<": + sort_ass(0, invert=True, custom_list=playlist, custom_name=col) + break + if cm_found: + continue - def resolve_stream(self, key: str) -> str: - return "http://" + prefs.sat_url + ":7814/api1/file/" + key + elif cm == "self": + selections.append(pctl.multi_playlist[pl].playlist_ids) - def resolve_picture(self, key: str) -> str: - return "http://" + prefs.sat_url + ":7814/api1/pic/medium/" + key + elif cm == "auto": + pass - def get(self, point: str): - url = "http://" + prefs.sat_url + ":7814/api1/" - data = None - try: - r = requests.get(url + point, timeout=10) - data = r.json() - except Exception as e: - logging.exception("Network error") - show_message(_("Network error"), str(e), mode="error") - return data - - def get_playlist(self, playlist_name: str | None = None, return_list: bool = False) -> list[int] | None: - - p = self.get("playlists") - - if not p or not p["playlists"]: - self.processing = False - return [] - - if playlist_name is None: - playlist_name = text_sat_playlist.text.strip() - if not playlist_name: - show_message(_("No playlist name")) - return [] - - id = None - name = "" - for pp in p["playlists"]: - if pp["name"].lower() == playlist_name.lower(): - id = pp["id"] - name = pp["name"] - - if id is None: - show_message(_("Playlist not found on target"), mode="error") - self.processing = False - return [] + elif cm.startswith("spl\""): + playlist.extend(tauon.spot_ctl.playlist(quote, return_list=True)) - try: - t = self.get("tracklist/" + id) - except Exception: - logging.exception("error getting tracklist") - return [] - at = t["tracks"] + elif cm.startswith("tpl\""): + playlist.extend(tauon.tidal.playlist(quote, return_list=True)) - exist = {} - for k, v in pctl.master_library.items(): - if v.is_network and v.file_ext == "TAU": - exist[v.url_key] = k + elif cm == "tfa": + playlist.extend(tauon.tidal.fav_albums(return_list=True)) - playlist = [] - for item in at: - replace_existing = True + elif cm == "tft": + playlist.extend(tauon.tidal.fav_tracks(return_list=True)) - tid = item["id"] - id = exist.get(str(tid)) - if id is None: - id = pctl.master_count - replace_existing = False + elif cm.startswith("tar\""): + playlist.extend(tauon.tidal.artist(quote, return_list=True)) - nt = TrackClass() - nt.index = id - nt.title = item.get("title", "") - nt.artist = item.get("artist", "") - nt.album = item.get("album", "") - nt.album_artist = item.get("album_artist", "") - nt.length = int(item.get("duration", 0) / 1000) - nt.track_number = item.get("track_number", 0) - - nt.fullpath = item.get("path", "") - nt.filename = os.path.basename(nt.fullpath) - nt.parent_folder_name = os.path.basename(os.path.dirname(nt.fullpath)) - nt.parent_folder_path = os.path.dirname(nt.fullpath) - - nt.url_key = str(tid) - nt.art_url_key = str(tid) - - nt.is_network = True - nt.file_ext = "TAU" - pctl.master_library[id] = nt - - if not replace_existing: - pctl.master_count += 1 - playlist.append(nt.index) - - if return_list: - self.processing = False - return playlist - - pctl.multi_playlist.append(pl_gen(title=name, playlist_ids=playlist)) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "tau path tn" - standard_sort(len(pctl.multi_playlist) - 1) - switch_playlist(len(pctl.multi_playlist) - 1) - self.processing = False + elif cm.startswith("tmix\""): + playlist.extend(tauon.tidal.mix(quote, return_list=True)) + elif cm == "sal": + playlist.extend(tauon.spot_ctl.get_library_albums(return_list=True)) -tau = TauService() -tauon.tau = tau + elif cm == "slt": + playlist.extend(tauon.spot_ctl.get_library_likes(return_list=True)) + elif cm == "plex": + if not plex.scanning: + playlist.extend(plex.get_albums(return_list=True)) -def get_network_thumbnail_url(track_object: TrackClass): - if track_object.file_ext == "TIDAL": - return track_object.art_url_key - if track_object.file_ext == "SPTY": - return track_object.art_url_key - if track_object.file_ext == "PLEX": - url = plex.resolve_thumbnail(track_object.art_url_key) - assert url is not None - return url -# if track_object.file_ext == "JELY": -# url = jellyfin.resolve_thumbnail(track_object.art_url_key) -# assert url is not None -# assert url != "" -# return url - if track_object.file_ext == "KOEL": - url = track_object.art_url_key - assert url - return url - if track_object.file_ext == "TAU": - url = tau.resolve_picture(track_object.art_url_key) - assert url - return url + elif cm.startswith("jelly\""): + if not jellyfin.scanning: + playlist.extend(jellyfin.get_playlist(quote, return_list=True)) - return None - - -def jellyfin_get_playlists_thread() -> None: - if jellyfin.scanning: - inp.mouse_click = False - show_message(_("Job already in progress!")) - return - jellyfin.scanning = True - shoot_dl = threading.Thread(target=jellyfin.get_playlists) - shoot_dl.daemon = True - shoot_dl.start() - -def jellyfin_get_library_thread() -> None: - pref_box.close() - save_prefs() - if jellyfin.scanning: - inp.mouse_click = False - show_message(_("Job already in progress!")) - return + elif cm == "jelly": + if not jellyfin.scanning: + playlist.extend(jellyfin.ingest_library(return_list=True)) - jellyfin.scanning = True - shoot_dl = threading.Thread(target=jellyfin.ingest_library) - shoot_dl.daemon = True - shoot_dl.start() + elif cm == "koel": + if not koel.scanning: + playlist.extend(koel.get_albums(return_list=True)) + elif cm == "tau": + if not tau.processing: + playlist.extend(tau.get_playlist(pctl.multi_playlist[pl].title, return_list=True)) -def plex_get_album_thread() -> None: - pref_box.close() - save_prefs() - if plex.scanning: - inp.mouse_click = False - show_message(_("Already scanning!")) - return - plex.scanning = True + elif cm == "air": + if not subsonic.scanning: + playlist.extend(subsonic.get_music3(return_list=True)) - shoot_dl = threading.Thread(target=plex.get_albums) - shoot_dl.daemon = True - shoot_dl.start() + elif cm == "a": + if not selections and not selections_searched: + for plist in pctl.multi_playlist: + code = pctl.gen_codes.get(plist.uuid_int) + if is_source_type(code): + selections.append(plist.playlist_ids) + temp = [] + for selection in selections: + temp += selection -def sub_get_album_thread() -> None: - # if prefs.backend != 1: - # show_message("This feature is currently only available with the BASS backend") - # return + playlist += list(OrderedDict.fromkeys(temp)) + selections.clear() - pref_box.close() - save_prefs() - if subsonic.scanning: - inp.mouse_click = False - show_message(_("Already scanning!")) - return - subsonic.scanning = True + elif cm == "cue": - shoot_dl = threading.Thread(target=subsonic.get_music3) - shoot_dl.daemon = True - shoot_dl.start() + for i in reversed(range(len(playlist))): + tr = pctl.get_track(playlist[i]) + if not tr.is_cue: + del playlist[i] + playlist = list(OrderedDict.fromkeys(playlist)) + elif cm == "today": + d = datetime.date.today() + for i in reversed(range(len(playlist))): + tr = pctl.get_track(playlist[i]) + if tr.date[5:7] != f"{d:%m}" or tr.date[8:10] != f"{d:%d}": + del playlist[i] + playlist = list(OrderedDict.fromkeys(playlist)) -def koel_get_album_thread() -> None: - # if prefs.backend != 1: - # show_message("This feature is currently only available with the BASS backend") - # return + elif cm.startswith("com\""): + for i in reversed(range(len(playlist))): + tr = pctl.get_track(playlist[i]) + if quote not in tr.comment: + del playlist[i] + playlist = list(OrderedDict.fromkeys(playlist)) - pref_box.close() - save_prefs() - if koel.scanning: - inp.mouse_click = False - show_message(_("Already scanning!")) - return - koel.scanning = True + elif cm.startswith("ext"): + value = quote.upper() + if value: + if not selections: + for plist in pctl.multi_playlist: + selections.append(plist.playlist_ids) - shoot_dl = threading.Thread(target=koel.get_albums) - shoot_dl.daemon = True - shoot_dl.start() + temp = [] + for selection in selections: + for track in selection: + tr = pctl.get_track(track) + if tr.file_ext == value: + temp.append(track) + playlist += list(OrderedDict.fromkeys(temp)) -if system == "Windows" or msys: - from lynxtray import SysTrayIcon + elif cm == "ypa": + playlist = year_sort(0, playlist) + elif cm == "tn": + sort_track_2(0, playlist) -class STray: + elif cm == "ia>": + playlist = gen_last_imported_folders(0, playlist) - def __init__(self) -> None: - self.active = False + elif cm == "ia<": + playlist = gen_last_imported_folders(0, playlist, reverse=True) - def up(self, systray: SysTrayIcon): - SDL_ShowWindow(t_window) - SDL_RaiseWindow(t_window) - SDL_RestoreWindow(t_window) - gui.lowered = False + elif cm == "m>": + playlist = gen_last_modified(0, playlist) - def down(self) -> None: - if self.active: - SDL_HideWindow(t_window) + elif cm == "m<": + playlist = gen_last_modified(0, playlist, reverse=False) - def advance(self, systray: SysTrayIcon) -> None: - pctl.advance() + elif cm == "ly" or cm == "lyrics": + playlist = gen_lyrics(0, playlist) - def back(self, systray: SysTrayIcon) -> None: - pctl.back() + elif cm == "l" or cm == "love" or cm == "loved": + playlist = gen_love(0, playlist) - def pause(self, systray: SysTrayIcon) -> None: - pctl.play_pause() + elif cm == "clr": + selections.clear() - def track_stop(self, systray: SysTrayIcon) -> None: - pctl.stop() + elif cm == "rv" or cm == "reverse": + playlist = gen_reverse(0, playlist) - def on_quit_callback(self, systray: SysTrayIcon) -> None: - tauon.exit("Exit called from tray.") + elif cm == "rva": + playlist = gen_folder_reverse(0, playlist) - def start(self) -> None: - menu_options = (("Show", None, self.up), - ("Play/Pause", None, self.pause), - ("Stop", None, self.track_stop), - ("Forward", None, self.advance), - ("Back", None, self.back)) - self.systray = SysTrayIcon( - str(install_directory / "assets" / "icon.ico"), "Tauon Music Box", - menu_options, on_quit=self.on_quit_callback) - self.systray.start() - self.active = True - gui.tray_active = True + elif cm == "rata>": - def stop(self) -> None: - self.systray.shutdown() - self.active = False + playlist = gen_folder_top_rating(0, custom_list=playlist) + elif cm == "rat>": -tray = STray() + def rat_key(track_id): + return star_store.get_rating(track_id) -if system == "Linux" and not macos and not msys: + playlist = sorted(playlist, key=rat_key, reverse=True) - gnome = Gnome(tauon) + elif cm == "rat<": - try: - gnomeThread = threading.Thread(target=gnome.main) - gnomeThread.daemon = True - gnomeThread.start() - except Exception: - logging.exception("Could not start Dbus thread") + def rat_key(track_id): + return star_store.get_rating(track_id) -if (system == "Windows" or msys): + playlist = sorted(playlist, key=rat_key) - tray.start() + elif cm[:4] == "rat=": + value = cm[4:] + try: + value = float(value) * 2 + temp = [] + for item in playlist: + if value == star_store.get_rating(item): + temp.append(item) + playlist = temp + except Exception: + logging.exception("Failed to get rating") + errors = True - if win_ver < 10: - logging.warning("Unsupported Windows version older than W10, hooking media keys the old way without SMTC!") - import keyboard + elif cm[:4] == "rat<": + value = cm[4:] + try: + value = float(value) * 2 + temp = [] + for item in playlist: + if value > star_store.get_rating(item): + temp.append(item) + playlist = temp + except Exception: + logging.exception("Failed to get rating") + errors = True - def key_callback(event): + elif cm[:4] == "rat>": + value = cm[4:] + try: + value = float(value) * 2 + temp = [] + for item in playlist: + if value < star_store.get_rating(item): + temp.append(item) + playlist = temp + except Exception: + logging.exception("Failed to get rating") + errors = True - if event.event_type == "down": - if event.scan_code == -179: - inp.media_key = "Play" - elif event.scan_code == -178: - inp.media_key = "Stop" - elif event.scan_code == -177: - inp.media_key = "Previous" - elif event.scan_code == -176: - inp.media_key = "Next" - gui.update += 1 - tauon.wake() + elif cm == "rat": + temp = [] + for item in playlist: + # tr = pctl.get_track(item) + if star_store.get_rating(item) > 0: + temp.append(item) + playlist = temp - keyboard.hook_key(-179, key_callback) - keyboard.hook_key(-178, key_callback) - keyboard.hook_key(-177, key_callback) - keyboard.hook_key(-176, key_callback) + elif cm == "norat": + temp = [] + for item in playlist: + if star_store.get_rating(item) == 0: + temp.append(item) + playlist = temp + elif cm == "d>": + playlist = gen_sort_len(0, custom_list=playlist) -class GStats: - def __init__(self): + elif cm == "d<": + playlist = gen_sort_len(0, custom_list=playlist) + playlist = list(reversed(playlist)) - self.last_db = 0 - self.last_pl = 0 - self.artist_list = [] - self.album_list = [] - self.genre_list = [] - self.genre_dict = {} + elif cm[:2] == "d<": + value = cm[2:] + if value and value.isdigit(): + value = int(value) + for i in reversed(range(len(playlist))): + tr = pctl.get_track(playlist[i]) + if not value > tr.length: + del playlist[i] - def update(self, playlist): + elif cm[:2] == "d>": + value = cm[2:] + if value and value.isdigit(): + value = int(value) + for i in reversed(range(len(playlist))): + tr = pctl.get_track(playlist[i]) + if not value < tr.length: + del playlist[i] - pt = 0 + elif cm == "path": + sort_path_pl(0, custom_list=playlist) - if pctl.master_count != self.last_db or self.last_pl != playlist: - self.last_db = pctl.master_count - self.last_pl = playlist + elif cm == "pa>": + playlist = gen_folder_top(0, custom_list=playlist) - artists = {} + elif cm == "pa<": + playlist = gen_folder_top(0, custom_list=playlist) + playlist = gen_folder_reverse(0, playlist) - for index in pctl.multi_playlist[playlist].playlist_ids: - artist = pctl.master_library[index].artist + elif cm == "pt>" or cm == "pc>": + playlist = gen_top_100(0, custom_list=playlist) - if artist == "": - artist = "<Artist Unspecified>" + elif cm == "pt<" or cm == "pc<": + playlist = gen_top_100(0, custom_list=playlist) + playlist = list(reversed(playlist)) - pt = int(star_store.get(index)) - if pt < 30: - continue + elif cm[:3] == "pt>": + value = cm[3:] + if value and value.isdigit(): + value = int(value) + for i in reversed(range(len(playlist))): + t_time = star_store.get(playlist[i]) + if t_time < value: + del playlist[i] - if artist in artists: - artists[artist] += pt - else: - artists[artist] = pt - - art_list = artists.items() - - sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) - - self.artist_list = copy.deepcopy(sorted_list) - - genres = {} - genre_dict = {} - - for index in pctl.multi_playlist[playlist].playlist_ids: - genre_r = pctl.master_library[index].genre - - pt = int(star_store.get(index)) - - gn = [] - if "," in genre_r: - for g in genre_r.split(","): - g = g.rstrip(" ").lstrip(" ") - if len(g) > 0: - gn.append(g) - elif ";" in genre_r: - for g in genre_r.split(";"): - g = g.rstrip(" ").lstrip(" ") - if len(g) > 0: - gn.append(g) - elif "/" in genre_r: - for g in genre_r.split("/"): - g = g.rstrip(" ").lstrip(" ") - if len(g) > 0: - gn.append(g) - elif " & " in genre_r: - for g in genre_r.split(" & "): - g = g.rstrip(" ").lstrip(" ") - if len(g) > 0: - gn.append(g) - else: - gn = [genre_r] - - pt = int(pt / len(gn)) - - for genre in gn: - - if genre.lower() in {"", "other", "unknown", "misc"}: - genre = "<Genre Unspecified>" - if genre.lower() in {"jpop", "japanese pop"}: - genre = "J-Pop" - if genre.lower() in {"jrock", "japanese rock"}: - genre = "J-Rock" - if genre.lower() in {"alternative music", "alt-rock", "alternative", "alternrock", "alt"}: - genre = "Alternative Rock" - if genre.lower() in {"jpunk", "japanese punk"}: - genre = "J-Punk" - if genre.lower() in {"post rock", "post-rock"}: - genre = "Post-Rock" - if genre.lower() in {"video game", "game", "game music", "video game music", "game ost"}: - genre = "Video Game Soundtrack" - if genre.lower() in {"general soundtrack", "ost", "Soundtracks"}: - genre = "Soundtrack" - if genre.lower() in ("anime", "アニメ", "anime ost"): - genre = "Anime Soundtrack" - if genre.lower() in {"同人"}: - genre = "Doujin" - if genre.lower() in {"chill, chill out", "chill-out"}: - genre = "Chillout" - - genre = genre.title() - - if len(genre) == 3 and genre[2] == "m": - genre = genre.upper() - - if genre in genres: - - genres[genre] += pt - else: - genres[genre] = pt + elif cm[:3] == "pt<": + value = cm[3:] + if value and value.isdigit(): + value = int(value) + for i in reversed(range(len(playlist))): + t_time = star_store.get(playlist[i]) + if t_time > value: + del playlist[i] - if genre in genre_dict: - genre_dict[genre].append(index) - else: - genre_dict[genre] = [index] - - art_list = genres.items() - sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) + elif cm[:3] == "pc>": + value = cm[3:] + if value and value.isdigit(): + value = int(value) + for i in reversed(range(len(playlist))): + t_time = star_store.get(playlist[i]) + tr = pctl.get_track(playlist[i]) + if tr.length > 0: + if not value < t_time / tr.length: + del playlist[i] - self.genre_list = copy.deepcopy(sorted_list) - self.genre_dict = genre_dict + elif cm[:3] == "pc<": + value = cm[3:] + if value and value.isdigit(): + value = int(value) + for i in reversed(range(len(playlist))): + t_time = star_store.get(playlist[i]) + tr = pctl.get_track(playlist[i]) + if tr.length > 0: + if not value > t_time / tr.length: + del playlist[i] - # logging.info('\n-----------------------\n') + elif cm == "y<": + playlist = gen_sort_date(0, False, playlist) - g_albums = {} + elif cm == "y>": + playlist = gen_sort_date(0, True, playlist) - for index in pctl.multi_playlist[playlist].playlist_ids: - album = pctl.master_library[index].album + elif cm[:2] == "y=": + value = cm[2:] + if value: + temp = [] + for item in playlist: + if value in pctl.master_library[item].date: + temp.append(item) + playlist = temp - if album == "": - album = "<Album Unspecified>" + elif cm[:3] == "y>=": + value = cm[3:] + if value and value.isdigit(): + value = int(value) + temp = [] + for item in playlist: + if pctl.master_library[item].date[:4].isdigit() and int( + pctl.master_library[item].date[:4]) >= value: + temp.append(item) + playlist = temp - pt = int(star_store.get(index)) + elif cm[:3] == "y<=": + value = cm[3:] + if value and value.isdigit(): + value = int(value) + temp = [] + for item in playlist: + if pctl.master_library[item].date[:4].isdigit() and int( + pctl.master_library[item].date[:4]) <= value: + temp.append(item) + playlist = temp - if pt < 30: - continue + elif cm[:2] == "y>": + value = cm[2:] + if value and value.isdigit(): + value = int(value) + temp = [] + for item in playlist: + if pctl.master_library[item].date[:4].isdigit() and int(pctl.master_library[item].date[:4]) > value: + temp.append(item) + playlist = temp - if album in g_albums: - g_albums[album] += pt - else: - g_albums[album] = pt + elif cm[:2] == "y<": + value = cm[2:] + if value and value.isdigit: + value = int(value) + temp = [] + for item in playlist: + if pctl.master_library[item].date[:4].isdigit() and int(pctl.master_library[item].date[:4]) < value: + temp.append(item) + playlist = temp - art_list = g_albums.items() + elif cm == "st" or cm == "rt" or cm == "r": + random.shuffle(playlist) - sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) + elif cm == "sf" or cm == "rf" or cm == "ra" or cm == "sa": + playlist = gen_folder_shuffle(0, custom_list=playlist) - self.album_list = copy.deepcopy(sorted_list) + elif cm.startswith("n"): + value = cm[1:] + if value.isdigit(): + playlist = playlist[:int(value)] + # SEARCH FOLDER + elif cm.startswith("p\"") and len(cm) > 3: -stats_gen = GStats() + if not selections: + for plist in pctl.multi_playlist: + code = pctl.gen_codes.get(plist.uuid_int) + if is_source_type(code): + selections.append(plist.playlist_ids) + search = quote + search_over.all_folders = True + search_over.sip = True + search_over.search_text.text = search + if worker2_lock.locked(): + try: + worker2_lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked worker2_lock") + else: + logging.exception("Unknown RuntimeError trying to release worker2_lock") + except Exception: + logging.exception("Unknown error trying to release worker2_lock") + while search_over.sip: + time.sleep(0.01) -def do_exit_button() -> None: - if mouse_up or ab_click: - if gui.tray_active and prefs.min_to_tray: - if key_shift_down: - tauon.exit("User clicked X button with shift key") - return - tauon.min_to_tray() - elif gui.sync_progress and not gui.stop_sync: - show_message(_("Stop the sync before exiting!")) - else: - tauon.exit("User clicked X button") + found_name = "" + for result in search_over.results: + if result[0] == 5: + found_name = result[1] + break + else: + logging.info("No folder search result found") + continue -def do_maximize_button() -> None: - global mouse_down - global drag_mode - if gui.fullscreen: - gui.fullscreen = False - SDL_SetWindowFullscreen(t_window, 0) - elif gui.maximized: - gui.maximized = False - SDL_RestoreWindow(t_window) - else: - gui.maximized = True - SDL_MaximizeWindow(t_window) + search_over.clear() - mouse_down = False - inp.mouse_click = False - drag_mode = False + playlist += search_over.click_meta(found_name, get_list=True, search_lists=selections) + # SEARCH GENRE + elif (cm.startswith(('g"', 'gm"', 'g="'))) and len(cm) > 3: -def do_minimize_button(): + if not selections: + for plist in pctl.multi_playlist: + code = pctl.gen_codes.get(plist.uuid_int) + if is_source_type(code): + selections.append(plist.playlist_ids) - global mouse_down - global drag_mode - if macos: - # hack - SDL_SetWindowBordered(t_window, True) - SDL_MinimizeWindow(t_window) - SDL_SetWindowBordered(t_window, False) - else: - SDL_MinimizeWindow(t_window) + g_search = quote.lower().replace("-", "") # .replace(" ", "") - mouse_down = False - inp.mouse_click = False - drag_mode = False + search = g_search + search_over.sip = True + search_over.search_text.text = search + if worker2_lock.locked(): + try: + worker2_lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked worker2_lock") + else: + logging.exception("Unknown RuntimeError trying to release worker2_lock") + except Exception: + logging.exception("Unknown error trying to release worker2_lock") + while search_over.sip: + time.sleep(0.01) + found_name = "" -mac_circle = asset_loader(scaled_asset_directory, loaded_asset_dc, "macstyle.png", True) + if cm.startswith("g=\""): + for result in search_over.results: + if result[0] == 3 and result[1].lower().replace("-", "").replace(" ", "") == g_search: + found_name = result[1] + break + elif cm.startswith("g\"") or not prefs.sep_genre_multi: + for result in search_over.results: + if result[0] == 3: + found_name = result[1] + break + elif cm.startswith("gm\""): + for result in search_over.results: + if result[0] == 3 and result[1].endswith("+"): + found_name = result[1] + break + if not found_name: + logging.warning("No genre search result found") + continue -def draw_window_tools(): - global mouse_down - global drag_mode + search_over.clear() - # rect = (window_size[0] - 55 * gui.scale, window_size[1] - 35 * gui.scale, 53 * gui.scale, 33 * gui.scale) - # fields.add(rect) - # prefs.left_window_control = not key_shift_down - macstyle = gui.macstyle + playlist += search_over.click_genre(found_name, get_list=True, search_lists=selections) - bg_off = colours.window_buttons_bg - bg_on = colours.window_buttons_bg_over - fg_off = colours.window_button_icon_off - fg_on = colours.window_buttons_icon_over - x_on = colours.window_button_x_on - x_off = colours.window_button_x_off + # SEARCH ARTIST + elif cm.startswith("a\"") and len(cm) > 3 and cm != "auto": + if not selections: + for plist in pctl.multi_playlist: + code = pctl.gen_codes.get(plist.uuid_int) + if is_source_type(code): + selections.append(plist.playlist_ids) - h = round(28 * gui.scale) - y = round(1 * gui.scale) - if macstyle: - y = round(9 * gui.scale) + search = quote + search_over.sip = True + search_over.search_text.text = "artist " + search + if worker2_lock.locked(): + try: + worker2_lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked worker2_lock") + else: + logging.exception("Unknown RuntimeError trying to release worker2_lock") + except Exception: + logging.exception("Unknown error trying to release worker2_lock") + while search_over.sip: + time.sleep(0.01) - x_width = round(26 * gui.scale) - ma_width = round(33 * gui.scale) - mi_width = round(35 * gui.scale) - re_width = round(30 * gui.scale) - last_width = 0 + found_name = "" - xx = 0 - l = prefs.left_window_control - r = not l - focused = window_is_focused() + for result in search_over.results: + if result[0] == 0: + found_name = result[1] + break + else: + logging.warning("No artist search result found") + continue - # Close - if r: - xx = window_size[0] - x_width - xx -= round(2 * gui.scale) - - if macstyle: - xx = window_size[0] - 27 * gui.scale - if l: - xx = round(4 * gui.scale) - rect = (xx + 5, y - 1, 14 * gui.scale, 14 * gui.scale) - fields.add(rect) - colour = mac_close - if not focused: - colour = (86, 85, 86, 255) - mac_circle.render(xx + 6 * gui.scale, y, colour) - if coll(rect) and not gui.mouse_unknown: - if coll_point(last_click_location, rect): - do_exit_button() - else: - rect = (xx, y, x_width, h) - last_width = x_width - ddt.rect((rect[0], rect[1], rect[2], rect[3]), bg_off) - fields.add(rect) - if coll(rect) and not gui.mouse_unknown: - ddt.rect((rect[0], rect[1], rect[2], rect[3]), bg_on) - top_panel.exit_button.render(rect[0] + 8 * gui.scale, rect[1] + 8 * gui.scale, x_on) - if coll_point(last_click_location, rect): - do_exit_button() - else: - top_panel.exit_button.render(rect[0] + 8 * gui.scale, rect[1] + 8 * gui.scale, x_off) - - # Macstyle restore - if gui.mode == 3: - if macstyle: - if r: - xx -= round(20 * gui.scale) - if l: - xx += round(20 * gui.scale) - rect = (xx + 5, y - 1, 14 * gui.scale, 14 * gui.scale) + search_over.clear() + # for item in search_over.click_artist(found_name, get_list=True, search_lists=selections): + # playlist.append(item) + playlist += search_over.click_artist(found_name, get_list=True, search_lists=selections) - fields.add(rect) - colour = (160, 55, 225, 255) - if not focused: - colour = (86, 85, 86, 255) - mac_circle.render(xx + 6 * gui.scale, y, colour) - if coll(rect) and not gui.mouse_unknown: - if (mouse_up or ab_click) and coll_point(last_click_location, rect): - restore_full_mode() - gui.update += 2 + elif cm.startswith("ff\""): - # maximize + for i in reversed(range(len(playlist))): + tr = pctl.get_track(playlist[i]) + line = " ".join([tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() - if draw_max_button and gui.mode != 3: - if macstyle: - if r: - xx -= round(20 * gui.scale) - if l: - xx += round(20 * gui.scale) - rect = (xx + 5, y - 1, 14 * gui.scale, 14 * gui.scale) + if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): + line = str(unidecode(line)) - fields.add(rect) - colour = mac_maximize - if not focused: - colour = (86, 85, 86, 255) - mac_circle.render(xx + 6 * gui.scale, y, colour) - if coll(rect) and not gui.mouse_unknown: - if (mouse_up or ab_click) and coll_point(last_click_location, rect): - do_minimize_button() + if not search_magic(quote.lower(), line): + del playlist[i] - else: - if r: - xx -= ma_width - if l: - xx += last_width - rect = (xx, y, ma_width, h) - last_width = ma_width - ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_off) - fields.add(rect) - if coll(rect): - ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_on) - top_panel.maximize_button.render(rect[0] + 10 * gui.scale, rect[1] + 10 * gui.scale, fg_on) - if (mouse_up or ab_click) and coll_point(last_click_location, rect): - do_maximize_button() - else: - top_panel.maximize_button.render(rect[0] + 10 * gui.scale, rect[1] + 10 * gui.scale, fg_off) + playlist = list(OrderedDict.fromkeys(playlist)) - # minimize + elif cm.startswith("fx\""): - if draw_min_button: + for i in reversed(range(len(playlist))): + tr = pctl.get_track(playlist[i]) + line = " ".join( + [tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() + if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): + line = str(unidecode(line)) - # x = window_size[0] - round(65 * gui.scale) - # if draw_max_button and not gui.mode == 3: - # x -= round(34 * gui.scale) - if macstyle: - if r: - xx -= round(20 * gui.scale) - if l: - xx += round(20 * gui.scale) - rect = (xx + 5, y - 1, 14 * gui.scale, 14 * gui.scale) + if search_magic(quote.lower(), line): + del playlist[i] - fields.add(rect) - colour = mac_minimize - if not focused: - colour = (86, 85, 86, 255) - mac_circle.render(xx + 6 * gui.scale, y, colour) - if coll(rect) and not gui.mouse_unknown: - if (mouse_up or ab_click) and coll_point(last_click_location, rect): - do_maximize_button() - else: - if r: - xx -= mi_width - if l: - xx += last_width + elif cm.startswith(('find"', 'f"', 'fs"')): - rect = (xx, y, mi_width, h) - last_width = mi_width - ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_off) - fields.add(rect) - if coll(rect): - ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_on) - ddt.rect_a((rect[0] + 11 * gui.scale, rect[1] + 16 * gui.scale), (14 * gui.scale, 3 * gui.scale), fg_on) - if (mouse_up or ab_click) and coll_point(last_click_location, rect): - do_minimize_button() - else: - ddt.rect_a( - (rect[0] + 11 * gui.scale, rect[1] + 16 * gui.scale), (14 * gui.scale, 3 * gui.scale), fg_off) + if not selections: + for plist in pctl.multi_playlist: + code = pctl.gen_codes.get(plist.uuid_int) + if is_source_type(code): + selections.append(plist.playlist_ids) - # restore + cooldown = 0 + dones = {} + for selection in selections: + for track_id in selection: + if track_id not in dones: + tr = pctl.get_track(track_id) - if gui.mode == 3: + if cm.startswith("fs\""): + line = "|".join([tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() + if quote.lower() in line: + playlist.append(track_id) - # bg_off = [0, 0, 0, 50] - # bg_on = [255, 255, 255, 10] - # fg_off =(255, 255, 255, 40) - # fg_on = (255, 255, 255, 60) - if macstyle: - pass - else: - if r: - xx -= re_width - if l: - xx += last_width + else: + line = " ".join([tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() - rect = (xx, y, re_width, h) - ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_off) - fields.add(rect) - if coll(rect): - ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_on) - top_panel.restore_button.render(rect[0] + 8 * gui.scale, rect[1] + 9 * gui.scale, fg_on) - if (inp.mouse_click or ab_click) and coll_point(click_location, rect): - restore_full_mode() - gui.update += 2 - else: - top_panel.restore_button.render(rect[0] + 8 * gui.scale, rect[1] + 9 * gui.scale, fg_off) + # if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): + # line = str(unidecode(line)) + if search_magic(quote.lower(), line): + playlist.append(track_id) -def draw_window_border(): - corner_icon.render(window_size[0] - corner_icon.w, window_size[1] - corner_icon.h, colours.corner_icon) + cooldown += 1 + if cooldown > 300: + time.sleep(0.005) + cooldown = 0 - corner_rect = (window_size[0] - 20 * gui.scale, window_size[1] - 20 * gui.scale, 20, 20) - fields.add(corner_rect) + dones[track_id] = None - right_rect = (window_size[0] - 3 * gui.scale, 20 * gui.scale, 10, window_size[1] - 40 * gui.scale) - fields.add(right_rect) + playlist = list(OrderedDict.fromkeys(playlist)) - # top_rect = (20 * gui.scale, 0, window_size[0] - 40 * gui.scale, 2 * gui.scale) - # fields.add(top_rect) - left_rect = (0, 10 * gui.scale, 4 * gui.scale, window_size[1] - 50 * gui.scale) - fields.add(left_rect) + elif cm.startswith(('s"', 'px"')): + pl_name = quote + target = None + for p in pctl.multi_playlist: + if p.title.lower() == pl_name.lower(): + target = p.playlist_ids + break + else: + for p in pctl.multi_playlist: + #logging.info(p.title.lower()) + #logging.info(pl_name.lower()) + if p.title.lower().startswith(pl_name.lower()): + target = p.playlist_ids + break + if target is None: + logging.warning(f"not found: {pl_name}") + logging.warning("Target playlist not found") + if cm.startswith("s\""): + selections_searched += 1 + errors = "playlist" + continue - bottom_rect = (20 * gui.scale, window_size[1] - 4, window_size[0] - 40 * gui.scale, 7 * gui.scale) - fields.add(bottom_rect) + if cm.startswith("s\""): + selections_searched += 1 + selections.append(target) + elif cm.startswith("px\""): + playlist[:] = [x for x in playlist if x not in target] - if coll(corner_rect): - gui.cursor_want = 4 - elif coll(right_rect): - gui.cursor_want = 8 - # elif coll(top_rect): - # gui.cursor_want = 9 - elif coll(left_rect): - gui.cursor_want = 10 - elif coll(bottom_rect): - gui.cursor_want = 11 + else: + errors = True - colour = colours.window_frame + gui.gen_code_errors = errors + if not playlist and not errors: + gui.gen_code_errors = "empty" - ddt.rect((0, 0, window_size[0], 1 * gui.scale), colour) - ddt.rect((0, 0, 1 * gui.scale, window_size[1]), colour) - ddt.rect((0, window_size[1] - 1 * gui.scale, window_size[0], 1 * gui.scale), colour) - ddt.rect((window_size[0] - 1 * gui.scale, 0, 1 * gui.scale, window_size[1]), colour) + if gui.rename_playlist_box and (not playlist or cmds.count("a") > 1): + pass + else: + source_playlist[:] = playlist[:] + tree_view_box.clear_target_pl(0, id) + pctl.regen_in_progress = False + gui.pl_update = 1 + reload() + pctl.notify_change() -# ------------------------------------------------------------------------------------------- -# initiate SDL2 --------------------------------------------------------------------C-IS----- + #logging.info(cmds) -cursor_hand = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND) -cursor_standard = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW) -cursor_shift = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEWE) -cursor_text = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM) +def make_auto_sorting(pl: int) -> None: + pctl.gen_codes[pl_to_id(pl)] = "self a path tn ypa auto" + show_message( + _("OK. This playlist will automatically sort on import from now on"), + _("You remove or edit this behavior by going \"Misc...\" > \"Edit generator...\""), mode="done") -cursor_br_corner = cursor_standard -cursor_right_side = cursor_standard -cursor_top_side = cursor_standard -cursor_left_side = cursor_standard -cursor_bottom_side = cursor_standard +def spotify_show_test(_): + return prefs.spot_mode -if msys: - cursor_br_corner = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZENWSE) - cursor_right_side = cursor_shift - cursor_left_side = cursor_shift - cursor_top_side = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZENS) - cursor_bottom_side = cursor_top_side -elif not msys and system == "Linux" and "XCURSOR_THEME" in os.environ and "XCURSOR_SIZE" in os.environ: - try: - class XcursorImage(ctypes.Structure): - _fields_ = [ - ("version", c_uint32), - ("size", c_uint32), - ("width", c_uint32), - ("height", c_uint32), - ("xhot", c_uint32), - ("yhot", c_uint32), - ("delay", c_uint32), - ("pixels", c_void_p), - ] +def jellyfin_show_test(_): + return prefs.jelly_password and prefs.jelly_username - try: - xcu = ctypes.cdll.LoadLibrary("libXcursor.so") - except Exception: - logging.exception("Failed to load libXcursor.so, will try libXcursor.so.1") - xcu = ctypes.cdll.LoadLibrary("libXcursor.so.1") - xcu.XcursorLibraryLoadImage.restype = ctypes.POINTER(XcursorImage) - - def get_xcursor(name: str): - if "XCURSOR_THEME" not in os.environ: - raise ValueError("Missing XCURSOR_THEME in env") - if "XCURSOR_SIZE" not in os.environ: - raise ValueError("Missing XCURSOR_SIZE in env") - xcursor_theme = os.environ["XCURSOR_THEME"] - xcursor_size = os.environ["XCURSOR_SIZE"] - c1 = xcu.XcursorLibraryLoadImage(c_char_p(name.encode()), c_char_p(xcursor_theme.encode()), c_int(int(xcursor_size))).contents - sdl_surface = SDL_CreateRGBSurfaceWithFormatFrom(c1.pixels, c1.width, c1.height, 32, c1.width * 4, SDL_PIXELFORMAT_ARGB8888) - cursor = SDL_CreateColorCursor(sdl_surface, round(c1.xhot), round(c1.yhot)) - xcu.XcursorImageDestroy(ctypes.byref(c1)) - SDL_FreeSurface(sdl_surface) - return cursor - - cursor_br_corner = get_xcursor("se-resize") - cursor_right_side = get_xcursor("right_side") - cursor_top_side = get_xcursor("top_side") - cursor_left_side = get_xcursor("left_side") - cursor_bottom_side = get_xcursor("bottom_side") - - if SDL_GetCurrentVideoDriver() == b"wayland": - cursor_standard = get_xcursor("left_ptr") - cursor_text = get_xcursor("xterm") - cursor_shift = get_xcursor("sb_h_double_arrow") - cursor_hand = get_xcursor("hand2") - SDL_SetCursor(cursor_standard) +def upload_jellyfin_playlist(pl: TauonPlaylist) -> None: + if jellyfin.scanning: + return + shooter(jellyfin.upload_playlist, [pl]) - except Exception: - logging.exception("Error loading xcursor") +def regen_playlist_async(pl: int) -> None: + if pctl.regen_in_progress: + show_message(_("A regen is already in progress...")) + return + shoot_dl = threading.Thread(target=regenerate_playlist, args=([pl])) + shoot_dl.daemon = True + shoot_dl.start() +def forget_pl_import_folder(pl: int) -> None: + pctl.multi_playlist[pl].last_folder = [] -if not maximized and gui.maximized: - SDL_MaximizeWindow(t_window) +def remove_duplicates(pl: int) -> None: + playlist = [] -# logging.error(SDL_GetError()) + for item in pctl.multi_playlist[pl].playlist_ids: + if item not in playlist: + playlist.append(item) -# t_window = SDL_CreateShapedWindow( -# window_title, -# SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, -# window_size[0], window_size[1], -# flags) + removed = len(pctl.multi_playlist[pl].playlist_ids) - len(playlist) + if not removed: + show_message(_("No duplicates were found")) + else: + show_message(_("{N} duplicates removed").format(N=removed), mode="done") -# logging.error(SDL_GetError()) + pctl.multi_playlist[pl].playlist_ids[:] = playlist[:] -if system == "Windows" or msys: - gui.window_id = sss.info.win.window +def start_quick_add(pl: int) -> None: + pctl.quick_add_target = pl_to_id(pl) + show_message( + _("You can now add/remove albums to this playlist by right clicking in gallery of any playlist"), + _("To exit this mode, click \"Disengage\" from main MENU")) +def auto_get_sync_targets(): + search_paths = [ + "/run/user/*/gvfs/*/*/[Mm]usic", + "/run/media/*/*/[Mm]usic"] + result_paths = [] + for item in search_paths: + result_paths.extend(glob.glob(item)) + return result_paths -# try: -# SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, b"1") -# -# except Exception: -# logging.exception("old version of SDL detected") +def auto_sync_thread(pl: int) -> None: + if prefs.transcode_inplace: + show_message(_("Cannot sync when in transcode inplace mode")) + return -# get window surface and set up renderer -# renderer = SDL_CreateRenderer(t_window, 0, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC) + # Find target path + gui.sync_progress = "Starting Sync..." + gui.update += 1 -# renderer = SDL_CreateRenderer(t_window, 0, SDL_RENDERER_ACCELERATED) -# -# # window_surface = SDL_GetWindowSurface(t_window) -# -# SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) -# -# display_index = SDL_GetWindowDisplayIndex(t_window) -# display_bounds = SDL_Rect(0, 0) -# SDL_GetDisplayBounds(display_index, display_bounds) -# -# icon = IMG_Load(os.path.join(asset_directory, "icon-64.png").encode()) -# SDL_SetWindowIcon(t_window, icon) -# SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best".encode()) -# -# SDL_SetWindowMinimumSize(t_window, round(560 * gui.scale), round(330 * gui.scale)) -# -# -# gui.max_window_tex = 1000 -# if window_size[0] > gui.max_window_tex or window_size[1] > gui.max_window_tex: -# -# while window_size[0] > gui.max_window_tex: -# gui.max_window_tex += 1000 -# while window_size[1] > gui.max_window_tex: -# gui.max_window_tex += 1000 -# -# gui.ttext = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.max_window_tex, gui.max_window_tex) -# -# # gui.pl_surf = SDL_CreateRGBSurfaceWithFormat(0, gui.max_window_tex, gui.max_window_tex, 32, SDL_PIXELFORMAT_RGB888) -# -# SDL_SetTextureBlendMode(gui.ttext, SDL_BLENDMODE_BLEND) -# -# gui.spec2_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.spec2_w, gui.spec2_y) -# gui.spec1_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.spec_w, gui.spec_h) -# gui.spec4_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.spec4_w, gui.spec4_h) -# gui.spec_level_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.level_ww, gui.level_hh) -# -# SDL_SetTextureBlendMode(gui.spec4_tex, SDL_BLENDMODE_BLEND) -# -# SDL_SetRenderTarget(renderer, None) -# -# gui.main_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.max_window_tex, gui.max_window_tex) -# gui.main_texture_overlay_temp = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.max_window_tex, gui.max_window_tex) -# -# SDL_SetRenderTarget(renderer, gui.main_texture) -# SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) -# -# SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) -# SDL_SetTextureBlendMode(gui.main_texture_overlay_temp, SDL_BLENDMODE_BLEND) -# SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) -# -# SDL_RenderClear(renderer) -# -# gui.abc = SDL_Rect(0, 0, gui.max_window_tex, gui.max_window_tex) -# gui.pl_update = 2 -# -# SDL_SetWindowOpacity(t_window, prefs.window_opacity) + path = Path(sync_target.text.strip().rstrip("/").rstrip("\\").replace("\n", "").replace("\r", "")) + logging.debug(f"sync_path: {path}") + if not path: + show_message(_("No target folder selected")) + gui.sync_progress = "" + gui.stop_sync = False + gui.update += 1 + return + if not path.is_dir(): + show_message(_("Target folder could not be found")) + gui.sync_progress = "" + gui.stop_sync = False + gui.update += 1 + return -# gui.spec1_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.spec_w, gui.spec_h) -# gui.spec4_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.spec4_w, gui.spec4_h) -# gui.spec_level_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.level_ww, gui.level_hh) -# SDL_SetTextureBlendMode(gui.spec4_tex, SDL_BLENDMODE_BLEND) + prefs.sync_target = str(path) + # Get list of folder names on device + logging.info("Getting folder list from device...") + d_folder_names = path.iterdir() + logging.info("Got list") -def bass_player_thread(player): - # logging.basicConfig(filename=user_directory + '/crash.log', level=logging.ERROR, - # format='%(asctime)s %(levelname)s %(name)s %(message)s') + # Get list of folders we want + folders = convert_playlist(pl, get_list=True) + folder_names: list[str] = [] + folder_dict = {} - try: - player(pctl, gui, prefs, lfm_scrobbler, star_store, tauon) - except Exception: - logging.exception("Exception on player thread") - show_message(_("Playback thread has crashed. Sorry about that."), _("App will need to be restarted."), mode="error") - time.sleep(1) - show_message(_("Playback thread has crashed. Sorry about that."), _("App will need to be restarted."), mode="error") - time.sleep(1) - show_message(_("Playback thread has crashed. Sorry about that."), _("App will need to be restarted."), mode="error") - raise + if gui.stop_sync: + gui.sync_progress = "" + gui.stop_sync = False + gui.update += 1 + # Find the folder names the transcode function would name them + for folder in folders: + name = encode_folder_name(pctl.get_track(folder[0])) + for item in folder: + if pctl.get_track(item).album != pctl.get_track(folder[0]).album: + name = os.path.basename(pctl.get_track(folder[0]).parent_folder_path) + break + folder_names.append(name) + folder_dict[name] = folder -if (system == "Windows" or msys) and taskbar_progress: + # ------ + # Find deletes + if prefs.sync_deletes: + for d_folder in d_folder_names: + d_folder = d_folder.name + if gui.stop_sync: + break + if d_folder not in folder_names: + gui.sync_progress = _("Deleting folders...") + gui.update += 1 + logging.warning(f"DELETING: {d_folder}") + shutil.rmtree(path / d_folder) - class WinTask: + # ------- + # Find todos + todos: list[str] = [] + for folder in folder_names: + if folder not in d_folder_names: + todos.append(folder) + logging.info(f"Want to add: {folder}") + else: + logging.error(f"Already exists: {folder}") - def __init__(self): - self.start = time.time() - self.updated_state = 0 - self.window_id = gui.window_id - import comtypes.client as cc - cc.GetModule(str(install_directory / "TaskbarLib.tlb")) - import comtypes.gen.TaskbarLib as tbl - self.taskbar = cc.CreateObject( - "{56FDF344-FD6D-11d0-958A-006097C9A090}", - interface=tbl.ITaskbarList3) - self.taskbar.HrInit() + gui.update += 1 + # ----- + # Prepare and copy + for i, item in enumerate(todos): + gui.sync_progress = _("Copying files to device") + if gui.stop_sync: + break - self.d_timer = Timer() + free_space = shutil.disk_usage(path)[2] / 8 / 100000000 # in GB + if free_space < 0.6: + show_message(_("Sync aborted! Low disk space on target device"), mode="warning") + break - def update(self, force=False): - if self.d_timer.get() > 2 or force: - self.d_timer.set() + if prefs.bypass_transcode or (prefs.smart_bypass and 0 < pctl.get_track(folder_dict[item][0]).bitrate <= 128): + logging.info("Smart bypass...") - if pctl.playing_state == 1 and self.updated_state != 1: - self.taskbar.SetProgressState(self.window_id, 0x2) + source_parent = Path(pctl.get_track(folder_dict[item][0]).parent_folder_path) + if source_parent.exists(): + if (path / item).exists(): + show_message( + _("Sync warning"), _("One or more folders to sync has the same name. Skipping."), mode="warning") + continue - if pctl.playing_state == 1: - self.updated_state = 1 - if pctl.playing_length > 2: - perc = int(pctl.playing_time * 100 / int(pctl.playing_length)) - if perc < 2: - perc = 1 - elif perc > 100: - prec = 100 - else: - perc = 0 + (path / item).mkdir() + encode_done = source_parent + else: + show_message(_("One or more folders is missing")) + continue - self.taskbar.SetProgressValue(self.window_id, perc, 100) + else: - elif pctl.playing_state == 2 and self.updated_state != 2: - self.updated_state = 2 - self.taskbar.SetProgressState(self.window_id, 0x8) + encode_done = prefs.encoder_output / item + # TODO(Martin): We should make sure that the length of the source and target matches or is greater, not just that the dir exists and is not empty! + if not encode_done.exists() or not any(encode_done.iterdir()): + logging.info("Need to transcode") + remain = len(todos) - i + if remain > 1: + gui.sync_progress = _("{N} Folders Remaining").format(N=str(remain)) + else: + gui.sync_progress = _("{N} Folder Remaining").format(N=str(remain)) + transcode_list.append(folder_dict[item]) + tauon.thread_manager.ready("worker") + while transcode_list: + time.sleep(1) + if gui.stop_sync: + break + else: + logging.warning("A transcode is already done") - elif pctl.playing_state == 0 and self.updated_state != 0: - self.updated_state = 0 - self.taskbar.SetProgressState(self.window_id, 0x2) - self.taskbar.SetProgressValue(self.window_id, 0, 100) + if encode_done.exists(): + if (path / item).exists(): + show_message( + _("Sync warning"), _("One or more folders to sync has the same name. Skipping."), mode="warning") + continue - if (install_directory / "TaskbarLib.tlb").is_file(): - logging.info("Taskbar progress enabled") - pctl.windows_progress = WinTask() + (path / item).mkdir() - else: - pctl.taskbar_progress = False - logging.warning("Could not find TaskbarLib.tlb") + for file in encode_done.iterdir(): + file = file.name + logging.info(f"Copy file {file} to {path / item}…") + # gui.sync_progress += "." + gui.update += 1 + if (encode_done / file).is_file(): + size = os.path.getsize(encode_done / file) + sync_file_timer.set() + try: + shutil.copyfile(encode_done / file, path / item / file) + except OSError as e: + if str(e).startswith("[Errno 22] Invalid argument: "): + sanitized_file = re.sub(r'[<>:"/\\|?*]', '_', file) + if sanitized_file == file: + logging.exception("Unknown OSError trying to copy file, maybe FS does not support the name?") + else: + shutil.copyfile(encode_done / file, path / item / sanitized_file) + logging.warning(f"Had to rename {file} to {sanitized_file} on the output! Probably a FS limitation!") + else: + logging.exception("Unknown OSError trying to copy file") + except Exception: + logging.exception("Unknown error trying to copy file") -# --------------------------------------------------------------------------------------------- -# ABSTRACT SDL DRAWING FUNCTIONS ----------------------------------------------------- + if gui.sync_speed == 0 or (sync_file_update_timer.get() > 1 and not file.endswith(".jpg")): + sync_file_update_timer.set() + gui.sync_speed = size / sync_file_timer.get() + gui.sync_progress = _("Copying files to device") + " @ " + get_filesize_string_rounded( + gui.sync_speed) + "/s" + if gui.stop_sync: + gui.sync_progress = _("Aborting Sync") + " @ " + get_filesize_string_rounded(gui.sync_speed) + "/s" + logging.info("Finished copying folder") -def coll_point(l, r): - # rect point collision detection - return r[0] < l[0] <= r[0] + r[2] and r[1] <= l[1] <= r[1] + r[3] + gui.sync_speed = 0 + gui.sync_progress = "" + gui.stop_sync = False + gui.update += 1 + show_message(_("Sync completed"), mode="done") +def auto_sync(pl: int) -> None: + shoot_dl = threading.Thread(target=auto_sync_thread, args=([pl])) + shoot_dl.daemon = True + shoot_dl.start() -def coll(r): - return r[0] < mouse_position[0] <= r[0] + r[2] and r[1] <= mouse_position[1] <= r[1] + r[3] +def set_sync_playlist(pl: int) -> None: + id = pl_to_id(pl) + if prefs.sync_playlist == id: + prefs.sync_playlist = None + else: + prefs.sync_playlist = pl_to_id(pl) +def sync_playlist_deco(pl: int): + text = _("Set as Sync Playlist") + id = pl_to_id(pl) + if id == prefs.sync_playlist: + text = _("Un-set as Sync Playlist") + return [colours.menu_text, colours.menu_background, text] -ddt = TDraw(renderer) -ddt.scale = gui.scale -ddt.force_subpixel_text = prefs.force_subpixel_text +def set_download_playlist(pl: int) -> None: + id = pl_to_id(pl) + if prefs.download_playlist == id: + prefs.download_playlist = None + else: + prefs.download_playlist = pl_to_id(pl) -launch = Launch(tauon, pctl, gui, ddt) +def set_podcast_playlist(pl: int) -> None: + pctl.multi_playlist[pl].persist_time_positioning ^= True +def set_download_deco(pl: int): + text = _("Set as Downloads Playlist") + if id == prefs.download_playlist: + text = _("Un-set as Downloads Playlist") + return [colours.menu_text, colours.menu_background, text] -class Drawing: +def set_podcast_deco(pl: int): + text = _("Set Use Persistent Time") + if pctl.multi_playlist[pl].persist_time_positioning: + text = _("Un-set Use Persistent Time") + return [colours.menu_text, colours.menu_background, text] - def button( - self, text, x, y, w=None, h=None, font=212, text_highlight_colour=None, text_colour=None, - background_colour=None, background_highlight_colour=None, press=None, tooltip=""): +def csv_string(item): + item = str(item) + item.replace("\"", "\"\"") + return f"\"{item}\"" - if w is None: - w = ddt.get_text_w(text, font) + 18 * gui.scale - if h is None: - h = 22 * gui.scale +def export_playlist_albums(pl: int) -> None: + p = pctl.multi_playlist[pl] + name = p.title + playlist = p.playlist_ids - rect = (x, y, w, h) - fields.add(rect) + albums = [] + playtimes = {} + last_folder = None + for i, id in enumerate(playlist): + track = pctl.get_track(id) + if last_folder != track.parent_folder_path: + last_folder = track.parent_folder_path + if id not in albums: + albums.append(id) - if text_highlight_colour is None: - text_highlight_colour = colours.box_button_text_highlight - if text_colour is None: - text_colour = colours.box_button_text - if background_colour is None: - background_colour = colours.box_button_background - if background_highlight_colour is None: - background_highlight_colour = colours.box_button_background_highlight + playtimes[last_folder] = playtimes.get(last_folder, 0) + int(star_store.get(id)) - click = False + filename = f"{user_directory}/{name}.csv" + xport = open(filename, "w") - if press is None: - press = inp.mouse_click + xport.write("Album name;Artist;Release date;Genre;Rating;Playtime;Folder path") - if coll(rect): - if tooltip: - tool_tip.test(x + 15 * gui.scale, y - 28 * gui.scale, tooltip) - ddt.rect(rect, background_highlight_colour) + for id in albums: + track = pctl.get_track(id) + artist = track.album_artist + if not artist: + artist = track.artist - # if background_highlight_colour[3] != 255: - # background_highlight_colour = None + xport.write("\n") + xport.write(csv_string(track.album) + ",") + xport.write(csv_string(artist) + ",") + xport.write(csv_string(track.date) + ",") + xport.write(csv_string(track.genre) + ",") + xport.write(str(int(album_star_store.get_rating(track)))) + xport.write(",") + xport.write(str(round(playtimes[track.parent_folder_path]))) + xport.write(",") + xport.write(csv_string(track.parent_folder_path)) - ddt.text( - (rect[0] + int(rect[2] / 2), rect[1] + 2 * gui.scale, 2), text, text_highlight_colour, font, bg=background_highlight_colour) - if press: - click = True - else: - ddt.rect(rect, background_colour) - if background_highlight_colour[3] != 255: - background_colour = None - ddt.text( - (rect[0] + int(rect[2] / 2), rect[1] + 2 * gui.scale, 2), text, text_colour, font, bg=background_colour) - return click + xport.close() + show_message(_("Export complete."), _("Saved as: ") + filename, mode="done") +def best(index: int): + # key = pctl.master_library[index].title + pctl.master_library[index].filename + if pctl.master_library[index].length < 1: + return 0 + return int(star_store.get(index)) -draw = Drawing() +def key_rating(index: int): + return star_store.get_rating(index) +def key_scrobbles(index: int): + return pctl.get_track(index).lfm_scrobbles -def prime_fonts(): - standard_font = prefs.linux_font - # if msys: - # standard_font = prefs.linux_font + ", Sans" # The CJK ones dont appear to be working - ddt.prime_font(standard_font, 8, 9) - ddt.prime_font(standard_font, 8, 10) - ddt.prime_font(standard_font, 8.5, 11) - ddt.prime_font(standard_font, 8.7, 11.5) - ddt.prime_font(standard_font, 9, 12) - ddt.prime_font(standard_font, 10, 13) - ddt.prime_font(standard_font, 10, 14) - ddt.prime_font(standard_font, 10.2, 14.5) - ddt.prime_font(standard_font, 11, 15) - ddt.prime_font(standard_font, 12, 16) - ddt.prime_font(standard_font, 12, 17) - ddt.prime_font(standard_font, 12, 18) - ddt.prime_font(standard_font, 13, 19) - ddt.prime_font(standard_font, 14, 20) - ddt.prime_font(standard_font, 24, 30) +def key_disc(index: int): + return pctl.get_track(index).disc_number - ddt.prime_font(standard_font, 9, 412) - ddt.prime_font(standard_font, 10, 413) +def key_cue(index: int): + return pctl.get_track(index).is_cue - standard_font = prefs.linux_font_semibold - # if msys: - # standard_font = prefs.linux_font_semibold + ", Noto Sans Med, Sans" #, Noto Sans CJK JP Medium, Noto Sans CJK Medium, Sans" +def key_playcount(index: int): + # key = pctl.master_library[index].title + pctl.master_library[index].filename + if pctl.master_library[index].length < 1: + return 0 + return star_store.get(index) / pctl.master_library[index].length + # if key in pctl.star_library: + # return pctl.star_library[key] / pctl.master_library[index].length + # else: + # return 0 - ddt.prime_font(standard_font, 8, 309) - ddt.prime_font(standard_font, 8, 310) - ddt.prime_font(standard_font, 8.5, 311) - ddt.prime_font(standard_font, 9, 312) - ddt.prime_font(standard_font, 10, 313) - ddt.prime_font(standard_font, 10.5, 314) - ddt.prime_font(standard_font, 11, 315) - ddt.prime_font(standard_font, 12, 316) - ddt.prime_font(standard_font, 12, 317) - ddt.prime_font(standard_font, 12, 318) - ddt.prime_font(standard_font, 13, 319) - ddt.prime_font(standard_font, 24, 330) +def add_pl_tag(text): + return f" <{text}>" - standard_font = prefs.linux_font_bold - # if msys: - # standard_font = prefs.linux_font_bold + ", Noto Sans, Sans Bold" +def gen_top_rating(index, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids + playlist = copy.deepcopy(source) + playlist = sorted(playlist, key=key_rating, reverse=True) - ddt.prime_font(standard_font, 6, 209) - ddt.prime_font(standard_font, 7, 210) - ddt.prime_font(standard_font, 8, 211) - ddt.prime_font(standard_font, 9, 212) - ddt.prime_font(standard_font, 10, 213) - ddt.prime_font(standard_font, 11, 214) - ddt.prime_font(standard_font, 12, 215) - ddt.prime_font(standard_font, 13, 216) - ddt.prime_font(standard_font, 14, 217) - ddt.prime_font(standard_font, 17, 218) - ddt.prime_font(standard_font, 19, 219) - ddt.prime_font(standard_font, 20, 220) - ddt.prime_font(standard_font, 25, 228) + if custom_list is not None: + return playlist - standard_font = prefs.linux_font_condensed - # if msys: - # standard_font = "Noto Sans ExtCond, Sans" - ddt.prime_font(standard_font, 10, 413) - ddt.prime_font(standard_font, 11, 414) - ddt.prime_font(standard_font, 12, 415) - ddt.prime_font(standard_font, 13, 416) + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("Top Rated Tracks")), + playlist_ids=copy.deepcopy(playlist), + hide_title=True)) - standard_font = prefs.linux_font_condensed_bold # "Noto Sans, ExtraCondensed Bold" - # if msys: - # standard_font = "Noto Sans ExtCond, Sans Bold" - # ddt.prime_font(standard_font, 9, 512) - ddt.prime_font(standard_font, 10, 513) - ddt.prime_font(standard_font, 11, 514) - ddt.prime_font(standard_font, 12, 515) - ddt.prime_font(standard_font, 13, 516) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rat>" +def gen_top_100(index, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids + playlist = copy.deepcopy(source) + playlist = sorted(playlist, key=best, reverse=True) -if system == "Linux": - prime_fonts() - -else: - # standard_font = "Meiryo" - standard_font = "Arial" - # semibold_font = "Meiryo Semibold" - semibold_font = "Arial Bold" - standard_weight = 500 - bold_weight = 600 - ddt.win_prime_font(standard_font, 14, 10, weight=standard_weight, y_offset=0) - ddt.win_prime_font(standard_font, 15, 11, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 15, 11.5, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 15, 12, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 15, 13, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 16, 14, weight=standard_weight, y_offset=0) - ddt.win_prime_font(standard_font, 16, 14.5, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 17, 15, weight=standard_weight, y_offset=-1) - ddt.win_prime_font(standard_font, 20, 16, weight=standard_weight, y_offset=-2) - ddt.win_prime_font(standard_font, 20, 17, weight=standard_weight, y_offset=-1) - - ddt.win_prime_font(standard_font, 30 + 4, 30, weight=standard_weight, y_offset=-12) - ddt.win_prime_font(semibold_font, 9, 209, weight=bold_weight, y_offset=1) - ddt.win_prime_font("Arial", 10 + 4, 210, weight=600, y_offset=2) - ddt.win_prime_font("Arial", 11 + 3, 211, weight=600, y_offset=2) - ddt.win_prime_font(semibold_font, 12 + 4, 212, weight=bold_weight, y_offset=1) - ddt.win_prime_font(semibold_font, 13 + 3, 213, weight=bold_weight, y_offset=-1) - ddt.win_prime_font(semibold_font, 14 + 2, 214, weight=bold_weight, y_offset=1) - ddt.win_prime_font(semibold_font, 15 + 2, 215, weight=bold_weight, y_offset=1) - ddt.win_prime_font(semibold_font, 16 + 2, 216, weight=bold_weight, y_offset=1) - ddt.win_prime_font(semibold_font, 17 + 2, 218, weight=bold_weight, y_offset=1) - ddt.win_prime_font(semibold_font, 18 + 2, 218, weight=bold_weight, y_offset=1) - ddt.win_prime_font(semibold_font, 19 + 2, 220, weight=bold_weight, y_offset=1) - ddt.win_prime_font(semibold_font, 28 + 2, 228, weight=bold_weight, y_offset=1) - - standard_weight = 550 - ddt.win_prime_font(standard_font, 14, 310, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 15, 311, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 16, 312, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 17, 313, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 18, 314, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 19, 315, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 20, 316, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 21, 317, weight=standard_weight, y_offset=1) - - standard_font = "Arial Narrow" - standard_weight = 500 - - ddt.win_prime_font(standard_font, 14, 410, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 15, 411, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 16, 412, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 17, 413, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 18, 414, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 19, 415, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 20, 416, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 21, 417, weight=standard_weight, y_offset=1) - - standard_weight = 600 - - ddt.win_prime_font(standard_font, 14, 510, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 15, 511, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 16, 512, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 17, 513, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 18, 514, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 19, 515, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 20, 516, weight=standard_weight, y_offset=1) - ddt.win_prime_font(standard_font, 21, 517, weight=standard_weight, y_offset=1) + if custom_list is not None: + return playlist + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("Top Played Tracks")), + playlist_ids=copy.deepcopy(playlist), + hide_title=True)) -class DropShadow: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a pt>" - def __init__(self): - self.readys = {} - self.underscan = int(15 * gui.scale) - self.radius = 4 - self.grow = 2 * gui.scale - self.opacity = 90 +def gen_folder_top(pl: int, get_sets: bool = False, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[pl].playlist_ids - def prepare(self, w, h): - fh = h + self.underscan - fw = w + self.underscan + if len(source) < 3: + return [] - im = Image.new("RGBA", (round(fw), round(fh)), 0x00000000) - draw = ImageDraw.Draw(im) - draw.rectangle(((self.underscan, self.underscan), (w + 2, h + 2)), fill="black") + sets = [] + se = [] + tr = pctl.get_track(source[0]) + last = tr.parent_folder_path + last_al = tr.album + for track in source: + if last != pctl.master_library[track].parent_folder_path or last_al != pctl.master_library[track].album: + last = pctl.master_library[track].parent_folder_path + last_al = pctl.master_library[track].album + sets.append(copy.deepcopy(se)) + se = [] + se.append(track) + sets.append(copy.deepcopy(se)) - im = im.filter(ImageFilter.GaussianBlur(self.radius)) + def best(folder): + #logging.info(folder) + total_star = 0 + for item in folder: + # key = pctl.master_library[item].title + pctl.master_library[item].filename + # if key in pctl.star_library: + # total_star += int(pctl.star_library[key]) + total_star += int(star_store.get(item)) + #logging.info(total_star) + return total_star - g = io.BytesIO() - g.seek(0) - im.save(g, "PNG") - g.seek(0) + if get_sets: + r = [] + for item in sets: + r.append((item, best(item))) + return r - wop = rw_from_object(g) - s_image = IMG_Load_RW(wop, 0) - c = SDL_CreateTextureFromSurface(renderer, s_image) - SDL_SetTextureAlphaMod(c, self.opacity) + sets = sorted(sets, key=best, reverse=True) - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) - SDL_QueryTexture(c, None, None, tex_w, tex_h) + playlist = [] - dst = SDL_Rect(0, 0) - dst.w = int(tex_w.contents.value) - dst.h = int(tex_h.contents.value) + for se in sets: + playlist += se - SDL_FreeSurface(s_image) - g.close() - im.close() + # pctl.multi_playlist.append( + # [pctl.multi_playlist[pl].title + " <Most Played Albums>", 0, copy.deepcopy(playlist), 0, 0, 0]) + if custom_list is not None: + return playlist - unit = (dst, c) - self.readys[(w, h)] = unit + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[pl].title + add_pl_tag(_("Top Played Albums")), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - def render(self, x, y, w, h): - if (w, h) not in self.readys: - self.prepare(w, h) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a pa>" - unit = self.readys[(w, h)] - unit[0].x = round(x) - round(self.underscan) - unit[0].y = round(y) - round(self.underscan) - SDL_RenderCopy(renderer, unit[1], None, unit[0]) +def gen_folder_top_rating(pl: int, get_sets: bool = False, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[pl].playlist_ids + if len(source) < 3: + return [] -drop_shadow = DropShadow() + sets = [] + se = [] + tr = pctl.get_track(source[0]) + last = tr.parent_folder_path + last_al = tr.album + for track in source: + if last != pctl.master_library[track].parent_folder_path or last_al != pctl.master_library[track].album: + last = pctl.master_library[track].parent_folder_path + last_al = pctl.master_library[track].album + sets.append(copy.deepcopy(se)) + se = [] + se.append(track) + sets.append(copy.deepcopy(se)) + def best(folder): + return album_star_store.get_rating(pctl.get_track(folder[0])) -class LyricsRenMini: + if get_sets: + r = [] + for item in sets: + r.append((item, best(item))) + return r - def __init__(self): - self.index = -1 - self.text = "" + sets = sorted(sets, key=best, reverse=True) - self.lyrics_position = 0 + playlist = [] - def generate(self, index, w): - self.text = pctl.master_library[index].lyrics - self.lyrics_position = 0 + for se in sets: + playlist += se - def render(self, index, x, y, w, h, p): - if index != self.index or self.text != pctl.master_library[index].lyrics: - self.index = index - self.generate(index, w) - - colour = colours.side_bar_line1 - - # if key_ctrl_down: - # if mouse_wheel < 0: - # prefs.lyrics_font_size += 1 - # if mouse_wheel > 0: - # prefs.lyrics_font_size -= 1 - - ddt.text((x, y, 4, w), self.text, colour, prefs.lyrics_font_size, w - (w % 2), colours.side_panel_background) - - -lyrics_ren_mini = LyricsRenMini() + if custom_list is not None: + return playlist + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[pl].title + add_pl_tag(_("Top Rated Albums")), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) -class LyricsRen: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a rata>" - def __init__(self): +def gen_lyrics(plpl: int, custom_list=None): + playlist = [] - self.index = -1 - self.text = "" + source = custom_list + if source is None: + source = pctl.multi_playlist[pl].playlist_ids - self.lyrics_position = 0 + for item in source: + if pctl.master_library[item].lyrics != "": + playlist.append(item) - def test_update(self, track_object: TrackClass): + if custom_list is not None: + return playlist - if track_object.index != self.index or self.text != track_object.lyrics: - self.index = track_object.index - self.text = track_object.lyrics - self.lyrics_position = 0 + if len(playlist) > 0: + pctl.multi_playlist.append( + pl_gen( + title=_("Tracks with lyrics"), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - def render(self, x, y, w, h, p): + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a ly" - colour = colours.lyrics - if test_lumi(colours.gallery_background) < 0.5: - colour = colours.grey(40) + else: + show_message(_("No tracks with lyrics were found.")) - ddt.text((x, y, 4, w), self.text, colour, 17, w, colours.playlist_panel_background) +def gen_incomplete(plpl: int, custom_list=None): + playlist = [] + source = custom_list + if source is None: + source = pctl.multi_playlist[pl].playlist_ids -lyrics_ren = LyricsRen() + albums = {} + nums = {} + for id in source: + track = pctl.get_track(id) + if track.album and track.track_number: + if type(track.track_number) is str and not track.track_number.isdigit(): + continue -def find_synced_lyric_data(track: TrackClass) -> list[str] | None: - if track.is_network: - return None + if track.album not in albums: + albums[track.album] = [] + nums[track.album] = [] - direc = track.parent_folder_path - name = os.path.splitext(track.filename)[0] + ".lrc" + if track not in albums[track.album]: + albums[track.album].append(track) + nums[track.album].append(int(track.track_number)) - if len(track.lyrics) > 20 and track.lyrics[0] == "[" and ":" in track.lyrics[:20] and "." in track.lyrics[:20]: - return track.lyrics.splitlines() + for album, tracks in albums.items(): + numbers = nums[album] + if len(numbers) > 2: + mi = min(numbers) + mx = max(numbers) + for track in tracks: + if type(track.track_total) is int or (type(track.track_total) is str and track.track_total.isdigit()): + mx = max(mx, int(track.track_total)) + r = list(range(int(mi), int(mx))) + for track in tracks: + if int(track.track_number) in r: + r.remove(int(track.track_number)) + if r or mi > 1: + for tr in tracks: + playlist.append(tr.index) - try: - if os.path.isfile(os.path.join(direc, name)): - with open(os.path.join(direc, name), encoding="utf-8") as f: - data = f.readlines() - else: - return None - except Exception: - logging.exception("Read lyrics file error") - return None + if custom_list is not None: + return playlist - return data + if len(playlist) > 0: + show_message(_("Note this may include albums that simply have tracks missing an album tag")) + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[pl].title + add_pl_tag(_("Incomplete Albums")), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) + # pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a ly" -class TimedLyricsToStatic: + else: + show_message(_("No incomplete albums were found.")) - def __init__(self): - self.cache_key = None - self.cache_lyrics = "" +def gen_codec_pl(codec): + playlist = [] - def get(self, track: TrackClass): - if track.lyrics: - return track.lyrics - if track.is_network: - return "" - if track == self.cache_key: - return self.cache_lyrics - data = find_synced_lyric_data(track) + for pl in pctl.multi_playlist: + for item in pl.playlist_ids: + if pctl.master_library[item].file_ext == codec and item not in playlist: + playlist.append(item) - if data is None: - self.cache_lyrics = "" - self.cache_key = track - return "" - text = "" + if len(playlist) > 0: + pctl.multi_playlist.append( + pl_gen( + title=_("Codec: ") + codec, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - for line in data: - if len(line) < 10: - continue +def gen_last_imported_folders(index, custom_list=None, reverse=True): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - if line[0] != "[" or line[9] != "]" or ":" not in line or "." not in line: - continue + a_cache = {} - text += line.split("]")[-1].rstrip("\n") + "\n" + def key_import(index: int): - self.cache_lyrics = text - self.cache_key = track - return text + track = pctl.master_library[index] + cached = a_cache.get((track.album, track.parent_folder_name)) + if cached is not None: + return cached + if track.album: + a_cache[(track.album, track.parent_folder_name)] = index + return index -tauon.synced_to_static_lyrics = TimedLyricsToStatic() + playlist = copy.deepcopy(source) + playlist = sorted(playlist, key=key_import, reverse=reverse) + sort_track_2(0, playlist) + if custom_list is not None: + return playlist -def get_real_time(): - offset = pctl.decode_time - (prefs.sync_lyrics_time_offset / 1000) - if prefs.backend == 4: - offset -= (prefs.device_buffer - 120) / 1000 - elif prefs.backend == 2: - offset += 0.1 - return max(0, offset) +def gen_last_modified(index, custom_list=None, reverse=True): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids + a_cache = {} -class TimedLyricsRen: + def key_modified(index: int): - def __init__(self): + track = pctl.master_library[index] + cached = a_cache.get((track.album, track.parent_folder_name)) + if cached is not None: + return cached - self.index = -1 + if track.album: + a_cache[(track.album, track.parent_folder_name)] = pctl.master_library[index].modified_time + return pctl.master_library[index].modified_time - self.scanned = {} - self.ready = False - self.data = [] + playlist = copy.deepcopy(source) + playlist = sorted(playlist, key=key_modified, reverse=reverse) + sort_track_2(0, playlist) - self.scroll_position = 0 + if custom_list is not None: + return playlist - def generate(self, track: TrackClass) -> bool | None: + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("File Modified")), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - if self.index == track.index: - return self.ready + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a m>" - self.ready = False - self.index = track.index - self.scroll_position = 0 - self.data.clear() +def gen_love(pl: int, custom_list=None): + playlist = [] - data = find_synced_lyric_data(track) - if data is None: - return None + source = custom_list + if source is None: + source = pctl.multi_playlist[pl].playlist_ids - for line in data: - if len(line) < 10: - continue + for item in source: + if get_love_index(item): + playlist.append(item) - if line[0] != "[" or "]" not in line or ":" not in line or "." not in line: - continue + playlist.sort(key=lambda x: get_love_timestamp_index(x), reverse=True) - try: + if custom_list is not None: + return playlist - text = line.split("]")[-1].rstrip("\n") - t = line + if len(playlist) > 0: + # pctl.multi_playlist.append(["Interesting Comments", 0, copy.deepcopy(playlist), 0, 0, 0]) + pctl.multi_playlist.append( + pl_gen( + title=_("Loved"), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a l" + else: + show_message(_("No loved tracks were found.")) - while t[0] == "[" and t[9] == "]" and ":" in t and "." in t: +def gen_comment(pl: int) -> None: + playlist = [] - a = t.lstrip("[") - t = t.split("]")[1] + "]" + for item in pctl.multi_playlist[pl].playlist_ids: + cm = pctl.master_library[item].comment + if len(cm) > 20 and \ + cm[0] != "0" and \ + "http://" not in cm and \ + "www." not in cm and \ + "Release" not in cm and \ + "EAC" not in cm and \ + "@" not in cm and \ + ".com" not in cm and \ + "ipped" not in cm and \ + "ncoded" not in cm and \ + "ExactA" not in cm and \ + "WWW." not in cm and \ + cm[2] != "+" and \ + cm[1] != "+": + playlist.append(item) - a = a.split("]")[0] - mm, b = a.split(":") - ss, ms = b.split(".") + if len(playlist) > 0: + # pctl.multi_playlist.append(["Interesting Comments", 0, copy.deepcopy(playlist), 0, 0, 0]) + pctl.multi_playlist.append( + pl_gen( + title=_("Interesting Comments"), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) + else: + show_message(_("Nothing of interest was found.")) - s = int(mm) * 60 + int(ss) - if len(ms) == 2: - s += int(ms) / 100 - elif len(ms) == 3: - s += int(ms) / 1000 +def gen_replay(pl: int) -> None: + playlist = [] - self.data.append((s, text)) + for item in pctl.multi_playlist[pl].playlist_ids: + if pctl.master_library[item].misc.get("replaygain_track_gain"): + playlist.append(item) - if len(t) < 10: - break - except Exception: - logging.exception("Failed generating timed lyrics") - continue + if len(playlist) > 0: + pctl.multi_playlist.append( + pl_gen( + title=_("ReplayGain Tracks"), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) + else: + show_message(_("No replay gain tags were found.")) - self.data = sorted(self.data, key=lambda x: x[0]) - # logging.info(self.data) +def gen_sort_len(index: int, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - self.ready = True - return True + def length(index: int) -> int: - def render(self, index: int, x: int, y: int, side_panel: bool = False, w: int = 0, h: int = 0) -> bool | None: + if pctl.master_library[index].length < 1: + return 0 + return int(pctl.master_library[index].length) - if index != self.index: - self.ready = False - self.generate(pctl.master_library[index]) + playlist = copy.deepcopy(source) + playlist = sorted(playlist, key=length, reverse=True) - if right_click and x and y and coll((x, y, w, h)): - showcase_menu.activate(pctl.master_library[index]) + if custom_list is not None: + return playlist - if not self.ready: - return False + # pctl.multi_playlist.append( + # [pctl.multi_playlist[index].title + " <Duration Sorted>", 0, copy.deepcopy(playlist), 0, 1, 0]) - if mouse_wheel and (pctl.playing_state != 1 or pctl.track_queue[pctl.queue_step] != index): - if side_panel: - if coll((x, y, w, h)): - self.scroll_position += int(mouse_wheel * 30 * gui.scale) - else: - self.scroll_position += int(mouse_wheel * 30 * gui.scale) + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("Duration Sorted")), + playlist_ids=copy.deepcopy(playlist), + hide_title=True)) - line_active = -1 - last = -1 + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a d>" - highlight = True +def gen_folder_duration(pl: int, get_sets: bool = False): + if len(pctl.multi_playlist[pl].playlist_ids) < 3: + return None - if side_panel: - bg = colours.top_panel_background - font_size = 15 - spacing = round(17 * gui.scale) - else: - bg = colours.playlist_panel_background - font_size = 17 - spacing = round(23 * gui.scale) - - test_time = get_real_time() - - if pctl.track_queue[pctl.queue_step] == index: - - for i, line in enumerate(self.data): - if line[0] < test_time: - last = i - - if line[0] > test_time: - pctl.wake_past_time = line[0] - line_active = last - break - else: - line_active = len(self.data) - 1 - - if pctl.playing_state == 1: - self.scroll_position = (max(0, line_active)) * spacing * -1 - - yy = y + self.scroll_position + sets = [] + se = [] + last = pctl.master_library[pctl.multi_playlist[pl].playlist_ids[0]].parent_folder_path + last_al = pctl.master_library[pctl.multi_playlist[pl].playlist_ids[0]].album + for track in pctl.multi_playlist[pl].playlist_ids: + if last != pctl.master_library[track].parent_folder_path or last_al != pctl.master_library[track].album: + last = pctl.master_library[track].parent_folder_path + last_al = pctl.master_library[track].album + sets.append(copy.deepcopy(se)) + se = [] + se.append(track) + sets.append(copy.deepcopy(se)) - for i, line in enumerate(self.data): + def best(folder): + total_duration = 0 + for item in folder: + total_duration += pctl.master_library[item].length + return total_duration - if 0 < yy < window_size[1]: + if get_sets: + r = [] + for item in sets: + r.append((item, best(item))) + return r - colour = colours.lyrics - if test_lumi(colours.gallery_background) < 0.5: - colour = colours.grey(40) + sets = sorted(sets, key=best, reverse=True) + playlist = [] - if i == line_active and highlight: - colour = [255, 210, 50, 255] - if colours.lm: - colour = [180, 130, 210, 255] + for se in sets: + playlist += se - h = ddt.text((x, yy, 4, w - 20 * gui.scale), line[1], colour, font_size, w - 20 * gui.scale, bg) - yy += max(h - round(6 * gui.scale), spacing) - else: - yy += spacing - return None + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[pl].title + add_pl_tag(_("Longest Albums")), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) -timed_lyrics_ren = TimedLyricsRen() +def gen_sort_date(index: int, rev: bool = False, custom_list=None): + def g_date(index: int): + if pctl.master_library[index].date != "": + return str(pctl.master_library[index].date) + return "z" -def draw_internel_link(x, y, text, colour, font): - tweak = font - while tweak > 100: - tweak -= 100 + playlist = [] + lowest = 0 + highest = 0 + first = True - if gui.scale == 2: - tweak *= 2 - tweak += 4 - if gui.scale == 1.25: - tweak = round(tweak * 1.25) - tweak += 1 + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - sp = ddt.text((x, y), text, colour, font) + for item in source: + date = pctl.master_library[item].date + if date != "": + playlist.append(item) + if len(date) > 4 and date[:4].isdigit(): + date = date[:4] + if len(date) == 4 and date.isdigit(): + year = int(date) + if first: + lowest = year + highest = year + first = False + lowest = min(year, lowest) + highest = max(year, highest) - rect = [x - 5 * gui.scale, y - 2 * gui.scale, sp + 11 * gui.scale, 23 * gui.scale] - fields.add(rect) + playlist = sorted(playlist, key=g_date, reverse=rev) - if coll(rect): - if not inp.mouse_click: - gui.cursor_want = 3 - ddt.line(x, y + tweak + 2, x + sp, y + tweak + 2, alpha_mod(colour, 180)) - if inp.mouse_click: - return True - return False + if custom_list is not None: + return playlist + line = add_pl_tag(_("Year Sorted")) + if lowest != highest and lowest != 0 and highest != 0: + if rev: + line = " <" + str(highest) + "-" + str(lowest) + ">" + else: + line = " <" + str(lowest) + "-" + str(highest) + ">" -# No hit detect -def draw_linked_text(location, text, colour, font, force=False, replace=""): - base = "" - link_text = "" - rest = "" - on_base = True + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + line, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - if force: - on_base = False - base = "" - link_text = text - rest = "" + if rev: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a y>" else: - for i in range(len(text)): - if text[i:i + 7] == "http://" or text[i:i + 4] == "www." or text[i:i + 8] == "https://": - on_base = False - if on_base: - base += text[i] - elif i == len(text) or text[i] in '\\) "\'': - rest = text[i:] - break - else: - link_text += text[i] + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a y<" - target_link = link_text - if replace: - link_text = replace +def gen_sort_date_new(index: int): + gen_sort_date(index, True) - left = ddt.get_text_w(base, font) - right = ddt.get_text_w(base + link_text, font) +def gen_500_random(index: int): + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - x = location[0] - y = location[1] + random.shuffle(playlist) - ddt.text((x, y), base, colour, font) - ddt.text((x + left, y), link_text, colours.link_text, font) - ddt.text((x + right, y), rest, colour, font) + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("Shuffled Tracks")), + playlist_ids=copy.deepcopy(playlist), + hide_title=True)) - tweak = font - while tweak > 100: - tweak -= 100 + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a st" - if gui.scale == 2: - tweak *= 2 - tweak += 4 - elif gui.scale != 1: - tweak = round(tweak * gui.scale) - tweak += 2 +def gen_folder_shuffle(index, custom_list=None): + folders = [] + dick = {} - if system == "Windows": - tweak += 1 + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - # ddt.line(x + left, y + tweak + 2, x + right, y + tweak + 2, alpha_mod(colours.link_text, 120)) - ddt.rect((x + left, y + tweak + 2, right - left, round(1 * gui.scale)), alpha_mod(colours.link_text, 120)) + for track in source: + parent = pctl.master_library[track].parent_folder_path + if parent not in folders: + folders.append(parent) + if parent not in dick: + dick[parent] = [] + dick[parent].append(track) - return left, right - left, target_link + random.shuffle(folders) + playlist = [] + for folder in folders: + playlist += dick[folder] -def draw_linked_text2(x, y, text, colour, font, click=False, replace=""): - link_pa = draw_linked_text( - (x, y), text, colour, font, replace=replace) - link_rect = [x + link_pa[0], y, link_pa[1], 18 * gui.scale] - if coll(link_rect): - if not click: - gui.cursor_want = 3 - if click: - webbrowser.open(link_pa[2], new=2, autoraise=True) - fields.add(link_rect) + if custom_list is not None: + return playlist + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("Shuffled Albums")), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) -def link_activate(x, y, link_pa, click=None): - link_rect = [x + link_pa[0], y - 2 * gui.scale, link_pa[1], 20 * gui.scale] + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a ra" - if click is None: - click = inp.mouse_click +def gen_best_random(index: int): + playlist = [] - fields.add(link_rect) - if coll(link_rect): - if not click: - gui.cursor_want = 3 - if click: - webbrowser.open(link_pa[2], new=2, autoraise=True) - track_box = True + for p in pctl.multi_playlist[index].playlist_ids: + time = star_store.get(p) + if time > 300: + playlist.append(p) -text_box_canvas_rect = SDL_Rect(0, 0, round(2000 * gui.scale), round(40 * gui.scale)) -text_box_canvas_hide_rect = SDL_Rect(0, 0, round(2000 * gui.scale), round(40 * gui.scale)) -text_box_canvas = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, text_box_canvas_rect.w, text_box_canvas_rect.h) -SDL_SetTextureBlendMode(text_box_canvas, SDL_BLENDMODE_BLEND) + random.shuffle(playlist) + if len(playlist) > 0: + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("Lucky Random")), + playlist_ids=copy.deepcopy(playlist), + hide_title=True)) -def pixel_to_logical(x): - return round((x / window_size[0]) * logical_size[0]) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a pt>300 rt" -class TextBox2: - cursor = True +def gen_reverse(index, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - def __init__(self) -> None: + playlist = list(reversed(source)) - self.text: str = "" - self.cursor_position = 0 - self.selection = 0 - self.offset = 0 - self.down_lock = False - self.paste_text = "" + if custom_list is not None: + return playlist - def paste(self) -> None: + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("Reversed")), + playlist_ids=copy.deepcopy(playlist), + hide_title=pctl.multi_playlist[index].hide_title)) - if SDL_HasClipboardText(): - clip = SDL_GetClipboardText().decode("utf-8") - self.paste_text = clip + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rv" - def copy(self) -> None: +def gen_folder_reverse(index: int, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - text = self.get_selection() - if not text: - text = self.text - if text != "": - SDL_SetClipboardText(text.encode("utf-8")) + folders = [] + dick = {} + for track in source: + parent = pctl.master_library[track].parent_folder_path + if parent not in folders: + folders.append(parent) + if parent not in dick: + dick[parent] = [] + dick[parent].append(track) - def set_text(self, text: str) -> None: + folders = list(reversed(folders)) + playlist = [] - self.text = text - if self.cursor_position > len(text): - self.cursor_position = 0 - self.selection = 0 - else: - self.selection = self.cursor_position + for folder in folders: + playlist += dick[folder] - def clear(self) -> None: - self.text = "" - #self.cursor_position = 0 - self.selection = self.cursor_position + if custom_list is not None: + return playlist - def highlight_all(self) -> None: - - self.selection = len(self.text) - self.cursor_position = 0 - - def eliminate_selection(self) -> None: - if self.selection != self.cursor_position: - if self.selection > self.cursor_position: - self.text = self.text[0: len(self.text) - self.selection] + self.text[len(self.text) - self.cursor_position:] - self.selection = self.cursor_position - else: - self.text = self.text[0: len(self.text) - self.cursor_position] + self.text[len(self.text) - self.selection:] - self.cursor_position = self.selection + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("Reversed Albums")), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - def get_selection(self, p: int = 1) -> str: - if self.selection != self.cursor_position: - if p == 1: - if self.selection > self.cursor_position: - return self.text[len(self.text) - self.selection: len(self.text) - self.cursor_position] + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rva" - return self.text[len(self.text) - self.cursor_position: len(self.text) - self.selection] - if p == 0: - return self.text[0: len(self.text) - max(self.cursor_position, self.selection)] - if p == 2: - return self.text[len(self.text) - min(self.cursor_position, self.selection):] +def gen_dupe(index: int) -> None: + playlist = pctl.multi_playlist[index].playlist_ids - else: - return "" + pctl.multi_playlist.append( + pl_gen( + title=gen_unique_pl_title(pctl.multi_playlist[index].title, _("Duplicate") + " ", 0), + playing=pctl.multi_playlist[index].playing, + playlist_ids=copy.deepcopy(playlist), + position=pctl.multi_playlist[index].position, + hide_title=pctl.multi_playlist[index].hide_title, + selected=pctl.multi_playlist[index].selected)) - def draw( - self, x, y, colour, active=True, secret=False, font=13, width=0, click=False, selection_height=18, big=False): +def gen_sort_path(index: int) -> None: + def path(index: int) -> str: + return pctl.master_library[index].fullpath - # A little bit messy - # For now, this is set up so where 'width' is set > 0, the cursor position becomes editable, - # otherwise it is fixed to end + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) + playlist = sorted(playlist, key=path) - SDL_SetRenderTarget(renderer, text_box_canvas) - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("Filepath Sorted")), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - text_box_canvas_rect.x = 0 - text_box_canvas_rect.y = 0 - SDL_RenderFillRect(renderer, text_box_canvas_rect) +def gen_sort_artist(index: int) -> None: + def artist(index: int) -> str: + return pctl.master_library[index].artist - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) + playlist = sorted(playlist, key=artist) - selection_height *= gui.scale + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("Artist Sorted")), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - if click is False: - click = inp.mouse_click - if mouse_down: - gui.update = 2 # TODO, more elegant fix +def gen_sort_album(index: int) -> None: + def album(index: int) -> None: + return pctl.master_library[index].album - rect = (x - 3, y - 2, width - 3, 21 * gui.scale) - select_rect = (x - 20 * gui.scale, y - 2, width + 20 * gui.scale, 21 * gui.scale) + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) + playlist = sorted(playlist, key=album) - fields.add(rect) + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + add_pl_tag(_("Album Sorted")), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - # Activate Menu - if coll(rect): - if right_click or level_2_right_click: - field_menu.activate(self) - - if width > 0 and active: - - if click and field_menu.active: - # field_menu.click() - click = False - - # Add text from input - if input_text != "": - self.eliminate_selection() - self.text = self.text[0: len(self.text) - self.cursor_position] + input_text + self.text[len( - self.text) - self.cursor_position:] - - def g(): - if len(self.text) == 0 or self.cursor_position == len(self.text): - return None - return self.text[len(self.text) - self.cursor_position - 1] - - def g2(): - if len(self.text) == 0 or self.cursor_position == 0: - return None - return self.text[len(self.text) - self.cursor_position] - - def d(): - self.text = self.text[0: len(self.text) - self.cursor_position - 1] + self.text[len( - self.text) - self.cursor_position:] - self.selection = self.cursor_position - - # Ctrl + Backspace to delete word - if inp.backspace_press and (key_ctrl_down or key_rctrl_down) and \ - self.cursor_position == self.selection and len(self.text) > 0 and self.cursor_position < len( - self.text): - while g() == " ": - d() - while g() != " " and g() != None: - d() - - # Ctrl + left to move cursor back a word - elif (key_ctrl_down or key_rctrl_down) and key_left_press: - while g() == " ": - self.cursor_position += 1 - if not key_shift_down: - self.selection = self.cursor_position - while g() != None and g() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": - self.cursor_position += 1 - if not key_shift_down: - self.selection = self.cursor_position - if g() == " ": - self.cursor_position -= 1 - if not key_shift_down: - self.selection = self.cursor_position - break +def get_playing_line() -> str: + if 3 > pctl.playing_state > 0: + title = pctl.master_library[pctl.track_queue[pctl.queue_step]].title + artist = pctl.master_library[pctl.track_queue[pctl.queue_step]].artist + return artist + " - " + title + return "Stopped" - # Ctrl + right to move cursor forward a word - elif (key_ctrl_down or key_rctrl_down) and key_right_press: - while g2() == " ": - self.cursor_position -= 1 - if not key_shift_down: - self.selection = self.cursor_position - while g2() != None and g2() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": - self.cursor_position -= 1 - if not key_shift_down: - self.selection = self.cursor_position - if g2() == " ": - self.cursor_position += 1 - if not key_shift_down: - self.selection = self.cursor_position - break +def reload_config_file(): + if transcode_list: + show_message(_("Cannot reload while a transcode is in progress!"), mode="error") + return - # Handle normal backspace - elif inp.backspace_press and len(self.text) > 0 and self.cursor_position < len(self.text): - while inp.backspace_press and len(self.text) > 0 and self.cursor_position < len(self.text): - if self.selection != self.cursor_position: - self.eliminate_selection() - else: - self.text = self.text[0:len(self.text) - self.cursor_position - 1] + self.text[len( - self.text) - self.cursor_position:] - inp.backspace_press -= 1 - elif inp.backspace_press and len(self.get_selection()) > 0: - self.eliminate_selection() - - # Left and right arrow keys to move cursor - if key_right_press: - if self.cursor_position > 0: - self.cursor_position -= 1 - if not key_shift_down and not key_shiftr_down: - self.selection = self.cursor_position - - if key_left_press: - if self.cursor_position < len(self.text): - self.cursor_position += 1 - if not key_shift_down and not key_shiftr_down: - self.selection = self.cursor_position - - if self.paste_text: - if "http://" in self.text and "http://" in self.paste_text: - self.text = "" - - self.paste_text = self.paste_text.rstrip(" ").lstrip(" ") - self.paste_text = self.paste_text.replace("\n", " ").replace("\r", "") - - self.eliminate_selection() - self.text = self.text[0: len(self.text) - self.cursor_position] + self.paste_text + self.text[len( - self.text) - self.cursor_position:] - self.paste_text = "" - - # Paste via ctrl-v - if key_ctrl_down and key_v_press: - clip = SDL_GetClipboardText().decode("utf-8") - self.eliminate_selection() - self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( - self.text) - self.cursor_position:] - - if key_ctrl_down and key_c_press: - self.copy() - - if key_ctrl_down and key_x_press: - if len(self.get_selection()) > 0: - text = self.get_selection() - if text != "": - SDL_SetClipboardText(text.encode("utf-8")) - self.eliminate_selection() - - if key_ctrl_down and key_a_press: - self.cursor_position = 0 - self.selection = len(self.text) - - # ddt.rect(rect, [255, 50, 50, 80], True) - if coll(rect) and not field_menu.active: - gui.cursor_want = 2 - - # Delete key to remove text in front of cursor - if key_del: - if self.selection != self.cursor_position: - self.eliminate_selection() - else: - self.text = self.text[0:len(self.text) - self.cursor_position] + self.text[len( - self.text) - self.cursor_position + 1:] - if self.cursor_position > 0: - self.cursor_position -= 1 - self.selection = self.cursor_position - - if key_home_press: - self.cursor_position = len(self.text) - if not key_shift_down and not key_shiftr_down: - self.selection = self.cursor_position - if key_end_press: - self.cursor_position = 0 - if not key_shift_down and not key_shiftr_down: - self.selection = self.cursor_position - - width -= round(15 * gui.scale) - t_len = ddt.get_text_w(self.text, font) - if active and editline and editline != input_text: - t_len += ddt.get_text_w(editline, font) - if not click and not self.down_lock: - cursor_x = ddt.get_text_w(self.text[:len(self.text) - self.cursor_position], font) - if self.cursor_position == 0 or cursor_x < self.offset + round( - 15 * gui.scale) or cursor_x > self.offset + width: - if t_len > width: - self.offset = t_len - width - - if cursor_x < self.offset: - self.offset = cursor_x - round(15 * gui.scale) - - self.offset = max(self.offset, 0) - else: - self.offset = 0 + load_prefs() + gui.opened_config_file = False - x -= self.offset + ddt.force_subpixel_text = prefs.force_subpixel_text + ddt.clear_text_cache() + pctl.playerCommand = "reload" + pctl.playerCommandReady = True + show_message(_("Configuration reloaded"), mode="done") + gui.update_layout() - if coll(select_rect): # coll((x - 15, y, width + 16, selection_height + 1)): - # ddt.rect_r((x - 15, y, width + 16, 19), [50, 255, 50, 50], True) - if click: - pre = 0 - post = 0 - if mouse_position[0] < x + 1: - self.cursor_position = len(self.text) - else: - for i in range(len(self.text)): - post = ddt.get_text_w(self.text[0:i + 1], font) - # pre_half = int((post - pre) / 2) - - if x + pre - 0 <= mouse_position[0] <= x + post + 0: - diff = post - pre - if mouse_position[0] >= x + pre + int(diff / 2): - self.cursor_position = len(self.text) - i - 1 - else: - self.cursor_position = len(self.text) - i - break - pre = post - else: - self.cursor_position = 0 - self.selection = 0 - self.down_lock = True - - if mouse_up: - self.down_lock = False - if self.down_lock: - pre = 0 - post = 0 - text = self.text - if secret: - text = "●" * len(self.text) - if mouse_position[0] < x + 1: - self.selection = len(text) - else: +def open_config_file(): + save_prefs() + target = str(config_directory / "tauon.conf") + if system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", "-t", target]) + else: + subprocess.call(["xdg-open", target]) + show_message(_("Config file opened."), _('Click "Reload" if you made any changes'), mode="arrow") + # reload_config_file() + # gui.message_box = False + gui.opened_config_file = True - for i in range(len(text)): - post = ddt.get_text_w(text[0:i + 1], font) - # pre_half = int((post - pre) / 2) +def open_keymap_file(): + target = str(config_directory / "input.txt") - if x + pre - 0 <= mouse_position[0] <= x + post + 0: - diff = post - pre + if not os.path.isfile(target): + show_message(_("Input file missing")) + return - if mouse_position[0] >= x + pre + int(diff / 2): - self.selection = len(text) - i - 1 + if system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", target]) + else: + subprocess.call(["xdg-open", target]) - else: - self.selection = len(text) - i +def open_file(target): + if not os.path.isfile(target): + show_message(_("Input file missing")) + return - break - pre = post + if system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", target]) + else: + subprocess.call(["xdg-open", target]) - else: - self.selection = 0 - - text = self.text[0: len(self.text) - self.cursor_position] - if secret: - text = "●" * len(text) - a = ddt.get_text_w(text, font) - - text = self.text[0: len(self.text) - self.selection] - if secret: - text = "●" * len(text) - b = ddt.get_text_w(text, font) - - top = y - if big: - top -= 12 * gui.scale - - ddt.rect([a, 0, b - a, selection_height], [40, 120, 180, 255]) - - if self.selection != self.cursor_position: - inf_comp = 0 - text = self.get_selection(0) - if secret: - text = "●" * len(text) - space = ddt.text((0, 0), text, colour, font) - text = self.get_selection(1) - if secret: - text = "●" * len(text) - space += ddt.text((0 + space - inf_comp, 0), text, [240, 240, 240, 255], font, bg=[40, 120, 180, 255]) - text = self.get_selection(2) - if secret: - text = "●" * len(text) - ddt.text((0 + space - (inf_comp * 2), 0), text, colour, font) - else: - text = self.text - if secret: - text = "●" * len(text) - ddt.text((0, 0), text, colour, font) +def open_data_directory(): + target = str(user_directory) + if system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", target]) + else: + subprocess.call(["xdg-open", target]) - text = self.text[0: len(self.text) - self.cursor_position] - if secret: - text = "●" * len(text) - space = ddt.get_text_w(text, font) +def remove_folder(index: int): + global default_playlist - if TextBox.cursor and self.selection == self.cursor_position: - # ddt.line(x + space, y + 2, x + space, y + 15, colour) + for b in range(len(default_playlist) - 1, -1, -1): + r_folder = pctl.master_library[index].parent_folder_name + if pctl.master_library[default_playlist[b]].parent_folder_name == r_folder: + del default_playlist[b] + reload() - ddt.rect((0 + space, 0 + 2, 1 * gui.scale, 14 * gui.scale), colour) +def convert_folder(index: int): + global default_playlist + global transcode_list - if click: - self.selection = self.cursor_position + if not tauon.test_ffmpeg(): + return - else: - width -= round(15 * gui.scale) - text = self.text - if secret: - text = "●" * len(text) - t_len = ddt.get_text_w(text, font) - ddt.text((0, 0), text, colour, font) - self.offset = 0 - if coll(rect) and not field_menu.active: - gui.cursor_want = 2 + folder = [] + if key_shift_down or key_shiftr_down: + track_object = pctl.get_track(index) + if track_object.is_network: + show_message(_("Transcoding tracks from network locations is not supported")) + return + folder = [index] - if active and editline != "" and editline != input_text: - ex = ddt.text((space + round(4 * gui.scale), 0), editline, [240, 230, 230, 255], font) - tw, th = ddt.get_text_wh(editline, font, max_x=2000) - ddt.rect((space + round(4 * gui.scale), th + round(2 * gui.scale), ex, round(1 * gui.scale)), [245, 245, 245, 255]) + if prefs.transcode_codec == "flac" and track_object.file_ext.lower() in ( + "mp3", "opus", + "mp4", "ogg", + "aac"): + show_message(_("NO! Bad user!"), _("Im not going to let you transcode a lossy codec to a lossless one!"), + mode="warning") - rect = SDL_Rect(pixel_to_logical(x + space + tw + (5 * gui.scale)), pixel_to_logical(y + th + 4 * gui.scale), 1, 1) - SDL_SetTextInputRect(rect) + return + folder = [index] - animate_monitor_timer.set() + else: + r_folder = pctl.master_library[index].parent_folder_path + for item in default_playlist: + if r_folder == pctl.master_library[item].parent_folder_path: - text_box_canvas_hide_rect.x = 0 - text_box_canvas_hide_rect.y = 0 + track_object = pctl.get_track(item) + if track_object.file_ext == "SPOT": # track_object.is_network: + show_message(_("Transcoding spotify tracks not possible")) + return - # if self.offset: - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) + if item not in folder: + folder.append(item) + #logging.info(prefs.transcode_codec) + #logging.info(track_object.file_ext) + if prefs.transcode_codec == "flac" and track_object.file_ext.lower() in ( + "mp3", "opus", + "mp4", "ogg", + "aac"): + show_message(_("NO! Bad user!"), _("Im not going to let you transcode a lossy codec to a lossless one!"), + mode="warning") - text_box_canvas_hide_rect.w = round(self.offset) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_RenderFillRect(renderer, text_box_canvas_hide_rect) + return - text_box_canvas_hide_rect.w = round(t_len) - text_box_canvas_hide_rect.x = round(self.offset + width + round(5 * gui.scale)) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_RenderFillRect(renderer, text_box_canvas_hide_rect) + #logging.info(folder) + transcode_list.append(folder) + tauon.thread_manager.ready("worker") - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) - SDL_SetRenderTarget(renderer, gui.main_texture) +def transfer(index: int, args) -> None: + global cargo + global default_playlist + old_cargo = copy.deepcopy(cargo) - text_box_canvas_rect.x = round(x) - text_box_canvas_rect.y = round(y) - SDL_RenderCopy(renderer, text_box_canvas, None, text_box_canvas_rect) + if args[0] == 1 or args[0] == 0: # copy + if args[1] == 1: # single track + cargo.append(index) + if args[0] == 0: # cut + del default_playlist[pctl.selected_in_playlist] + elif args[1] == 2: # folder + for b in range(len(default_playlist)): + if pctl.master_library[default_playlist[b]].parent_folder_name == pctl.master_library[ + index].parent_folder_name: + cargo.append(default_playlist[b]) + if args[0] == 0: # cut + for b in reversed(range(len(default_playlist))): + if pctl.master_library[default_playlist[b]].parent_folder_name == pctl.master_library[ + index].parent_folder_name: + del default_playlist[b] -class TextBox: - cursor = True + elif args[1] == 3: # playlist + cargo += default_playlist + if args[0] == 0: # cut + default_playlist = [] - def __init__(self) -> None: + elif args[0] == 2: # Drop + if args[1] == 1: # Before - self.text = "" - self.cursor_position = 0 - self.selection = 0 - self.down_lock = False + insert = pctl.selected_in_playlist + while insert > 0 and pctl.master_library[default_playlist[insert]].parent_folder_name == \ + pctl.master_library[index].parent_folder_name: + insert -= 1 + if insert == 0: + break + else: + insert += 1 - def paste(self) -> None: + while len(cargo) > 0: + default_playlist.insert(insert, cargo.pop()) - if SDL_HasClipboardText(): - clip = SDL_GetClipboardText().decode("utf-8") + elif args[1] == 2: # After + insert = pctl.selected_in_playlist - if "http://" in self.text and "http://" in clip: - self.text = "" + while insert < len(default_playlist) and pctl.master_library[default_playlist[insert]].parent_folder_name == \ + pctl.master_library[index].parent_folder_name: + insert += 1 - clip = clip.rstrip(" ").lstrip(" ") - clip = clip.replace("\n", " ").replace("\r", "") + while len(cargo) > 0: + default_playlist.insert(insert, cargo.pop()) + elif args[1] == 3: # End + default_playlist += cargo + # cargo = [] - self.eliminate_selection() - self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( - self.text) - self.cursor_position:] + cargo = old_cargo + reload() - def copy(self) -> None: +def temp_copy_folder(ref): + global cargo + cargo = [] + transfer(ref, args=[1, 2]) - text = self.get_selection() - if not text: - text = self.text - if text != "": - SDL_SetClipboardText(text.encode("utf-8")) +def activate_track_box(index: int): + global track_box + global r_menu_index + r_menu_index = index + track_box = True + track_box_path_tool_timer.set() - def set_text(self, text): +def menu_paste(position): + paste(None, position) - self.text = text - self.cursor_position = 0 - self.selection = 0 +def s_copy(): + # Copy tracks to internal clipboard + # gui.lightning_copy = False + # if key_shift_down: + gui.lightning_copy = True - def clear(self) -> None: - self.text = "" + clip = copy_from_clipboard() + if "file://" in clip: + copy_to_clipboard("") - def highlight_all(self) -> None: + global cargo + cargo = [] + if default_playlist: + for item in shift_selection: + cargo.append(default_playlist[item]) - self.selection = len(self.text) - self.cursor_position = 0 + if not cargo and -1 < pctl.selected_in_playlist < len(default_playlist): + cargo.append(default_playlist[pctl.selected_in_playlist]) - def highlight_none(self) -> None: - self.selection = 0 - self.cursor_position = 0 + tauon.copied_track = None - def eliminate_selection(self) -> None: - if self.selection != self.cursor_position: - if self.selection > self.cursor_position: - self.text = self.text[0: len(self.text) - self.selection] + self.text[ - len(self.text) - self.cursor_position:] - self.selection = self.cursor_position - else: - self.text = self.text[0: len(self.text) - self.cursor_position] + self.text[ - len(self.text) - self.selection:] - self.cursor_position = self.selection - - def get_selection(self, p: int = 1): - if self.selection != self.cursor_position: - if p == 1: - if self.selection > self.cursor_position: - return self.text[len(self.text) - self.selection: len(self.text) - self.cursor_position] - - return self.text[len(self.text) - self.cursor_position: len(self.text) - self.selection] - if p == 0: - return self.text[0: len(self.text) - max(self.cursor_position, self.selection)] - if p == 2: - return self.text[len(self.text) - min(self.cursor_position, self.selection):] + if len(cargo) == 1: + tauon.copied_track = cargo[0] - else: - return "" - - def draw( - self, x: int, y: int, colour: list[int], active: bool = True, secret: bool = False, - font: int = 13, width: int = 0, click: bool = False, selection_height: int = 18, big: bool = False): +def directory_size(path: str) -> int: + total = 0 + for dirpath, dirname, filenames in os.walk(path): + for file in filenames: + path = os.path.join(dirpath, file) + total += os.path.getsize(path) + return total - # A little bit messy - # For now, this is set up so where 'width' is set > 0, the cursor position becomes editable, - # otherwise it is fixed to end +def lightning_paste(): + move = True + # if not key_shift_down: + # move = False - selection_height *= gui.scale + move_track = pctl.get_track(cargo[0]) + move_path = move_track.parent_folder_path - if click is False: - click = inp.mouse_click + for item in cargo: + if move_path != pctl.get_track(item).parent_folder_path: + show_message( + _("More than one folder is in the clipboard"), + _("This function can only move one folder at a time."), mode="info") + return - if width > 0 and active: + match_track = pctl.get_track(default_playlist[shift_selection[0]]) + match_path = match_track.parent_folder_path - rect = (x - 3, y - 2, width - 3, 21 * gui.scale) - select_rect = (x - 20 * gui.scale, y - 2, width + 20 * gui.scale, 21 * gui.scale) - if big: - rect = (x - 3, y - 15 * gui.scale, width - 3, 35 * gui.scale) - select_rect = (x - 50 * gui.scale, y - 15 * gui.scale, width + 50 * gui.scale, 35 * gui.scale) + if pctl.playing_state > 0 and move: + if pctl.playing_object().parent_folder_path == move_path: + pctl.stop(True) - # Activate Menu - if coll(rect): - if right_click or level_2_right_click: - field_menu.activate(self) - - if click and field_menu.active: - # field_menu.click() - click = False - - # Add text from input - if input_text != "": - self.eliminate_selection() - self.text = self.text[0: len(self.text) - self.cursor_position] + input_text + self.text[ - len(self.text) - self.cursor_position:] - - def g(): - if len(self.text) == 0 or self.cursor_position == len(self.text): - return None - return self.text[len(self.text) - self.cursor_position - 1] - - def g2(): - if len(self.text) == 0 or self.cursor_position == 0: - return None - return self.text[len(self.text) - self.cursor_position] - - def d(): - self.text = self.text[0: len(self.text) - self.cursor_position - 1] + self.text[ - len(self.text) - self.cursor_position:] - self.selection = self.cursor_position - - # Ctrl + Backspace to delete word - if inp.backspace_press and (key_ctrl_down or key_rctrl_down) and \ - self.cursor_position == self.selection and len(self.text) > 0 and self.cursor_position < len( - self.text): - while g() == " ": - d() - while g() != " " and g() != None: - d() - - # Ctrl + left to move cursor back a word - elif (key_ctrl_down or key_rctrl_down) and key_left_press: - while g() == " ": - self.cursor_position += 1 - if not key_shift_down: - self.selection = self.cursor_position - while g() != None and g() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": - self.cursor_position += 1 - if not key_shift_down: - self.selection = self.cursor_position - if g() == " ": - self.cursor_position -= 1 - if not key_shift_down: - self.selection = self.cursor_position - break + p = Path(match_path) + s = list(p.parts) + base = s[0] + c = base + del s[0] - # Ctrl + right to move cursor forward a word - elif (key_ctrl_down or key_rctrl_down) and key_right_press: - while g2() == " ": - self.cursor_position -= 1 - if not key_shift_down: - self.selection = self.cursor_position - while g2() != None and g2() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": - self.cursor_position -= 1 - if not key_shift_down: - self.selection = self.cursor_position - if g2() == " ": - self.cursor_position += 1 - if not key_shift_down: - self.selection = self.cursor_position - break + to_move = [] + for pl in pctl.multi_playlist: + for i in reversed(range(len(pl.playlist_ids))): + if pctl.get_track(pl.playlist_ids[i]).parent_folder_path == move_track.parent_folder_path: + to_move.append(pl.playlist_ids[i]) - # Handle normal backspace - elif inp.backspace_press and len(self.text) > 0 and self.cursor_position < len(self.text): - while inp.backspace_press and len(self.text) > 0 and self.cursor_position < len(self.text): - if self.selection != self.cursor_position: - self.eliminate_selection() - else: - self.text = self.text[0:len(self.text) - self.cursor_position - 1] + self.text[ - len(self.text) - self.cursor_position:] - inp.backspace_press -= 1 - elif inp.backspace_press and len(self.get_selection()) > 0: - self.eliminate_selection() - - # Left and right arrow keys to move cursor - if key_right_press: - if self.cursor_position > 0: - self.cursor_position -= 1 - if not key_shift_down and not key_shiftr_down: - self.selection = self.cursor_position - - if key_left_press: - if self.cursor_position < len(self.text): - self.cursor_position += 1 - if not key_shift_down and not key_shiftr_down: - self.selection = self.cursor_position - - # Paste via ctrl-v - if key_ctrl_down and key_v_press: - clip = SDL_GetClipboardText().decode("utf-8") - self.eliminate_selection() - self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( - self.text) - self.cursor_position:] - - if key_ctrl_down and key_c_press: - self.copy() - - if key_ctrl_down and key_x_press: - if len(self.get_selection()) > 0: - text = self.get_selection() - if text != "": - SDL_SetClipboardText(text.encode("utf-8")) - self.eliminate_selection() - - if key_ctrl_down and key_a_press: - self.cursor_position = 0 - self.selection = len(self.text) - - # ddt.rect_r(rect, [255, 50, 50, 80], True) - if coll(rect) and not field_menu.active: - gui.cursor_want = 2 + to_move = list(set(to_move)) - fields.add(rect) + for level in s: + upper = c + c = os.path.join(c, level) - # Delete key to remove text in front of cursor - if key_del: - if self.selection != self.cursor_position: - self.eliminate_selection() - else: - self.text = self.text[0:len(self.text) - self.cursor_position] + self.text[len( - self.text) - self.cursor_position + 1:] - if self.cursor_position > 0: - self.cursor_position -= 1 - self.selection = self.cursor_position - - if key_home_press: - self.cursor_position = len(self.text) - if not key_shift_down and not key_shiftr_down: - self.selection = self.cursor_position - if key_end_press: - self.cursor_position = 0 - if not key_shift_down and not key_shiftr_down: - self.selection = self.cursor_position - - if coll(select_rect): - # ddt.rect_r((x - 15, y, width + 16, 19), [50, 255, 50, 50], True) - if click: - pre = 0 - post = 0 - if mouse_position[0] < x + 1: - self.cursor_position = len(self.text) - else: - for i in range(len(self.text)): - post = ddt.get_text_w(self.text[0:i + 1], font) - # pre_half = int((post - pre) / 2) - - if x + pre - 0 <= mouse_position[0] <= x + post + 0: - diff = post - pre - if mouse_position[0] >= x + pre + int(diff / 2): - self.cursor_position = len(self.text) - i - 1 - else: - self.cursor_position = len(self.text) - i - break - pre = post - else: - self.cursor_position = 0 - self.selection = 0 - self.down_lock = True - - if mouse_up: - self.down_lock = False - if self.down_lock: - pre = 0 - post = 0 - if mouse_position[0] < x + 1: - - self.selection = len(self.text) - else: + t_artist = match_track.artist + ta_artist = match_track.album_artist - for i in range(len(self.text)): - post = ddt.get_text_w(self.text[0:i + 1], font) - # pre_half = int((post - pre) / 2) + t_artist = filename_safe(t_artist) + ta_artist = filename_safe(ta_artist) - if x + pre - 0 <= mouse_position[0] <= x + post + 0: - diff = post - pre + if (len(t_artist) > 0 and t_artist in level) or \ + (len(ta_artist) > 0 and ta_artist in level): - if mouse_position[0] >= x + pre + int(diff / 2): - self.selection = len(self.text) - i - 1 + logging.info("found target artist level") + logging.info(t_artist) + logging.info("Upper folder is: " + upper) - else: - self.selection = len(self.text) - i + if len(move_path) < 4: + show_message(_("Safety interupt! The source path seems oddly short."), move_path, mode="error") + return - break - pre = post + if not os.path.isdir(upper): + show_message(_("The target directory is missing!"), upper, mode="warning") + return - else: - self.selection = 0 + if not os.path.isdir(move_path): + show_message(_("The source directory is missing!"), move_path, mode="warning") + return - a = ddt.get_text_w(self.text[0: len(self.text) - self.cursor_position], font) - # logging.info("") - # logging.info(self.selection) - # logging.info(self.cursor_position) + protect = ("", "Documents", "Music", "Desktop", "Downloads") + for fo in protect: + if move_path.strip("\\/") == os.path.join(os.path.expanduser("~"), fo).strip("\\/"): + show_message(_("Better not do anything to that folder!"), os.path.join(os.path.expanduser("~"), fo), + mode="warning") + return - b = ddt.get_text_w(self.text[0: len(self.text) - self.selection], font) + if directory_size(move_path) > 3000000000: + show_message(_("Folder size safety limit reached! (3GB)"), move_path, mode="warning") + return - # rint((a, b)) + if len(next(os.walk(move_path))[2]) > max(20, len(to_move) * 2): + show_message(_("Safety interupt! The source folder seems to have many files."), move_path, mode="warning") + return - top = y - if big: - top -= 12 * gui.scale + artist = move_track.artist + if move_track.album_artist != "": + artist = move_track.album_artist - ddt.rect([x + a, top, b - a, selection_height], [40, 120, 180, 255]) + artist = filename_safe(artist) - if self.selection != self.cursor_position: - inf_comp = 0 - space = ddt.text((x, y), self.get_selection(0), colour, font) - space += ddt.text( - (x + space - inf_comp, y), self.get_selection(1), [240, 240, 240, 255], font, - bg=[40, 120, 180, 255]) - ddt.text((x + space - (inf_comp * 2), y), self.get_selection(2), colour, font) - else: - ddt.text((x, y), self.text, colour, font) + if artist == "": + show_message(_("The track needs to have an artist name.")) + return - space = ddt.get_text_w(self.text[0: len(self.text) - self.cursor_position], font) + artist_folder = os.path.join(upper, artist) - if TextBox.cursor and self.selection == self.cursor_position: - # ddt.line(x + space, y + 2, x + space, y + 15, colour) + logging.info("Target will be: " + artist_folder) - if big: - # ddt.rect_r((xx + 1 , yy - 12 * gui.scale, 2 * gui.scale, 27 * gui.scale), colour, True) - ddt.rect((x + space, y - 15 * gui.scale + 2, 1 * gui.scale, 30 * gui.scale), colour) - else: - ddt.rect((x + space, y + 2, 1 * gui.scale, 14 * gui.scale), colour) + if os.path.isdir(artist_folder): + logging.info("The target artist folder already exists") + else: + logging.info("Need to make artist folder") + os.makedirs(artist_folder) - if click: - self.selection = self.cursor_position + logging.info("The folder to be moved is: " + move_path) + load_order = LoadClass() + load_order.target = os.path.join(artist_folder, move_track.parent_folder_name) + load_order.playlist = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - else: - if active: - self.text += input_text - if input_text != "": - self.cursor = True + insert = shift_selection[0] + old_insert = insert + while insert < len(default_playlist) and pctl.master_library[ + pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids[insert]].parent_folder_name == \ + pctl.master_library[ + pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids[old_insert]].parent_folder_name: + insert += 1 - while inp.backspace_press and len(self.text) > 0: - self.text = self.text[:-1] - inp.backspace_press -= 1 + load_order.playlist_position = insert - if key_ctrl_down and key_v_press: - self.paste() + move_jobs.append( + (move_path, os.path.join(artist_folder, move_track.parent_folder_name), move, + move_track.parent_folder_name, load_order)) + tauon.thread_manager.ready("worker") + # Remove all tracks with the old paths + for pl in pctl.multi_playlist: + for i in reversed(range(len(pl.playlist_ids))): + if pctl.get_track(pl.playlist_ids[i]).parent_folder_path == move_track.parent_folder_path: + del pl.playlist_ids[i] - if secret: - space = ddt.text((x, y), "●" * len(self.text), colour, font) - else: - space = ddt.text((x, y), self.text, colour, font) + break + else: + show_message(_("Could not find a folder with the artist's name to match level at.")) + return - if active and TextBox.cursor: - xx = x + space + 1 - yy = y + 3 - if big: - ddt.rect((xx + 1, yy - 12 * gui.scale, 2 * gui.scale, 27 * gui.scale), colour) - else: - ddt.rect((xx, yy, 1 * gui.scale, 14 * gui.scale), colour) + # for file in os.listdir(artist_folder): + # - if active and editline != "" and editline != input_text: - ex = ddt.text((x + space + round(4 * gui.scale), y), editline, [240, 230, 230, 255], font) - tw, th = ddt.get_text_wh(editline, font, max_x=2000) - ddt.rect((x + space + round(4 * gui.scale), (y + th) - round(4 * gui.scale), ex, round(1 * gui.scale)), - [245, 245, 245, 255]) + if album_mode: + prep_gal() + reload_albums(True) - rect = SDL_Rect(pixel_to_logical(x + space + tw + 5 * gui.scale), pixel_to_logical(y + th + 4 * gui.scale), 1, 1) - SDL_SetTextInputRect(rect) + cargo.clear() + gui.lightning_copy = False - animate_monitor_timer.set() +def paste(playlist_no=None, track_id=None): + clip = copy_from_clipboard() + logging.info(clip) + if "tidal.com/album/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + if num and num.isnumeric(): + logging.info(num) + tauon.tidal.append_album(num) + clip = False + elif "tidal.com/playlist/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + tauon.tidal.playlist(num) + clip = False -rename_text_area = TextBox() -gst_output_field = TextBox2() -gst_output_field.text = prefs.gst_output -search_text = TextBox() -rename_files = TextBox2() -sub_lyrics_a = TextBox2() -sub_lyrics_b = TextBox2() -sync_target = TextBox2() -edit_artist = TextBox2() -edit_album = TextBox2() -edit_title = TextBox2() -edit_album_artist = TextBox2() + elif "tidal.com/mix/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + tauon.tidal.mix(num) + clip = False -rename_files.text = prefs.rename_tracks_template -if rename_files_previous: - rename_files.text = rename_files_previous + elif "tidal.com/browse/track/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + tauon.tidal.track(num) + clip = False -text_plex_usr = TextBox2() -text_plex_pas = TextBox2() -text_plex_ser = TextBox2() + elif "tidal.com/browse/artist/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + tauon.tidal.artist(num) + clip = False -text_jelly_usr = TextBox2() -text_jelly_pas = TextBox2() -text_jelly_ser = TextBox2() + elif "spotify" in clip: + cargo.clear() + for link in clip.split("\n"): + logging.info(link) + link = link.strip() + if clip.startswith(("https://open.spotify.com/track/", "spotify:track:")): + tauon.spot_ctl.append_track(link) + elif clip.startswith(("https://open.spotify.com/album/", "spotify:album:")): + l = tauon.spot_ctl.append_album(link, return_list=True) + if l: + cargo.extend(l) + elif clip.startswith("https://open.spotify.com/playlist/"): + tauon.spot_ctl.playlist(link) + if album_mode: + reload_albums() + gui.pl_update += 1 + clip = False -text_koel_usr = TextBox2() -text_koel_pas = TextBox2() -text_koel_ser = TextBox2() + found = False + if clip: + clip = clip.split("\n") + for i, line in enumerate(clip): + if line.startswith(("file://", "/")): + target = str(urllib.parse.unquote(line)).replace("file://", "").replace("\r", "") + load_order = LoadClass() + load_order.target = target + load_order.playlist = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int -text_air_usr = TextBox2() -text_air_pas = TextBox2() -text_air_ser = TextBox2() + if playlist_no is not None: + load_order.playlist = pl_to_id(playlist_no) + if track_id is not None: + load_order.playlist_position = r_menu_position -text_spot_client = TextBox2() -text_spot_secret = TextBox2() -text_spot_username = TextBox2() -text_spot_password = TextBox2() + load_orders.append(copy.deepcopy(load_order)) + found = True -text_maloja_url = TextBox2() -text_maloja_key = TextBox2() + if not found: -text_sat_url = TextBox2() -text_sat_playlist = TextBox2() + if playlist_no is None: + if track_id is None: + transfer(0, (2, 3)) + else: + transfer(track_id, (2, 2)) + else: + append_playlist(playlist_no) -rename_folder = TextBox2() -rename_folder.text = prefs.rename_folder_template -if rename_folder_previous: - rename_folder.text = rename_folder_previous + gui.pl_update += 1 -temp_dest = SDL_Rect(0, 0) +def s_cut(): + s_copy() + del_selected() -def img_slide_update_gall(value, pause: bool = True) -> None: - global album_mode_art_size - gui.halt_image_rendering = True +def paste_playlist_coast_fire(): + url = None + if tauon.spot_ctl.coasting and pctl.playing_state == 3: + url = tauon.spot_ctl.get_album_url_from_local(pctl.playing_object()) + elif pctl.playing_ready() and "spotify-album-url" in pctl.playing_object().misc: + url = pctl.playing_object().misc["spotify-album-url"] + if url: + default_playlist.extend(tauon.spot_ctl.append_album(url, return_list=True)) + gui.pl_update += 1 - album_mode_art_size = value +def paste_playlist_track_coast_fire(): + url = None + # if tauon.spot_ctl.coasting and pctl.playing_state == 3: + # url = tauon.spot_ctl.get_album_url_from_local(pctl.playing_object()) + if pctl.playing_ready() and "spotify-track-url" in pctl.playing_object().misc: + url = pctl.playing_object().misc["spotify-track-url"] + if url: + tauon.spot_ctl.append_track(url) + gui.pl_update += 1 - clear_img_cache(False) - if pause: - gallery_load_delay.set() - gui.frame_callback_list.append(TestTimer(0.6)) - gui.halt_image_rendering = False +def paste_playlist_coast_album(): + shoot_dl = threading.Thread(target=paste_playlist_coast_fire) + shoot_dl.daemon = True + shoot_dl.start() - # Update sizes - tauon.gall_ren.size = album_mode_art_size +def paste_playlist_coast_track(): + shoot_dl = threading.Thread(target=paste_playlist_track_coast_fire) + shoot_dl.daemon = True + shoot_dl.start() - if album_mode_art_size > 150: - prefs.thin_gallery_borders = False +def paste_playlist_coast_album_deco(): + if tauon.spot_ctl.coasting or tauon.spot_ctl.playing: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled + return [line_colour, colours.menu_background, None] -def clear_img_cache(delete_disk: bool = True) -> None: - global album_art_gen - album_art_gen.clear_cache() - prefs.failed_artists.clear() - prefs.failed_background_artists.clear() - tauon.gall_ren.key_list = [] - - i = 0 - while len(tauon.gall_ren.queue) > 0: - time.sleep(0.01) - i += 1 - if i > 5 / 0.01: - break - - for key, value in tauon.gall_ren.gall.items(): - SDL_DestroyTexture(value[2]) - tauon.gall_ren.gall = {} +def refind_playing(): + # Refind playing index + if pctl.playing_ready(): + for i, n in enumerate(default_playlist): + if pctl.track_queue[pctl.queue_step] == n: + pctl.playlist_playing_position = i + break - if delete_disk: - dirs = [g_cache_dir, n_cache_dir, e_cache_dir] - for direc in dirs: - if os.path.isdir(direc): - for item in os.listdir(direc): - path = os.path.join(direc, item) - os.remove(path) +def del_selected(force_delete: bool = False): + global shift_selection - prefs.failed_artists.clear() - for key, value in artist_list_box.thumb_cache.items(): - if value: - SDL_DestroyTexture(value[0]) - artist_list_box.thumb_cache.clear() gui.update += 1 + gui.pl_update = 1 + if not shift_selection: + shift_selection = [pctl.selected_in_playlist] -def clear_track_image_cache(track: TrackClass): - gui.halt_image_rendering = True - if tauon.gall_ren.queue: - time.sleep(0.05) - if tauon.gall_ren.queue: - time.sleep(0.2) - if tauon.gall_ren.queue: - time.sleep(0.5) - - direc = os.path.join(g_cache_dir) - if os.path.isdir(direc): - for item in os.listdir(direc): - n = item.split("-") - if len(n) > 2 and n[2] == str(track.index): - os.remove(os.path.join(direc, item)) - logging.info("Cleared cache thumbnail: " + os.path.join(direc, item)) + if not default_playlist: + return - keys = set() - for key, value in tauon.gall_ren.gall.items(): - if key[0] == track: - SDL_DestroyTexture(value[2]) - if key not in keys: - keys.add(key) - for key in keys: - del tauon.gall_ren.gall[key] - if key in tauon.gall_ren.key_list: - tauon.gall_ren.key_list.remove(key) + li = [] - gui.halt_image_rendering = False - album_art_gen.clear_cache() + for item in reversed(shift_selection): + if item > len(default_playlist) - 1: + return + li.append((item, default_playlist[item])) # take note for force delete -class ImageObject: - def __init__(self) -> None: - self.index = 0 - self.texture = None - self.rect = None - self.request_size = (0, 0) - self.original_size = (0, 0) - self.actual_size = (0, 0) - self.source = "" - self.offset = 0 - self.stats = True - self.format = "" + # Correct track playing position + if pctl.active_playlist_playing == pctl.active_playlist_viewing: + if 0 < pctl.playlist_playing_position + 1 > item: + pctl.playlist_playing_position -= 1 + del default_playlist[item] -class AlbumArt: - def __init__(self): - self.image_types = {"jpg", "JPG", "jpeg", "JPEG", "PNG", "png", "BMP", "bmp", "GIF", "gif", "jxl", "JXL"} - self.art_folder_names = { - "art", "scans", "scan", "booklet", "images", "image", "cover", - "covers", "coverart", "albumart", "gallery", "jacket", "artwork", - "bonus", "bk", "cover artwork", "cover art"} - self.source_cache: dict[int, list[tuple[int, str]]] = {} - self.image_cache: list[ImageObject] = [] - self.current_wu = None + if force_delete: + for item in li: - self.blur_texture = None - self.blur_rect = None - self.loaded_bg_type = 0 + tr = pctl.get_track(item[1]) + if not tr.is_network: + try: + send2trash(tr.fullpath) + show_message(_("Tracks sent to trash")) + except Exception: + logging.exception("One or more tracks could not be sent to trash") + show_message(_("One or more tracks could not be sent to trash")) - self.download_in_progress = False - self.downloaded_image = None - self.downloaded_track = None + if force_delete: + try: + os.remove(tr.fullpath) + show_message(_("Files deleted"), mode="info") + except Exception: + logging.exception("Error deleting one or more files") + show_message(_("Error deleting one or more files"), mode="error") + else: + undo.bk_tracks(pctl.active_playlist_viewing, li) - self.base64cache = (0, 0, "") - self.processing64on = None + reload() + tree_view_box.clear_target_pl(pctl.active_playlist_viewing) - self.bin_cached = (None, None, None) # track, subsource, bin + pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(default_playlist) - 1) - self.embed_cached = (None, None) + shift_selection = [pctl.selected_in_playlist] + gui.pl_update += 1 + refind_playing() + pctl.notify_change() - def async_download_image(self, track: TrackClass, subsource: list[tuple[int, str]]) -> None: +def force_del_selected(): + del_selected(force_delete=True) - self.downloaded_image = album_art_gen.get_source_raw(0, 0, track, subsource=subsource) - self.downloaded_track = track - self.download_in_progress = False - gui.update += 1 +def test_show(dummy): + return album_mode - def get_info(self, track_object: TrackClass) -> list[tuple[str, int, int, int, str]]: +def show_in_gal(track: TrackClass, silent: bool = False): + # goto_album(pctl.playlist_selected) + gui.gallery_animate_highlight_on = goto_album(pctl.selected_in_playlist) + if not silent: + gallery_select_animate_timer.set() - sources = self.get_sources(track_object) - if len(sources) == 0: - return None +def last_fm_test(ignore): + if lastfm.connected: + return True + return False - offset = self.get_offset(track_object.fullpath, sources) +def heart_xmenu_colour(): + global r_menu_index + if love(False, r_menu_index): + return [245, 60, 60, 255] + if colours.lm: + return [255, 150, 180, 255] + return None - o_size = (0, 0) - format = "ERROR" +def spot_heart_xmenu_colour(): + if not (pctl.playing_state == 1 or pctl.playing_state == 2): + return None + tr = pctl.playing_object() + if tr and "spotify-liked" in tr.misc: + return [30, 215, 96, 255] + return None - for item in self.image_cache: - if item.index == track_object.index and item.offset == offset: - o_size = item.original_size - format = item.format - break +def love_decox(): + global r_menu_index - else: - # Hacky fix - # A quirk is the index stays of the cached image - # This workaround can be done since (currently) cache has max size of 1 - if self.image_cache: - o_size = self.image_cache[0].original_size - format = self.image_cache[0].format + if love(False, r_menu_index): + return [colours.menu_text, colours.menu_background, _("Un-Love Track")] + return [colours.menu_text, colours.menu_background, _("Love Track")] - return [sources[offset][0], len(sources), offset, o_size, format] +def love_index(): + global r_menu_index - def get_sources(self, tr: TrackClass) -> list[tuple[int, str]]: + notify = False + if not gui.show_hearts: + notify = True - filepath = tr.fullpath - ext = tr.file_ext + # love(True, r_menu_index) + shoot_love = threading.Thread(target=love, args=[True, r_menu_index, False, notify]) + shoot_love.daemon = True + shoot_love.start() - # Check if source list already exists, if not, make it - if tr.index in self.source_cache: - return self.source_cache[tr.index] +def toggle_spotify_like_ref(): + tr = pctl.get_track(r_menu_index) + if tr: + shoot_dl = threading.Thread(target=toggle_spotify_like_active2, args=([tr])) + shoot_dl.daemon = True + shoot_dl.start() - source_list: list[tuple[int, str]] = [] # istag, +def toggle_spotify_like3(): + toggle_spotify_like_active2(pctl.get_track(r_menu_index)) - # Source type the is first element in list - # 0 = File - # 1 = Embedded in tag - # 2 = Network location +def toggle_spotify_like_row_deco(): + tr = pctl.get_track(r_menu_index) + text = _("Spotify Like Track") - if tr.is_network: - # Add url if network target - if tr.art_url_key: - source_list.append([2, tr.art_url_key]) - else: - # Check for local image files - direc = os.path.dirname(filepath) - try: - items_in_dir = os.listdir(direc) - except FileNotFoundError: - logging.warning(f"Failed to find directory: {direc}") - return [] - except Exception: - logging.exception(f"Unknown error loading directory: {direc}") - return [] + # if pctl.playing_state == 0 or not tr or not "spotify-track-url" in tr.misc: + # return [colours.menu_text_disabled, colours.menu_background, text] + if "spotify-liked" in tr.misc: + text = _("Un-like Spotify Track") - # Check for embedded image - try: - pic = self.get_embed(tr) - if pic: - source_list.append([1, filepath]) - except Exception: - logging.exception("Failed to get embedded image") + return [colours.menu_text, colours.menu_background, text] - if not tr.is_network: +def spot_like_show_test(x): + return spotify_show_test and pctl.get_track(r_menu_index).file_ext == "SPTY" - dirs_in_dir = [ - subdirec for subdirec in items_in_dir if - os.path.isdir(os.path.join(direc, subdirec)) and subdirec.lower() in self.art_folder_names] +def spot_heart_menu_colour(): + tr = pctl.get_track(r_menu_index) + if tr and "spotify-liked" in tr.misc: + return [30, 215, 96, 255] + return None - ins = len(source_list) - for i in range(len(items_in_dir)): - if os.path.splitext(items_in_dir[i])[1][1:] in self.image_types: - dir_path = os.path.join(direc, items_in_dir[i]).replace("\\", "/") - # The image name "Folder" is likely desired to be prioritised over other names - if os.path.splitext(os.path.basename(dir_path))[0] in ("Folder", "folder", "Cover", "cover"): - source_list.insert(ins, [0, dir_path]) - else: - source_list.append([0, dir_path]) +def add_to_queue(ref): + pctl.force_queue.append(queue_item_gen(ref, r_menu_position, pl_to_id(pctl.active_playlist_viewing))) + queue_timer_set() + if prefs.stop_end_queue: + pctl.auto_stop = False - for i in range(len(dirs_in_dir)): - subdirec = os.path.join(direc, dirs_in_dir[i]) - items_in_dir2 = os.listdir(subdirec) +def add_selected_to_queue(): + gui.pl_update += 1 + if prefs.stop_end_queue: + pctl.auto_stop = False + if gui.album_tab_mode: + add_album_to_queue(default_playlist[get_album_info(pctl.selected_in_playlist)[1][0]], pctl.selected_in_playlist) + queue_timer_set() + else: + pctl.force_queue.append( + queue_item_gen(default_playlist[pctl.selected_in_playlist], + pctl.selected_in_playlist, + pl_to_id(pctl.active_playlist_viewing))) + queue_timer_set() - for y in range(len(items_in_dir2)): - if os.path.splitext(items_in_dir2[y])[1][1:] in self.image_types: - dir_path = os.path.join(subdirec, items_in_dir2[y]).replace("\\", "/") - source_list.append([0, dir_path]) +def add_selected_to_queue_multi(): + if prefs.stop_end_queue: + pctl.auto_stop = False + for index in shift_selection: + pctl.force_queue.append( + queue_item_gen(default_playlist[index], + index, + pl_to_id(pctl.active_playlist_viewing))) - self.source_cache[tr.index] = source_list +def queue_timer_set(plural: bool = False, queue_object: TauonQueueItem | None = None) -> None: + queue_add_timer.set() + gui.frame_callback_list.append(TestTimer(2.51)) + gui.queue_toast_plural = plural + if queue_object: + gui.toast_queue_object = queue_object + elif pctl.force_queue: + gui.toast_queue_object = pctl.force_queue[-1] - return source_list +def split_queue_album(id: int) -> int | None: + item = pctl.force_queue[0] - def get_error_img(self, size: float) -> ImageFile: - im = Image.open(str(install_directory / "assets" / "load-error.png")) - im.thumbnail((size, size), Image.Resampling.LANCZOS) - return im + pl = id_to_pl(item.playlist_id) + if pl is None: + return None - def fast_display(self, index, location, box, source: list[tuple[int, str]], offset) -> int: - """Renders cached image only by given size for faster performance""" + playlist = pctl.multi_playlist[pl].playlist_ids - found_unit = None - max_h = 0 + i = pctl.playlist_playing_position + 1 + parts = [] + album_parent_path = pctl.get_track(item.track_id).parent_folder_path - for unit in self.image_cache: - if unit.source == source[offset][1]: - if unit.actual_size[1] > max_h: - max_h = unit.actual_size[1] - found_unit = unit + while i < len(playlist): + if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: + break - if found_unit == None: - return 1 + parts.append((playlist[i], i)) + i += 1 - unit = found_unit + del pctl.force_queue[0] - temp_dest.x = round(location[0]) - temp_dest.y = round(location[1]) + for part in reversed(parts): + pctl.force_queue.insert(0, queue_item_gen(part[0], part[1], item.type)) + return (len(parts)) - temp_dest.w = unit.original_size[0] # round(box[0]) - temp_dest.h = unit.original_size[1] # round(box[1]) +def add_to_queue_next(ref: int) -> None: + if pctl.force_queue and pctl.force_queue[0].album_stage == 1: + split_queue_album(None) - bh = round(box[1]) - bw = round(box[0]) + pctl.force_queue.insert(0, queue_item_gen(ref, r_menu_position, pl_to_id(pctl.active_playlist_viewing))) - if prefs.zoom_art: - temp_dest.w, temp_dest.h = fit_box((unit.original_size[0], unit.original_size[1]), box) - else: +def delete_track(track_ref): + tr = pctl.get_track(track_ref) + fullpath = tr.fullpath - # Constrain image to given box - if temp_dest.w > bw: - temp_dest.w = bw - temp_dest.h = int(bw * (unit.original_size[1] / unit.original_size[0])) + if system == "Windows" or msys: + fullpath = fullpath.replace("/", "\\") - if temp_dest.h > bh: - temp_dest.h = bh - temp_dest.w = int(temp_dest.h * (unit.original_size[0] / unit.original_size[1])) + if tr.is_network: + show_message(_("Cannot delete a network track")) + return - # prevent scaling larger than original image size - if temp_dest.w > unit.original_size[0] or temp_dest.h > unit.original_size[1]: - temp_dest.w = unit.original_size[0] - temp_dest.h = unit.original_size[1] + while track_ref in default_playlist: + default_playlist.remove(track_ref) - # center the image - temp_dest.x = int((box[0] - temp_dest.w) / 2) + temp_dest.x - temp_dest.y = int((box[1] - temp_dest.h) / 2) + temp_dest.y + try: + send2trash(fullpath) - # render the image - SDL_RenderCopy(renderer, unit.texture, None, temp_dest) - style_overlay.hole_punches.append(temp_dest) + if os.path.exists(fullpath): + try: + os.remove(fullpath) + show_message(_("File deleted"), fullpath, mode="info") + except Exception: + logging.exception("Error deleting file") + show_message(_("Error deleting file"), fullpath, mode="error") + else: + show_message(_("File moved to trash")) - gui.art_drawn_rect = (temp_dest.x, temp_dest.y, temp_dest.w, temp_dest.h) + except Exception: + try: + os.remove(fullpath) + show_message(_("File deleted"), fullpath, mode="info") + except Exception: + logging.exception("Error deleting file") + show_message(_("Error deleting file"), fullpath, mode="error") - return 0 + reload() + refind_playing() + pctl.notify_change() - def open_external(self, track_object: TrackClass) -> int: +def rename_tracks_deco(track_id: int): + if key_shift_down or key_shiftr_down: + return [colours.menu_text, colours.menu_background, _("Rename (Single track)")] + return [colours.menu_text, colours.menu_background, _("Rename Tracks…")] - index = track_object.index +def activate_trans_editor(): + trans_edit_box.active = True - source = self.get_sources(track_object) - if len(source) == 0: - return 0 +def delete_folder(index, force=False): + track = pctl.master_library[index] - offset = self.get_offset(track_object.fullpath, source) + if track.is_network: + show_message(_("Cannot physically delete"), _("One or more tracks is from a network location!"), mode="info") + return - if track_object.is_network: - show_message(_("Saving network images not implemented")) - return 0 - if source[offset][0] > 0: - pic = album_art_gen.get_embed(track_object) - if not pic: - show_message(_("Image save error."), _("No embedded album art."), mode="warning") - return 0 - - source_image = io.BytesIO(pic) - im = Image.open(source_image) - source_image.close() - - ext = "." + im.format.lower() - if im.format == "JPEG": - ext = ".jpg" - target = str(cache_directory / "open-image") - if not os.path.exists(target): - os.makedirs(target) - target = os.path.join(target, "embed-" + str(im.height) + "px-" + str(track_object.index) + ext) - - if len(pic) > 30: - with open(target, "wb") as w: - w.write(pic) + old = track.parent_folder_path - else: - target = source[offset][1] + if len(old) < 5: + show_message(_("This folder path seems short, I don't wanna try delete that"), mode="warning") + return - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", target]) - else: - subprocess.call(["xdg-open", target]) + if not os.path.exists(old): + show_message(_("Error deleting folder. The folder seems to be missing."), _("It's gone! Just gone!"), mode="error") + return - return 0 + protect = ("", "Documents", "Music", "Desktop", "Downloads") - def cycle_offset(self, track_object: TrackClass, reverse: bool = False) -> int: + for fo in protect: + if old.strip("\\/") == os.path.join(os.path.expanduser("~"), fo).strip("\\/"): + show_message(_("Woah, careful there!"), _("I don't think we should delete that folder."), mode="warning") + return - filepath = track_object.fullpath - sources = self.get_sources(track_object) - if len(sources) == 0: - return 0 - parent_folder = os.path.dirname(filepath) - # Find cached offset - if parent_folder in folder_image_offsets: + if directory_size(old) > 1500000000: + show_message(_("Delete size safety limit reached! (1.5GB)"), old, mode="warning") + return - if reverse: - folder_image_offsets[parent_folder] -= 1 - else: - folder_image_offsets[parent_folder] += 1 + try: + if pctl.playing_state > 0 and os.path.normpath( + pctl.master_library[pctl.track_queue[pctl.queue_step]].parent_folder_path) == os.path.normpath(old): + pctl.stop(True) - folder_image_offsets[parent_folder] %= len(sources) - return 0 + if force: + shutil.rmtree(old) + elif system == "Windows" or msys: + send2trash(old.replace("/", "\\")) + else: + send2trash(old) - def cycle_offset_reverse(self, track_object: TrackClass) -> None: - self.cycle_offset(track_object, True) + for i in reversed(range(len(default_playlist))): - def get_offset(self, filepath: str, source: list[tuple[int, str]]) -> int: + if old == pctl.master_library[default_playlist[i]].parent_folder_path: + del default_playlist[i] - # Check if folder offset already exsts, if not, make it - parent_folder = os.path.dirname(filepath) + if not os.path.exists(old): + if force: + show_message(_("Folder deleted."), old, mode="done") + else: + show_message(_("Folder sent to trash."), old, mode="done") + else: + show_message(_("Hmm, its still there"), old, mode="error") - if parent_folder in folder_image_offsets: + if album_mode: + prep_gal() + reload_albums() - # Reset the offset if greater than number of images available - if folder_image_offsets[parent_folder] > len(source) - 1: - folder_image_offsets[parent_folder] = 0 + except Exception: + if force: + logging.exception("Unable to comply, could not delete folder. Try checking permissions.") + show_message(_("Unable to comply."), _("Could not delete folder. Try checking permissions."), mode="error") else: - folder_image_offsets[parent_folder] = 0 - - return folder_image_offsets[parent_folder] + logging.exception("Folder could not be trashed, try again while holding shift to force delete.") + show_message(_("Folder could not be trashed."), _("Try again while holding shift to force delete."), + mode="error") - def get_embed(self, track: TrackClass): + tree_view_box.clear_target_pl(pctl.active_playlist_viewing) + gui.pl_update += 1 + pctl.notify_change() - # cached = self.embed_cached - # if cached[0] == track: - # #logging.info("used cached") - # return cached[1] +def rename_parent(index: int, template: str) -> None: + # template = prefs.rename_folder_template + template = template.strip("/\\") + track = pctl.master_library[index] - filepath = track.fullpath + if track.is_network: + show_message(_("Cannot rename"), _("One or more tracks is from a network location!"), mode="info") + return - # Use cached file if present - if prefs.precache and tauon.cachement: - path = tauon.cachement.get_file_cached_only(track) - if path: - filepath = path + old = track.parent_folder_path + #logging.info(old) - pic = None + new = parse_template2(template, track) - if track.file_ext == "MP3": - try: - tag = mutagen.id3.ID3(filepath) - frame = tag.getall("APIC") - if frame: - pic = frame[0].data - except Exception: - logging.exception(f"Failed to get tags on file: {filepath}") - - if pic is not None and len(pic) < 30: - pic = None - - elif track.file_ext == "FLAC": - with Flac(filepath) as tag: - tag.read(True) - if tag.has_picture and len(tag.picture) > 30: - pic = tag.picture - - elif track.file_ext == "APE": - with Ape(filepath) as tag: - tag.read() - if tag.has_picture and len(tag.picture) > 30: - pic = tag.picture - - elif track.file_ext == "M4A": - with M4a(filepath) as tag: - tag.read(True) - if tag.has_picture and len(tag.picture) > 30: - pic = tag.picture - - elif track.file_ext == "OPUS" or track.file_ext == "OGG" or track.file_ext == "OGA": - with Opus(filepath) as tag: - tag.read() - if tag.has_picture and len(tag.picture) > 30: - with io.BytesIO(base64.b64decode(tag.picture)) as a: - a.seek(0) - image = parse_picture_block(a) - pic = image - - # self.embed_cached = (track, pic) - return pic - - def get_source_raw(self, offset: int, sources: list[tuple[int, str]] | int, track: TrackClass, subsource: list[tuple[int, str]] | None = None): - - source_image = None - - if subsource is None: - subsource = sources[offset] - - if subsource[0] == 1: - # Target is a embedded image\\\ - pic = self.get_embed(track) - assert pic - source_image = io.BytesIO(pic) - - elif subsource[0] == 2: - try: - if track.file_ext == "RADIO" or track.file_ext == "Spotify": - if pctl.radio_image_bin: - return pctl.radio_image_bin + if len(new) < 1: + show_message(_("Rename error."), _("The generated name is too short"), mode="warning") + return - cached_path = os.path.join(n_cache_dir, hashlib.md5(track.art_url_key.encode()).hexdigest()[:12]) - if os.path.isfile(cached_path): - source_image = open(cached_path, "rb") - else: - if track.file_ext == "SUB": - source_image = subsonic.get_cover(track) - elif track.file_ext == "JELY": - source_image = jellyfin.get_cover(track) - else: - response = urllib.request.urlopen(get_network_thumbnail_url(track), context=ssl_context) - source_image = io.BytesIO(response.read()) - if source_image: - with Path(cached_path).open("wb") as file: - file.write(source_image.read()) - source_image.seek(0) + if len(old) < 5: + show_message(_("Rename error."), _("This folder path seems short, I don't wanna try rename that"), mode="warning") + return - except Exception: - logging.exception("Failed to get source") + if not os.path.exists(old): + show_message(_("Rename Failed. The original folder is missing."), mode="warning") + return - else: - source_image = open(subsource[1], "rb") + protect = ("", "Documents", "Music", "Desktop", "Downloads") - return source_image + for fo in protect: + if os.path.normpath(old) == os.path.normpath(os.path.join(os.path.expanduser("~"), fo)): + show_message(_("Woah, careful there!"), _("I don't think we should rename that folder."), mode="warning") + return - def get_base64(self, track: TrackClass, size): + logging.info(track.parent_folder_path) + re = os.path.dirname(track.parent_folder_path.rstrip("/\\")) + logging.info(re) + new_parent_path = os.path.join(re, new) + logging.info(new_parent_path) - # Wait if an identical track is already being processed - if self.processing64on == track: - t = 0 - while True: - if self.processing64on is None: - break - time.sleep(0.05) - t += 1 - if t > 20: - break + pre_state = 0 - cahced = self.base64cache - if track == cahced[0] and size == cahced[1]: - return cahced[2] + for key, object in pctl.master_library.items(): - self.processing64on = track + if object.fullpath == "": + continue - filepath = track.fullpath - sources = self.get_sources(track) + if old == object.parent_folder_path: - if len(sources) == 0: - self.processing64on = None - return False + new_fullpath = os.path.join(new_parent_path, object.filename) - offset = self.get_offset(filepath, sources) + if os.path.normpath(new_parent_path) == os.path.normpath(old): + show_message(_("The folder already has that name.")) + return - # Get source IO - source_image = self.get_source_raw(offset, sources, track) + if os.path.exists(new_parent_path): + show_message(_("Rename Failed."), _("A folder with that name already exists"), mode="warning") + return - if source_image is None: - self.processing64on = None - return "" + if key == pctl.track_queue[pctl.queue_step] and pctl.playing_state > 0: + pre_state = pctl.stop(True) - im = Image.open(source_image) - if im.mode != "RGB": - im = im.convert("RGB") - im.thumbnail(size, Image.Resampling.LANCZOS) - buff = io.BytesIO() - im.save(buff, format="JPEG") - sss = base64.b64encode(buff.getvalue()) - - self.base64cache = (track, size, sss) - self.processing64on = None - return sss - - def get_background(self, track: TrackClass) -> BytesIO | BufferedReader | None: - #logging.info("Find background...") - # Determine artist name to use - artist = get_artist_safe(track) - if not artist: - return None + object.parent_folder_name = new + object.parent_folder_path = new_parent_path + object.fullpath = new_fullpath - # Check cache for existing image - path = os.path.join(b_cache_dir, artist) - if os.path.isfile(path): - logging.info("Load cached background") - return open(path, "rb") + search_string_cache.pop(object.index, None) + search_dia_string_cache.pop(object.index, None) - # Try last.fm background - path = artist_info_box.get_data(artist, get_img_path=True) - if os.path.isfile(path): - logging.info("Load cached background lfm") - return open(path, "rb") + # Fix any other tracks paths that contain the old path + if os.path.normpath(object.fullpath)[:len(old)] == os.path.normpath(old) \ + and os.path.normpath(object.fullpath)[len(old)] in ("/", "\\"): + object.fullpath = os.path.join(new_parent_path, object.fullpath[len(old):].lstrip("\\/")) + object.parent_folder_path = os.path.join(new_parent_path, object.parent_folder_path[len(old):].lstrip("\\/")) - # Check we've not already attempted a search for this artist - if artist in prefs.failed_background_artists: - return None + search_string_cache.pop(object.index, None) + search_dia_string_cache.pop(object.index, None) - # Get artist MBID + if new_parent_path is not None: try: - s = musicbrainzngs.search_artists(artist, limit=1) - artist_id = s["artist-list"][0]["id"] + os.rename(old, new_parent_path) + logging.info(new_parent_path) except Exception: - logging.exception(f"Failed to find artist MBID for: {artist}") - prefs.failed_background_artists.append(artist) - return None + logging.exception("Rename failed, something went wrong!") + show_message(_("Rename Failed!"), _("Something went wrong, sorry."), mode="error") + return - # Search fanart.tv for background - try: + show_message(_("Folder renamed."), _("Renamed to: {name}").format(name=new), mode="done") - r = requests.get( - "https://webservice.fanart.tv/v3/music/" \ - + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) + if pre_state == 1: + pctl.revert() - artlink = r.json()["artistbackground"][0]["url"] + tree_view_box.clear_target_pl(pctl.active_playlist_viewing) + pctl.notify_change() - response = urllib.request.urlopen(artlink, context=ssl_context) - info = response.info() +def rename_folders_disable_test(index: int) -> bool: + return pctl.get_track(index).is_network - assert info.get_content_maintype() == "image" +def rename_folders(index: int): + global track_box + global rename_index + global input_text - t = io.BytesIO() - t.seek(0) - t.write(response.read()) - t.seek(0, 2) - l = t.tell() - t.seek(0) + track_box = False + rename_index = index - assert l > 1000 + if rename_folders_disable_test(index): + show_message(_("Not applicable for a network track.")) + return - # Cache image for future use - path = os.path.join(a_cache_dir, artist + "-ftv-full.jpg") - with open(path, "wb") as f: - f.write(t.read()) - t.seek(0) - return t + gui.rename_folder_box = True + input_text = "" + shift_selection.clear() - except Exception: - logging.exception(f"Failed to find fanart background for: {artist}") - if not gui.artist_info_panel: - artist_info_box.get_data(artist) - path = artist_info_box.get_data(artist, get_img_path=True) - if os.path.isfile(path): - logging.debug("Downloaded background lfm") - return open(path, "rb") + global quick_drag + global playlist_hold + quick_drag = False + playlist_hold = False +def move_folder_up(index: int, do: bool = False) -> bool | None: + track = pctl.master_library[index] - prefs.failed_background_artists.append(artist) - return None + if track.is_network: + show_message(_("Cannot move"), _("One or more tracks is from a network location!"), mode="info") + return None - def get_blur_im(self, track: TrackClass) -> BytesIO | bool | None: + parent_folder = os.path.dirname(track.parent_folder_path) + folder_name = track.parent_folder_name + move_target = track.parent_folder_path + upper_folder = os.path.dirname(parent_folder) - source_image = None - self.loaded_bg_type = 0 - if prefs.enable_fanart_bg: - source_image = self.get_background(track) - if source_image: - self.loaded_bg_type = 1 + if not os.path.exists(track.parent_folder_path): + if do: + show_message(_("Error shifting directory"), _("The directory does not appear to exist"), mode="warning") + return False - if source_image is None: - filepath = track.fullpath - sources = self.get_sources(track) + if len(os.listdir(parent_folder)) > 1: + return False - if len(sources) == 0: - return False + if do is False: + return True - offset = self.get_offset(filepath, sources) + pre_state = 0 + if pctl.playing_state > 0 and track.parent_folder_path in pctl.playing_object().parent_folder_path: + pre_state = pctl.stop(True) - source_image = self.get_source_raw(offset, sources, track) + try: - if source_image is None: - return None + # Rename the track folder to something temporary + os.rename(move_target, os.path.join(parent_folder, "RMTEMP000")) - im = Image.open(source_image) + # Move the temporary folder up 2 levels + shutil.move(os.path.join(parent_folder, "RMTEMP000"), upper_folder) - ox_size = im.size[0] - oy_size = im.size[1] + # Delete the old directory that contained the original folder + shutil.rmtree(parent_folder) - format = im.format - if im.format == "JPEG": - format = "JPG" + # Rename the moved folder back to its original name + os.rename(os.path.join(upper_folder, "RMTEMP000"), os.path.join(upper_folder, folder_name)) - #logging.info(im.size) - if im.mode != "RGB": - im = im.convert("RGB") + except Exception as e: + logging.exception("System Error!") + show_message(_("System Error!"), str(e), mode="error") - ratio = window_size[0] / ox_size - ratio += 0.2 + # Fix any other tracks paths that contain the old path + old = track.parent_folder_path + new_parent_path = os.path.join(upper_folder, folder_name) + for key, object in pctl.master_library.items(): - if (oy_size * ratio) - ((oy_size * ratio) // 4) < window_size[1]: - logging.info("Adjust bg vertical") - ratio = window_size[1] / (oy_size - (oy_size // 4)) - ratio += 0.2 + if os.path.normpath(object.fullpath)[:len(old)] == os.path.normpath(old) \ + and os.path.normpath(object.fullpath)[len(old)] in ("/", "\\"): + object.fullpath = os.path.join(new_parent_path, object.fullpath[len(old):].lstrip("\\/")) + object.parent_folder_path = os.path.join( + new_parent_path, object.parent_folder_path[len(old):].lstrip("\\/")) - new_x = round(ox_size * ratio) - new_y = round(oy_size * ratio) + search_string_cache.pop(object.index, None) + search_dia_string_cache.pop(object.index, None) - im = im.resize((new_x, new_y)) + logging.info(object.fullpath) + logging.info(object.parent_folder_path) - if self.loaded_bg_type == 1: - artist = get_artist_safe(track) - if artist and artist in prefs.bg_flips: - im = im.transpose(Image.FLIP_LEFT_RIGHT) + if pre_state == 1: + pctl.revert() - if (ox_size < 500 or prefs.art_bg_always_blur) or gui.mode == 3: - blur = prefs.art_bg_blur - if prefs.mini_mode_mode == 5 and gui.mode == 3: - blur = 160 - pix = im.getpixel((new_x // 2, new_y // 4 * 3)) - pixel_sum = sum(pix) / (255 * 3) - if pixel_sum > 0.6: - enhancer = ImageEnhance.Brightness(im) - deduct = 1 - ((pixel_sum - 0.6) * 1.5) - im = enhancer.enhance(deduct) - logging.info(deduct) +def clean_folder(index: int, do: bool = False) -> int | None: + track = pctl.master_library[index] - gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 4 * 3)) + if track.is_network: + show_message(_("Cannot clean"), _("One or more tracks is from a network location!"), mode="info") + return None - im = im.filter(ImageFilter.GaussianBlur(blur)) + folder = track.parent_folder_path + found = 0 + to_purge = [] + if not os.path.isdir(folder): + return 0 + try: + for item in os.listdir(folder): + if (item[:8] == "AlbumArt" and ".jpg" in item.lower()) \ + or item == "desktop.ini" \ + or item == "Thumbs.db" \ + or item == ".DS_Store": + to_purge.append(item) + found += 1 + elif item == "__MACOSX" and os.path.isdir(os.path.join(folder, item)): + found += 1 + found += 1 + if do: + logging.info("Deleting Folder: " + os.path.join(folder, item)) + shutil.rmtree(os.path.join(folder, item)) - gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 2)) + if do: + for item in to_purge: + if os.path.isfile(os.path.join(folder, item)): + logging.info("Deleting File: " + os.path.join(folder, item)) + os.remove(os.path.join(folder, item)) + # clear_img_cache() - g = io.BytesIO() - g.seek(0) + for track_id in default_playlist: + if pctl.get_track(track_id).parent_folder_path == folder: + clear_track_image_cache(pctl.get_track(track_id)) - a_channel = Image.new("L", im.size, 255) # 'L' 8-bit pixels, black and white - im.putalpha(a_channel) + except Exception: + logging.exception("Error deleting files, may not have permission or file may be set to read-only") + show_message(_("Error deleting files."), _("May not have permission or file may be set to read-only"), mode="warning") + return 0 - im.save(g, "PNG") - g.seek(0) + return found - # source_image.close() +def reset_play_count(index: int): + star_store.remove(index) - return g +def vacuum_playtimes(index: int): + todo = [] + for k in default_playlist: + if pctl.master_library[index].parent_folder_name == pctl.master_library[k].parent_folder_name: + todo.append(k) - def save_thumb(self, track_object: TrackClass, size: tuple[int, int], save_path: str, png=False, zoom=False): + for track in todo: - filepath = track_object.fullpath - sources = self.get_sources(track_object) + tr = pctl.get_track(track) - if len(sources) == 0: - logging.error("Error thumbnailing; no source images found") - return False + total_playtime = 0 + flags = "" - offset = self.get_offset(filepath, sources) - source_image = self.get_source_raw(offset, sources, track_object) + to_del = [] - im = Image.open(source_image) - if im.mode != "RGB": - im = im.convert("RGB") + for key, value in star_store.db.items(): + if key[0].lower() == tr.artist.lower() and tr.artist and key[1].lower().replace( + " ", "") == tr.title.lower().replace( + " ", "") and tr.title: + to_del.append(key) + total_playtime += value[0] + flags = "".join(set(flags + value[1])) - if not zoom: - im.thumbnail(size, Image.Resampling.LANCZOS) - else: - w, h = im.size - if w != h: - m = min(w, h) - im = im.crop(( - (w - m) / 2, - (h - m) / 2, - (w + m) / 2, - (h + m) / 2, - )) - - im = im.resize(size, Image.Resampling.LANCZOS) - - if not save_path: - g = io.BytesIO() - g.seek(0) - if png: - im.save(g, "PNG") - else: - im.save(g, "JPEG") - g.seek(0) - return g + for key in to_del: + del star_store.db[key] - if png: - im.save(save_path + ".png", "PNG") + key = star_store.object_key(tr) + value = [total_playtime, flags, 0] + if key not in star_store.db: + logging.info("Saving value") + star_store.db[key] = value else: - im.save(save_path + ".jpg", "JPEG") + logging.error("ERROR KEY ALREADY HERE?") - def display(self, track: TrackClass, location, box, fast: bool = False, theme_only: bool = False) -> int | None: - index = track.index - filepath = track.fullpath +def reload_metadata(input, keep_star: bool = True) -> None: + global todo - if prefs.colour_from_image and track.album != gui.theme_temp_current and box[0] != 115: - if track.album in gui.temp_themes: - global colours - colours = gui.temp_themes[track.album] - gui.theme_temp_current = track.album + # vacuum_playtimes(index) + # return + todo = [] - source = self.get_sources(track) + if isinstance(input, list): + todo = input - if len(source) == 0: - return 1 + else: + for k in default_playlist: + if pctl.master_library[input].parent_folder_path == pctl.master_library[k].parent_folder_path: + todo.append(pctl.master_library[k]) - offset = self.get_offset(filepath, source) + for i in reversed(range(len(todo))): + if todo[i].is_cue: + del todo[i] - if not theme_only: - # Check if request matches previous - if self.current_wu is not None and self.current_wu.source == source[offset][1] and \ - self.current_wu.request_size == box: - self.render(self.current_wu, location) - return 0 + for track in todo: - if fast: - return self.fast_display(track, location, box, source, offset) + search_string_cache.pop(track.index, None) + search_dia_string_cache.pop(track.index, None) - # Check if cached - for unit in self.image_cache: - if unit.index == index and unit.request_size == box and unit.offset == offset: - self.render(unit, location) - return 0 + #logging.info('Reloading Metadata for ' + track.filename) + if keep_star: + to_scan.append(track.index) + else: + # if keep_star: + # star = star_store.full_get(track.index) + # star_store.remove(track.index) - close = True - # Render new - try: - # Get source IO - if source[offset][0] == 1: - # Target is a embedded image - # source_image = io.BytesIO(self.get_embed(track)) - source_image = self.get_source_raw(0, 0, track, source[offset]) - - elif source[offset][0] == 2: - idea = prefs.encoder_output / encode_folder_name(track) / "cover.jpg" - if idea.is_file(): - source_image = idea.open("rb") - else: - try: - close = False - # We want to download the image asynchronously as to not block the UI - if self.downloaded_image and self.downloaded_track == track: - source_image = self.downloaded_image + pctl.master_library[track.index] = tag_scan(track) - elif self.download_in_progress: - return 0 + # if keep_star: + # if star is not None and (star[0] > 0 or star[1] or star[2] > 0): + # star_store.merge(track.index, star) - else: - self.download_in_progress = True - shoot_dl = threading.Thread( - target=self.async_download_image, - args=([track, source[offset]])) - shoot_dl.daemon = True - shoot_dl.start() - - # We'll block with a small timeout to avoid unwanted flashing between frames - s = 0 - while self.download_in_progress: - s += 1 - time.sleep(0.01) - if s > 20: # 200 ms - break + pctl.notify_change() - if self.downloaded_track != track: - return None + gui.pl_update += 1 + tauon.thread_manager.ready("worker") - assert self.downloaded_image - source_image = self.downloaded_image +def reload_metadata_selection() -> None: + cargo = [] + for item in shift_selection: + cargo.append(default_playlist[item]) + for k in cargo: + if pctl.master_library[k].is_cue == False: + to_scan.append(k) + tauon.thread_manager.ready("worker") - except Exception: - logging.exception("IMAGE NETWORK LOAD ERROR") - raise +def editor(index: int | None) -> None: + todo = [] + obs = [] - else: - # source_image = open(source[offset][1], 'rb') - source_image = self.get_source_raw(0, 0, track, source[offset]) + if key_shift_down and index is not None: + todo = [index] + obs = [pctl.master_library[index]] + elif index is None: + for item in shift_selection: + todo.append(default_playlist[item]) + obs.append(pctl.master_library[default_playlist[item]]) + if len(todo) > 0: + index = todo[0] + else: + for k in default_playlist: + if pctl.master_library[index].parent_folder_path == pctl.master_library[k].parent_folder_path: + if pctl.master_library[k].is_cue == False: + todo.append(k) + obs.append(pctl.master_library[k]) - # Generate - g = io.BytesIO() - g.seek(0) - im = Image.open(source_image) - o_size = im.size + # Keep copy of play times + old_stars = [] + for track in todo: + item = [] + item.append(pctl.get_track(track)) + item.append(star_store.key(track)) + item.append(star_store.full_get(track)) + old_stars.append(item) - format = im.format + file_line = "" + for track in todo: + file_line += ' "' + file_line += pctl.master_library[track].fullpath + file_line += '"' - try: - if im.format == "JPEG": - format = "JPG" + if system == "Windows" or msys: + file_line = file_line.replace("/", "\\") - if im.mode != "RGB": - im = im.convert("RGB") - except Exception: - logging.exception("Failed to convert image") - if theme_only: - source_image.close() - g.close() - return None - im = Image.open(str(install_directory / "assets" / "load-error.png")) - o_size = im.size + prefix = "" + app = prefs.tag_editor_target + if (system == "Windows" or msys) and app: + if app[0] != '"': + app = '"' + app + if app[-1] != '"': + app = app + '"' - if not theme_only: + app_switch = "" - if prefs.zoom_art: - new_size = fit_box(o_size, box) - try: - im = im.resize(new_size, Image.Resampling.LANCZOS) - except Exception: - logging.exception("Failed to resize image") - im = Image.open(str(install_directory / "assets" / "load-error.png")) - o_size = im.size - new_size = fit_box(o_size, box) - im = im.resize(new_size, Image.Resampling.LANCZOS) - else: - try: - im.thumbnail((box[0], box[1]), Image.Resampling.LANCZOS) - except Exception: - logging.exception("Failed to convert image to thumbnail") - im = Image.open(str(install_directory / "assets" / "load-error.png")) - o_size = im.size - im.thumbnail((box[0], box[1]), Image.Resampling.LANCZOS) - im.save(g, "BMP") - g.seek(0) + ok = False - # Processing for "Carbon" theme - if track == pctl.playing_object() and gui.theme_name == "Carbon" and track.parent_folder_path != colours.last_album: + prefix = launch_prefix - # Find main image colours - try: - im.thumbnail((50, 50), Image.Resampling.LANCZOS) - except Exception: - logging.exception("theme gen error") - source_image.close() - g.close() - return None - pixels = im.getcolors(maxcolors=2500) - pixels = sorted(pixels, key=lambda x: x[0], reverse=True)[:] - colour = pixels[0][1] - - # Try and find a colour that is not grayscale - for c in pixels: - cc = c[1] - av = sum(cc) / 3 - if abs(cc[0] - av) > 10 or abs(cc[1] - av) > 10 or abs(cc[2] - av) > 10: - colour = cc - break + if system == "Linux": + ok = whicher(prefs.tag_editor_target) + else: - h_colour = rgb_to_hls(colour[0], colour[1], colour[2]) + if not os.path.isfile(prefs.tag_editor_target.strip('"')): + logging.info(prefs.tag_editor_target) + show_message(_("Application not found"), prefs.tag_editor_target, mode="info") + return - l = .51 - s = .44 + ok = True - hh = h_colour[0] - if 0.14 < hh < 0.3: # Yellow and green are hard to read text on, so lower the luminance for those - l = .45 - if check_equal(colour): # Default to theme purple if source colour was grayscale - hh = 0.72 + if not ok: + show_message(_("Tag editor app does not appear to be installed."), mode="warning") - colours.bottom_panel_colour = hls_to_rgb(hh, l, s) - colours.last_album = track.parent_folder_path + if flatpak_mode: + show_message( + _("App not found on host OR insufficient Flatpak permissions."), + _(" For details, see {link}").format(link="https://github.com/Taiko2k/Tauon/wiki/Flatpak-Extra-Steps"), + mode="bubble") - # Processing for "Auto-theme" setting - if prefs.colour_from_image and box[0] != 115 and track.album != gui.theme_temp_current \ - and track.album not in gui.temp_themes: # and pctl.master_library[index].parent_folder_path != colours.last_album: #mark2233 - colours.last_album = track.parent_folder_path + return - colours = copy.deepcopy(colours) + if "picard" in prefs.tag_editor_target: + app_switch = " --d " - im.thumbnail((50, 50), Image.Resampling.LANCZOS) - pixels = im.getcolors(maxcolors=2500) - #logging.info(pixels) - pixels = sorted(pixels, key=lambda x: x[0], reverse=True)[:] - #logging.info(pixels) + line = prefix + app + app_switch + file_line - min_colour_varience = 75 + show_message( + prefs.tag_editor_name + " launched.", "Fields will be updated once application is closed.", mode="arrow") + gui.update = 1 - x_colours = [] - for item in pixels: - colour = item[1] - for cc in x_colours: - if abs( - colour[0] - cc[0]) < min_colour_varience and abs( - colour[1] - cc[1]) < min_colour_varience and abs( - colour[2] - cc[2]) < min_colour_varience: - break - else: - x_colours.append(colour) + complete = subprocess.run(shlex.split(line), stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) - #logging.info(x_colours) - colours.playlist_panel_bg = colours.side_panel_background - colours.playlist_box_background = colours.side_panel_background + if "picard" in prefs.tag_editor_target: + r = complete.stderr.decode() + for line in r.split("\n"): + if "file._rename" in line and " Moving file " in line: + a, b = line.split(" Moving file ")[1].split(" => ") + a = a.strip("'").strip('"') + b = b.strip("'").strip('"') - colours.playlist_panel_background = x_colours[0] + (255,) - if len(x_colours) > 1: - colours.side_panel_background = x_colours[1] + (255,) - colours.playlist_box_background = colours.side_panel_background - if len(x_colours) > 2: - colours.title_text = x_colours[2] + (255,) - colours.title_playing = x_colours[2] + (255,) - if len(x_colours) > 3: - colours.artist_text = x_colours[3] + (255,) - colours.artist_playing = x_colours[3] + (255,) - if len(x_colours) > 4: - colours.playlist_box_background = x_colours[4] + (255,) + for track in todo: + if pctl.master_library[track].fullpath == a: + pctl.master_library[track].fullpath = b + pctl.master_library[track].filename = os.path.basename(b) + logging.info("External Edit: File rename detected.") + logging.info(" Renaming: " + a) + logging.info(" To: " + b) + break + else: + logging.warning("External Edit: A file rename was detected but track was not found.") - colours.queue_background = colours.side_panel_background - # Check artist text colour - if contrast_ratio(colours.artist_text, colours.playlist_panel_background) < 1.9: + gui.message_box = False + reload_metadata(obs, keep_star=False) - black = [25, 25, 25, 255] - white = [220, 220, 220, 255] + # Re apply playtime data in case file names change + for item in old_stars: - con_b = contrast_ratio(black, colours.playlist_panel_background) - con_w = contrast_ratio(white, colours.playlist_panel_background) + old_key = item[1] + old_value = item[2] - choice = black - if con_w > con_b: - choice = white + if not old_value: # ignore if there was no old playcount metadata + continue - colours.artist_text = choice - colours.artist_playing = choice + new_key = star_store.object_key(item[0]) + new_value = star_store.full_get(item[0].index) - # Check title text colour - if contrast_ratio(colours.title_text, colours.playlist_panel_background) < 1.9: + if old_key == new_key: + continue - black = [60, 60, 60, 255] - white = [180, 180, 180, 255] + if new_value is None: + new_value = [0, "", 0] - con_b = contrast_ratio(black, colours.playlist_panel_background) - con_w = contrast_ratio(white, colours.playlist_panel_background) + new_value[0] += old_value[0] + new_value[1] = "".join(set(new_value[1] + old_value[1])) - choice = black - if con_w > con_b: - choice = white + if old_key in star_store.db: + del star_store.db[old_key] - colours.title_text = choice - colours.title_playing = choice + star_store.db[new_key] = new_value - if test_lumi(colours.side_panel_background) < 0.50: - colours.side_bar_line1 = [25, 25, 25, 255] - colours.side_bar_line2 = [35, 35, 35, 255] - else: - colours.side_bar_line1 = [250, 250, 250, 255] - colours.side_bar_line2 = [235, 235, 235, 255] + gui.pl_update = 1 + gui.update = 1 + pctl.notify_change() - colours.album_text = colours.title_text - colours.album_playing = colours.title_playing +def launch_editor(index: int): + if snap_mode: + show_message(_("Sorry, this feature isn't (yet) available with Snap.")) + return - gui.pl_update = 1 + if launch_editor_disable_test(index): + show_message(_("Cannot edit tags of a network track.")) + return - prcl = 100 - int(test_lumi(colours.playlist_panel_background) * 100) - - if prcl > 45: - ce = alpha_blend([0, 0, 0, 180], colours.playlist_panel_background) # [40, 40, 40, 255] - colours.index_text = ce - colours.index_playing = ce - colours.time_text = ce - colours.bar_time = ce - colours.folder_title = ce - colours.star_line = [60, 60, 60, 255] - colours.row_select_highlight = [0, 0, 0, 30] - colours.row_playing_highlight = [0, 0, 0, 20] - colours.gallery_background = rgb_add_hls(colours.playlist_panel_background, 0, -0.03, -0.03) - else: - ce = alpha_blend([255, 255, 255, 160], colours.playlist_panel_background) # [165, 165, 165, 255] - colours.index_text = ce - colours.index_playing = ce - colours.time_text = ce - colours.bar_time = ce - colours.folder_title = ce - colours.star_line = ce # [150, 150, 150, 255] - colours.row_select_highlight = [255, 255, 255, 12] - colours.row_playing_highlight = [255, 255, 255, 8] - colours.gallery_background = rgb_add_hls(colours.playlist_panel_background, 0, 0.03, 0.03) - - gui.temp_themes[track.album] = copy.deepcopy(colours) - colours = gui.temp_themes[track.album] - gui.theme_temp_current = track.album - - if theme_only: - source_image.close() - g.close() - return None + mini_t = threading.Thread(target=editor, args=[index]) + mini_t.daemon = True + mini_t.start() - wop = rw_from_object(g) - s_image = IMG_Load_RW(wop, 0) - #logging.error(IMG_GetError()) +def launch_editor_selection_disable_test(index: int): + for position in shift_selection: + if pctl.get_track(default_playlist[position]).is_network: + return True + return False - c = SDL_CreateTextureFromSurface(renderer, s_image) +def launch_editor_selection(index: int): + if launch_editor_selection_disable_test(index): + show_message(_("Cannot edit tags of a network track.")) + return - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) + mini_t = threading.Thread(target=editor, args=[None]) + mini_t.daemon = True + mini_t.start() - SDL_QueryTexture(c, None, None, tex_w, tex_h) +def edit_deco(index: int): + if key_shift_down or key_shiftr_down: + return [colours.menu_text, colours.menu_background, prefs.tag_editor_name + " (Single track)"] + return [colours.menu_text, colours.menu_background, _("Edit with ") + prefs.tag_editor_name] - dst = SDL_Rect(round(location[0]), round(location[1])) - dst.w = int(tex_w.contents.value) - dst.h = int(tex_h.contents.value) +def launch_editor_disable_test(index: int): + return pctl.get_track(index).is_network - # Clean uo - SDL_FreeSurface(s_image) - source_image.close() - g.close() - # if close: - # source_image.close() +def show_lyrics_menu(index: int): + global track_box + track_box = False + enter_showcase_view(track_id=r_menu_index) + inp.mouse_click = False - unit = ImageObject() - unit.index = index - unit.texture = c - unit.rect = dst - unit.request_size = box - unit.original_size = o_size - unit.actual_size = (dst.w, dst.h) - unit.source = source[offset][1] - unit.offset = offset - unit.format = format +def recode(text, enc): + return text.encode("Latin-1", "ignore").decode(enc, "ignore") - self.current_wu = unit - self.image_cache.append(unit) +def intel_moji(index: int): + gui.pl_update += 1 + gui.update += 1 - self.render(unit, location) + track = pctl.master_library[index] - if len(self.image_cache) > 5 or (prefs.colour_from_image and len(self.image_cache) > 1): - SDL_DestroyTexture(self.image_cache[0].texture) - del self.image_cache[0] + lot = [] - # temp fix - global move_on_title - global playlist_hold - global quick_drag - quick_drag = False - move_on_title = False - playlist_hold = False + for item in default_playlist: - except Exception: - logging.exception("Image load error") - logging.error("-- Associated track: " + track.fullpath) + if track.album == pctl.master_library[item].album and \ + track.parent_folder_name == pctl.master_library[item].parent_folder_name: + lot.append(item) + + lot = set(lot) + + l_artist = track.artist.encode("Latin-1", "ignore") + l_album = track.album.encode("Latin-1", "ignore") + detect = None - self.current_wu = None + if track.artist not in track.parent_folder_path: + for enc in encodings: try: - del self.source_cache[index][offset] + q_artist = l_artist.decode(enc) + if q_artist.strip(" ") in track.parent_folder_path.strip(" "): + detect = enc + break except Exception: - logging.exception(" -- Error, no source cache?") + logging.exception("Error decoding artist") + continue - return 1 + if detect is None and track.album not in track.parent_folder_path: + for enc in encodings: + try: + q_album = l_album.decode(enc) + if q_album in track.parent_folder_path: + detect = enc + break + except Exception: + logging.exception("Error decoding album") + continue - return 0 + for item in lot: + t_track = pctl.master_library[item] - def render(self, unit, location) -> None: + if detect is None: + for enc in encodings: + test = recode(t_track.artist, enc) + for cha in test: + if cha in j_chars: + detect = enc + logging.info("This looks like Japanese: " + test) + break + if detect is not None: + break - rect = unit.rect + if detect is None: + for enc in encodings: + test = recode(t_track.title, enc) + for cha in test: + if cha in j_chars: + detect = enc + logging.info("This looks like Japanese: " + test) + break + if detect is not None: + break + if detect is not None: + break - gui.art_aspect_ratio = unit.actual_size[0] / unit.actual_size[1] + if detect is not None: + logging.info("Fix Mojibake: Detected encoding as: " + detect) + for item in lot: + track = pctl.master_library[item] + # key = pctl.master_library[item].title + pctl.master_library[item].filename + key = star_store.full_get(item) + star_store.remove(item) - rect.x = round(int((unit.request_size[0] - unit.actual_size[0]) / 2) + location[0]) - rect.y = round(int((unit.request_size[1] - unit.actual_size[1]) / 2) + location[1]) + track.title = recode(track.title, detect) + track.album = recode(track.album, detect) + track.artist = recode(track.artist, detect) + track.album_artist = recode(track.album_artist, detect) + track.genre = recode(track.genre, detect) + track.comment = recode(track.comment, detect) + track.lyrics = recode(track.lyrics, detect) - style_overlay.hole_punches.append(rect) + if key != None: + star_store.insert(item, key) - SDL_RenderCopy(renderer, unit.texture, None, rect) + search_string_cache.pop(track.index, None) + search_dia_string_cache.pop(track.index, None) - gui.art_drawn_rect = (rect.x, rect.y, rect.w, rect.h) + else: + show_message(_("Autodetect failed")) - def clear_cache(self) -> None: +def sel_to_car(): + global default_playlist + cargo = [] - for unit in self.image_cache: - SDL_DestroyTexture(unit.texture) + for item in shift_selection: + cargo.append(default_playlist[item]) - self.image_cache.clear() - self.source_cache.clear() - self.current_wu = None - self.downloaded_track = None +def cut_selection(): + sel_to_car() + del_selected() - self.base64cahce = (0, 0, "") - self.processing64on = None - self.bin_cached = (None, None, None) - self.loading_bin = (None, None) - self.embed_cached = (None, None) +def clip_ar_al(index: int): + line = pctl.master_library[index].artist + " - " + pctl.master_library[index].album + SDL_SetClipboardText(line.encode("utf-8")) - gui.temp_themes.clear() - gui.theme_temp_current = -1 - colours.last_album = "" +def clip_ar(index: int): + if pctl.master_library[index].album_artist != "": + line = pctl.master_library[index].album_artist + else: + line = pctl.master_library[index].artist + SDL_SetClipboardText(line.encode("utf-8")) +def clip_title(index: int): + n_track = pctl.master_library[index] -album_art_gen = AlbumArt() + if not prefs.use_title and n_track.album_artist != "" and n_track.album != "": + line = n_track.album_artist + " - " + n_track.album + else: + line = n_track.parent_folder_name + SDL_SetClipboardText(line.encode("utf-8")) +def lightning_copy(): + s_copy() + gui.lightning_copy = True -# 0 - blank -# 1 - preparing first -# 2 - render first -# 3 - preparing 2nd +def toggle_transcode(mode: int = 0) -> bool: + if mode == 1: + return prefs.enable_transcode + prefs.enable_transcode ^= True + return None -class StyleOverlay: +def toggle_chromecast(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_chromecast + prefs.show_chromecast ^= True + return None - def __init__(self): +def toggle_transfer(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_transfer + prefs.show_transfer ^= True - self.min_on_timer = Timer() - self.fade_on_timer = Timer(0) - self.fade_off_timer = Timer() + if prefs.show_transfer: + show_message( + _("Warning! Using this function moves physical folders."), + _("This menu entry appears after selecting 'copy'. See manual (github wiki) for more info."), + mode="info") + return None - self.stage = 0 +def transcode_deco(): + if key_shift_down or key_shiftr_down: + return [colours.menu_text, colours.menu_background, _("Transcode Single")] + return [colours.menu_text, colours.menu_background, _("Transcode Folder")] - self.im = None +def get_album_spot_url(track_id: int): + track_object = pctl.get_track(track_id) + url = tauon.spot_ctl.get_album_url_from_local(track_object) + if url: + copy_to_clipboard(url) + show_message(_("URL copied to clipboard"), mode="done") + else: + show_message(_("No results found")) - self.a_texture = None - self.a_rect = None +def get_album_spot_url_deco(track_id: int): + track_object = pctl.get_track(track_id) + if "spotify-album-url" in track_object.misc: + text = _("Copy Spotify Album URL") + else: + text = _("Lookup Spotify Album URL") + return [colours.menu_text, colours.menu_background, text] - self.b_texture = None - self.b_rect = None +def add_to_spotify_library_deco(track_id: int): + track_object = pctl.get_track(track_id) + text = _("Save Album to Spotify") + if track_object.file_ext != "SPTY": + return (colours.menu_text_disabled, colours.menu_background, text) - self.a_type = 0 - self.b_type = 0 + album_url = track_object.misc.get("spotify-album-url") + if album_url and album_url in tauon.spot_ctl.cache_saved_albums: + text = _("Un-save Spotify Album") + return (colours.menu_text, colours.menu_background, text) - self.window_size = None - self.parent_path = None +def add_to_spotify_library2(album_url: str) -> None: + if album_url in tauon.spot_ctl.cache_saved_albums: + tauon.spot_ctl.remove_album_from_library(album_url) + else: + tauon.spot_ctl.add_album_to_library(album_url) - self.hole_punches = [] - self.hole_refills = [] + for i, p in enumerate(pctl.multi_playlist): + code = pctl.gen_codes.get(p.uuid_int) + if code and code.startswith("sal"): + logging.info("Fetching Spotify Library...") + regenerate_playlist(i, silent=True) - self.go_to_sleep = False +def add_to_spotify_library(track_id: int) -> None: + track_object = pctl.get_track(track_id) + album_url = track_object.misc.get("spotify-album-url") + if track_object.file_ext != "SPTY" or not album_url: + return - self.current_track_album = "none" - self.current_track_id = -1 + shoot_dl = threading.Thread(target=add_to_spotify_library2, args=([album_url])) + shoot_dl.daemon = True + shoot_dl.start() - def worker(self) -> None: +def selection_queue_deco(): + total = 0 + for item in shift_selection: + total += pctl.get_track(default_playlist[item]).length - if self.stage == 0: + total = get_hms_time(total) - if (gui.mode == 3 and prefs.mini_mode_mode == 5): - pass - elif prefs.bg_showcase_only and not gui.combo_mode: - return + text = (_("Queue {N}").format(N=len(shift_selection))) + f" [{total}]" - if pctl.playing_ready() and self.min_on_timer.get() > 0: + return [colours.menu_text, colours.menu_background, text] - track = pctl.playing_object() +def toggle_rym(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_rym + prefs.show_rym ^= True + return None - self.window_size = copy.copy(window_size) - self.parent_path = track.parent_folder_path - self.current_track_id = track.index - self.current_track_album = track.album +def toggle_band(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_band + prefs.show_band ^= True + return None - try: - self.im = album_art_gen.get_blur_im(track) - except Exception: - logging.exception("Blur blackground error") - raise - #logging.debug(track.fullpath) +def toggle_wiki(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_wiki + prefs.show_wiki ^= True + return None - if self.im is None or self.im is False: - if self.a_texture: - self.stage = 2 - self.fade_off_timer.set() - self.go_to_sleep = True - return - self.flush() - self.min_on_timer.force_set(-4) - return +# def toggle_show_discord(mode: int = 0) -> bool: +# if mode == 1: +# return prefs.discord_show +# if prefs.discord_show is False and discord_allow is False: +# show_message(_("Warning: pypresence package not installed")) +# prefs.discord_show ^= True - self.stage = 1 - gui.update += 1 - return +def toggle_gen(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_gen + prefs.show_gen ^= True + return None - def flush(self): - - if self.a_texture is not None: - SDL_DestroyTexture(self.a_texture) - self.a_texture = None - if self.b_texture is not None: - SDL_DestroyTexture(self.b_texture) - self.b_texture = None - self.min_on_timer.force_set(-0.2) - self.parent_path = "None" - self.stage = 0 - tauon.thread_manager.ready("worker") - gui.style_worker_timer.set() - gui.delay_frame(0.25) +def ser_band_done(result: str) -> None: + if result: + webbrowser.open(result, new=2, autoraise=True) + gui.message_box = False gui.update += 1 + else: + show_message(_("No matching artist result found")) - def display(self) -> None: - - if self.min_on_timer.get() < 0: - return +def ser_band(track_id: int) -> None: + tr = pctl.get_track(track_id) + if tr.artist: + shoot_dl = threading.Thread(target=bandcamp_search, args=([tr.artist, ser_band_done])) + shoot_dl.daemon = True + shoot_dl.start() + show_message(_("Searching...")) - if self.stage == 1: +def ser_rym(index: int) -> None: + if len(pctl.master_library[index].artist) < 2: + return + line = "https://rateyourmusic.com/search?searchtype=a&searchterm=" + urllib.parse.quote( + pctl.master_library[index].artist) + webbrowser.open(line, new=2, autoraise=True) - wop = rw_from_object(self.im) - s_image = IMG_Load_RW(wop, 0) +def copy_to_clipboard(text: str) -> None: + SDL_SetClipboardText(text.encode(errors="surrogateescape")) - c = SDL_CreateTextureFromSurface(renderer, s_image) +def copy_from_clipboard(): + return SDL_GetClipboardText().decode() - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) +def clip_aar_al(index: int): + if pctl.master_library[index].album_artist == "": + line = pctl.master_library[index].artist + " - " + pctl.master_library[index].album + else: + line = pctl.master_library[index].album_artist + " - " + pctl.master_library[index].album + SDL_SetClipboardText(line.encode("utf-8")) - SDL_QueryTexture(c, None, None, tex_w, tex_h) +def ser_gen_thread(tr): + s_artist = tr.artist + s_title = tr.title - dst = SDL_Rect(round(-40, 0)) - dst.w = int(tex_w.contents.value) - dst.h = int(tex_h.contents.value) + if s_artist in prefs.lyrics_subs: + s_artist = prefs.lyrics_subs[s_artist] + if s_title in prefs.lyrics_subs: + s_title = prefs.lyrics_subs[s_title] - # Clean uo - SDL_FreeSurface(s_image) - self.im.close() + line = genius(s_artist, s_title, return_url=True) - # SDL_SetTextureAlphaMod(c, 10) - self.fade_on_timer.set() + r = requests.head(line, timeout=10) - if self.a_texture is not None: - self.b_texture = self.a_texture - self.b_rect = self.a_rect - self.b_type = self.a_type + if r.status_code != 404: + webbrowser.open(line, new=2, autoraise=True) + gui.message_box = False + else: + line = "https://genius.com/search?q=" + urllib.parse.quote(f"{s_artist} {s_title}") + webbrowser.open(line, new=2, autoraise=True) + gui.message_box = False - self.a_texture = c - self.a_rect = dst - self.a_type = album_art_gen.loaded_bg_type +def ser_gen(track_id, get_lyrics=False): + tr = pctl.master_library[track_id] + if len(tr.title) < 1: + return - self.stage = 2 - self.radio_meta = None + show_message(_("Searching...")) - gui.update += 1 + shoot = threading.Thread(target=ser_gen_thread, args=[tr]) + shoot.daemon = True + shoot.start() - if self.stage == 2: - track = pctl.playing_object() +def ser_wiki(index: int) -> None: + if len(pctl.master_library[index].artist) < 2: + return + line = "https://en.wikipedia.org/wiki/Special:Search?search=" + urllib.parse.quote(pctl.master_library[index].artist) + webbrowser.open(line, new=2, autoraise=True) - if pctl.playing_state == 3 and not tauon.spot_ctl.coasting: - if self.radio_meta != pctl.tag_meta: - self.radio_meta = pctl.tag_meta - self.current_track_id = -1 - self.stage = 0 +def clip_ar_tr(index: int) -> None: + line = pctl.master_library[index].artist + " - " + pctl.master_library[index].title + SDL_SetClipboardText(line.encode("utf-8")) - elif not self.go_to_sleep and self.b_texture is None and self.current_track_id != track.index: - self.radio_meta = None - if not track.album: - self.stage = 0 - else: - self.current_track_id = track.index - if ( - self.parent_path != pctl.playing_object().parent_folder_path or self.current_track_album != pctl.playing_object().album): - self.stage = 0 +def tidal_copy_album(index: int) -> None: + t = pctl.master_library.get(index) + if t and t.file_ext == "TIDAL": + id = t.misc.get("tidal_album") + if id: + url = "https://listen.tidal.com/album/" + str(id) + copy_to_clipboard(url) - if gui.mode == 3 and prefs.mini_mode_mode == 5: - pass - elif prefs.bg_showcase_only: - if not gui.combo_mode: - return +def is_tidal_track(_) -> bool: + return pctl.master_library[r_menu_index].file_ext == "TIDAL" - t = self.fade_on_timer.get() - SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) - SDL_RenderClear(renderer) +# def get_track_spot_url_show_test(_): +# if pctl.get_track(r_menu_index).misc.get("spotify-track-url"): +# return True +# return False - if self.a_texture is not None: - if self.window_size != window_size: - self.flush() +def get_track_spot_url(track_id: int) -> None: + track_object = pctl.get_track(track_id) + url = track_object.misc.get("spotify-track-url") + if url: + copy_to_clipboard(url) + show_message(_("Url copied to clipboard"), mode="done") + else: + show_message(_("No results found")) - if self.b_texture is not None: +def get_track_spot_url_deco(): + if pctl.get_track(r_menu_index).misc.get("spotify-track-url"): + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled + return [line_colour, colours.menu_background, None] - self.b_rect.y = 0 - self.b_rect.h // 4 - if self.b_type == 1: - self.b_rect.y = 0 +def get_spot_artist_track(index: int) -> None: + get_artist_spot(pctl.get_track(index)) - if t < 0.4: +def get_album_spot_active(tr: TrackClass | None = None) -> None: + if tr is None: + tr = pctl.playing_object() + if not tr: + return + url = tauon.spot_ctl.get_album_url_from_local(tr) + if not url: + show_message(_("No results found")) + return + l = tauon.spot_ctl.append_album(url, return_list=True) + if len(l) < 2: + show_message(_("Looks like that's the only track in the album")) + return + pctl.multi_playlist.append( + pl_gen( + title=f"{pctl.get_track(l[0]).artist} - {pctl.get_track(l[0]).album}", + playlist_ids=l, + hide_title=False)) + switch_playlist(len(pctl.multi_playlist) - 1) - SDL_RenderCopy(renderer, self.b_texture, None, self.b_rect) +def get_spot_album_track(index: int): + get_album_spot_active(pctl.get_track(index)) - else: - SDL_DestroyTexture(self.b_texture) - self.b_texture = None - self.b_rect = None +# def get_spot_recs(tr: TrackClass | None = None) -> None: +# if not tr: +# tr = pctl.playing_object() +# if not tr: +# return +# url = tauon.spot_ctl.get_artist_url_from_local(tr) +# if not url: +# show_message(_("No results found")) +# return +# track_url = tr.misc.get("spotify-track-url") +# +# show_message(_("Fetching...")) +# shooter(tauon.spot_ctl.rec_playlist, (url, track_url)) +# +# def get_spot_recs_track(index: int): +# get_spot_recs(pctl.get_track(index)) - if self.a_texture is not None: +def drop_tracks_to_new_playlist(track_list: list[int], hidden: bool = False) -> None: + pl = new_playlist(switch=False) + albums = [] + artists = [] + for item in track_list: + albums.append(pctl.get_track(default_playlist[item]).album) + artists.append(pctl.get_track(default_playlist[item]).artist) + pctl.multi_playlist[pl].playlist_ids.append(default_playlist[item]) - self.a_rect.y = 0 - self.a_rect.h // 4 - if self.a_type == 1: - self.a_rect.y = 0 + if len(track_list) > 1: + if len(albums) > 0 and albums.count(albums[0]) == len(albums): + track = pctl.get_track(default_playlist[track_list[0]]) + artist = track.artist + if track.album_artist != "": + artist = track.album_artist + pctl.multi_playlist[pl].title = artist + " - " + albums[0][:50] - if t < 0.4: - fade = round(t / 0.4 * 255) - gui.update += 1 + elif len(track_list) == 1 and artists: + pctl.multi_playlist[pl].title = artists[0] - else: - fade = 255 + if tree_view_box.dragging_name: + pctl.multi_playlist[pl].title = tree_view_box.dragging_name - if self.go_to_sleep: - t = self.fade_off_timer.get() - gui.update += 1 + pctl.notify_change() - if t < 1: - fade = 255 - elif t < 1.4: - fade = 255 - round((t - 1) / 0.4 * 255) - else: - self.go_to_sleep = False - self.flush() - return +def queue_deco(): + if len(pctl.force_queue) > 0: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled + return [line_colour, colours.menu_background, None] - if prefs.bg_showcase_only and not (prefs.mini_mode_mode == 5 and gui.mode == 3): - tb = SDL_Rect(0, 0, window_size[0], gui.panelY) - bb = SDL_Rect(0, window_size[1] - gui.panelBY, window_size[0], gui.panelBY) - self.hole_punches.append(tb) - self.hole_punches.append(bb) +def bass_test(_) -> bool: + # return True + return prefs.backend == 1 - # Center image - if window_size[0] < 900 * gui.scale: - self.a_rect.x = (window_size[0] // 2) - self.a_rect.w // 2 - else: - self.a_rect.x = -40 +def gstreamer_test(_) -> bool: + # return True + return prefs.backend == 2 - SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) +def field_copy(text_field) -> None: + text_field.copy() - SDL_SetTextureAlphaMod(self.a_texture, fade) - SDL_RenderCopy(renderer, self.a_texture, None, self.a_rect) +def field_paste(text_field) -> None: + text_field.paste() - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) +def field_clear(text_field) -> None: + text_field.clear() - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - for rect in self.hole_punches: - SDL_RenderFillRect(renderer, rect) +def vis_off() -> None: + gui.vis_want = 0 + gui.update_layout() + # gui.turbo = False - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) +def level_on() -> None: + if gui.vis_want == 1 and gui.turbo is True: + gui.level_meter_colour_mode += 1 + if gui.level_meter_colour_mode > 4: + gui.level_meter_colour_mode = 0 - SDL_SetRenderTarget(renderer, gui.main_texture) - opacity = prefs.art_bg_opacity - if prefs.mini_mode_mode == 5 and gui.mode == 3: - opacity = 255 + gui.vis_want = 1 + gui.update_layout() + # if prefs.backend == 2: + # show_message("Visualisers not implemented in GStreamer mode") + # gui.turbo = True - SDL_SetTextureAlphaMod(gui.main_texture_overlay_temp, opacity) - SDL_RenderCopy(renderer, gui.main_texture_overlay_temp, None, None) +def spec_on() -> None: + gui.vis_want = 2 + # if prefs.backend == 2: + # show_message("Not implemented") + gui.update_layout() - SDL_SetRenderTarget(renderer, gui.main_texture) +def spec2_def() -> None: + if gui.vis_want == 3: + prefs.spec2_colour_mode += 1 + if prefs.spec2_colour_mode > 1: + prefs.spec2_colour_mode = 0 - else: - SDL_SetRenderTarget(renderer, gui.main_texture) + gui.vis_want = 3 + if prefs.backend == 2: + show_message(_("Not implemented")) + # gui.turbo = True + prefs.spec2_colour_setting = "custom" + gui.update_layout() -style_overlay = StyleOverlay() +def sa_remove(h: int) -> None: + if len(gui.pl_st) > 1: + del gui.pl_st[h] + gui.update_layout() + else: + show_message(_("Cannot remove the only column.")) +def sa_artist() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Artist", 220, False]) + gui.update_layout() -def trunc_line(line: str, font: str, px: int, dots: bool = True) -> str: - """This old function is slow and should be avoided""" - if ddt.get_text_w(line, font) < px + 10: - return line +def sa_album_artist() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Album Artist", 220, False]) + gui.update_layout() - if dots: - while ddt.get_text_w(line.rstrip(" ") + gui.trunk_end, font) > px: - if len(line) == 0: - return gui.trunk_end - line = line[:-1] - return line.rstrip(" ") + gui.trunk_end +def sa_composer() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Composer", 220, False]) + gui.update_layout() - while ddt.get_text_w(line, font) > px: +def sa_title() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Title", 220, False]) + gui.update_layout() - line = line[:-1] - if len(line) < 2: - break +def sa_album() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Album", 220, False]) + gui.update_layout() - return line +def sa_comment() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Comment", 300, False]) + gui.update_layout() +def sa_track() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["#", 25, True]) + gui.update_layout() -def right_trunc(line: str, font: str, px: int, dots: bool = True) -> str: - if ddt.get_text_w(line, font) < px + 10: - return line +def sa_count() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["P", 25, True]) + gui.update_layout() - if dots: - while ddt.get_text_w(line.rstrip(" ") + gui.trunk_end, font) > px: - if len(line) == 0: - return gui.trunk_end - line = line[1:] - return gui.trunk_end + line.rstrip(" ") +def sa_scrobbles() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["S", 25, True]) + gui.update_layout() - while ddt.get_text_w(line, font) > px: - # trunk = True - line = line[1:] - if len(line) < 2: - break - # if trunk and dots: - # line = line.rstrip(" ") + gui.trunk_end - return line +def sa_time() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Time", 55, True]) + gui.update_layout() +def sa_date() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Date", 55, True]) + gui.update_layout() -# def trunc_line2(line, font, px): -# trunk = False -# p = ddt.get_text_w(line, font) -# if p == 0 or p < px + 15: -# return line -# -# tl = line[0:(int(px / p * len(line)) + 3)] -# -# if ddt.get_text_w(line.rstrip(" ") + gui.trunk_end, font) > px: -# line = tl -# -# while ddt.get_text_w(line.rstrip(" ") + gui.trunk_end, font) > px + 10: -# trunk = True -# line = line[:-1] -# if len(line) < 1: -# break -# -# return line.rstrip(" ") + gui.trunk_end +def sa_genre() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Genre", 150, False]) + gui.update_layout() +def sa_file() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Filepath", 350, False]) + gui.update_layout() -click_time = time.time() -scroll_hold = False -scroll_point = 0 -scroll_bpoint = 0 -sbl = 50 -sbp = 100 +def sa_filename() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Filename", 300, False]) + gui.update_layout() -asbp = 50 -album_scroll_hold = False +def sa_codec() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Codec", 65, True]) + gui.update_layout() +def sa_bitrate() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Bitrate", 65, True]) + gui.update_layout() -def fix_encoding(index, mode, enc): - global default_playlist - global enc_field +def sa_lyrics() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Lyrics", 50, True]) + gui.update_layout() - todo = [] +def sa_cue() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["CUE", 50, True]) + gui.update_layout() - if mode == 1: - todo = [index] - elif mode == 0: - for b in range(len(default_playlist)): - if pctl.master_library[default_playlist[b]].parent_folder_name == pctl.master_library[ - index].parent_folder_name: - todo.append(default_playlist[b]) +def sa_star() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Starline", 80, True]) + gui.update_layout() - for q in range(len(todo)): +def sa_disc() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Disc", 50, True]) + gui.update_layout() - # key = pctl.master_library[todo[q]].title + pctl.master_library[todo[q]].filename - old_star = star_store.full_get(todo[q]) - if old_star != None: - star_store.remove(todo[q]) +def sa_rating() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Rating", 80, True]) + gui.update_layout() - if enc_field == "All" or enc_field == "Artist": - line = pctl.master_library[todo[q]].artist - line = line.encode("Latin-1", "ignore") - line = line.decode(enc, "ignore") - pctl.master_library[todo[q]].artist = line +def sa_love() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["❤", 25, True]) + # gui.pl_st.append(["❤", 25, True]) + gui.update_layout() - if enc_field == "All" or enc_field == "Album": - line = pctl.master_library[todo[q]].album - line = line.encode("Latin-1", "ignore") - line = line.decode(enc, "ignore") - pctl.master_library[todo[q]].album = line +def key_love(index: int) -> bool: + return get_love_index(index) - if enc_field == "All" or enc_field == "Title": - line = pctl.master_library[todo[q]].title - line = line.encode("Latin-1", "ignore") - line = line.decode(enc, "ignore") - pctl.master_library[todo[q]].title = line +def key_artist(index: int) -> str: + return pctl.master_library[index].artist.lower() - if old_star != None: - star_store.insert(todo[q], old_star) +def key_album_artist(index: int) -> str: + return pctl.master_library[index].album_artist.lower() - # if key in pctl.star_library: - # newkey = pctl.master_library[todo[q]].title + pctl.master_library[todo[q]].filename - # if newkey not in pctl.star_library: - # pctl.star_library[newkey] = copy.deepcopy(pctl.star_library[key]) - # # del pctl.star_library[key] +def key_composer(index: int) -> str: + return pctl.master_library[index].composer.lower() +def key_comment(index: int) -> str: + return pctl.master_library[index].comment -def transfer_tracks(index, mode, to): - todo = [] +def key_title(index: int) -> str: + return pctl.master_library[index].title.lower() - if mode == 0: - todo = [index] - elif mode == 1: - for b in range(len(default_playlist)): - if pctl.master_library[default_playlist[b]].parent_folder_name == pctl.master_library[ - index].parent_folder_name: - todo.append(default_playlist[b]) - elif mode == 2: - todo = default_playlist +def key_album(index: int) -> str: + return pctl.master_library[index].album.lower() - pctl.multi_playlist[to].playlist_ids += todo +def key_duration(index: int) -> int: + return pctl.master_library[index].length +def key_date(index: int) -> str: + return pctl.master_library[index].date -def prep_gal(): - global albums - albums = [] +def key_genre(index: int) -> str: + return pctl.master_library[index].genre.lower() - folder = "" +def key_t(index: int): + # return str(pctl.master_library[index].track_number) + return index_key(index) - for index in default_playlist: +def key_codec(index: int) -> str: + return pctl.master_library[index].file_ext - if folder != pctl.master_library[index].parent_folder_name: - albums.append([index, 0]) - folder = pctl.master_library[index].parent_folder_name +def key_bitrate(index: int) -> int: + return pctl.master_library[index].bitrate +def key_hl(index: int) -> int: + if len(pctl.master_library[index].lyrics) > 5: + return 0 + return 1 -def add_stations(stations: list[dict[str, int | str]], name: str): - if len(stations) == 1: - for i, s in enumerate(pctl.radio_playlists): - if s["name"] == "Default": - s["items"].insert(0, stations[0]) - s["scroll"] = 0 - pctl.radio_playlist_viewing = i - break - else: - r = {} - r["uid"] = uid_gen() - r["name"] = "Default" - r["items"] = stations - r["scroll"] = 0 - pctl.radio_playlists.append(r) - pctl.radio_playlist_viewing = len(pctl.radio_playlists) - 1 - else: - r = {} - r["uid"] = uid_gen() - r["name"] = name - r["items"] = stations - r["scroll"] = 0 - pctl.radio_playlists.append(r) - pctl.radio_playlist_viewing = len(pctl.radio_playlists) - 1 - if not gui.radio_view: - enter_radio_view() +def sort_ass(h, invert=False, custom_list=None, custom_name=""): + global default_playlist + if custom_list is None: + if pl_is_locked(pctl.active_playlist_viewing): + show_message(_("Playlist is locked")) + return -def load_m3u(path: str) -> None: - name = os.path.basename(path)[:-4] - playlist = [] - stations = [] + name = gui.pl_st[h][0] + playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + else: + name = custom_name + playlist = custom_list - location_dict = {} - titles = {} + key = None + ns = False - if not os.path.isfile(path): - return + if name == "Filepath": + key = key_filepath + if use_natsort: + key = key_fullpath + ns = True + if name == "Filename": + key = key_filepath # key_filename + if use_natsort: + key = key_fullpath + ns = True + if name == "Artist": + key = key_artist + if name == "Album Artist": + key = key_album_artist + if name == "Title": + key = key_title + if name == "Album": + key = key_album + if name == "Composer": + key = key_composer + if name == "Time": + key = key_duration + if name == "Date": + key = key_date + if name == "Genre": + key = key_genre + if name == "#": + key = key_t + if name == "S": + key = key_scrobbles + if name == "P": + key = key_playcount + if name == "Starline": + key = best + if name == "Rating": + key = key_rating + if name == "Comment": + key = key_comment + if name == "Codec": + key = key_codec + if name == "Bitrate": + key = key_bitrate + if name == "Lyrics": + key = key_hl + if name == "❤": + key = key_love + if name == "Disc": + key = key_disc + if name == "CUE": + key = key_cue - with Path(path).open(encoding="utf-8") as file: - lines = file.readlines() + if custom_list is None: + if key is not None: - for i, line in enumerate(lines): - line = line.strip("\r\n").strip() - if not line.startswith("#"): # line.startswith("http"): + if ns: + key = natsort.natsort_keygen(key=key, alg=natsort.PATH) - # Get title if present - line_title = "" - if i > 0: - bline = lines[i - 1] - if "," in bline and bline.startswith("#EXTINF:"): - line_title = bline.split(",", 1)[1].strip("\r\n").strip() + playlist.sort(key=key, reverse=invert) - if line.startswith("http"): - radio: dict[str, int | str] = {} - radio["stream_url"] = line + pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids = playlist + default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - if line_title: - radio["title"] = line_title - else: - radio["title"] = os.path.splitext(os.path.basename(path))[0].strip() + pctl.playlist_view_position = 0 + logging.debug("Position changed by sort") + gui.pl_update = 1 - stations.append(radio) + elif custom_list is not None: + playlist.sort(key=key, reverse=invert) + reload() - if gui.auto_play_import: - gui.auto_play_import = False - radiobox.start(radio) - else: - line = uri_parse(line) - # Join file path if possibly relative - if not line.startswith("/"): - line = os.path.join(os.path.dirname(path), line) +def sort_dec(h): + sort_ass(h, True) - # Cache datbase file paths for quick lookup - if not location_dict: - for key, value in pctl.master_library.items(): - if value.fullpath: - location_dict[value.fullpath] = value - if value.title: - titles[value.artist + " - " + value.title] = value +def hide_set_bar(): + gui.set_bar = False + gui.update_layout() + gui.pl_update = 1 - # Is file path already imported? - logging.info(line) - if line in location_dict: - playlist.append(location_dict[line].index) - logging.info("found imported") - # Or... does the file exist? Then import it - elif os.path.isfile(line): - nt = TrackClass() - nt.index = pctl.master_count - set_path(nt, line) - nt = tag_scan(nt) - pctl.master_library[pctl.master_count] = nt - playlist.append(pctl.master_count) - pctl.master_count += 1 - logging.info("found file") - # Last resort, guess based on title - elif line_title in titles: - playlist.append(titles[line_title].index) - logging.info("found title") - else: - logging.info("not found") +def show_set_bar(): + gui.set_bar = True + gui.update_layout() + gui.pl_update = 1 - if playlist: - pctl.multi_playlist.append( - pl_gen(title=name, playlist_ids=playlist)) - if stations: - add_stations(stations, name) +def bass_features_deco(): + line_colour = colours.menu_text + if prefs.backend != 1: + line_colour = colours.menu_text_disabled + return [line_colour, colours.menu_background, None] - gui.update = 1 +def toggle_dim_albums(mode: int = 0) -> bool: + if mode == 1: + return prefs.dim_art + prefs.dim_art ^= True + gui.pl_update = 1 + gui.update += 1 -def read_pls(lines: list[str], path: str, followed: bool = False) -> None: - ids = [] - urls = {} - titles = {} +def toggle_gallery_combine(mode: int = 0) -> bool: + if mode == 1: + return prefs.gallery_combine_disc - for line in lines: - line = line.strip("\r\n") - if "=" in line and line.startswith("File") and "http" in line: - # Get number - n = line.split("=")[0][4:] - if n.isdigit(): - if n not in ids: - ids.append(n) - urls[n] = line.split("=", 1)[1].strip() + prefs.gallery_combine_disc ^= True + reload_albums() - if "=" in line and line.startswith("Title"): - # Get number - n = line.split("=")[0][5:] - if n.isdigit(): - if n not in ids: - ids.append(n) - titles[n] = line.split("=", 1)[1].strip() +def toggle_gallery_click(mode: int = 0) -> bool: + if mode == 1: + return prefs.gallery_single_click - stations: list[dict[str, int | str]] = [] - for id in ids: - if id in urls: - radio: dict[str, int | str] = {} - radio["stream_url"] = urls[id] - radio["title"] = os.path.splitext(os.path.basename(path))[0] - radio["scroll"] = 0 - if id in titles: - radio["title"] = titles[id] + prefs.gallery_single_click ^= True - if ".pls" in radio["stream_url"]: - if not followed: - try: - logging.info("Download .pls") - response = requests.get(radio["stream_url"], stream=True, timeout=15) - if int(response.headers["Content-Length"]) < 2000: - read_pls(response.content.decode().splitlines(), path, followed=True) - except Exception: - logging.exception("Failed to retrieve .pls") - else: - stations.append(radio) - if gui.auto_play_import: - gui.auto_play_import = False - radiobox.start(radio) - if stations: - add_stations(stations, os.path.basename(path)) +def toggle_gallery_thin(mode: int = 0) -> bool: + if mode == 1: + return prefs.thin_gallery_borders + prefs.thin_gallery_borders ^= True + gui.update += 1 + update_layout_do() -def load_pls(path: str) -> None: - if os.path.isfile(path): - f = open(path) - lines = f.readlines() - read_pls(lines, path) - f.close() +def toggle_gallery_row_space(mode: int = 0) -> bool: + if mode == 1: + return prefs.increase_gallery_row_spacing + prefs.increase_gallery_row_spacing ^= True + gui.update += 1 + update_layout_do() -def load_xspf(path: str) -> None: - global to_got +def toggle_galler_text(mode: int = 0) -> bool: + if mode == 1: + return gui.gallery_show_text - name = os.path.basename(path)[:-5] - # tauon.log("Importing XSPF playlist: " + path, title=True) - logging.info("Importing XSPF playlist: " + path) + gui.gallery_show_text ^= True + gui.update += 1 + update_layout_do() - try: - parser = ET.XMLParser(encoding="utf-8") - e = ET.parse(path, parser).getroot() + # Jump to playing album + if album_mode and gui.first_in_grid is not None: - a = [] - b = {} - info = "" + if gui.first_in_grid < len(default_playlist): + goto_album(gui.first_in_grid, force=True) - for top in e: +def toggle_card_style(mode: int = 0) -> bool: + if mode == 1: + return prefs.use_card_style - if top.tag.endswith("info"): - info = top.text - if top.tag.endswith("title"): - name = top.text - if top.tag.endswith("trackList"): - for track in top: - if track.tag.endswith("track"): - for field in track: - logging.info(field.tag) - logging.info(field.text) - if "title" in field.tag and field.text: - b["title"] = field.text - if "location" in field.tag and field.text: - l = field.text - l = str(urllib.parse.unquote(l)) - if l[:5] == "file:": - l = l.replace("file:", "") - l = l.lstrip("/") - l = "/" + l + prefs.use_card_style ^= True + gui.update += 1 - b["location"] = l - if "creator" in field.tag and field.text: - b["artist"] = field.text - if "album" in field.tag and field.text: - b["album"] = field.text - if "duration" in field.tag and field.text: - b["duration"] = field.text +def toggle_side_panel(mode: int = 0) -> bool: + global update_layout + global album_mode - b["info"] = info - b["name"] = name - a.append(copy.deepcopy(b)) - b = {} + if mode == 1: + return prefs.prefer_side - except Exception: - logging.exception("Error importing/parsing XSPF playlist") - show_message(_("Error importing XSPF playlist."), _("Sorry about that."), mode="warning") - return + prefs.prefer_side ^= True + update_layout = True - # Extract internet streams first - stations: list[dict[str, int | str]] = [] - for i in reversed(range(len(a))): - item = a[i] - if item["location"].startswith("http"): - radio: dict[str, int | str] = {} - radio["stream_url"] = item["location"] - radio["title"] = item["name"] - radio["scroll"] = 0 - if item["info"].startswith("http"): - radio["website_url"] = item["info"] + if album_mode or prefs.prefer_side is True: + gui.rsp = True + else: + gui.rsp = False - stations.append(radio) + if prefs.prefer_side: + gui.rspw = gui.pref_rspw - if gui.auto_play_import: - gui.auto_play_import = False - radiobox.start(radio) +def force_album_view(): + toggle_album_mode(True) - del a[i] - if stations: - add_stations(stations, os.path.basename(path)) - playlist = [] - missing = 0 +def enter_combo(): + if not gui.combo_mode: + gui.combo_was_album = album_mode + gui.showcase_mode = False + gui.radio_view = False + if album_mode: + toggle_album_mode() + if gui.rsp: + gui.rsp = False + gui.combo_mode = True + gui.update_layout() - if len(a) > 5000: - to_got = "xspfl" - - # Generate location dict - location_dict = {} - base_names = {} - r_base_names = {} - titles = {} - for key, value in pctl.master_library.items(): - if value.fullpath != "": - location_dict[value.fullpath] = key - if value.filename != "": - base_names[value.filename] = 0 - r_base_names[key] = value.filename - if value.title != "": - titles[value.title] = 0 +def exit_combo(restore=False): + if gui.combo_mode: + if gui.combo_was_album and restore: + force_album_view() + gui.showcase_mode = False + gui.radio_view = False + if prefs.prefer_side: + gui.rsp = True + gui.update_layout() + gui.combo_mode = False + gui.was_radio = False - for track in a: - found = False +def enter_showcase_view(track_id=None): + if not gui.combo_mode: + enter_combo() + gui.was_radio = False + gui.showcase_mode = True + gui.radio_view = False + if track_id is None or pctl.playing_object() is None or pctl.playing_object().index == track_id: + pass + else: + gui.force_showcase_index = track_id + inp.mouse_click = False + gui.update_layout() - # Check if we already have a track with full file path in database - if not found and "location" in track: +def enter_radio_view(): + if not gui.combo_mode: + enter_combo() + gui.showcase_mode = False + gui.radio_view = True + inp.mouse_click = False + gui.update_layout() - location = track["location"] - if location in location_dict: - playlist.append(location_dict[location]) - if not os.path.isfile(location): - missing += 1 - found = True +def standard_size(): + global album_mode + global window_size + global update_layout - if found is True: - continue + global album_mode_art_size - # Then check for title, artist and filename match - if not found and "location" in track and "duration" in track and "title" in track and "artist" in track: - base = os.path.basename(track["location"]) - if base in base_names: - for index, bn in r_base_names.items(): - va = pctl.master_library[index] - if va.artist == track["artist"] and va.title == track["title"] and \ - os.path.isfile(va.fullpath) and \ - va.filename == base: - playlist.append(index) - if not os.path.isfile(va.fullpath): - missing += 1 - found = True - break - if found is True: - continue + album_mode = False + gui.rsp = True + window_size = window_default_size + SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - # Then check for just title and artist match - if not found and "title" in track and "artist" in track and track["title"] in titles: - for key, value in pctl.master_library.items(): - if value.artist == track["artist"] and value.title == track["title"] and os.path.isfile(value.fullpath): - playlist.append(key) - if not os.path.isfile(value.fullpath): - missing += 1 - found = True - break - if found is True: - continue + gui.rspw = 80 + int(window_size[0] * 0.18) + update_layout = True + album_mode_art_size = 130 + # clear_img_cache() - if (not found and "location" in track) or "title" in track: - nt = TrackClass() - nt.index = pctl.master_count - nt.found = False +def path_stem_to_playlist(path: str, title: str) -> None: + """Used with gallery power bar""" + playlist = [] - if "location" in track: - location = track["location"] - set_path(nt, location) - if os.path.isfile(location): - nt.found = True - elif "album" in track: - nt.parent_folder_name = track["album"] - if "artist" in track: - nt.artist = track["artist"] - if "title" in track: - nt.title = track["title"] - if "duration" in track: - nt.length = int(float(track["duration"]) / 1000) - if "album" in track: - nt.album = track["album"] - nt.is_cue = False - if nt.found: - nt = tag_scan(nt) + # Hack for networked tracks + if path.lstrip("/") == title: + for item in pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids: + if title == os.path.basename(pctl.master_library[item].parent_folder_path): + playlist.append(item) + else: + for item in pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids: + if path in pctl.master_library[item].parent_folder_path: + playlist.append(item) - pctl.master_library[pctl.master_count] = nt - playlist.append(pctl.master_count) - pctl.master_count += 1 - if nt.found: - continue + pctl.multi_playlist.append(pl_gen( + title=os.path.basename(title).upper(), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - missing += 1 - logging.error("-- Failed to locate track") - if "location" in track: - logging.error("-- -- Expected path: " + track["location"]) - if "title" in track: - logging.error("-- -- Title: " + track["title"]) - if "artist" in track: - logging.error("-- -- Artist: " + track["artist"]) - if "album" in track: - logging.error("-- -- Album: " + track["album"]) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pctl.active_playlist_viewing].title + "\" f\"" + path + "\"" - if missing > 0: - show_message( - _("Failed to locate {N} out of {T} tracks.") - .format(N=str(missing), T=str(len(a)))) - #logging.info(playlist) - if playlist: - pctl.multi_playlist.append( - pl_gen(title=name, playlist_ids=playlist)) - gui.update = 1 + switch_playlist(len(pctl.multi_playlist) - 1) - # tauon.log("Finished importing XSPF") +def goto_album(playlist_no: int, down: bool = False, force: bool = False) -> list | int | None: + logging.debug("Postion set by album locate") + if core_timer.get() < 0.5: + return None -bb_type = 0 + global album_dex -# gui.scroll_hide_box = (0, gui.panelY, 28, window_size[1] - gui.panelBY - gui.panelY) + # ---- + w = gui.rspw + if window_size[0] < 750 * gui.scale: + w = window_size[0] - 20 * gui.scale + if gui.lsp: + w -= gui.lspw + area_x = w + 38 * gui.scale + row_len = int((area_x - album_h_gap) / (album_mode_art_size + album_h_gap)) + global last_row + last_row = row_len + # ---- -encoding_menu = False -enc_index = 0 -enc_setting = 0 -enc_field = "All" + px = 0 + row = 0 + re = 0 -gen_menu = False + for i in range(len(album_dex)): + if i == len(album_dex) - 1: + re = i + break + if album_dex[i + 1] - 1 > playlist_no - 1: + re = i + break + row += 1 + if row > row_len - 1: + row = 0 + px += album_mode_art_size + album_v_gap -transfer_setting = 0 + # If the album is within the view port already, dont jump to it + # (unless we really want to with force) + if not force and gui.album_scroll_px + album_v_slide_value < px < gui.album_scroll_px + window_size[1]: -b_panel_size = 300 -b_info_bar = False + # Dont chance the view since its alread in the view port + # But if the album is just out of view on the bottom, bring it into view on to bottom row + if window_size[1] > (album_mode_art_size + album_v_gap) * 2: + while not gui.album_scroll_px - 20 < px + (album_mode_art_size + album_v_gap + 3) < gui.album_scroll_px + \ + window_size[1] - 40: + gui.album_scroll_px += 1 + else: + # Set the view to the calculated position + gui.album_scroll_px = px + gui.album_scroll_px -= album_v_slide_value -message_info_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "notice.png") -message_warning_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "warning.png") -message_tick_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "done.png") -message_arrow_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "ext.png") -message_error_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "error.png") -message_bubble_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "bubble.png") -message_download_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "ddl.png") + gui.album_scroll_px = max(gui.album_scroll_px, 0 - album_v_slide_value) + if len(album_dex) > 0: + return album_dex[re] + return 0 -class ToolTip: + gui.update += 1 # TODO(Martin): WTF Unreachable?? - def __init__(self) -> None: - self.text = "" - self.h = 24 * gui.scale - self.w = 62 * gui.scale - self.x = 0 - self.y = 0 - self.timer = Timer() - self.trigger = 1.1 - self.font = 13 - self.called = False - self.a = False +def toggle_album_mode(force_on=False): + global album_mode + global window_size + global update_layout + global album_playlist_width + global old_album_pos - def test(self, x, y, text): + gui.gall_tab_enter = False - if self.text != text or x != self.x or y != self.y: - self.text = text - # self.timer.set() - self.a = False + if album_mode is True: - self.x = x - self.y = y - self.w = ddt.get_text_w(text, self.font) + 20 * gui.scale + album_mode = False + # album_playlist_width = gui.playlist_width + # old_album_pos = gui.album_scroll_px + gui.rspw = gui.pref_rspw + gui.rsp = prefs.prefer_side + gui.album_tab_mode = False + else: + album_mode = True + if gui.combo_mode: + exit_combo() - self.called = True + gui.rsp = True - if self.a is False: - self.timer.set() - gui.frame_callback_list.append(TestTimer(self.trigger)) - self.a = True + gui.rspw = gui.pref_gallery_w - def render(self) -> None: + space = window_size[0] - gui.rspw + if gui.lsp: + space -= gui.lspw - if self.called is True: + if album_mode and gui.set_mode and len(gui.pl_st) > 6 and space < 600 * gui.scale: + gui.set_mode = False + gui.pl_update = True + gui.update_layout() - if self.timer.get() > self.trigger: + reload_albums(quiet=True) - ddt.rect((self.x, self.y, self.w, self.h), colours.box_button_background) - # ddt.rect((self.x, self.y, self.w, self.h), colours.grey(45)) - ddt.text( - (self.x + int(self.w / 2), self.y + 4 * gui.scale, 2), self.text, - colours.menu_text, self.font, bg=colours.box_button_background) - else: - # gui.update += 1 - pass - else: - self.timer.set() - self.a = False + # if pctl.active_playlist_playing == pctl.active_playlist_viewing: + # goto_album(pctl.playlist_playing_position) - self.called = False + if album_mode: + if pctl.selected_in_playlist < len(pctl.playing_playlist()): + goto_album(pctl.selected_in_playlist) +def toggle_gallery_keycontrol(always_exit=False): + if is_level_zero(): + if not album_mode: + toggle_album_mode() + gui.gall_tab_enter = True + gui.album_tab_mode = True + show_in_gal(pctl.selected_in_playlist, silent=True) + elif gui.gall_tab_enter or always_exit: + # Exit gallery and tab mode + toggle_album_mode() + else: + gui.album_tab_mode ^= True + if gui.album_tab_mode: + show_in_gal(pctl.selected_in_playlist, silent=True) + +def check_auto_update_okay(code, pl=None): + try: + cmds = shlex.split(code) + except Exception: + logging.exception("Malformed generator code!") + return False + return "auto" in cmds or ( + prefs.always_auto_update_playlists and + pctl.active_playlist_playing != pl and + "sf" not in cmds and + "rf" not in cmds and + "ra" not in cmds and + "sa" not in cmds and + "st" not in cmds and + "rt" not in cmds and + "plex" not in cmds and + "jelly" not in cmds and + "koel" not in cmds and + "tau" not in cmds and + "air" not in cmds and + "sal" not in cmds and + "slt" not in cmds and + "spl\"" not in code and + "tpl\"" not in code and + "tar\"" not in code and + "tmix\"" not in code and + "r" not in cmds) -tool_tip = ToolTip() -tool_tip2 = ToolTip() -tool_tip2.trigger = 1.8 -track_box_path_tool_timer = Timer() +def switch_playlist(number, cycle=False, quiet=False): + global default_playlist + global search_index + global shift_selection -def ex_tool_tip(x, y, text1_width, text, font): - text2_width = ddt.get_text_w(text, font) - if text2_width == text1_width: + # Close any active menus + # for instance in Menu.instances: + # instance.active = False + close_all_menus() + if gui.radio_view: + if cycle: + pctl.radio_playlist_viewing += number + else: + pctl.radio_playlist_viewing = number + if pctl.radio_playlist_viewing > len(pctl.radio_playlists) - 1: + pctl.radio_playlist_viewing = 0 return - y -= 10 * gui.scale + gui.previous_playlist_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - w = ddt.get_text_w(text, 312) + 24 * gui.scale - h = 24 * gui.scale + gui.pl_update = 1 + search_index = 0 + gui.column_d_click_on = -1 + gui.search_error = False + if quick_search_mode: + gui.force_search = True - x -= int(w / 2) + # if pl_follow: + # pctl.multi_playlist[pctl.playlist_active][1] = copy.deepcopy(pctl.playlist_playing) - border = 1 * gui.scale - ddt.rect((x - border, y - border, w + border * 2, h + border * 2), colours.grey(60)) - ddt.rect((x, y, w, h), colours.menu_background) - p = ddt.text((x + int(w / 2), y + 3 * gui.scale, 2), text, colours.menu_text, 312, bg=colours.menu_background) + if gui.showcase_mode and gui.combo_mode and not quiet: + view_standard() + pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids = default_playlist + pctl.multi_playlist[pctl.active_playlist_viewing].position = pctl.playlist_view_position + pctl.multi_playlist[pctl.active_playlist_viewing].selected = pctl.selected_in_playlist -class ToolTip3: + if gall_pl_switch_timer.get() > 240: + gui.gallery_positions.clear() + gall_pl_switch_timer.set() - def __init__(self) -> None: - self.x = 0 - self.y = 0 - self.text = "" - self.font = None - self.show = False - self.width = 0 - self.height = 24 * gui.scale - self.timer = Timer() - self.pl_position = 0 - self.click_exclude_point = (0, 0) + gui.gallery_positions[gui.previous_playlist_id] = gui.album_scroll_px - def set(self, x, y, text, font, rect): + if cycle: + pctl.active_playlist_viewing += number + else: + pctl.active_playlist_viewing = number - y -= round(11 * gui.scale) - if self.show == False or self.y != y or x != self.x or self.pl_position != pctl.playlist_view_position: - self.timer.set() + while pctl.active_playlist_viewing > len(pctl.multi_playlist) - 1: + pctl.active_playlist_viewing -= len(pctl.multi_playlist) + while pctl.active_playlist_viewing < 0: + pctl.active_playlist_viewing += len(pctl.multi_playlist) - if point_proximity_test(self.click_exclude_point, mouse_position, 20 * gui.scale): - self.timer.set() - return + default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + pctl.playlist_view_position = pctl.multi_playlist[pctl.active_playlist_viewing].position + pctl.selected_in_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].selected + logging.debug("Position changed by playlist change") + shift_selection = [pctl.selected_in_playlist] - if inp.mouse_click: - self.click_exclude_point = copy.copy(mouse_position) - self.timer.set() - return + id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - self.x = x - self.y = y - self.text = text - self.font = font - self.show = True - self.rect = rect - self.pl_position = pctl.playlist_view_position + code = pctl.gen_codes.get(id) + if code is not None and check_auto_update_okay(code, pctl.active_playlist_viewing): + gui.regen_single_id = id + tauon.thread_manager.ready("worker") - def render(self): + if album_mode: + reload_albums(True) + if id in gui.gallery_positions: + gui.album_scroll_px = gui.gallery_positions[id] + else: + goto_album(pctl.playlist_view_position) - if not self.show: - return + if prefs.auto_goto_playing: + pctl.show_current(this_only=True, playing=False, highlight=True, no_switch=True) - if not point_proximity_test(self.click_exclude_point, mouse_position, 20 * gui.scale): - self.click_exclude_point = (0, 0) + if prefs.shuffle_lock: + view_box.lyrics(hit=True) + if pctl.active_playlist_viewing: + pctl.active_playlist_playing = pctl.active_playlist_viewing + random_track() - if not coll( - self.rect) or inp.mouse_click or gui.level_2_click or self.pl_position != pctl.playlist_view_position: - self.show = False +def cycle_playlist_pinned(step): + if gui.radio_view: - gui.frame_callback_list.append(TestTimer(0.02)) + pctl.radio_playlist_viewing += step * -1 + if pctl.radio_playlist_viewing > len(pctl.radio_playlists) - 1: + pctl.radio_playlist_viewing = 0 + if pctl.radio_playlist_viewing < 0: + pctl.radio_playlist_viewing = len(pctl.radio_playlists) - 1 + return - if self.timer.get() < 0.6: - return + if step > 0: + p = pctl.active_playlist_viewing + le = len(pctl.multi_playlist) + on = p + on -= 1 + while True: + if on < 0: + on = le - 1 + if on == p: + break + if pctl.multi_playlist[on].hidden is False or not prefs.tabs_on_top or ( + gui.lsp and prefs.left_panel_mode == "playlist"): + switch_playlist(on) + break + on -= 1 - w = ddt.get_text_w(self.text, 312) + self.height - x = self.x # - int(self.width / 2) - y = self.y - h = self.height + elif step < 0: + p = pctl.active_playlist_viewing + le = len(pctl.multi_playlist) + on = p + on += 1 + while True: + if on == le: + on = 0 + if on == p: + break + if pctl.multi_playlist[on].hidden is False or not prefs.tabs_on_top or ( + gui.lsp and prefs.left_panel_mode == "playlist"): + switch_playlist(on) + break + on += 1 - border = 1 * gui.scale +def activate_info_box(): + fader.rise() + pref_box.enabled = True - ddt.rect((x - border, y - border, w + border * 2, h + border * 2), colours.grey(60)) - ddt.rect((x, y, w, h), colours.menu_background) - p = ddt.text( - (x + int(w / 2), y + 3 * gui.scale, 2), self.text, colours.menu_text, 312, bg=colours.menu_background) +def activate_radio_box(): + radiobox.active = True + radiobox.radio_field.clear() + radiobox.radio_field_title.clear() - if not coll(self.rect): - self.show = False +def new_playlist_colour_callback(): + if gui.radio_view: + return [120, 90, 245, 255] + return [237, 80, 221, 255] +def new_playlist_deco(): + if gui.radio_view: + text = _("New Radio List") + else: + text = _("New Playlist") + return [colours.menu_text, colours.menu_background, text] -columns_tool_tip = ToolTip3() +def clean_db_show_test(_): + return gui.suggest_clean_db -tool_tip_instant = ToolTip3() +def clean_db_fast(): + keys = set(pctl.master_library.keys()) + for pl in pctl.multi_playlist: + keys -= set(pl.playlist_ids) + for item in keys: + pctl.purge_track(item, fast=True) + gui.show_message(_("Done! {N} old items were removed.").format(N=len(keys)), mode="done") + gui.suggest_clean_db = False -def close_all_menus(): - for menu in Menu.instances: - menu.active = False - Menu.active = False +def clean_db_deco(): + return [colours.menu_text, [30, 150, 120, 255], _("Clean Database!")] +def import_spotify_playlist() -> None: + clip = copy_from_clipboard() + for line in clip.split("\n"): + if line.startswith(("https://open.spotify.com/playlist/", "spotify:playlist:")): + clip = clip.strip() + tauon.spot_ctl.playlist(line) -def menu_standard_or_grey(bool: bool): - if bool: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + if album_mode: + reload_albums() + gui.pl_update += 1 - return [line_colour, colours.menu_background, None] +def import_spotify_playlist_deco(): + clip = copy_from_clipboard() + if clip.startswith(("https://open.spotify.com/playlist/", "spotify:playlist:")): + return [colours.menu_text, colours.menu_background, None] + return [colours.menu_text_disabled, colours.menu_background, None] +def show_import_music(_): + return gui.add_music_folder_ready -# Create empty area menu -playlist_menu = Menu(130) -radio_entry_menu = Menu(125) -showcase_menu = Menu(135) -center_info_menu = Menu(125) -cancel_menu = Menu(100) -gallery_menu = Menu(175, show_icons=True) -artist_info_menu = Menu(135) -queue_menu = Menu(150) -repeat_menu = Menu(120) -shuffle_menu = Menu(120) -artist_list_menu = Menu(165, show_icons=True) -lightning_menu = Menu(165) -lsp_menu = Menu(145) -folder_tree_menu = Menu(175, show_icons=True) -folder_tree_stem_menu = Menu(190, show_icons=True) -overflow_menu = Menu(175) -spotify_playlist_menu = Menu(175) -radio_context_menu = Menu(175) -#chrome_menu = Menu(175) +def import_music(): + pl = pl_gen(_("Music")) + pl.last_folder = [str(music_directory)] + pctl.multi_playlist.append(pl) + load_order = LoadClass() + load_order.target = str(music_directory) + load_order.playlist = pl.uuid_int + load_orders.append(load_order) + switch_playlist(len(pctl.multi_playlist) - 1) + gui.add_music_folder_ready = False +def stt2(sec): + days, rem = divmod(sec, 86400) + hours, rem = divmod(rem, 3600) + min, sec = divmod(rem, 60) + s_day = str(days) + "d" + if s_day == "0d": + s_day = " " + s_hours = str(hours) + "h" + if s_hours == "0h" and s_day == " ": + s_hours = " " -def enable_artist_list(): - if prefs.left_panel_mode != "artist list": - gui.last_left_panel_mode = prefs.left_panel_mode - prefs.left_panel_mode = "artist list" - gui.lsp = True - gui.update_layout() + s_min = str(min) + "m" + return s_day.rjust(3) + " " + s_hours.rjust(3) + " " + s_min.rjust(3) +def export_database(): + path = str(user_directory / "DatabaseExport.csv") + xport = open(path, "w") -def enable_playlist_list(): - if prefs.left_panel_mode != "playlist": - gui.last_left_panel_mode = prefs.left_panel_mode - prefs.left_panel_mode = "playlist" - gui.lsp = True - gui.update_layout() + xport.write("Artist;Title;Album;Album artist;Track number;Type;Duration;Release date;Genre;Playtime;File path") + for index, track in pctl.master_library.items(): -def enable_queue_panel(): - if prefs.left_panel_mode != "queue": - gui.last_left_panel_mode = prefs.left_panel_mode - prefs.left_panel_mode = "queue" - gui.lsp = True - gui.update_layout() + xport.write("\n") + xport.write(csv_string(track.artist) + ",") + xport.write(csv_string(track.title) + ",") + xport.write(csv_string(track.album) + ",") + xport.write(csv_string(track.album_artist) + ",") + xport.write(csv_string(track.track_number) + ",") + type = "File" + if track.is_network: + type = "Network" + elif track.is_cue: + type = "CUE File" + xport.write(type + ",") + xport.write(str(track.length) + ",") + xport.write(csv_string(track.date) + ",") + xport.write(csv_string(track.genre) + ",") + xport.write(str(int(star_store.get_by_object(track))) + ",") + xport.write(csv_string(track.fullpath)) -def enable_folder_list(): - if prefs.left_panel_mode != "folder view": - gui.last_left_panel_mode = prefs.left_panel_mode - prefs.left_panel_mode = "folder view" - gui.lsp = True - gui.update_layout() + xport.close() + show_message(_("Export complete."), _("Saved as: ") + path, mode="done") +def q_to_playlist(): + pctl.multi_playlist.append(pl_gen( + title=_("Play History"), + playing=0, + playlist_ids=list(reversed(copy.deepcopy(pctl.track_queue))), + position=0, + hide_title=True, + selected=0)) -def lsp_menu_test_queue(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "queue" +def clean_db() -> None: + global cm_clean_db + prefs.remove_network_tracks = False + cm_clean_db = True + tauon.thread_manager.ready("worker") +def clean_db2() -> None: + global cm_clean_db + prefs.remove_network_tracks = True + cm_clean_db = True + tauon.thread_manager.ready("worker") -def lsp_menu_test_playlist(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "playlist" +def import_fmps() -> None: + unique = set() + for playlist in pctl.multi_playlist: + for id in playlist.playlist_ids: + tr = pctl.get_track(id) + if "FMPS_Rating" in tr.misc: + rating = round(tr.misc["FMPS_Rating"] * 10) + star_store.set_rating(tr.index, rating) + unique.add(tr.index) + show_message(_("{N} ratings imported").format(N=str(len(unique))), mode="done") -def lsp_menu_test_tree(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "folder view" + gui.pl_update += 1 +def import_popm(): + unique = set() + skipped = set() + for playlist in pctl.multi_playlist: + for id in playlist.playlist_ids: + tr = pctl.get_track(id) + if "POPM" in tr.misc: + rating = tr.misc["POPM"] + t_rating = 0 + if rating <= 1: + t_rating = 2 + elif rating <= 64: + t_rating = 4 + elif rating <= 128: + t_rating = 6 + elif rating <= 196: + t_rating = 8 + elif rating <= 255: + t_rating = 10 -def lsp_menu_test_artist(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "artist list" + if star_store.get_rating(tr.index) == 0: + star_store.set_rating(tr.index, t_rating) + unique.add(tr.index) + else: + logging.info("Won't import POPM because track is already rated") + skipped.add(tr.index) + s = str(len(unique)) + " ratings imported" + if len(skipped) > 0: + s += f", {len(skipped)} skipped" + show_message(s, mode="done") -def toggle_left_last(): - gui.lsp = True - t = prefs.left_panel_mode - if t != gui.last_left_panel_mode: - prefs.left_panel_mode = gui.last_left_panel_mode - gui.last_left_panel_mode = t + gui.pl_update += 1 +def clear_ratings() -> None: + if not key_shift_down: + show_message( + _("This will delete all track and album ratings from the local database!"), + _("Press button again while holding shift key if you're sure you want to do that."), + mode="warning") + return + for key, star in star_store.db.items(): + star[2] = 0 + album_star_store.db.clear() + gui.pl_update += 1 -# . Menu entry: A side panel view layout +def find_incomplete() -> None: + gen_incomplete(pctl.active_playlist_viewing) -lsp_menu.add(MenuItem(_("Playlists + Queue"), enable_playlist_list, disable_test=lsp_menu_test_playlist)) -lsp_menu.add(MenuItem(_("Queue"), enable_queue_panel, disable_test=lsp_menu_test_queue)) -# . Menu entry: Side panel view layout showing a list of artists with thumbnails -lsp_menu.add(MenuItem(_("Artist List"), enable_artist_list, disable_test=lsp_menu_test_artist)) -# . Menu entry: A side panel view layout. Alternative name: Folder Tree -lsp_menu.add(MenuItem(_("Folder Navigator"), enable_folder_list, disable_test=lsp_menu_test_tree)) +def cast_deco(): + line_colour = colours.menu_text + if tauon.chrome_mode: + return [line_colour, colours.menu_background, _("Stop Cast")] # [24, 25, 60, 255] + return [line_colour, colours.menu_background, None] +def cast_search2() -> None: + chrome.rescan() -class RenameTrackBox: +def cast_search() -> None: + if tauon.chrome_mode: + pctl.stop() + chrome.end() + else: + if not chrome: + show_message(_("pychromecast not found")) + return + show_message(_("Searching for Chomecasts...")) + shooter(cast_search2) - def __init__(self): +def clear_queue() -> None: + pctl.force_queue = [] + gui.pl_update = 1 + pctl.pause_queue = False - self.active = False - self.target_track_id = None - self.single_only = False +def set_mini_mode_A1() -> None: + prefs.mini_mode_mode = 0 + set_mini_mode() - def activate(self, track_id): +def set_mini_mode_B1() -> None: + prefs.mini_mode_mode = 1 + set_mini_mode() - self.active = True - self.target_track_id = track_id - if key_shift_down or key_shiftr_down: - self.single_only = True - else: - self.single_only = False +def set_mini_mode_A2() -> None: + prefs.mini_mode_mode = 2 + set_mini_mode() - def disable_test(self, track_id): - if key_shift_down or key_shiftr_down: - single_only = True - else: - single_only = False +def set_mini_mode_C1() -> None: + prefs.mini_mode_mode = 5 + set_mini_mode() - if not single_only: - for item in default_playlist: - if pctl.master_library[item].parent_folder_path == pctl.master_library[track_id].parent_folder_path: +def set_mini_mode_B2() -> None: + prefs.mini_mode_mode = 3 + set_mini_mode() - if pctl.master_library[item].is_network is True: - return True - return False +def set_mini_mode_D() -> None: + prefs.mini_mode_mode = 4 + set_mini_mode() - def render(self): +def copy_bb_metadata() -> str | None: + tr = pctl.playing_object() + if tr is None: + return None + if not tr.title and not tr.artist and pctl.playing_state == 3: + return pctl.tag_meta + text = f"{tr.artist} - {tr.title}".strip(" -") + if text: + copy_to_clipboard(text) + else: + show_message(_("No metadata available to copy")) + return None - if not self.active: - return +def stop() -> None: + pctl.stop() - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False +def random_track() -> None: + playlist = pctl.multi_playlist[pctl.active_playlist_playing].playlist_ids + if playlist: + random_position = random.randrange(0, len(playlist)) + track_id = playlist[random_position] + pctl.jump(track_id, random_position) + pctl.show_current() - w = 420 * gui.scale - h = 155 * gui.scale - x = int(window_size[0] / 2) - int(w / 2) - y = int(window_size[1] / 2) - int(h / 2) - - ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) - ddt.rect_a((x, y), (w, h), colours.box_background) - ddt.text_background_colour = colours.box_background - - if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not coll((x, y, w, h))): - rename_track_box.active = False - - r_todo = [] - - # Find matching folder tracks in playlist - if not self.single_only: - for item in default_playlist: - if pctl.master_library[item].parent_folder_path == pctl.master_library[ - self.target_track_id].parent_folder_path: - - # Close and display error if any tracks are not single local files - if pctl.master_library[item].is_network is True: - rename_track_box.active = False - show_message(_("Cannot rename"), _("One or more tracks is from a network location!"), mode="info") - if pctl.master_library[item].is_cue is True: - rename_track_box.active = False - show_message(_("This function does not support renaming CUE Sheet tracks.")) - else: - r_todo.append(item) - else: - r_todo = [self.target_track_id] +def random_album() -> None: + folders = {} + playlist = pctl.multi_playlist[pctl.active_playlist_playing].playlist_ids + if playlist: + for i, id in enumerate(playlist): + track = pctl.get_track(id) + if track.parent_folder_path not in folders: + folders[track.parent_folder_path] = (id, i) - ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Track Renaming"), colours.grey(230), 213) + key = random.choice(list(folders.keys())) + result = folders[key] + pctl.jump(*result) + pctl.show_current() - # if draw.button("Default", x + 230 * gui.scale, y + 8 * gui.scale, - if rename_files.text != prefs.rename_tracks_template and draw.button( - _("Default"), x + w - 85 * gui.scale, y + h - 35 * gui.scale, 70 * gui.scale): - rename_files.text = prefs.rename_tracks_template +def radio_random() -> None: + pctl.advance(rr=True) - # ddt.draw_text((x + 14, y + 40,), NRN + cursor, colours.grey(150), 12) - rename_files.draw(x + 14 * gui.scale, y + 39 * gui.scale, colours.box_input_text, width=300) - NRN = rename_files.text +def heart_menu_colour() -> list[int] | None: + if not (pctl.playing_state == 1 or pctl.playing_state == 2): + if colours.lm: + return [255, 150, 180, 255] + return None + if love(False): + return [245, 60, 60, 255] + if colours.lm: + return [255, 150, 180, 255] + return None - ddt.rect_s( - (x + 8 * gui.scale, y + 36 * gui.scale, 300 * gui.scale, 22 * gui.scale), colours.box_text_border, 1 * gui.scale) +def draw_rating_widget(x: int, y: int, n_track: TrackClass, album: bool = False): + if album: + rat = album_star_store.get_rating(n_track) + else: + rat = star_store.get_rating(n_track.index) - afterline = "" - warn = False - underscore = False + rect = (x - round(5 * gui.scale), y - round(4 * gui.scale), round(80 * gui.scale), round(16 * gui.scale)) + gui.heart_fields.append(rect) - for item in r_todo: + if coll(rect) and (inp.mouse_click or (is_level_zero() and not quick_drag)): + gui.pl_update = 2 + pp = mouse_position[0] - x - if pctl.master_library[item].track_number == "" or pctl.master_library[item].artist == "" or \ - pctl.master_library[item].title == "" or pctl.master_library[item].album == "": - warn = True + if pp < 5 * gui.scale: + rat = 0 + elif pp > 70 * gui.scale: + rat = 10 + else: + rat = pp // (star_row_icon.w // 2) - if item == self.target_track_id: - afterline = parse_template2(NRN, pctl.master_library[item]) + if inp.mouse_click: + rat = min(rat, 10) + if album: + album_star_store.set_rating(n_track, rat) + else: + star_store.set_rating(n_track.index, rat, write=True) - ddt.text((x + 10 * gui.scale, y + 68 * gui.scale), _("BEFORE"), colours.box_text_label, 212) - line = trunc_line(pctl.master_library[self.target_track_id].filename, 12, 335) - ddt.text((x + 70 * gui.scale, y + 68 * gui.scale), line, colours.grey(210), 211, max_w=340) + # bg = colours.grey(40) + bg = [255, 255, 255, 17] + fg = colours.grey(210) - ddt.text((x + 10 * gui.scale, y + 83 * gui.scale), _("AFTER"), colours.box_text_label, 212) - ddt.text((x + 70 * gui.scale, y + 83 * gui.scale), afterline, colours.grey(210), 211, max_w=340) + if gui.tracklist_bg_is_light: + bg = [0, 0, 0, 25] + fg = colours.grey(70) - if (len(NRN) > 3 and len(pctl.master_library[self.target_track_id].filename) > 3 and afterline[-3:].lower() != - pctl.master_library[self.target_track_id].filename[-3:].lower()) or len(NRN) < 4 or "." not in afterline[-5:]: - ddt.text( - (x + 10 * gui.scale, y + 108 * gui.scale), _("Warning: This may change the file extension"), - [245, 90, 90, 255], - 13) + playtime_stars = 0 + if prefs.rating_playtime_stars and rat == 0 and not album: + playtime_stars = star_count3(star_store.get(n_track.index), n_track.length) + if gui.tracklist_bg_is_light: + fg2 = alpha_blend([0, 0, 0, 70], ddt.text_background_colour) + else: + fg2 = alpha_blend([255, 255, 255, 50], ddt.text_background_colour) - colour_warn = [143, 186, 65, 255] - if not unique_template(NRN): - ddt.text( - (x + 10 * gui.scale, y + 123 * gui.scale), _("Warning: The filename might not be unique"), - [245, 90, 90, 255], - 13) - if warn: - ddt.text( - (x + 10 * gui.scale, y + 135 * gui.scale), _("Warning: A track has incomplete metadata"), - [245, 90, 90, 255], - 13) - colour_warn = [180, 60, 60, 255] + for ss in range(5): - label = _("Write") + " (" + str(len(r_todo)) + ")" + xx = x + ss * star_row_icon.w - if draw.button( - label, x + (8 + 300 + 10) * gui.scale, y + 36 * gui.scale, 80 * gui.scale, - text_highlight_colour=colours.grey(255), background_highlight_colour=colour_warn, - tooltip=_("Physically renames all the tracks in the folder")) or inp.level_2_enter: - - inp.mouse_click = False - total_todo = len(r_todo) - pre_state = 0 - - for item in r_todo: + if playtime_stars: + if playtime_stars - 1 < ss * 2: + star_row_icon.render(xx, y, bg) + elif playtime_stars - 1 == ss * 2: + star_row_icon.render(xx, y, bg) + star_half_row_icon.render(xx, y, fg2) + else: + star_row_icon.render(xx, y, fg2) + else: - if pctl.playing_state > 0 and item == pctl.track_queue[pctl.queue_step]: - pre_state = pctl.stop(True) + if rat - 1 < ss * 2: + star_row_icon.render(xx, y, bg) + elif rat - 1 == ss * 2: + star_row_icon.render(xx, y, bg) + star_half_row_icon.render(xx, y, fg) + else: + star_row_icon.render(xx, y, fg) - try: +def love_deco(): + if love(False): + return [colours.menu_text, colours.menu_background, _("Un-Love Track")] + if pctl.playing_state == 1 or pctl.playing_state == 2: + return [colours.menu_text, colours.menu_background, _("Love Track")] + return [colours.menu_text_disabled, colours.menu_background, _("Love Track")] - afterline = parse_template2(NRN, pctl.master_library[item], strict=True) +def bar_love(notify: bool = False) -> None: + shoot_love = threading.Thread(target=love, args=[True, None, False, notify]) + shoot_love.daemon = True + shoot_love.start() - oldname = pctl.master_library[item].filename - oldpath = pctl.master_library[item].fullpath +def bar_love_notify() -> None: + bar_love(notify=True) - logging.info("Renaming...") +def select_love(notify: bool = False) -> None: + selected = pctl.selected_in_playlist + playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + if -1 < selected < len(playlist): + track_id = playlist[selected] - star = star_store.full_get(item) - star_store.remove(item) + shoot_love = threading.Thread(target=love, args=[True, track_id, False, notify]) + shoot_love.daemon = True + shoot_love.start() - oldpath = pctl.master_library[item].fullpath +def toggle_spotify_like_active2(tr: TrackClass) -> None: + if "spotify-track-url" in tr.misc: + if "spotify-liked" in tr.misc: + tauon.spot_ctl.unlike_track(tr) + else: + tauon.spot_ctl.like_track(tr) + gui.pl_update += 1 + for i, p in enumerate(pctl.multi_playlist): + code = pctl.gen_codes.get(p.uuid_int) + if code and code.startswith("slt"): + logging.info("Fetching Spotify likes...") + regenerate_playlist(i, silent=True) + gui.pl_update += 1 - oldsplit = os.path.split(oldpath) +def toggle_spotify_like_active() -> None: + tr = pctl.playing_object() + if tr: + shoot_dl = threading.Thread(target=toggle_spotify_like_active2, args=([tr])) + shoot_dl.daemon = True + shoot_dl.start() - if os.path.exists(os.path.join(oldsplit[0], afterline)): - logging.error("A file with that name already exists") - total_todo -= 1 - continue +def toggle_spotify_like_active_deco(): + tr = pctl.playing_object() + text = _("Spotify Like Track") - if not afterline: - logging.error("Rename Error") - total_todo -= 1 - continue + if pctl.playing_state == 0 or not tr or "spotify-track-url" not in tr.misc: + return [colours.menu_text_disabled, colours.menu_background, text] + if "spotify-liked" in tr.misc: + text = _("Un-like Spotify Track") - if "." in afterline and not afterline.split(".")[0]: - logging.error("A file does not have a target filename") - total_todo -= 1 - continue + return [colours.menu_text, colours.menu_background, text] - os.rename(pctl.master_library[item].fullpath, os.path.join(oldsplit[0], afterline)) +def locate_artist() -> None: + track = pctl.playing_object() + if not track: + return - pctl.master_library[item].fullpath = os.path.join(oldsplit[0], afterline) - pctl.master_library[item].filename = afterline + artist = track.artist + if track.album_artist: + artist = track.album_artist - search_string_cache.pop(item, None) - search_dia_string_cache.pop(item, None) + block_starts = [] + current = False + for i in range(len(default_playlist)): + track = pctl.get_track(default_playlist[i]) + if current is False: + if track.artist == artist or track.album_artist == artist or ( + "artists" in track.misc and artist in track.misc["artists"]): + block_starts.append(i) + current = True + elif (track.artist != artist and track.album_artist != artist) or ( + "artists" in track.misc and artist in track.misc["artists"]): + current = False - if star is not None: - star_store.insert(item, star) + if block_starts: - except Exception: - logging.exception("Rendering error") - total_todo -= 1 + next = False + for start in block_starts: - rename_track_box.active = False - logging.info("Done") - if pre_state == 1: - pctl.revert() + if next: + pctl.selected_in_playlist = start + pctl.playlist_view_position = start + shift_selection.clear() + break - if total_todo != len(r_todo): - show_message( - _("Rename complete."), - _("{N} / {T} filenames were written.") - .format(N=str(total_todo), T=str(len(r_todo))), mode="warning") - else: - show_message( - _("Rename complete."), - _("{N} / {T} filenames were written.") - .format(N=str(total_todo), T=str(len(r_todo))), mode="done") - pctl.notify_change() + if pctl.selected_in_playlist == start: + next = True + continue + else: + pctl.selected_in_playlist = block_starts[0] + pctl.playlist_view_position = block_starts[0] + shift_selection.clear() -rename_track_box = RenameTrackBox() + tree_view_box.show_track(pctl.get_track(default_playlist[pctl.selected_in_playlist])) + else: + show_message(_("No exact matching artist could be found in this playlist")) + logging.debug("Position changed by artist locate") -class TransEditBox: + gui.pl_update += 1 - def __init__(self): - self.active = False - self.active_field = 1 - self.selected = [] - self.playlist = -1 +def activate_search_overlay() -> None: + if cm_clean_db: + show_message(_("Please wait for cleaning process to finish")) + return + search_over.active = True + search_over.delay_enter = False + search_over.search_text.selection = 0 + search_over.search_text.cursor_position = 0 + search_over.spotify_mode = False - def render(self): +def get_album_spot_url_active() -> None: + tr = pctl.playing_object() + if tr: + url = tauon.spot_ctl.get_album_url_from_local(tr) - if not self.active: - return + if url: + copy_to_clipboard(url) + show_message(_("URL copied to clipboard"), mode="done") + else: + show_message(_("No results found")) - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False +def get_album_spot_url_actove_deco(): + tr = pctl.playing_object() + text = _("Copy Album URL") + if not tr: + return [colours.menu_text_disabled, colours.menu_background, text] + if "spotify-album-url" not in tr.misc: + text = _("Lookup Spotify Album") - w = 500 * gui.scale - h = 255 * gui.scale - x = int(window_size[0] / 2) - int(w / 2) - y = int(window_size[1] / 2) - int(h / 2) - - ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) - ddt.rect_a((x, y), (w, h), colours.box_background) - ddt.text_background_colour = colours.box_background - - if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not coll((x, y, w, h))): - self.active = False - - select = list(set(shift_selection)) - if not select and pctl.selected_ready(): - select = [pctl.selected_in_playlist] - - titles = [pctl.get_track(default_playlist[s]).title for s in select] - artists = [pctl.get_track(default_playlist[s]).artist for s in select] - albums = [pctl.get_track(default_playlist[s]).album for s in select] - album_artists = [pctl.get_track(default_playlist[s]).album_artist for s in select] - - #logging.info(select) - if select != self.selected or pctl.active_playlist_viewing != self.playlist: - #logging.info("reset") - self.selected = select - self.playlist = pctl.active_playlist_viewing - edit_album.clear() - edit_artist.clear() - edit_title.clear() - edit_album_artist.clear() - - if len(select) == 0: - return + return [colours.menu_text, colours.menu_background, text] - tr = pctl.get_track(default_playlist[select[0]]) - edit_title.set_text(tr.title) +def goto_playing_extra() -> None: + pctl.show_current(highlight=True) - if check_equal(artists): - edit_artist.set_text(artists[0]) +def show_spot_playing_deco(): + if not (tauon.spot_ctl.coasting or tauon.spot_ctl.playing): + return [colours.menu_text, colours.menu_background, None] + return [colours.menu_text_disabled, colours.menu_background, None] - if check_equal(albums): - edit_album.set_text(albums[0]) +def show_spot_coasting_deco(): + if tauon.spot_ctl.coasting: + return [colours.menu_text, colours.menu_background, None] + return [colours.menu_text_disabled, colours.menu_background, None] - if check_equal(album_artists): - edit_album_artist.set_text(album_artists[0]) +def show_spot_playing() -> None: + if pctl.playing_state != 0 and pctl.playing_state != 3 and not tauon.spot_ctl.coasting and not tauon.spot_ctl.playing: + pctl.stop() + tauon.spot_ctl.update(start=True) - x += round(20 * gui.scale) - y += round(18 * gui.scale) +def spot_transfer_playback_here() -> None: + tauon.spot_ctl.preparing_spotify = True + if not (tauon.spot_ctl.playing or tauon.spot_ctl.coasting): + tauon.spot_ctl.update(start=True) + pctl.playerCommand = "spotcon" + pctl.playerCommandReady = True + pctl.playing_state = 3 + shooter(tauon.spot_ctl.transfer_to_tauon) - ddt.text((x, y), _("Simple tag editor"), colours.box_title_text, 215) +def spot_import_albums() -> None: + if not tauon.spot_ctl.spotify_com: + tauon.spot_ctl.spotify_com = True + shoot = threading.Thread(target=tauon.spot_ctl.get_library_albums) + shoot.daemon = True + shoot.start() + else: + show_message(_("Please wait until current job is finished")) - if draw.button(_("?"), x + 440 * gui.scale, y): - show_message( - _("Press Enter in each field to apply its changes to local database."), - _("When done, press WRITE TAGS to save to tags in actual files. (Optional but recommended)"), - mode="info") +def spot_import_tracks() -> None: + if not tauon.spot_ctl.spotify_com: + tauon.spot_ctl.spotify_com = True + shoot = threading.Thread(target=tauon.spot_ctl.get_library_likes) + shoot.daemon = True + shoot.start() + else: + show_message(_("Please wait until current job is finished")) - y += round(24 * gui.scale) - ddt.text((x, y), _("Number of tracks selected: {N}").format(N=len(select)), colours.box_title_text, 313) +def spot_import_playlists() -> None: + if not tauon.spot_ctl.spotify_com: + show_message(_("Importing Spotify playlists...")) + shoot_dl = threading.Thread(target=tauon.spot_ctl.import_all_playlists) + shoot_dl.daemon = True + shoot_dl.start() + else: + show_message(_("Please wait until current job is finished")) - y += round(24 * gui.scale) +def spot_import_playlist_menu() -> None: + if not tauon.spot_ctl.spotify_com: + playlists = tauon.spot_ctl.get_playlist_list() + spotify_playlist_menu.items.clear() + if playlists: + for item in playlists: + spotify_playlist_menu.add(MenuItem(item[0], tauon.spot_ctl.playlist, pass_ref=True, set_ref=item[1])) - if inp.key_tab_press: - if key_shift_down or key_shiftr_down: - self.active_field -= 1 - else: - self.active_field += 1 - - if self.active_field < 0: - self.active_field = 3 - if self.active_field == 4: - self.active_field = 0 - if len(select) > 1: - self.active_field = 1 - - def field_edit(x, y, label, field_number, names, text_box): - changed = 0 - ddt.text((x, y), label, colours.box_text_label, 11) - y += round(16 * gui.scale) - rect1 = (x, y, round(370 * gui.scale), round(17 * gui.scale)) - fields.add(rect1) - if (coll(rect1) and inp.mouse_click) or (inp.key_tab_press and self.active_field == field_number): - self.active_field = field_number - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - tc = colours.box_input_text - if names and check_equal(names) and text_box.text == names[0]: - h, l, s = rgb_to_hls(tc[0], tc[1], tc[2]) - l *= 0.7 - tc = hls_to_rgb(h, l, s) - else: - changed = 1 - if not (names and check_equal(names)) and not text_box.text: - changed = 0 - ddt.text((x + round(2 * gui.scale), y), _("<Multiple selected>"), colours.box_text_label, 12) - text_box.draw(x + round(3 * gui.scale), y, tc, self.active_field == field_number, width=370 * gui.scale) - if changed: - ddt.text((x + 377 * gui.scale, y - 1 * gui.scale), "⮨", colours.box_title_text, 214) - return changed - - changed = 0 - if len(select) == 1: - changed = field_edit(x, y, _("Track title"), 0, titles, edit_title) - y += round(40 * gui.scale) - changed += field_edit(x, y, _("Album name"), 1, albums, edit_album) - y += round(40 * gui.scale) - changed += field_edit(x, y, _("Artist name"), 2, artists, edit_artist) - y += round(40 * gui.scale) - changed += field_edit(x, y, _("Album-artist name"), 3, album_artists, edit_album_artist) - - y += round(40 * gui.scale) - for s in select: - tr = pctl.get_track(default_playlist[s]) - if tr.is_network: - ddt.text((x, y), _("Editing network tracks is not recommended!"), [245, 90, 90, 255], 312) + spotify_playlist_menu.add(MenuItem(_("> Import All Playlists"), spot_import_playlists)) + spotify_playlist_menu.activate(position=(extra_menu.pos[0], window_size[1] - gui.panelBY)) + else: + show_message(_("Please wait until current job is finished")) - if inp.key_return_press: +def spot_import_context() -> None: + shooter(tauon.spot_ctl.import_context) - gui.pl_update += 1 - if self.active_field == 0 and len(select) == 1: - for s in select: - tr = pctl.get_track(default_playlist[s]) - star = star_store.full_get(tr.index) - star_store.remove(tr.index) - tr.title = edit_title.text - star_store.merge(tr.index, star) - - if self.active_field == 1: - for s in select: - tr = pctl.get_track(default_playlist[s]) - tr.album = edit_album.text - if self.active_field == 2: - for s in select: - tr = pctl.get_track(default_playlist[s]) - star = star_store.full_get(tr.index) - star_store.remove(tr.index) - tr.artist = edit_artist.text - star_store.merge(tr.index, star) - if self.active_field == 3: - for s in select: - tr = pctl.get_track(default_playlist[s]) - tr.album_artist = edit_album_artist.text - tauon.bg_save() - - - ww = ddt.get_text_w(_("WRITE TAGS"), 212) + round(48 * gui.scale) - if gui.write_tag_in_progress: - text = f"{gui.tag_write_count}/{len(select)}" - text = _("WRITE TAGS") - if draw.button(text, (x + w) - ww, y - round(0) * gui.scale): - if changed: - show_message(_("Press enter on fields to apply your changes first!")) - return +def get_album_spot_deco(): + tr = pctl.playing_object() + text = _("Show Full Album") + if not tr: + return [colours.menu_text_disabled, colours.menu_background, text] + if "spotify-album-url" not in tr.misc: + text = _("Lookup Spotify Album") + return [colours.menu_text, colours.menu_background, text] - if gui.write_tag_in_progress: - return +def get_artist_spot(tr: TrackClass = None) -> None: + if not tr: + tr = pctl.playing_object() + if not tr: + return + url = tauon.spot_ctl.get_artist_url_from_local(tr) + if not url: + show_message(_("No results found")) + return + show_message(_("Fetching...")) + shooter(tauon.spot_ctl.artist_playlist, (url,)) - def write_tag_go(): +# def spot_transfer_playback_here_deco(): +# tr = pctl.playing_state == 3: +# text = _("Show Full Album") +# if not tr: +# return [colours.menu_text_disabled, colours.menu_background, text] +# if not "spotify-album-url" in tr.misc: +# text = _("Lookup Spotify Album") +# +# return [colours.menu_text, colours.menu_background, text] +def toggle_auto_theme(mode: int = 0) -> None: + if mode == 1: + return prefs.colour_from_image - for s in select: - tr = pctl.get_track(default_playlist[s]) + prefs.colour_from_image ^= True + gui.theme_temp_current = -1 - if tr.is_network: - show_message(_("Writing to a network track is not applicable!"), mode="error") - gui.write_tag_in_progress = True - return - if tr.is_cue: - show_message(_("Cannot write CUE sheet types!"), mode="error") - gui.write_tag_in_progress = True - return + gui.reload_theme = True - muta = mutagen.File(tr.fullpath, easy=True) + # if prefs.colour_from_image and prefs.art_bg and not key_shift_down: + # toggle_auto_bg() - def write_tag(track: TrackClass, muta, field_name_tauon, field_name_muta): - item = muta.get(field_name_muta) - if item and len(item) > 1: - show_message(_("Cannot handle multi-field! Please use external tag editor"), mode="error") - return 0 - if not getattr(tr, field_name_tauon): # Want delete tag field - if item: - del muta[field_name_muta] - else: - muta[field_name_muta] = getattr(tr, field_name_tauon) - return 1 +def toggle_auto_bg(mode: int= 0) -> bool | None: + if mode == 1: + return prefs.art_bg + prefs.art_bg ^= True - write_tag(tr, muta, "artist", "artist") - write_tag(tr, muta, "album", "album") - write_tag(tr, muta, "title", "title") - write_tag(tr, muta, "album_artist", "albumartist") - - muta.save() - gui.tag_write_count += 1 - gui.update += 1 - tauon.bg_save() - if not gui.message_box: - show_message(_("{N} files rewritten").format(N=gui.tag_write_count), mode="done") - gui.write_tag_in_progress = False - if not gui.write_tag_in_progress: - gui.tag_write_count = 0 - gui.write_tag_in_progress = True - shooter(write_tag_go) - -trans_edit_box = TransEditBox() + if prefs.art_bg: + gui.update = 60 + style_overlay.flush() + tauon.thread_manager.ready("style") + # if prefs.colour_from_image and prefs.art_bg and not key_shift_down: + # toggle_auto_theme() + return None -class SubLyricsBox: +def toggle_auto_bg_strong(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.art_bg_stronger == 2 - def __init__(self): + if prefs.art_bg_stronger == 2: + prefs.art_bg_stronger = 1 + else: + prefs.art_bg_stronger = 2 + gui.update_layout() + return None - self.active = False - self.target_track = None - self.active_field = 1 +def toggle_auto_bg_strong1(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.art_bg_stronger == 1 + prefs.art_bg_stronger = 1 + gui.update_layout() + return None - def activate(self, track: TrackClass): +def toggle_auto_bg_strong2(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.art_bg_stronger == 2 + prefs.art_bg_stronger = 2 + gui.update_layout() + if prefs.art_bg: + gui.update = 60 + return None - self.active = True - gui.box_over = True - self.target_track = track +def toggle_auto_bg_strong3(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.art_bg_stronger == 3 + prefs.art_bg_stronger = 3 + gui.update_layout() + if prefs.art_bg: + gui.update = 60 + return None - sub_lyrics_a.text = prefs.lyrics_subs.get(self.target_track.artist, "") - sub_lyrics_b.text = prefs.lyrics_subs.get(self.target_track.title, "") +def toggle_auto_bg_blur(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.art_bg_always_blur + prefs.art_bg_always_blur ^= True + style_overlay.flush() + tauon.thread_manager.ready("style") + return None - if not sub_lyrics_a.text: - sub_lyrics_a.text = self.target_track.artist - if not sub_lyrics_b.text: - sub_lyrics_b.text = self.target_track.title +def toggle_auto_bg_showcase(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.bg_showcase_only + prefs.bg_showcase_only ^= True + gui.update_layout() + return None - def render(self): +def toggle_notifications(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.show_notifications - if not self.active: - return + prefs.show_notifications ^= True - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False + if prefs.show_notifications: + if not de_notify_support: + show_message(_("Notifications for this DE not supported"), "", mode="warning") + return None - w = 400 * gui.scale - h = 155 * gui.scale - x = int(window_size[0] / 2) - int(w / 2) - y = int(window_size[1] / 2) - int(h / 2) - - ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) - ddt.rect_a((x, y), (w, h), colours.box_background) - ddt.text_background_colour = colours.box_background - - if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not coll((x, y, w, h))): - self.active = False - gui.box_over = False - - if sub_lyrics_a.text and sub_lyrics_a.text != self.target_track.artist: - prefs.lyrics_subs[self.target_track.artist] = sub_lyrics_a.text - elif self.target_track.artist in prefs.lyrics_subs: - del prefs.lyrics_subs[self.target_track.artist] - - if sub_lyrics_b.text and sub_lyrics_b.text != self.target_track.title: - prefs.lyrics_subs[self.target_track.title] = sub_lyrics_b.text - elif self.target_track.title in prefs.lyrics_subs: - del prefs.lyrics_subs[self.target_track.title] - - ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Substitute Lyric Search"), colours.grey(230), 213) - - y += round(35 * gui.scale) - x += round(23 * gui.scale) - - xx = x - xx += ddt.text( - (x + round(0 * gui.scale), y + round(0 * gui.scale)), _("Substitute"), colours.box_text_label, 212) - xx += round(6 * gui.scale) - ddt.text((xx, y + round(0 * gui.scale)), self.target_track.artist, colours.box_sub_text, 312) - - y += round(19 * gui.scale) - xx = x - xx += ddt.text((xx + round(0 * gui.scale), y + round(0 * gui.scale)), _("with"), colours.box_text_label, 212) - xx += round(6 * gui.scale) - rect1 = (xx, y, round(250 * gui.scale), round(17 * gui.scale)) - fields.add(rect1) - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - if (coll(rect1) and inp.mouse_click) or (inp.key_tab_press and self.active_field == 2): - self.active_field = 1 - inp.key_tab_press = False +# def toggle_al_pref_album_artist(mode: int = 0) -> bool: +# +# if mode == 1: +# return prefs.artist_list_prefer_album_artist +# +# prefs.artist_list_prefer_album_artist ^= True +# artist_list_box.saves.clear() +# return None - sub_lyrics_a.draw( - xx + round(4 * gui.scale), y, colours.box_input_text, self.active_field == 1, - width=rect1[2] - 8 * gui.scale) +def toggle_mini_lyrics(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.show_lyrics_side + prefs.show_lyrics_side ^= True + return None - y += round(28 * gui.scale) +def toggle_showcase_vis(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.showcase_vis - xx = x - xx += ddt.text( - (x + round(0 * gui.scale), y + round(0 * gui.scale)), _("Substitute"), colours.box_text_label, 212) - xx += round(6 * gui.scale) - ddt.text((xx, y + round(0 * gui.scale)), self.target_track.title, colours.box_sub_text, 312) + prefs.showcase_vis ^= True + gui.update_layout() + return None - y += round(19 * gui.scale) - xx = x - xx += ddt.text((xx + round(0 * gui.scale), y + round(0 * gui.scale)), _("with"), colours.box_text_label, 212) - xx += round(6 * gui.scale) - rect1 = (xx, y, round(250 * gui.scale), round(16 * gui.scale)) - fields.add(rect1) - if (coll(rect1) and inp.mouse_click) or (inp.key_tab_press and self.active_field == 1): - self.active_field = 2 - # ddt.rect(rect1, [40, 40, 40, 255], True) - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - sub_lyrics_b.draw( - xx + round(4 * gui.scale), y, colours.box_input_text, self.active_field == 2, width=rect1[2] - 8 * gui.scale) +def toggle_level_meter(mode: int = 0) -> bool | None: + if mode == 1: + return gui.vis_want != 0 + if gui.vis_want == 0: + gui.vis_want = 1 + else: + gui.vis_want = 0 -sub_lyrics_box = SubLyricsBox() + gui.update_layout() + return None +# def toggle_force_subpixel(mode: int = 0) -> bool | None: +# +# if mode == 1: +# return prefs.force_subpixel_text != 0 +# +# prefs.force_subpixel_text ^= True +# ddt.force_subpixel_text = prefs.force_subpixel_text +# ddt.clear_text_cache() -class ExportPlaylistBox: +def level_meter_special_2(): + gui.level_meter_colour_mode = 2 - def __init__(self): +def last_fm_menu_deco(): + if prefs.scrobble_hold: + if not prefs.auto_lfm and lb.enable: + line = _("ListenBrainz is Paused") + else: + line = _("Scrobbling is Paused") + bg = colours.menu_background + else: + if not prefs.auto_lfm and lb.enable: + line = _("ListenBrainz is Active") + else: + line = _("Scrobbling is Active") - self.active = False - self.id = None - self.directory_text_box = TextBox2() - self.default = { - "path": str(music_directory) if music_directory else str(user_directory / "playlists"), - "type": "xspf", - "relative": False, - "auto": False, - } + bg = colours.menu_background - def activate(self, playlist): + return [colours.menu_text, bg, line] - self.active = True - gui.box_over = True - self.id = pl_to_id(playlist) +def lastfm_colour() -> list[int] | None: + if not prefs.scrobble_hold: + return [250, 50, 50, 255] + return None - # Prune old enteries - ids = [] - for playlist in pctl.multi_playlist: - ids.append(playlist.uuid_int) - for key in list(prefs.playlist_exports.keys()): - if key not in ids: - del prefs.playlist_exports[key] +def lastfm_menu_test(a) -> bool: + if (prefs.auto_lfm and prefs.last_fm_token is not None) or prefs.enable_lb or prefs.maloja_enable: + return True + return False - def render(self) -> None: - if not self.active: - return +def lb_mode() -> bool: + return prefs.enable_lb - w = 500 * gui.scale - h = 220 * gui.scale - x = int(window_size[0] / 2) - int(w / 2) - y = int(window_size[1] / 2) - int(h / 2) - - ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) - ddt.rect_a((x, y), (w, h), colours.box_background) - ddt.text_background_colour = colours.box_background - - if key_esc_press or ((inp.mouse_click or gui.level_2_click or right_click or level_2_right_click) and not coll( - (x, y, w, h))): - self.active = False - gui.box_over = False - - current = prefs.playlist_exports.get(self.id) - if not current: - current = copy.copy(self.default) - - ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Export Playlist"), colours.grey(230), 213) - - x += round(15 * gui.scale) - y += round(25 * gui.scale) - - ddt.text((x, y + 8 * gui.scale), _("Save directory"), colours.grey(230), 11) - y += round(30 * gui.scale) - - rect1 = (x, y, round(450 * gui.scale), round(16 * gui.scale)) - fields.add(rect1) - # ddt.rect(rect1, [40, 40, 40, 255], True) - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - self.directory_text_box.text = current["path"] - self.directory_text_box.draw( - x + round(4 * gui.scale), y, colours.box_input_text, True, - width=rect1[2] - 8 * gui.scale, click=gui.level_2_click) - current["path"] = self.directory_text_box.text - - y += round(30 * gui.scale) - if pref_box.toggle_square(x, y, current["type"] == "xspf", "XSPF", gui.level_2_click): - current["type"] = "xspf" - if pref_box.toggle_square(x + round(80 * gui.scale), y, current["type"] == "m3u", "M3U", gui.level_2_click): - current["type"] = "m3u" - # pref_box.toggle_square(x + round(160 * gui.scale), y, False, "PLS", gui.level_2_click) - y += round(35 * gui.scale) - current["relative"] = pref_box.toggle_square( - x, y, current["relative"], _("Use relative paths"), - gui.level_2_click) - y += round(60 * gui.scale) - current["auto"] = pref_box.toggle_square(x, y, current["auto"], _("Auto-export"), gui.level_2_click) - - y += round(0 * gui.scale) - ww = ddt.get_text_w(_("Export"), 211) - x = ((int(window_size[0] / 2) - int(w / 2)) + w) - (ww + round(40 * gui.scale)) - - prefs.playlist_exports[self.id] = current - - if draw.button(_("Export"), x, y, press=gui.level_2_click): - self.run_export(current, self.id, warnings=True) - - def run_export(self, current, id, warnings=True) -> None: - logging.info("Export playlist") - path = current["path"] - if not os.path.isdir(path): - if warnings: - show_message(_("Directory does not exist"), mode="warning") - return - target = "" - if current["type"] == "xspf": - target = export_xspf(id_to_pl(id), direc=path, relative=current["relative"], show=False) - if current["type"] == "m3u": - target = export_m3u(id_to_pl(id), direc=path, relative=current["relative"], show=False) +def get_album_art_url(tr: TrackClass): + artist = tr.album_artist + if not tr.album: + return None + if not artist: + artist = tr.artist + if not artist: + return None - if warnings and target != 1: - show_message(_("Playlist exported"), target, mode="done") + release_id = None + release_group_id = None + if (artist, tr.album) in pctl.album_mbid_release_cache or (artist, tr.album) in pctl.album_mbid_release_group_cache: + release_id = pctl.album_mbid_release_cache[(artist, tr.album)] + release_group_id = pctl.album_mbid_release_group_cache[(artist, tr.album)] + if release_id is None and release_group_id is None: + return None + if not release_group_id: + release_group_id = tr.misc.get("musicbrainz_releasegroupid") -export_playlist_box = ExportPlaylistBox() + if not release_id: + release_id = tr.misc.get("musicbrainz_albumid") + if not release_group_id: + try: + #logging.info("lookup release group id") + s = musicbrainzngs.search_release_groups(tr.album, artist=artist, limit=1) + release_group_id = s["release-group-list"][0]["id"] + tr.misc["musicbrainz_releasegroupid"] = release_group_id + #logging.info("got release group id") + except Exception: + logging.exception("Error lookup mbid for discord") + pctl.album_mbid_release_group_cache[(artist, tr.album)] = None -def toggle_repeat() -> None: - gui.update += 1 - pctl.repeat_mode ^= True - if pctl.mpris is not None: - pctl.mpris.update_loop() + if not release_id: + try: + #logging.info("lookup release id") + s = musicbrainzngs.search_releases(tr.album, artist=artist, limit=1) + release_id = s["release-list"][0]["id"] + tr.misc["musicbrainz_albumid"] = release_id + #logging.info("got release group id") + except Exception: + logging.exception("Error lookup mbid for discord") + pctl.album_mbid_release_cache[(artist, tr.album)] = None + image_data = None + final_id = None + if release_group_id: + url = pctl.mbid_image_url_cache.get(release_group_id) + if url: + return url -tauon.toggle_repeat = toggle_repeat + base_url = "https://coverartarchive.org/release-group/" + url = f"{base_url}{release_group_id}" + try: + #logging.info("lookup image url from release group") + response = requests.get(url, timeout=10) + response.raise_for_status() + image_data = response.json() + final_id = release_group_id + except (requests.RequestException, ValueError): + logging.exception("No image found for release group") + pctl.album_mbid_release_group_cache[(artist, tr.album)] = None + except Exception: + logging.exception("Unknown error finding image for release group") -def menu_repeat_off() -> None: - pctl.repeat_mode = False - pctl.album_repeat_mode = False - if pctl.mpris is not None: - pctl.mpris.update_loop() + if release_id and not image_data: + url = pctl.mbid_image_url_cache.get(release_id) + if url: + return url + base_url = "https://coverartarchive.org/release/" + url = f"{base_url}{release_id}" -def menu_set_repeat() -> None: - pctl.repeat_mode = True - pctl.album_repeat_mode = False - if pctl.mpris is not None: - pctl.mpris.update_loop() + try: + #logging.print("lookup image url from album id") + response = requests.get(url, timeout=10) + response.raise_for_status() + image_data = response.json() + final_id = release_id + except (requests.RequestException, ValueError): + logging.exception("No image found for album id") + pctl.album_mbid_release_cache[(artist, tr.album)] = None + except Exception: + logging.exception("Unknown error getting image found for album id") + if image_data: + for image in image_data["images"]: + if image.get("front") and ("250" in image["thumbnails"] or "small" in image["thumbnails"]): + pctl.album_mbid_release_cache[(artist, tr.album)] = release_id + pctl.album_mbid_release_group_cache[(artist, tr.album)] = release_group_id -def menu_album_repeat() -> None: - pctl.repeat_mode = True - pctl.album_repeat_mode = True - if pctl.mpris is not None: - pctl.mpris.update_loop() + url = image["thumbnails"].get("250") + if url is None: + url = image["thumbnails"].get("small") + if url: + logging.info("got mb image url for discord") + pctl.mbid_image_url_cache[final_id] = url + return url -tauon.menu_album_repeat = menu_album_repeat -tauon.menu_repeat_off = menu_repeat_off -tauon.menu_set_repeat = menu_set_repeat + pctl.album_mbid_release_cache[(artist, tr.album)] = None + pctl.album_mbid_release_group_cache[(artist, tr.album)] = None -repeat_menu.add(MenuItem(_("Repeat OFF"), menu_repeat_off)) -repeat_menu.add(MenuItem(_("Repeat Track"), menu_set_repeat)) -repeat_menu.add(MenuItem(_("Repeat Album"), menu_album_repeat)) + return None +def discord_loop() -> None: + prefs.discord_active = True -def toggle_random(): - gui.update += 1 - pctl.random_mode ^= True - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + try: + if not pctl.playing_ready(): + return + asyncio.set_event_loop(asyncio.new_event_loop()) + # logging.info("Attempting to connect to Discord...") + client_id = "954253873160286278" + RPC = Presence(client_id) + RPC.connect() -tauon.toggle_random = toggle_random + logging.info("Discord RPC connection successful.") + time.sleep(1) + start_time = time.time() + idle_time = Timer() + state = 0 + index = -1 + br = False + gui.discord_status = "Connected" + gui.update += 1 + current_state = 0 -def toggle_random_on(): - pctl.random_mode = True - if pctl.mpris is not None: - pctl.mpris.update_shuffle() - + while True: + while True: -def toggle_random_off(): - pctl.random_mode = False - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + current_index = pctl.playing_object().index + if pctl.playing_state == 3: + current_index = radiobox.song_key + if current_state == 0 and pctl.playing_state in (1, 3): + current_state = 1 + elif current_state == 1 and pctl.playing_state not in (1, 3): + current_state = 0 + idle_time.set() -def menu_shuffle_off(): - pctl.random_mode = False - pctl.album_shuffle_mode = False - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + if state != current_state or index != current_index: + if pctl.a_time > 4 or current_state != 1: + state = current_state + index = current_index + start_time = time.time() - pctl.playing_time + break -def menu_set_random(): - pctl.random_mode = True - pctl.album_shuffle_mode = False - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + if current_state == 0 and idle_time.get() > 13: + logging.info("Pause discord RPC...") + gui.discord_status = "Idle" + RPC.clear(pid) + # RPC.close() + while True: + if prefs.disconnect_discord: + break + if pctl.playing_state == 1: + logging.info("Reconnect discord...") + RPC.connect() + gui.discord_status = "Connected" + break + time.sleep(2) -def menu_album_random(): - pctl.random_mode = True - pctl.album_shuffle_mode = True - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + if not prefs.disconnect_discord: + continue + time.sleep(2) -def toggle_shuffle_layout(albums=False): - prefs.shuffle_lock ^= True - if prefs.shuffle_lock: + if prefs.disconnect_discord: + RPC.clear(pid) + RPC.close() + prefs.disconnect_discord = False + gui.discord_status = "Not connected" + br = True + break - gui.shuffle_was_showcase = gui.showcase_mode - gui.shuffle_was_random = pctl.random_mode - gui.shuffle_was_repeat = pctl.repeat_mode + if br: + break - if not gui.combo_mode: - view_box.lyrics(hit=True) - pctl.random_mode = True - pctl.repeat_mode = False - if albums: - prefs.album_shuffle_lock_mode = True - if pctl.playing_state == 0: - pctl.advance() - else: - pctl.random_mode = gui.shuffle_was_random - pctl.repeat_mode = gui.shuffle_was_repeat - prefs.album_shuffle_lock_mode = False - if not gui.shuffle_was_showcase: - exit_combo() + title = _("Unknown Track") + tr = pctl.playing_object() + if tr.artist != "" and tr.title != "": + title = tr.title + " | " + tr.artist + if len(title) > 150: + title = _("Unknown Track") + if tr.album: + album = tr.album + else: + album = _("Unknown Album") + if pctl.playing_state == 3: + album = radiobox.loaded_station["title"] -def toggle_shuffle_layout_albums(): - toggle_shuffle_layout(albums=True) + if len(album) == 1: + album += " " + if state == 1: + #logging.info("PLAYING: " + title) + #logging.info(start_time) + url = get_album_art_url(pctl.playing_object()) -def exit_shuffle_layout(_): - return prefs.shuffle_lock + large_image = "tauon-standard" + small_image = None + if url: + large_image = url + small_image = "tauon-standard" + RPC.update( + pid=pid, + state=album, + details=title, + start=int(start_time), + large_image=large_image, + small_image=small_image) + else: + #logging.info("Discord RPC - Stop") + RPC.update( + pid=pid, + state="Idle", + large_image="tauon-standard") -shuffle_menu.add(MenuItem(_("Shuffle Lockdown"), toggle_shuffle_layout)) -shuffle_menu.add(MenuItem(_("Shuffle Lockdown Albums"), toggle_shuffle_layout_albums)) -shuffle_menu.br() -shuffle_menu.add(MenuItem(_("Shuffle OFF"), menu_shuffle_off)) -shuffle_menu.add(MenuItem(_("Shuffle Tracks"), menu_set_random)) -shuffle_menu.add(MenuItem(_("Random Albums"), menu_album_random)) + time.sleep(5) + if prefs.disconnect_discord: + RPC.clear(pid) + RPC.close() + prefs.disconnect_discord = False + break -def bio_set_large(): - # if window_size[0] >= round(1000 * gui.scale): - # gui.artist_panel_height = 320 * gui.scale - prefs.bio_large = True - if gui.artist_info_panel: - artist_info_box.get_data(artist_info_box.artist_on) + except Exception: + logging.exception("Error connecting to Discord - is Discord running?") + # show_message(_("Error connecting to Discord", mode='error') + gui.discord_status = _("Error - Discord not running?") + prefs.disconnect_discord = False + finally: + loop = asyncio.get_event_loop() + if not loop.is_closed(): + loop.close() + prefs.discord_active = False -def bio_set_small(): - # gui.artist_panel_height = 200 * gui.scale - prefs.bio_large = False - update_layout_do() - if gui.artist_info_panel: - artist_info_box.get_data(artist_info_box.artist_on) +def hit_discord() -> None: + if prefs.discord_enable and prefs.discord_allow and not prefs.discord_active: + discord_t = threading.Thread(target=discord_loop) + discord_t.daemon = True + discord_t.start() +def open_donate_link() -> None: + webbrowser.open("https://github.com/sponsors/Taiko2k", new=2, autoraise=True) -def artist_info_panel_close(): - gui.artist_info_panel ^= True - gui.update_layout() +def stop_quick_add() -> None: + pctl.quick_add_target = None +def show_stop_quick_add(_) -> bool: + return pctl.quick_add_target is not None -def toggle_bio_size_deco(): - line = _("Make Large Size") - if prefs.bio_large: - line = _("Make Compact Size") +def view_tracks() -> None: + # if gui.show_playlist is False: + # gui.show_playlist = True + if album_mode: + toggle_album_mode() + if gui.combo_mode: + exit_combo() + if gui.rsp: + toggle_side_panel() - return [colours.menu_text, colours.menu_background, line] +# def view_standard_full(): +# # if gui.show_playlist is False: +# # gui.show_playlist = True +# +# if album_mode: +# toggle_album_mode() +# if gui.combo_mode: +# toggle_combo_view(off=True) +# if not gui.rsp: +# toggle_side_panel() +# global update_layout +# update_layout = True +# gui.rspw = window_size[0] +def view_standard_meta() -> None: + # if gui.show_playlist is False: + # gui.show_playlist = True + if album_mode: + toggle_album_mode() -def toggle_bio_size(): - if prefs.bio_large: - prefs.bio_large = False - update_layout_do() - # bio_set_small() + if gui.combo_mode: + exit_combo() - else: - prefs.bio_large = True - update_layout_do() - # bio_set_large() - # gui.update_layout() + if not gui.rsp: + toggle_side_panel() + global update_layout + update_layout = True + # gui.rspw = 80 + int(window_size[0] * 0.18) -def flush_artist_bio(artist): - if os.path.isfile(os.path.join(a_cache_dir, artist + "-lfm.txt")): - os.remove(os.path.join(a_cache_dir, artist + "-lfm.txt")) - artist_info_box.text = "" - artist_info_box.artist_on = None +def view_standard() -> None: + # if gui.show_playlist is False: + # gui.show_playlist = True + if album_mode: + toggle_album_mode() + if gui.combo_mode: + exit_combo() + if not gui.rsp: + toggle_side_panel() +def standard_view_deco(): + if album_mode or gui.combo_mode or not gui.rsp: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled + return [line_colour, colours.menu_background, None] -def test_shift(_): - return key_shift_down or key_shiftr_down +# def gallery_only_view(): +# if gui.show_playlist is False: +# return +# if not album_mode: +# toggle_album_mode() +# gui.show_playlist = False +# global album_playlist_width +# global update_layout +# update_layout = True +# gui.rspw = window_size[0] +# album_playlist_width = gui.playlist_width +# #gui.playlist_width = -19 +def toggle_library_mode() -> None: + if gui.set_mode: + gui.set_mode = False + # gui.set_bar = False + else: + gui.set_mode = True + # gui.set_bar = True + gui.update_layout() -def test_artist_dl(_): - return not prefs.auto_dl_artist_data +def library_deco(): + tc = colours.menu_text + if gui.combo_mode or (gui.show_playlist is False and album_mode): + tc = colours.menu_text_disabled + if gui.set_mode: + return [tc, colours.menu_background, _("Disable Columns")] + return [tc, colours.menu_background, _("Enable Columns")] -artist_info_menu.add(MenuItem(_("Close Panel"), artist_info_panel_close)) -artist_info_menu.add(MenuItem(_("Make Large"), toggle_bio_size, toggle_bio_size_deco)) +def break_deco(): + tex = colours.menu_text + if gui.combo_mode or (gui.show_playlist is False and album_mode): + tex = colours.menu_text_disabled + if not break_enable: + tex = colours.menu_text_disabled + if not pctl.multi_playlist[pctl.active_playlist_viewing].hide_title: + return [tex, colours.menu_background, _("Disable Title Breaks")] + return [tex, colours.menu_background, _("Enable Title Breaks")] -def show_in_playlist(): - if album_mode and window_size[0] < 750 * gui.scale: - toggle_album_mode() +def toggle_playlist_break() -> None: + pctl.multi_playlist[pctl.active_playlist_viewing].hide_title ^= 1 + gui.pl_update = 1 - pctl.playlist_view_position = pctl.selected_in_playlist - logging.debug("Position changed by show in playlist") - shift_selection.clear() - shift_selection.append(pctl.selected_in_playlist) - pctl.render_playlist() +def transcode_single(item: list[tuple[int, str]], manual_directory: str | None = None, manual_name: str | None = None): + global core_use + global dl_use + if manual_directory != None: + codec = "opus" + output = manual_directory + track = item + core_use += 1 + bitrate = 48 + else: + track = item[0] + codec = prefs.transcode_codec + output = prefs.encoder_output / item[1] + bitrate = prefs.transcode_bitrate -filter_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "filter.png", True)) -filter_icon.colour = [43, 213, 255, 255] -filter_icon.xoff = 1 + t = pctl.master_library[track] -folder_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "folder.png", True)) -info_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "info.png", True)) + path = t.fullpath + cleanup = False -folder_icon.colour = [244, 220, 66, 255] -info_icon.colour = [61, 247, 163, 255] + if t.is_network: + while dl_use > 1: + time.sleep(0.2) + dl_use += 1 + try: + url, params = pctl.get_url(t) + assert url + path = os.path.join(tmp_cache_dir(), str(t.index)) + if os.path.exists(path): + os.remove(path) + logging.info("Downloading file...") + with requests.get(url, params=params, timeout=60) as response, open(path, "wb") as out_file: + out_file.write(response.content) + logging.info("Download complete") + cleanup = True + except Exception: + logging.exception("Error downloading file") + dl_use -= 1 -power_bar_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "power.png", True) + if not os.path.isfile(path): + show_message(_("Encoding warning: Missing one or more files")) + core_use -= 1 + return + out_line = encode_track_name(t) -def open_folder_stem(path): - if system == "Windows" or msys: - line = r'explorer /select,"%s"' % ( - path.replace("/", "\\")) - subprocess.Popen(line) - else: - line = path - line += "/" - if macos: - subprocess.Popen(["open", line]) - else: - subprocess.Popen(["xdg-open", line]) + if not (output / _("output")).exists(): + (output / _("output")).mkdir() + target_out = str(output / _("output") / (str(track) + "." + codec)) + command = tauon.get_ffmpeg() + " " -def open_folder_disable_test(index: int): - track = pctl.master_library[index] - return track.is_network and not os.path.isdir(track.parent_folder_path) + if not t.is_cue: + command += '-i "' + else: + command += "-ss " + str(t.start_time) + command += " -t " + str(t.length) -def open_folder(index: int): - track = pctl.master_library[index] - if open_folder_disable_test(index): - show_message(_("Can't open folder of a network track.")) - return + command += ' -i "' - if system == "Windows" or msys: - line = r'explorer /select,"%s"' % ( - track.fullpath.replace("/", "\\")) - subprocess.Popen(line) - else: - line = track.parent_folder_path - line += "/" - if macos: - line = track.fullpath - subprocess.Popen(["open", "-R", line]) - else: - subprocess.Popen(["xdg-open", line]) + command += path.replace('"', '\\"') + command += '" ' + if pctl.master_library[track].is_cue: + if t.title != "": + command += '-metadata title="' + t.title.replace('"', "").replace("'", "") + '" ' + if t.artist != "": + command += '-metadata artist="' + t.artist.replace('"', "").replace("'", "") + '" ' + if t.album != "": + command += '-metadata album="' + t.album.replace('"', "").replace("'", "") + '" ' + if t.track_number != "": + command += '-metadata track="' + str(t.track_number).replace('"', "").replace("'", "") + '" ' + if t.date != "": + command += '-metadata year="' + str(t.date).replace('"', "").replace("'", "") + '" ' -def tag_to_new_playlist(tag_item): - path_stem_to_playlist(tag_item.path, tag_item.name) + if codec != "flac": + command += " -b:a " + str(bitrate) + "k -vn " + command += '"' + target_out.replace('"', '\\"') + '"' -def folder_to_new_playlist_by_track_id(track_id: int) -> None: - track = pctl.get_track(track_id) - path_stem_to_playlist(track.parent_folder_path, track.parent_folder_name) + # logging.info(shlex.split(command)) + startupinfo = None + if system == "Windows" or msys: + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + if not msys: + command = shlex.split(command) -def stem_to_new_playlist(path: str) -> None: - path_stem_to_playlist(path, os.path.basename(path)) + subprocess.call(command, stdout=subprocess.PIPE, shell=False, startupinfo=startupinfo) + logging.info("FFmpeg finished") + if codec == "opus" and prefs.transcode_opus_as: + codec = "ogg" -move_jobs = [] -move_in_progress = False + # logging.info(target_out) + if manual_name is None: + final_out = output / (out_line + "." + codec) + final_name = out_line + "." + codec + os.rename(target_out, final_out) + else: + final_out = output / (manual_name + "." + codec) + final_name = manual_name + "." + codec + os.rename(target_out, final_out) -def move_playing_folder_to_tree_stem(path: str) -> None: - move_playing_folder_to_stem(path, pl_id=tree_view_box.get_pl_id()) + if prefs.transcode_inplace and not t.is_network and not t.is_cue: + logging.info("MOVE AND REPLACE!") + if os.path.isfile(final_out) and os.path.getsize(final_out) > 1000: + new_name = os.path.join(t.parent_folder_path, final_name) + logging.info(new_name) + shutil.move(final_out, new_name) + old_key = star_store.key(track) + old_star = star_store.full_get(track) -def move_playing_folder_to_stem(path: str, pl_id: int | None = None) -> None: - if not pl_id: - pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + try: + send2trash(pctl.master_library[track].fullpath) + except Exception: + logging.exception("File trash error") - track = pctl.playing_object() + if os.path.isfile(pctl.master_library[track].fullpath): + try: + os.remove(pctl.master_library[track].fullpath) + except Exception: + logging.exception("File delete error") - if not track or pctl.playing_state == 0: - show_message(_("No item is currently playing")) - return + pctl.master_library[track].fullpath = new_name + pctl.master_library[track].file_ext = codec.upper() - move_folder = track.parent_folder_path + # Update and merge playtimes + new_key = star_store.key(track) + if old_star and (new_key != old_key): - # Stop playing track if its in the current folder - if pctl.playing_state > 0: - if move_folder in pctl.playing_object().parent_folder_path: - pctl.stop(True) + new_star = star_store.full_get(track) + if new_star is None: + new_star = star_store.new_object() - target_base = path + new_star[0] += old_star[0] + if old_star[2] > 0 and new_star[2] == 0: + new_star[2] = old_star[2] + new_star[1] = "".join(set(new_star[1] + old_star[1])) - # Determine name for artist folder - artist = track.artist - if track.album_artist: - artist = track.album_artist + if old_key in star_store.db: + del star_store.db[old_key] - # Make filename friendly - artist = filename_safe(artist) - if not artist: - artist = "unknown artist" + star_store.db[new_key] = new_star - # Sanity checks - if track.is_network: - show_message(_("This track is a networked track."), mode="error") - return + gui.transcoding_bach_done += 1 + if cleanup: + os.remove(path) + core_use -= 1 + gui.update += 1 - if not os.path.isdir(move_folder): - show_message(_("The source folder does not exist."), mode="error") - return +def cue_scan(content: str, tn: TrackClass) -> int | None: + # Get length from backend - if not os.path.isdir(target_base): - show_message(_("The destination folder does not exist."), mode="error") - return + lasttime = tn.length - if os.path.normpath(target_base) == os.path.normpath(move_folder): - show_message(_("The destination and source folders are the same."), mode="error") - return + content = content.replace("\r", "") + content = content.split("\n") - if len(target_base) < 4: - show_message(_("Safety interupt! The source path seems oddly short."), target_base, mode="error") - return + #logging.info(content) - protect = ("", "Documents", "Music", "Desktop", "Downloads") - for fo in protect: - if move_folder.strip("\\/") == os.path.join(os.path.expanduser("~"), fo).strip("\\/"): - show_message( - _("Better not do anything to that folder!"), os.path.join(os.path.expanduser("~"), fo), - mode="warning") - return + global added - if directory_size(move_folder) > 3000000000: - show_message(_("Folder size safety limit reached! (3GB)"), move_folder, mode="warning") - return + cued = [] - # Use target folder if it already is an artist folder - if os.path.basename(target_base).lower() == artist.lower(): - artist_folder = target_base - - # Make artist folder if it does not exist - else: - artist_folder = os.path.join(target_base, artist) - if not os.path.exists(artist_folder): - os.makedirs(artist_folder) - - # Remove all tracks with the old paths - for pl in pctl.multi_playlist: - for i in reversed(range(len(pl.playlist_ids))): - if pctl.get_track(pl.playlist_ids[i]).parent_folder_path == track.parent_folder_path: - del pl.playlist_ids[i] - - # Find insert location - pl = pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids + LENGTH = 0 + PERFORMER = "" + TITLE = "" + START = 0 + DATE = "" + ALBUM = "" + GENRE = "" + MAIN_PERFORMER = "" - matches = [] - insert = 0 + for LINE in content: + if 'TITLE "' in LINE: + ALBUM = LINE[7:len(LINE) - 2] - for i, item in enumerate(pl): - if pctl.get_track(item).fullpath.startswith(target_base): - insert = i + if 'PERFORMER "' in LINE: + while LINE[0] != "P": + LINE = LINE[1:] - for i, item in enumerate(pl): - if pctl.get_track(item).fullpath.startswith(artist_folder): - insert = i + MAIN_PERFORMER = LINE[11:len(LINE) - 2] - logging.info("The folder to be moved is: " + move_folder) - load_order = LoadClass() - load_order.target = os.path.join(artist_folder, track.parent_folder_name) - load_order.playlist = pl_id - load_order.playlist_position = insert + if "REM DATE" in LINE: + DATE = LINE[9:len(LINE) - 1] - logging.info(artist_folder) - logging.info(os.path.join(artist_folder, track.parent_folder_name)) - move_jobs.append( - (move_folder, os.path.join(artist_folder, track.parent_folder_name), True, - track.parent_folder_name, load_order)) - tauon.thread_manager.ready("worker") + if "REM GENRE" in LINE: + GENRE = LINE[10:len(LINE) - 1] + if "TRACK " in LINE: + break -def move_playing_folder_to_tag(tag_item): - move_playing_folder_to_stem(tag_item.path) + for LINE in reversed(content): + if len(LINE) > 100: + return 1 + if "INDEX 01 " in LINE: + temp = "" + pos = len(LINE) + pos -= 1 + while LINE[pos] != ":": + pos -= 1 + if pos < 8: + break + START = int(LINE[pos - 2:pos]) + (int(LINE[pos - 5:pos - 3]) * 60) + LENGTH = int(lasttime) - START + lasttime = START -def re_import4(id): - p = None - for i, idd in enumerate(default_playlist): - if idd == id: - p = i - break + elif 'PERFORMER "' in LINE: + switch = 0 + for i in range(len(LINE)): + if switch == 1 and LINE[i] == '"': + break + if switch == 1: + PERFORMER += LINE[i] + if LINE[i] == '"': + switch = 1 - load_order = LoadClass() + elif 'TITLE "' in LINE: - if p is not None: - load_order.playlist_position = p + switch = 0 + for i in range(len(LINE)): + if switch == 1 and LINE[i] == '"': + break + if switch == 1: + TITLE += LINE[i] + if LINE[i] == '"': + switch = 1 - load_order.replace_stem = True - load_order.target = pctl.get_track(id).parent_folder_path - load_order.notify = True - load_order.playlist = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - load_orders.append(copy.deepcopy(load_order)) - show_message(_("Rescanning folder..."), pctl.get_track(id).parent_folder_path, mode="info") + elif "TRACK " in LINE: + pos = 0 + while LINE[pos] != "K": + pos += 1 + if pos > 15: + return 1 + TN = LINE[pos + 2:pos + 4] -def re_import3(stem): - p = None - for i, id in enumerate(default_playlist): - if pctl.get_track(id).fullpath.startswith(stem + "/"): - p = i - break + TN = int(TN) - load_order = LoadClass() + # try: + # bitrate = audio.info.bitrate + # except Exception: + # logging.exception("Failed to set audio bitrate") + # bitrate = 0 - if p is not None: - load_order.playlist_position = p + if PERFORMER == "": + PERFORMER = MAIN_PERFORMER - load_order.replace_stem = True - load_order.target = stem - load_order.notify = True - load_order.playlist = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - load_orders.append(copy.deepcopy(load_order)) - show_message(_("Rescanning folder..."), stem, mode="info") + nt = copy.deepcopy(tn) + nt.cue_sheet = "" + nt.is_embed_cue = True -def collapse_tree_deco(): - pl_id = tree_view_box.get_pl_id() + nt.index = pctl.master_count + # nt.fullpath = filepath.replace('\\', '/') + # nt.filename = filename + # nt.parent_folder_path = os.path.dirname(filepath.replace('\\', '/')) + # nt.parent_folder_name = os.path.splitext(os.path.basename(filepath))[0] + # nt.file_ext = os.path.splitext(os.path.basename(filepath))[1][1:].upper() + if MAIN_PERFORMER: + nt.album_artist = MAIN_PERFORMER + if PERFORMER: + nt.artist = PERFORMER + if GENRE: + nt.genre = GENRE + nt.title = TITLE + nt.length = LENGTH + # nt.bitrate = source_track.bitrate + if ALBUM: + nt.album = ALBUM + if DATE: + nt.date = DATE.replace('"', "") + nt.track_number = TN + nt.start_time = START + nt.is_cue = True + nt.size = 0 # source_track.size + # nt.samplerate = source_track.samplerate + if TN == 1: + nt.size = os.path.getsize(nt.fullpath) - if tree_view_box.opens.get(pl_id): - return [colours.menu_text, colours.menu_background, None] - return [colours.menu_text_disabled, colours.menu_background, None] + pctl.master_library[pctl.master_count] = nt + cued.append(pctl.master_count) + # loaded_pathes_cache[filepath.replace('\\', '/')] = pctl.master_count + # added.append(pctl.master_count) -def collapse_tree(): - tree_view_box.collapse_all() + pctl.master_count += 1 + LENGTH = 0 + PERFORMER = "" + TITLE = "" + START = 0 + TN = 0 + added += reversed(cued) -def lock_folder_tree(): - if tree_view_box.lock_pl: - tree_view_box.lock_pl = None - else: - tree_view_box.lock_pl = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + # cue_list.append(filepath) +def get_album_from_first_track(track_position, track_id=None, pl_number=None, pl_id: int | None = None): + if pl_number is None: -def lock_folder_tree_deco(): - if tree_view_box.lock_pl: - return [colours.menu_text, colours.menu_background, _("Unlock Panel")] - return [colours.menu_text, colours.menu_background, _("Lock Panel")] + if pl_id: + pl_number = id_to_pl(pl_id) + else: + pl_number = pctl.active_playlist_viewing + playlist = pctl.multi_playlist[pl_number].playlist_ids -folder_tree_stem_menu.add(MenuItem(_("Open Folder"), open_folder_stem, pass_ref=True, icon=folder_icon)) -folder_tree_menu.add(MenuItem(_("Open Folder"), open_folder, pass_ref=True, pass_ref_deco=True, icon=folder_icon, disable_test=open_folder_disable_test)) + if track_id is None: + track_id = playlist[track_position] -lightning_menu.add(MenuItem(_("Filter to New Playlist"), tag_to_new_playlist, pass_ref=True, icon=filter_icon)) -folder_tree_menu.add(MenuItem(_("Filter to New Playlist"), folder_to_new_playlist_by_track_id, pass_ref=True, icon=filter_icon)) -folder_tree_stem_menu.add(MenuItem(_("Filter to New Playlist"), stem_to_new_playlist, pass_ref=True, icon=filter_icon)) -folder_tree_stem_menu.add(MenuItem(_("Rescan Folder"), re_import3, pass_ref=True)) -folder_tree_menu.add(MenuItem(_("Rescan Folder"), re_import4, pass_ref=True)) -lightning_menu.add(MenuItem(_("Move Playing Folder Here"), move_playing_folder_to_tag, pass_ref=True)) + if playlist[track_position] != track_id: + return [] -folder_tree_stem_menu.add(MenuItem(_("Move Playing Folder Here"), move_playing_folder_to_tree_stem, pass_ref=True)) + tracks = [] + album_parent_path = pctl.get_track(track_id).parent_folder_path -folder_tree_stem_menu.br() + i = track_position -folder_tree_stem_menu.add(MenuItem(_("Collapse All"), collapse_tree, collapse_tree_deco)) + while i < len(playlist): + if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: + break -folder_tree_stem_menu.add(MenuItem("lock", lock_folder_tree, lock_folder_tree_deco)) -# folder_tree_menu.add("lock", lock_folder_tree, lock_folder_tree_deco) + tracks.append(playlist[i]) + i += 1 -gallery_menu.add(MenuItem(_("Open Folder"), open_folder, pass_ref=True, pass_ref_deco=True, icon=folder_icon, disable_test=open_folder_disable_test)) + return tracks -gallery_menu.add(MenuItem(_("Show in Playlist"), show_in_playlist)) +def worker3(): + while True: + # time.sleep(0.04) + # if tauon.thread_manager.exit_worker3: + # tauon.thread_manager.exit_worker3 = False + # return + # time.sleep(1) -def finish_current(): - playing_object = pctl.playing_object() - if playing_object is None: - show_message("") + tauon.gall_ren.worker_render() - if not pctl.force_queue: - pctl.force_queue.insert( - 0, queue_item_gen(playing_object.index, - pctl.playlist_playing_position, - pl_to_id(pctl.active_playlist_playing), 1, 1)) +def worker4(): + gui.style_worker_timer.set() + while True: + if prefs.art_bg or (gui.mode == 3 and prefs.mini_mode_mode == 5): + style_overlay.worker() + time.sleep(0.01) + if pctl.playing_state > 0 and pctl.playing_time < 5: + gui.style_worker_timer.set() + if gui.style_worker_timer.get() > 5: + return -def add_album_to_queue(ref, position=None, playlist_id=None): - if position is None: - position = r_menu_position - if playlist_id is None: - playlist_id = pl_to_id(pctl.active_playlist_viewing) +def worker2(): + while True: + worker2_lock.acquire() - partway = 0 - playing_object = pctl.playing_object() - if not pctl.force_queue and playing_object is not None: - if pctl.get_track(ref).parent_folder_path == playing_object.parent_folder_path: - partway = 1 - - queue_object = queue_item_gen(ref, position, playlist_id, 1, partway) - pctl.force_queue.append(queue_object) - queue_timer_set(queue_object=queue_object) - if prefs.stop_end_queue: - pctl.auto_stop = False + if search_over.search_text.text and not (len(search_over.search_text.text) == 1 and ord(search_over.search_text.text[0]) < 128): + if search_over.spotify_mode: + t = spot_search_rate_timer.get() + if t < 1: + time.sleep(1 - t) + spot_search_rate_timer.set() + logging.info("Spotify search") + search_over.results.clear() + results = tauon.spot_ctl.search(search_over.search_text.text) + if results is not None: + search_over.results = results + else: + search_over.active = False + gui.show_message(_( + "Global search + Tab triggers Spotify search but Spotify is not enabled in settings!"), + mode="warning") + search_over.searched_text = search_over.search_text.text + search_over.sip = False -def add_album_to_queue_fc(ref): - playing_object = pctl.playing_object() - if playing_object is None: - show_message("") + elif True: + # perf_timer.set() - queue_item = None + temp_results = [] - if not pctl.force_queue: - queue_item = queue_item_gen( - playing_object.index, pctl.playlist_playing_position, pl_to_id(pctl.active_playlist_playing), 1, 1) - pctl.force_queue.insert(0, queue_item) - add_album_to_queue(ref) - return + search_over.searched_text = search_over.search_text.text - if pctl.force_queue[0].album_stage == 1: - queue_item = queue_item_gen(ref, pctl.playlist_playing_position, pl_to_id(pctl.active_playlist_playing), 1, 0) - pctl.force_queue.insert(1, queue_item) - else: + artists = {} + albums = {} + genres = {} + metas = {} + composers = {} + years = {} - p = pctl.get_track(ref).parent_folder_path - p = "" - if pctl.playing_ready(): - p = pctl.playing_object().parent_folder_path + tracks = set() - # fixme for network tracks + br = 0 - for i, item in enumerate(pctl.force_queue): + if search_over.searched_text in ("the", "and"): + continue - if p != pctl.get_track(item.track_id).parent_folder_path: - queue_item = queue_item_gen( - ref, - pctl.playlist_playing_position, - pl_to_id(pctl.active_playlist_playing), 1, 0) - pctl.force_queue.insert(i, queue_item) - break + search_over.sip = True + gui.update += 1 - else: - queue_item = queue_item_gen( - ref, pctl.playlist_playing_position, pl_to_id(pctl.active_playlist_playing), 1, 0) - pctl.force_queue.insert(len(pctl.force_queue), queue_item) - if queue_item: - queue_timer_set(queue_object=queue_item) - if prefs.stop_end_queue: - pctl.auto_stop = False + o_text = search_over.search_text.text.lower().replace("-", "") + dia_mode = False + if all([ord(c) < 128 for c in o_text]): + dia_mode = True + artist_mode = False + if o_text.startswith("artist "): + o_text = o_text[7:] + artist_mode = True -gallery_menu.add_sub(_("Image…"), 160) -gallery_menu.add(MenuItem(_("Add Album to Queue"), add_album_to_queue, pass_ref=True)) -gallery_menu.add(MenuItem(_("Enqueue Album Next"), add_album_to_queue_fc, pass_ref=True)) + album_mode = False + if o_text.startswith("album "): + o_text = o_text[6:] + album_mode = True + composer_mode = False + if o_text.startswith("composer "): + o_text = o_text[9:] + composer_mode = True -def cancel_import(): - if transcode_list: - del transcode_list[1:] - gui.tc_cancel = True - if loading_in_progress: - gui.im_cancel = True - if gui.sync_progress: - gui.stop_sync = True - gui.sync_progress = _("Aborting Sync") + year_mode = False + if o_text.startswith("year "): + o_text = o_text[5:] + year_mode = True + cn_mode = False + if use_cc and re.search(r"[\u4e00-\u9fff\u3400-\u4dbf\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2b820-\u2ceaf\uf900-\ufaff\u2f800-\u2fa1f]", o_text): + t_cn = s2t.convert(o_text) + s_cn = t2s.convert(o_text) + cn_mode = True -cancel_menu.add(MenuItem(_("Cancel"), cancel_import)) + s_text = o_text + searched = set() -def toggle_lyrics_show(a): - return not gui.combo_mode + for playlist in pctl.multi_playlist: + # if "<" in playlist.title: + # #logging.info("Skipping search on derivative playlist: " + playlist.title) + # continue -def toggle_side_art_deco(): - colour = colours.menu_text - if prefs.show_side_lyrics_art_panel: - line = _("Hide Metadata Panel") - else: - line = _("Show Metadata Panel") + for track in playlist.playlist_ids: - if gui.combo_mode: - colour = colours.menu_text_disabled + if track in searched: + continue + searched.add(track) - return [colour, colours.menu_background, line] + if cn_mode: + s_text = o_text + cache_string = search_string_cache.get(track) + if cache_string: + if search_magic_any(s_text, cache_string): + pass + elif search_magic_any(t_cn, cache_string): + s_text = t_cn + elif search_magic_any(s_cn, cache_string): + s_text = s_cn -def toggle_lyrics_panel_position_deco(): - colour = colours.menu_text - if prefs.lyric_metadata_panel_top: - line = _("Panel Below Lyrics") - else: - line = _("Panel Above Lyrics") + if dia_mode: + cache_string = search_dia_string_cache.get(track) + if cache_string is not None: + if not search_magic_any(s_text, cache_string): + continue + # if s_text not in cache_string: + # continue + else: + cache_string = search_string_cache.get(track) + if cache_string is not None: + if not search_magic_any(s_text, cache_string): + continue - if gui.combo_mode or not prefs.show_side_lyrics_art_panel: - colour = colours.menu_text_disabled + t = pctl.master_library[track] - return [colour, colours.menu_background, line] + title = t.title.lower().replace("-", "") + artist = t.artist.lower().replace("-", "") + album_artist = t.album_artist.lower().replace("-", "") + composer = t.composer.lower().replace("-", "") + date = t.date.lower().replace("-", "") + album = t.album.lower().replace("-", "") + genre = t.genre.lower().replace("-", "") + filename = t.filename.lower().replace("-", "") + stem = os.path.dirname(t.parent_folder_path).lower().replace("-", "") + sartist = t.misc.get("artist_sort", "").lower() + if cache_string is None: + if not dia_mode: + search_string_cache[ + track] = title + artist + album_artist + composer + date + album + genre + sartist + filename + stem -def toggle_lyrics_panel_position(): - prefs.lyric_metadata_panel_top ^= True + if cn_mode: + cache_string = search_string_cache.get(track) + if cache_string: + if search_magic_any(s_text, cache_string): + pass + elif search_magic_any(t_cn, cache_string): + s_text = t_cn + elif search_magic_any(s_cn, cache_string): + s_text = s_cn + if dia_mode: + title = unidecode(title) -def lyrics_in_side_show(track_object: TrackClass): - if gui.combo_mode or not prefs.show_lyrics_side: - return False - return True + artist = unidecode(artist) + album_artist = unidecode(album_artist) + composer = unidecode(composer) + album = unidecode(album) + filename = unidecode(filename) + sartist = unidecode(sartist) + if cache_string is None: + search_dia_string_cache[ + track] = title + artist + album_artist + composer + date + album + genre + sartist + filename + stem -def toggle_side_art(): - prefs.show_side_lyrics_art_panel ^= True + stem = os.path.dirname(t.parent_folder_path) + if len(s_text) > 2 and s_text in stem.replace("-", "").lower(): + # if search_over.all_folders or (artist not in stem.lower() and album not in stem.lower()): -def toggle_lyrics_deco(track_object: TrackClass): - colour = colours.menu_text + if stem in metas: + metas[stem] += 2 + else: + temp_results.append([5, stem, track, playlist.uuid_int, 0]) + metas[stem] = 2 - if gui.combo_mode: - if prefs.show_lyrics_showcase: - line = _("Hide Lyrics") - else: - line = _("Show Lyrics") - if not track_object or (track_object.lyrics == "" and not timed_lyrics_ren.generate(track_object)): - colour = colours.menu_text_disabled - return [colour, colours.menu_background, line] + if s_text in genre: - if prefs.side_panel_layout == 1: # and prefs.show_side_art: + if "/" in genre or "," in genre or ";" in genre: - if prefs.show_lyrics_side: - line = _("Hide Lyrics") - else: - line = _("Show Lyrics") - if (track_object.lyrics == "" and not timed_lyrics_ren.generate(track_object)): - colour = colours.menu_text_disabled - return [colour, colours.menu_background, line] + for split in genre.replace(";", "/").replace(",", "/").split("/"): + if s_text in split: - if prefs.show_lyrics_side: - line = _("Hide Lyrics") - else: - line = _("Show Lyrics") - if (track_object.lyrics == "" and not timed_lyrics_ren.generate(track_object)): - colour = colours.menu_text_disabled - return [colour, colours.menu_background, line] + split = genre_correct(split) + if prefs.sep_genre_multi: + split += "+" + if split in genres: + genres[split] += 3 + else: + temp_results.append([3, split, track, playlist.uuid_int, 0]) + genres[split] = 1 + else: + name = genre_correct(t.genre) + if name in genres: + genres[name] += 3 + else: + temp_results.append([3, name, track, playlist.uuid_int, 0]) + genres[name] = 1 + if s_text in composer: -def toggle_lyrics(track_object: TrackClass): - if not track_object: - return - - if gui.combo_mode: - prefs.show_lyrics_showcase ^= True - if prefs.show_lyrics_showcase and track_object.lyrics == "" and timed_lyrics_ren.generate(track_object): - prefs.prefer_synced_lyrics = True - # if prefs.show_lyrics_showcase and track_object.lyrics == "": - # show_message("No lyrics for this track") - else: - - # Handling for alt panel layout - # if prefs.side_panel_layout == 1 and prefs.show_side_art: - # #prefs.show_side_art = False - # prefs.show_lyrics_side = True - # return + if t.composer in composers: + composers[t.composer] += 2 + else: + temp_results.append([6, t.composer, track, playlist.uuid_int, 0]) + composers[t.composer] = 2 - prefs.show_lyrics_side ^= True - if prefs.show_lyrics_side and track_object.lyrics == "" and timed_lyrics_ren.generate(track_object): - prefs.prefer_synced_lyrics = True - # if prefs.show_lyrics_side and track_object.lyrics == "": - # show_message("No lyrics for this track") + if s_text in date: + year = get_year_from_string(date) + if year: -def get_lyric_fire(track_object: TrackClass, silent: bool = False) -> str | None: - lyrics_ren.lyrics_position = 0 + if year in years: + years[year] += 1 + else: + temp_results.append([7, year, track, playlist.uuid_int, 0]) + years[year] = 1000 - if not prefs.lyrics_enables: - if not silent: - show_message( - _("There are no lyric sources enabled."), - _("See 'lyrics settings' under 'functions' tab in settings."), mode="info") - return None + if search_magic(s_text, title + artist + filename + album + sartist + album_artist): - t = lyrics_fetch_timer.get() - logging.info("Lyric rate limit timer is: " + str(t) + " / -60") - if t < -40: - logging.info("Lets try again later") - if not silent: - show_message(_("Let's be polite and try later.")) + if "artists" in t.misc and t.misc["artists"]: + for a in t.misc["artists"]: + if search_magic(s_text, a.lower()): - if t < -65: - show_message(_("Stop requesting lyrics AAAAAA."), mode="error") + value = 1 + if a.lower().startswith(s_text): + value = 5 - # If the user keeps pressing, lets mess with them haha - lyrics_fetch_timer.force_set(t - 5) + # Add artist + if a in artists: + artists[a] += value + else: + temp_results.append([0, a, track, playlist.uuid_int, 0]) + artists[a] = value - return "later" + if t.album in albums: + albums[t.album] += 1 + else: + temp_results.append([1, t.album, track, playlist.uuid_int, 0]) + albums[t.album] = 1 - if t > 0: - lyrics_fetch_timer.set() - t = 0 + elif search_magic(s_text, artist + sartist): - lyrics_fetch_timer.force_set(t - 10) + value = 1 + if artist.startswith(s_text): + value = 10 - if not silent: - show_message(_("Searching...")) + # Add artist + if t.artist in artists: + artists[t.artist] += value + else: + temp_results.append([0, t.artist, track, playlist.uuid_int, 0]) + artists[t.artist] = value - s_artist = track_object.artist - s_title = track_object.title + if t.album in albums: + albums[t.album] += 1 + else: + temp_results.append([1, t.album, track, playlist.uuid_int, 0]) + albums[t.album] = 1 - if s_artist in prefs.lyrics_subs: - s_artist = prefs.lyrics_subs[s_artist] - if s_title in prefs.lyrics_subs: - s_title = prefs.lyrics_subs[s_title] + elif search_magic(s_text, album_artist): - logging.info(f"Searching for lyrics: {s_artist} - {s_title}") + # Add album artist + value = 1 + if t.album_artist.startswith(s_text): + value = 5 - found = False - for name in prefs.lyrics_enables: + if t.album_artist in artists: + artists[t.album_artist] += value + else: + temp_results.append([0, t.album_artist, track, playlist.uuid_int, 0]) + artists[t.album_artist] = value - if name in lyric_sources.keys(): - func = lyric_sources[name] + if t.album in albums: + albums[t.album] += 1 + else: + temp_results.append([1, t.album, track, playlist.uuid_int, 0]) + albums[t.album] = 1 - try: - lyrics = func(s_artist, s_title) - if lyrics: - logging.info(f"Found lyrics from {name}") - track_object.lyrics = lyrics - found = True - break - except Exception: - logging.exception("Failed to find lyrics") + if s_text in album: - if not found: - logging.error(f"Could not find lyrics from source {name}") + value = 1 + if s_text == album: + value = 3 - if not found: - if not silent: - show_message(_("No lyrics for this track were found")) - else: - gui.message_box = False - if not gui.showcase_mode: - prefs.show_lyrics_side = True - gui.update += 1 - lyrics_ren.lyrics_position = 0 - pctl.notify_change() + if t.album in albums: + albums[t.album] += value + else: + temp_results.append([1, t.album, track, playlist.uuid_int, 0]) + albums[t.album] = value + if search_magic(s_text, artist + sartist) or search_magic(s_text, album): -def get_lyric_wiki(track_object: TrackClass): - if track_object.artist == "" or track_object.title == "": - show_message(_("Insufficient metadata to get lyrics"), mode="warning") - return + if t.album in albums: + albums[t.album] += 3 + else: + temp_results.append([1, t.album, track, playlist.uuid_int, 0]) + albums[t.album] = 3 - shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object])) - shoot_dl.daemon = True - shoot_dl.start() + elif search_magic_any(s_text, artist + sartist) and search_magic_any(s_text, album): - logging.info("..Done") + if t.album in albums: + albums[t.album] += 3 + else: + temp_results.append([1, t.album, track, playlist.uuid_int, 0]) + albums[t.album] = 3 + if s_text in title: -def get_lyric_wiki_silent(track_object: TrackClass): - logging.info("Searching for lyrics...") + if t not in tracks: - if track_object.artist == "" or track_object.title == "": - return + value = 50 + if s_text == title: + value = 200 - shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object, True])) - shoot_dl.daemon = True - shoot_dl.start() + temp_results.append([2, t.title, track, playlist.uuid_int, value]) - logging.info("..Done") + tracks.add(t) + elif t not in tracks: + temp_results.append([2, t.title, track, playlist.uuid_int, 1]) -def test_auto_lyrics(track_object: TrackClass): - if not track_object: - return + tracks.add(t) - if prefs.auto_lyrics and not track_object.lyrics and track_object.index not in prefs.auto_lyrics_checked: - if lyrics_check_timer.get() > 5 and pctl.playing_time > 1: - result = get_lyric_wiki_silent(track_object) - if result == "later": - pass - else: - lyrics_check_timer.set() - prefs.auto_lyrics_checked.append(track_object.index) + br += 1 + if br > 800: + time.sleep(0.005) # Throttle thread + br = 0 + if search_over.searched_text != search_over.search_text.text: + break + search_over.sip = False + search_over.on = 0 + gui.update += 1 -def get_bio(track_object: TrackClass): - if track_object.artist != "": - lastfm.get_bio(track_object.artist) + # Remove results not matching any filter keyword + if artist_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 0: + del temp_results[i] -def search_lyrics_deco(track_object: TrackClass): - if not track_object.lyrics: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + elif album_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 1: + del temp_results[i] - return [line_colour, colours.menu_background, None] + elif composer_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 6: + del temp_results[i] + elif year_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 7: + del temp_results[i] -showcase_menu.add(MenuItem(_("Search for Lyrics"), get_lyric_wiki, search_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + # Sort results by weightings + for i, item in enumerate(temp_results): + if item[0] == 0: + temp_results[i][4] = artists[item[1]] + if item[0] == 1: + temp_results[i][4] = albums[item[1]] + if item[0] == 3: + temp_results[i][4] = genres[item[1]] + if item[0] == 5: + temp_results[i][4] = metas[item[1]] + if not search_over.all_folders: + if metas[item[1]] < 42: + temp_results[i] = None + if item[0] == 6: + temp_results[i][4] = composers[item[1]] + if item[0] == 7: + temp_results[i][4] = years[item[1]] + # 8 is playlists + temp_results[:] = [item for item in temp_results if item is not None] + search_over.results = sorted(temp_results, key=lambda x: x[4], reverse=True) + #logging.info(search_over.results) -def toggle_synced_lyrics(tr): - prefs.prefer_synced_lyrics ^= True - -def toggle_synced_lyrics_deco(track): - if prefs.prefer_synced_lyrics: - text = _("Show static lyrics") - else: - text = _("Show synced lyrics") - if timed_lyrics_ren.generate(track) and track.lyrics: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled - if not track.lyrics: - text = _("Show static lyrics") - if not timed_lyrics_ren.generate(track): - text = _("Show synced lyrics") - - return [line_colour, colours.menu_background, text] + i = 0 + for playlist in pctl.multi_playlist: + if search_magic(s_text, playlist.title.lower()): + item = [8, playlist.title, None, playlist.uuid_int, 100000] + search_over.results.insert(0, item) + i += 1 + if i > 3: + break -showcase_menu.add(MenuItem("Toggle synced", toggle_synced_lyrics, toggle_synced_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + search_over.on = 0 + search_over.force_select = 0 + #logging.info(perf_timer.get()) -def paste_lyrics_deco(): - if SDL_HasClipboardText(): - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled +def worker1(): + global cue_list + global loaderCommand + global loaderCommandReady + global DA_Formats + global home + global loading_in_progress + global added + global to_get + global to_got - return [line_colour, colours.menu_background, None] + loaded_pathes_cache = {} + loaded_cue_cache = {} + added = [] -def paste_lyrics(track_object: TrackClass): - if SDL_HasClipboardText(): - clip = SDL_GetClipboardText() - #logging.info(clip) - track_object.lyrics = clip.decode("utf-8") - else: - logging.warning("NO TEXT TO PASTE") + def get_quoted_from_line(line): -#def chord_lyrics_paste_show_test(_) -> bool: -# return gui.combo_mode and prefs.guitar_chords -# showcase_menu.add(MenuItem(_("Search GuitarParty"), search_guitarparty, pass_ref=True, show_test=chord_lyrics_paste_show_test)) + # Extract quoted or unquoted string from a line + # e.g., 'FILE "01 - Track01.wav" WAVE' or 'TITLE Track01' or "PERFORMER 'Artist Name'" -#guitar_chords = GuitarChords(user_directory=user_directory, ddt=ddt, inp=inp, gui=gui, pctl=pctl) -#showcase_menu.add(MenuItem(_("Paste Chord Lyrics"), guitar_chords.paste_chord_lyrics, pass_ref=True, show_test=chord_lyrics_paste_show_test)) -#showcase_menu.add(MenuItem(_("Clear Chord Lyrics"), guitar_chords.clear_chord_lyrics, pass_ref=True, show_test=chord_lyrics_paste_show_test)) + parts = line.split(None, 1) + if len(parts) < 2: + return "" + content = parts[1].strip() -def copy_lyrics_deco(track_object: TrackClass): - if track_object.lyrics: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + if content.startswith('"'): + end = content.find('"', 1) + return content[1:end] if end != -1 else content[1:] + if content.startswith("'"): + end = content.find("'", 1) + return content[1:end] if end != -1 else content[1:] + # If not quoted, return the first word + return content.split()[0] - return [line_colour, colours.menu_background, None] + def add_from_cue(path): + global added -def copy_lyrics(track_object: TrackClass): - copy_to_clipboard(track_object.lyrics) + if not msys: # Windows terminal doesn't like unicode + logging.info("Reading CUE file: " + path) + try: -def clear_lyrics(track_object: TrackClass): - track_object.lyrics = "" + try: + with open(path, encoding="utf_8") as f: + content = f.readlines() + logging.info("-- Reading as UTF-8") + except Exception: + logging.exception("Failed opening file as UTF-8") + try: + with open(path, encoding="utf_16") as f: + content = f.readlines() + logging.info("-- Reading as UTF-16") + except Exception: + logging.exception("Failed opening file as UTF-16") + try: + j = False + try: + with open(path, encoding="shiftjis") as f: + content = f.readlines() + for line in content: + for c in j_chars: + if c in line: + j = True + logging.info("-- Reading as SHIFT-JIS") + break + except Exception: + logging.exception("Failed opening file as shiftjis") + if not j: + with open(path, encoding="windows-1251") as f: + content = f.readlines() + logging.info("-- Fallback encoding read as windows-1251") + except Exception: + logging.exception("Abort: Can't detect encoding of CUE file") + return 1 -def clear_lyrics_deco(track_object: TrackClass): - if track_object.lyrics: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + f.close() - return [line_colour, colours.menu_background, None] + # We want to detect if this is a cue sheet that points to either a single file with subtracks, or multiple + # files with mutiple subtracks, but not multiple files that are individual tracks + # i.e, is there really any splitting going on + files = 0 + files_with_subtracks = 0 + subtrack_count = 0 + for line in content: + if line.startswith("FILE "): + files += 1 + if subtrack_count > 2: # A hack way to avoid non-compliant EAC CUE sheet + files_with_subtracks += 1 + subtrack_count = 0 + elif line.strip().startswith("TRACK "): + subtrack_count += 1 + if subtrack_count > 2: + files_with_subtracks += 1 -def split_lyrics(track_object: TrackClass): - if track_object.lyrics != "": - track_object.lyrics = track_object.lyrics.replace(". ", ". \n") - else: - pass + if files == 1: + pass + elif files_with_subtracks > 1: + pass + else: + return 1 + cue_performer = "" + cue_date = "" + cue_album = "" + cue_genre = "" + cue_main_performer = "" + cue_songwriter = "" + cue_disc = 0 + cue_disc_total = 0 -def show_sub_search(track_object: TrackClass): - sub_lyrics_box.activate(track_object) + cd = [] + cds = [] + file_name = "" + file_path = "" -showcase_menu.add(MenuItem(_("Toggle Lyrics"), toggle_lyrics, toggle_lyrics_deco, pass_ref=True, pass_ref_deco=True)) -showcase_menu.add_sub(_("Misc…"), 150) -showcase_menu.add_to_sub(0, MenuItem(_("Substitute Search..."), show_sub_search, pass_ref=True)) -showcase_menu.add_to_sub(0, MenuItem(_("Paste Lyrics"), paste_lyrics, paste_lyrics_deco, pass_ref=True)) -showcase_menu.add_to_sub(0, MenuItem(_("Copy Lyrics"), copy_lyrics, copy_lyrics_deco, pass_ref=True, pass_ref_deco=True)) -showcase_menu.add_to_sub(0, MenuItem(_("Clear Lyrics"), clear_lyrics, clear_lyrics_deco, pass_ref=True, pass_ref_deco=True)) -showcase_menu.add_to_sub(0, MenuItem(_("Toggle art panel"), toggle_side_art, toggle_side_art_deco, show_test=lyrics_in_side_show)) -showcase_menu.add_to_sub(0, MenuItem(_("Toggle art position"), - toggle_lyrics_panel_position, toggle_lyrics_panel_position_deco, show_test=lyrics_in_side_show)) - -center_info_menu.add(MenuItem(_("Search for Lyrics"), get_lyric_wiki, search_lyrics_deco, pass_ref=True, pass_ref_deco=True)) -center_info_menu.add(MenuItem(_("Toggle Lyrics"), toggle_lyrics, toggle_lyrics_deco, pass_ref=True, pass_ref_deco=True)) -center_info_menu.add_sub(_("Misc…"), 150) -center_info_menu.add_to_sub(0, MenuItem(_("Substitute Search..."), show_sub_search, pass_ref=True)) -center_info_menu.add_to_sub(0, MenuItem(_("Paste Lyrics"), paste_lyrics, paste_lyrics_deco, pass_ref=True)) -center_info_menu.add_to_sub(0, MenuItem(_("Copy Lyrics"), copy_lyrics, copy_lyrics_deco, pass_ref=True, pass_ref_deco=True)) -center_info_menu.add_to_sub(0, MenuItem(_("Clear Lyrics"), clear_lyrics, clear_lyrics_deco, pass_ref=True, pass_ref_deco=True)) -center_info_menu.add_to_sub(0, MenuItem(_("Toggle art panel"), toggle_side_art, toggle_side_art_deco, show_test=lyrics_in_side_show)) -center_info_menu.add_to_sub(0, MenuItem(_("Toggle art position"), - toggle_lyrics_panel_position, toggle_lyrics_panel_position_deco, show_test=lyrics_in_side_show)) + in_header = True -def save_embed_img_disable_test(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - return track_object.is_network + i = -1 + while True: + i += 1 -def save_embed_img(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - filepath = track_object.fullpath - folder = track_object.parent_folder_path - ext = track_object.file_ext + if i > len(content) - 1: + break - if save_embed_img_disable_test(track_object): - show_message(_("Saving network images not implemented")) - return + line = content[i].strip() - try: - pic = album_art_gen.get_embed(track_object) + if in_header: + if line.startswith("REM "): + line = line[4:] - if not pic: - show_message(_("Image save error."), _("No embedded album art found file."), mode="warning") - return + if line.startswith("TITLE "): + cue_album = get_quoted_from_line(line) + if line.startswith("PERFORMER "): + cue_performer = get_quoted_from_line(line) + if line.startswith("MAIN PERFORMER "): + cue_main_performer = get_quoted_from_line(line) + if line.startswith("SONGWRITER "): + cue_songwriter = get_quoted_from_line(line) + if line.startswith("GENRE "): + cue_genre = get_quoted_from_line(line) + if line.startswith("DATE "): + cue_date = get_quoted_from_line(line) + if line.startswith("DISCNUMBER "): + cue_disc = get_quoted_from_line(line) + if line.startswith("TOTALDISCS "): + cue_disc_total = get_quoted_from_line(line) - source_image = io.BytesIO(pic) - im = Image.open(source_image) + if line.startswith("FILE "): + in_header = False + else: + continue - source_image.close() + if line.startswith("FILE "): - ext = "." + im.format.lower() - if im.format == "JPEG": - ext = ".jpg" + if cd: + cds.append(cd) + cd = [] - target = os.path.join(folder, "embed-" + str(im.height) + "px-" + str(track_object.index) + ext) + file_name = get_quoted_from_line(line) + file_path = os.path.join(os.path.dirname(path), file_name) - if len(pic) > 30: - with open(target, "wb") as w: - w.write(pic) + if not os.path.isfile(file_path): + if files == 1: + logging.info("-- The referenced source file wasn't found. Searching for matching file name...") + for item in os.listdir(os.path.dirname(path)): + if os.path.splitext(item)[0] == os.path.splitext(os.path.basename(path))[0]: + if ".cue" not in item.lower() and item.split(".")[-1].lower() in DA_Formats: + file_name = item + file_path = os.path.join(os.path.dirname(path), file_name) + logging.info("-- Source found at: " + file_path) + break + else: + logging.error("-- Abort: Source file not found") + return 1 + else: + logging.error("-- Abort: Source file not found") + return 1 - open_folder(track_object.index) + if line.startswith("TRACK "): + line = line[6:] + if line.endswith("AUDIO"): + line = line[:-5] - except Exception: - logging.exception("Unknown error trying to save an image") - show_message(_("Image save error."), _("A mysterious error occurred"), mode="error") + c = loaded_cue_cache.get((file_path.replace("\\", "/"), int(line.strip()))) + if c is not None: + nt = c + else: + nt = TrackClass() + nt.index = pctl.master_count + pctl.master_count += 1 + nt.fullpath = file_path + nt.filename = file_name + nt.parent_folder_path = os.path.dirname(file_path.replace("\\", "/")) + nt.parent_folder_name = os.path.splitext(os.path.basename(file_path))[0] + nt.file_ext = os.path.splitext(file_name)[1][1:].upper() + nt.is_cue = True -picture_menu = Menu(175) + nt.album_artist = cue_main_performer + if not cue_main_performer: + nt.album_artist = cue_performer + nt.artist = cue_performer + nt.composer = cue_songwriter + nt.genre = cue_genre + nt.album = cue_album + nt.date = cue_date.replace('"', "") + nt.track_number = int(line.strip()) + if nt.track_number == 1: + nt.size = os.path.getsize(nt.fullpath) + nt.misc["parent-size"] = os.path.getsize(nt.fullpath) + while True: + i += 1 + if i > len(content) - 1 or content[i].startswith("FILE ") or content[i].strip().startswith( + "TRACK"): + break -def open_image_deco(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - info = album_art_gen.get_info(track_object) + line = content[i] + line = line.strip() - if info is None: - return [colours.menu_text_disabled, colours.menu_background, None] + if line.startswith("TITLE"): + nt.title = get_quoted_from_line(line) + if line.startswith("PERFORMER"): + nt.artist = get_quoted_from_line(line) + if line.startswith("SONGWRITER"): + nt.composer = get_quoted_from_line(line) + if line.startswith("INDEX 01 ") and ":" in line: + line = line[9:] + times = line.split(":") + nt.start_time = int(times[0]) * 60 + int(times[1]) + int(times[2]) / 100 - line_colour = colours.menu_text + i -= 1 + cd.append(nt) - return [line_colour, colours.menu_background, None] + if cd: + cds.append(cd) + for cdn, cd in enumerate(cds): -def open_image_disable_test(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - return track_object.is_network + last_end = None + end_track = TrackClass() + end_track.fullpath = cd[-1].fullpath + tag_scan(end_track) -def open_image(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - album_art_gen.open_external(track_object) + # Remove target track if already imported + for i in reversed(range(len(added))): + if pctl.get_track(added[i]).fullpath == end_track.fullpath: + del added[i] + # Update with proper length + for track in reversed(cd): -def extract_image_deco(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - info = album_art_gen.get_info(track_object) + if last_end == None: + last_end = end_track.length - if info is None: - return [colours.menu_text_disabled, colours.menu_background, None] + track.length = last_end - track.start_time + track.samplerate = end_track.samplerate + track.bitrate = end_track.bitrate + track.bit_depth = end_track.bit_depth + track.misc["parent-length"] = end_track.length + last_end = track.start_time - if info[0] == 1: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + # inherit missing metadata + if not track.date: + track.date = end_track.date + if not track.album_artist: + track.album_artist = end_track.album_artist + if not track.album: + track.album = end_track.album + if not track.artist: + track.artist = end_track.artist + if not track.genre: + track.genre = end_track.genre + if not track.comment: + track.comment = end_track.comment + if not track.composer: + track.composer = end_track.composer - return [line_colour, colours.menu_background, None] + if cue_disc: + track.disc_number = cue_disc + elif len(cds) == 0: + track.disc_number = "" + else: + track.disc_number = str(cdn) + if cue_disc_total: + track.disc_total = cue_disc_total + elif len(cds) == 0: + track.disc_total = "" + else: + track.disc_total = str(len(cds)) -picture_menu.add(MenuItem(_("Open Image"), open_image, open_image_deco, pass_ref=True, pass_ref_deco=True, disable_test=open_image_disable_test)) + # Add all tracks for import to playlist + for cd in cds: + for track in cd: + pctl.master_library[track.index] = track + if track.fullpath not in cue_list: + cue_list.append(track.fullpath) + loaded_pathes_cache[track.fullpath] = track.index + added.append(track.index) -def cycle_image_deco(track_object: TrackClass): - info = album_art_gen.get_info(track_object) + except Exception: + logging.exception("Internal error processing CUE file") - if pctl.playing_state != 0 and (info is not None and info[1] > 1): - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + def add_file(path, force_scan: bool = False) -> int | None: + # bm.get("add file start") + global DA_Formats + global to_got - return [line_colour, colours.menu_background, None] + if not os.path.isfile(path): + logging.error("File to import missing") + return 0 -def cycle_image_gal_deco(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - info = album_art_gen.get_info(track_object) + if os.path.splitext(path)[1][1:] in {"CUE", "cue"}: + add_from_cue(path) + return 0 - if info is not None and info[1] > 1: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + if path.lower().endswith(".xspf"): + logging.info("Found XSPF file at: " + path) + load_xspf(path) + return 0 - return [line_colour, colours.menu_background, None] + if path.lower().endswith(".m3u") or path.lower().endswith(".m3u8"): + load_m3u(path) + return 0 -def cycle_offset(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - album_art_gen.cycle_offset(track_object) + if path.endswith(".pls"): + load_pls(path) + return 0 + if os.path.splitext(path)[1][1:].lower() not in DA_Formats: + if os.path.splitext(path)[1][1:].lower() in Archive_Formats: + if not prefs.auto_extract: + show_message( + _("You attempted to drop an archive."), + _('However the "extract archive" function is not enabled.'), mode="info") + else: + type = os.path.splitext(path)[1][1:].lower() + split = os.path.splitext(path) + target_dir = split[0] + if prefs.extract_to_music and music_directory is not None: + target_dir = os.path.join(str(music_directory), os.path.basename(target_dir)) + #logging.info(os.path.getsize(path)) + if os.path.getsize(path) > 4e+9: + logging.warning("Archive file is large!") + show_message(_("Skipping oversize zip file (>4GB)")) + return 1 + if not os.path.isdir(target_dir) and not os.path.isfile(target_dir): + if type == "zip": + try: + b = to_got + to_got = "ex" + gui.update += 1 + zip_ref = zipfile.ZipFile(path, "r") -def cycle_offset_back(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - album_art_gen.cycle_offset_reverse(track_object) + zip_ref.extractall(target_dir) + zip_ref.close() + except RuntimeError as e: + logging.exception("Zip error") + to_got = b + if "encrypted" in e: + show_message( + _("Failed to extract zip archive."), + _("The archive is encrypted. You'll need to extract it manually with the password."), + mode="warning") + else: + show_message( + _("Failed to extract zip archive."), + _("Maybe archive is corrupted? Does disk have enough space and have write permission?"), + mode="warning") + return 1 + except Exception: + logging.exception("Zip error 2") + to_got = b + show_message( + _("Failed to extract zip archive."), + _("Maybe archive is corrupted? Does disk have enough space and have write permission?"), + mode="warning") + return 1 + elif type == "rar": + b = to_got + try: + to_got = "ex" + gui.update += 1 + line = launch_prefix + "unrar x -y -p- " + shlex.quote(path) + " " + shlex.quote( + target_dir) + os.sep + result = subprocess.run(shlex.split(line), check=True) + logging.info(result) + except Exception: + logging.exception("Failed to extract rar archive.") + to_got = b + show_message(_("Failed to extract rar archive."), mode="warning") -# Next and previous pictures -picture_menu.add(MenuItem(_("Next Image"), cycle_offset, cycle_image_deco, pass_ref=True, pass_ref_deco=True)) -#picture_menu.add(_("Previous"), cycle_offset_back, cycle_image_deco, pass_ref=True, pass_ref_deco=True) + return 1 -# Extract embedded artwork from file -picture_menu.add(MenuItem(_("Extract Image"), save_embed_img, extract_image_deco, pass_ref=True, pass_ref_deco=True, disable_test=save_embed_img_disable_test)) + elif type == "7z": + b = to_got + try: + to_got = "ex" + gui.update += 1 + line = launch_prefix + "7z x -y " + shlex.quote(path) + " -o" + shlex.quote( + target_dir) + os.sep + result = subprocess.run(shlex.split(line), check=True) + logging.info(result) + except Exception: + logging.exception("Failed to extract 7z archive.") + to_got = b + show_message(_("Failed to extract 7z archive."), mode="warning") + return 1 -def dl_art_deco(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - if not track_object.album or not track_object.artist: - return [colours.menu_text_disabled, colours.menu_background, None] - return [colours.menu_text, colours.menu_background, None] + upper = os.path.dirname(target_dir) + cont = os.listdir(target_dir) + new = upper + "/temporaryfolderd" + error = False + if len(cont) == 1 and os.path.isdir(split[0] + "/" + cont[0]): + logging.info("one thing") + os.rename(target_dir, new) + try: + shutil.move(new + "/" + cont[0], upper) + except Exception: + logging.exception("Could not move file") + error = True + shutil.rmtree(new) + logging.info(new) + target_dir = upper + "/" + cont[0] + if not os.path.isdir(target_dir): + logging.error("Extract error, expected directory not found") + if True and not error and prefs.auto_del_zip: + logging.info("Moving archive file to trash: " + path) + try: + send2trash(path) + except Exception: + logging.exception("Could not move archive to trash") + show_message(_("Could not move archive to trash"), path, mode="info") -def download_art1(tr): - if tr.is_network: - show_message(_("Cannot download art for network tracks.")) - return + to_got = b + gets(target_dir) + quick_import_done.append(target_dir) + # gets(target_dir) - # Determine noise of folder ---------------- - siblings = [] - parent = tr.parent_folder_path + return 1 - for pl in pctl.multi_playlist: - for ti in pl.playlist_ids: - tr = pctl.get_track(ti) - if tr.parent_folder_path == parent: - siblings.append(tr) + to_got += 1 + gui.update = 1 - album_tags = [] - date_tags = [] + path = path.replace("\\", "/") - for tr in siblings: - album_tags.append(tr.album) - date_tags.append(tr.date) + if path in loaded_pathes_cache: + de = loaded_pathes_cache[path] - album_tags = set(album_tags) - date_tags = set(date_tags) + if pctl.master_library[de].fullpath in cue_list: + logging.info("File has an associated .cue file... Skipping") + return None - if len(album_tags) > 2 or len(date_tags) > 2: - show_message(_("It doesn't look like this folder belongs to a single album, sorry")) - return + if pctl.master_library[de].file_ext.lower() in GME_Formats: + # Skip cache for subtrack formats + pass + else: + added.append(de) + return None - # ------------------------------------------- + time.sleep(0.002) - if not os.path.isdir(tr.parent_folder_path): - show_message(_("Directory missing.")) - return + # audio = auto.File(path) - try: - show_message(_("Looking up MusicBrainz ID...")) + nt = TrackClass() - if "musicbrainz_releasegroupid" not in tr.misc or "musicbrainz_artistids" not in tr.misc or not tr.misc[ - "musicbrainz_artistids"]: + nt.index = pctl.master_count + set_path(nt, path) - logging.info("MusicBrainz ID lookup...") + def commit_track(nt): + pctl.master_library[pctl.master_count] = nt + added.append(pctl.master_count) - artist = tr.album_artist - if not tr.album: - return - if not artist: - artist = tr.artist + if prefs.auto_sort or force_scan: + tag_scan(nt) + else: + after_scan.append(nt) + tauon.thread_manager.ready("worker") - s = musicbrainzngs.search_release_groups(tr.album, artist=artist, limit=1) + pctl.master_count += 1 - album_id = s["release-group-list"][0]["id"] - artist_id = s["release-group-list"][0]["artist-credit"][0]["artist"]["id"] + # nt = tag_scan(nt) + if nt.cue_sheet != "": + tag_scan(nt) + cue_scan(nt.cue_sheet, nt) + del nt - logging.info("Found release group ID: " + album_id) - logging.info("Found artist ID: " + artist_id) + elif nt.file_ext.lower() in GME_Formats and gme: - else: + emu = ctypes.c_void_p() + err = gme.gme_open_file(nt.fullpath.encode("utf-8"), ctypes.byref(emu), -1) + if not err: + n = gme.gme_track_count(emu) + for i in range(n): + nt = TrackClass() + set_path(nt, path) + nt.index = pctl.master_count + nt.subtrack = i + commit_track(nt) - album_id = tr.misc["musicbrainz_releasegroupid"] - artist_id = tr.misc["musicbrainz_artistids"][0] + gme.gme_delete(emu) - logging.info("Using tagged release group ID: " + album_id) - logging.info("Using tagged artist ID: " + artist_id) + else: - if prefs.enable_fanart_cover: - try: - show_message(_("Searching fanart.tv for cover art...")) + commit_track(nt) - r = requests.get("https://webservice.fanart.tv/v3/music/albums/" \ - + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) + # bm.get("fill entry") + if gui.auto_play_import: + pctl.jump(pctl.master_count - 1) + gui.auto_play_import = False - artlink = r.json()["albums"][album_id]["albumcover"][0]["url"] - id = r.json()["albums"][album_id]["albumcover"][0]["id"] + # Count the approx number of files to be imported + def pre_get(direc): - response = urllib.request.urlopen(artlink, context=ssl_context) - info = response.info() + global to_get - t = io.BytesIO() - t.seek(0) - t.write(response.read()) - t.seek(0, 2) - l = t.tell() - t.seek(0) + to_get = 0 + for root, dirs, files in os.walk(direc): + to_get += len(files) + if gui.im_cancel: + return + gui.update = 3 - if info.get_content_maintype() == "image" and l > 1000: + def gets(direc, force_scan=False): - if info.get_content_subtype() == "jpeg": - filepath = os.path.join(tr.parent_folder_path, "cover-" + id + ".jpg") - elif info.get_content_subtype() == "png": - filepath = os.path.join(tr.parent_folder_path, "cover-" + id + ".png") - else: - show_message(_("Could not detect downloaded filetype."), mode="error") - return + global DA_Formats - f = open(filepath, "wb") - f.write(t.read()) - f.close() + if os.path.basename(direc) == "__MACOSX": + return - show_message(_("Cover art downloaded from fanart.tv"), mode="done") - # clear_img_cache() - for track_id in default_playlist: - if tr.parent_folder_path == pctl.get_track(track_id).parent_folder_path: - clear_track_image_cache(pctl.get_track(track_id)) - return - except Exception: - logging.exception("Failed to get from fanart.tv") + try: + items_in_dir = os.listdir(direc) + if use_natsort: + items_in_dir = natsort.os_sorted(items_in_dir) + else: + items_in_dir.sort() + except PermissionError: + logging.exception("Permission error accessing one or more files") + if snap_mode: + show_message( + _("Permission error accessing one or more files."), + _("If this location is on external media, see https://") + "github.com/Taiko2k/TauonMusicBox/wiki/Snap-Permissions", + mode="bubble") + else: + show_message(_("Permission error accessing one or more files"), mode="warning") - show_message(_("Searching MusicBrainz for cover art...")) - t = io.BytesIO(musicbrainzngs.get_release_group_image_front(album_id, size=None)) - l = 0 - t.seek(0, 2) - l = t.tell() - t.seek(0) - if l > 1000: - filepath = os.path.join(tr.parent_folder_path, album_id + ".jpg") - f = open(filepath, "wb") - f.write(t.read()) - f.close() + return + except Exception: + logging.exception("Unknown error accessing one or more files") + return - show_message(_("Cover art downloaded from MusicBrainz"), mode="done") - # clear_img_cache() - clear_track_image_cache(tr) + for q in range(len(items_in_dir)): + if items_in_dir[q][0] == ".": + continue + if os.path.isdir(os.path.join(direc, items_in_dir[q])): + gets(os.path.join(direc, items_in_dir[q])) + if gui.im_cancel: + return - for track_id in default_playlist: - if tr.parent_folder_path == pctl.get_track(track_id).parent_folder_path: - clear_track_image_cache(pctl.get_track(track_id)) + for q in range(len(items_in_dir)): + if items_in_dir[q][0] == ".": + continue + if os.path.isdir(os.path.join(direc, items_in_dir[q])) is False: - return + if os.path.splitext(items_in_dir[q])[1][1:].lower() in DA_Formats: - except Exception: - logging.exception("Matching cover art or ID could not be found.") - show_message(_("Matching cover art or ID could not be found.")) + if len(items_in_dir[q]) > 2 and items_in_dir[q][0:2] == "._": + continue + add_file(os.path.join(direc, items_in_dir[q]).replace("\\", "/"), force_scan) -def download_art1_fire_disable_test(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - return track_object.is_network + elif os.path.splitext(items_in_dir[q])[1][1:] in {"CUE", "cue"}: + add_from_cue(os.path.join(direc, items_in_dir[q]).replace("\\", "/")) -def download_art1_fire(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - shoot_dl = threading.Thread(target=download_art1, args=[track_object]) - shoot_dl.daemon = True - shoot_dl.start() + if gui.im_cancel: + return + def cache_paths(): + dic = {} + dic2 = {} + for key, value in pctl.master_library.items(): + if value.is_network: + continue + dic[value.fullpath.replace("\\", "/")] = key + if value.is_cue: + dic2[(value.fullpath.replace("\\", "/"), value.track_number)] = value + return dic, dic2 -def remove_embed_picture(track_object: TrackClass, dry: bool = True) -> int | None: - """Return amount of removed objects or None""" - index = track_object.index - if key_shift_down or key_shiftr_down: - tracks = [index] - if track_object.is_cue or track_object.is_network: - show_message(_("Error - No handling for this kind of track"), mode="warning") - return None - else: - tracks = [] - original_parent_folder = track_object.parent_folder_name - for k in default_playlist: - tr = pctl.get_track(k) - if original_parent_folder == tr.parent_folder_name: - tracks.append(k) + #logging.info(pctl.master_library) - removed = 0 - if not dry: - pr = pctl.stop(True) - try: - for item in tracks: + global transcode_list + global transcode_state + global album_art_gen + global cm_clean_db + global to_got + global to_get + global move_in_progress - tr = pctl.get_track(item) + active_timer = Timer() + while True: - if tr.is_cue: - continue + if not after_scan: + time.sleep(0.1) - if tr.is_network: - continue + if after_scan or load_orders or \ + artist_list_box.load or \ + artist_list_box.to_fetch or \ + gui.regen_single_id or \ + gui.regen_single > -1 or \ + pctl.after_import_flag or \ + tauon.worker_save_state or \ + move_jobs or \ + cm_clean_db or \ + transcode_list or \ + to_scan or \ + loaderCommandReady: + active_timer.set() + elif active_timer.get() > 5: + return - if dry: - removed += 1 - else: - if tr.file_ext == "MP3": - try: - tag = mutagen.id3.ID3(tr.fullpath) - tag.delall("APIC") - remove = True - tag.save(padding=no_padding) - removed += 1 - except Exception: - logging.exception("No MP3 APIC found") + if after_scan: + i = 0 + while after_scan: + i += 1 - if tr.file_ext == "M4A": - try: - tag = mutagen.mp4.MP4(tr.fullpath) - del tag.tags["covr"] - tag.save(padding=no_padding) - removed += 1 - except Exception: - logging.exception("No m4A covr tag found") + if i > 123: + break - if tr.file_ext in ("OGA", "OPUS", "OGG"): - show_message(_("Removing vorbis image not implemented")) - # try: - # tag = mutagen.File(tr.fullpath).tags - # logging.info(tag) - # removed += 1 - # except Exception: - # logging.exception("Failed to manipulate tags") + tag_scan(after_scan[0]) - if tr.file_ext == "FLAC": - try: - tag = mutagen.flac.FLAC(tr.fullpath) - tag.clear_pictures() - tag.save(padding=no_padding) - removed += 1 - except Exception: - logging.exception("Failed to save tags on FLAC") + gui.update = 2 + gui.pl_update = 1 + # time.sleep(0.001) + if pctl.running: + del after_scan[0] + else: + break - clear_track_image_cache(tr) + album_artist_dict.clear() - except Exception: - logging.exception("Image remove error") - show_message(_("Image remove error"), mode="error") - return None + artist_list_box.worker() - if dry: - return removed + # Update smart playlists + if gui.regen_single_id is not None: + regenerate_playlist(pl=-1, silent=True, id=gui.regen_single_id) + gui.regen_single_id = None - if removed == 0: - show_message(_("Image removal failed."), mode="error") - return None - if removed == 1: - show_message(_("Deleted embedded picture from file"), mode="done") - else: - show_message(_("{N} files processed").local(N=removed), mode="done") - if pr == 1: - pctl.revert() + # Update smart playlists + if gui.regen_single > -1: + target = gui.regen_single + gui.regen_single = -1 + regenerate_playlist(target, silent=True) + if pctl.after_import_flag and not after_scan and not search_over.active and not loading_in_progress: + pctl.after_import_flag = False -del_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "del.png", True) -delete_icon = MenuIcon(del_icon) + for i, plist in enumerate(pctl.multi_playlist): + if pl_to_id(i) in pctl.gen_codes: + code = pctl.gen_codes[pl_to_id(i)] + try: + if check_auto_update_okay(code, pl=i): + if not pl_is_locked(i): + logging.info("Reloading smart playlist: " + plist.title) + regenerate_playlist(i, silent=True) + time.sleep(0.02) + except Exception: + logging.exception("Failed to handle playlist") + tree_view_box.clear_all() -def delete_file_image(track_object: TrackClass): - try: - showc = album_art_gen.get_info(track_object) - if showc is not None and showc[0] == 0: - source = album_art_gen.get_sources(track_object)[showc[2]][1] - os.remove(source) - # clear_img_cache() - clear_track_image_cache(track_object) - logging.info("Deleted file: " + source) - except Exception: - logging.exception("Failed to delete file") - show_message(_("Something went wrong"), mode="error") + if tauon.worker_save_state and \ + not gui.pl_pulse and \ + not loading_in_progress and \ + not to_scan and not after_scan and \ + not plex.scanning and \ + not jellyfin.scanning and \ + not cm_clean_db and \ + not lastfm.scanning_friends and \ + not move_in_progress and \ + (gui.lowered or not window_is_focused() or not gui.mouse_in_window): + save_state() + cue_list.clear() + tauon.worker_save_state = False + # Folder moving + if len(move_jobs) > 0: + gui.update += 1 + move_in_progress = True + job = move_jobs[0] + del move_jobs[0] -def delete_track_image_deco(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - info = album_art_gen.get_info(track_object) + if job[0].strip("\\/") == job[1].strip("\\/"): + show_message(_("Folder copy error."), _("The target and source are the same."), mode="info") + gui.update += 1 + move_in_progress = False + continue - text = _("Delete Image File") - line_colour = colours.menu_text + try: + shutil.copytree(job[0], job[1]) + except Exception: + logging.exception("Failed to copy directory") + move_in_progress = False + gui.update += 1 + show_message(_("The folder copy has failed!"), _("Some files may have been written."), mode="warning") + continue - if info is None or track_object.is_network: - return [colours.menu_text_disabled, colours.menu_background, None] + if job[2] == True: + try: + shutil.rmtree(job[0]) - if info and info[0] == 0: - text = _("Delete Image File") + except Exception: + logging.exception("Failed to delete directory") + show_message(_("Something has gone horribly wrong!"), _("Could not delete {name}").format(name=job[0]), mode="error") + gui.update += 1 + move_in_progress = False + return - elif info and info[0] == 1: - if pctl.playing_state > 0 and track_object.file_ext in ("MP3", "FLAC", "M4A"): - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + show_message(_("Folder move complete."), _("Folder name: {name}").format(name=job[3]), mode="done") + else: + show_message(_("Folder copy complete."), _("Folder name: {name}").format(name=job[3]), mode="done") - text = _("Delete Embedded | Folder") - if key_shift_down or key_shiftr_down: - text = _("Delete Embedded | Track") + move_in_progress = False + load_orders.append(job[4]) + gui.update += 1 - return [line_colour, colours.menu_background, text] + # Clean database + if cm_clean_db is True: + items_removed = 0 + # old_db = copy.deepcopy(pctl.master_library) + to_got = 0 + to_get = len(pctl.master_library) + search_over.results.clear() -def delete_track_image(track_object: TrackClass): - if type(track_object) is int: - track_object = pctl.master_library[track_object] - if track_object.is_network: - return - info = album_art_gen.get_info(track_object) - if info and info[0] == 0: - delete_file_image(track_object) - elif info and info[0] == 1: - n = remove_embed_picture(track_object, dry=True) - gui.message_box_confirm_callback = remove_embed_picture - gui.message_box_confirm_reference = (track_object, False) - show_message(_("This will erase any embedded image in {N} files. Are you sure?").format(N=n), mode="confirm") + keys = set(pctl.master_library.keys()) + for index in keys: + time.sleep(0.0001) + track = pctl.master_library[index] + to_got += 1 + if to_got % 100 == 0: + gui.update = 1 + if not prefs.remove_network_tracks and track.file_ext == "SPTY": -picture_menu.add( - MenuItem(_("Delete Image File"), delete_track_image, delete_track_image_deco, pass_ref=True, - pass_ref_deco=True, icon=delete_icon)) + for playlist in pctl.multi_playlist: + if index in playlist.playlist_ids: + break + else: + pctl.purge_track(index) + items_removed += 1 -picture_menu.add(MenuItem(_("Quick-Fetch Cover Art"), download_art1_fire, dl_art_deco, pass_ref=True, pass_ref_deco=True, disable_test=download_art1_fire_disable_test)) + continue + if (prefs.remove_network_tracks is False and not track.is_network and not os.path.isfile( + track.fullpath)) or \ + (prefs.remove_network_tracks is True and track.is_network): -def toggle_gimage(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_gimage - prefs.show_gimage ^= True - return None - + if track.is_network and track.file_ext == "SPTY": + continue -def search_image_deco(track_object: TrackClass): - if track_object.artist and track_object.album: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + pctl.purge_track(index) + items_removed += 1 - return [line_colour, colours.menu_background, None] + cm_clean_db = False + show_message( + _("Cleaning complete."), + _("{N} items were removed from the database.").format(N=str(items_removed)), mode="done") + if album_mode: + reload_albums(True) + if gui.combo_mode: + reload_albums() + gui.update = 1 + gui.pl_update = 1 + pctl.notify_change() -def ser_gimage(track_object: TrackClass): - if track_object.artist and track_object.album: - line = "https://www.google.com/search?tbm=isch&q=" + urllib.parse.quote( - track_object.artist + " " + track_object.album) - webbrowser.open(line, new=2, autoraise=True) + search_dia_string_cache.clear() + search_string_cache.clear() + search_over.results.clear() + pctl.notify_change() -# picture_menu.add(_('Search Google for Images'), ser_gimage, search_image_deco, pass_ref=True, pass_ref_deco=True, show_test=toggle_gimage) + # FOLDER ENC + if transcode_list: -# picture_menu.add(_('Toggle art box'), toggle_side_art, toggle_side_art_deco) + try: + transcode_state = "" + gui.update += 1 -picture_menu.add(MenuItem(_("Search for Lyrics"), get_lyric_wiki, search_lyrics_deco, pass_ref=True, pass_ref_deco=True)) -picture_menu.add(MenuItem(_("Toggle Lyrics"), toggle_lyrics, toggle_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + folder_items = transcode_list[0] -gallery_menu.add_to_sub(0, MenuItem(_("Next"), cycle_offset, cycle_image_gal_deco, pass_ref=True, pass_ref_deco=True)) -gallery_menu.add_to_sub(0, MenuItem(_("Previous"), cycle_offset_back, cycle_image_gal_deco, pass_ref=True, pass_ref_deco=True)) -gallery_menu.add_to_sub(0, MenuItem(_("Open Image"), open_image, open_image_deco, pass_ref=True, pass_ref_deco=True, disable_test=open_image_disable_test)) -gallery_menu.add_to_sub(0, MenuItem(_("Extract Image"), save_embed_img, extract_image_deco, pass_ref=True, pass_ref_deco=True, disable_test=save_embed_img_disable_test)) -gallery_menu.add_to_sub(0, MenuItem(_("Delete Image <combined>"), delete_track_image, delete_track_image_deco, pass_ref=True, pass_ref_deco=True)) #, icon=delete_icon) -gallery_menu.add_to_sub(0, MenuItem(_("Quick-Fetch Cover Art"), download_art1_fire, dl_art_deco, pass_ref=True, pass_ref_deco=True, disable_test=download_art1_fire_disable_test)) + ref_track_object = pctl.master_library[folder_items[0]] + ref_album = ref_track_object.album -def append_here(): - global cargo - global default_playlist - default_playlist += cargo + # Generate a folder name based on artist and album of first track in batch + folder_name = encode_folder_name(ref_track_object) + # If folder contains tracks from multiple albums, use original folder name instead + for item in folder_items: + test_object = pctl.master_library[item] + if test_object.album != ref_album: + folder_name = ref_track_object.parent_folder_name + break -def paste_deco(): - active = False - line = None - if len(cargo) > 0: - active = True - elif SDL_HasClipboardText(): - text = copy_from_clipboard() - if text.startswith(("/", "spotify")) or "file://" in text: - active = True - elif prefs.spot_mode and text.startswith("https://open.spotify.com/album/"): # or text.startswith("https://open.spotify.com/track/"): - active = True - line = _("Paste Spotify Album") + logging.info("Transcoding folder: " + folder_name) - if active: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + # Remove any existing matching folder + if (prefs.encoder_output / folder_name).is_dir(): + shutil.rmtree(prefs.encoder_output / folder_name) - return [line_colour, colours.menu_background, line] + # Create new empty folder to output tracks to + (prefs.encoder_output / folder_name).mkdir(parents=True) + full_wav_out_p = prefs.encoder_output / "output.wav" + full_target_out_p = prefs.encoder_output / ("output." + prefs.transcode_codec) + if full_wav_out_p.is_file(): + full_wav_out_p.unlink() + if full_target_out_p.is_file(): + full_target_out_p.unlink() -def lightning_move_test(discard): - return gui.lightning_copy and prefs.show_transfer + cache_dir = tmp_cache_dir() + if not os.path.isdir(cache_dir): + os.makedirs(cache_dir) + if prefs.transcode_codec in ("opus", "ogg", "flac", "mp3"): + global core_use + cores = os.cpu_count() -# def copy_deco(): -# line = "Copy" -# if key_shift_down: -# line = "Copy" #Folder From Library" -# else: -# line = "Copy" -# -# -# return [colours.menu_text, colours.menu_background, line] + total = len(folder_items) + gui.transcoding_batch_total = total + gui.transcoding_bach_done = 0 + dones = [] + q = 0 + while True: + if core_use < cores and q < len(folder_items): + agg = [[folder_items[q], folder_name]] + if agg not in dones: + core_use += 1 + dones.append(agg) + loaderThread = threading.Thread(target=transcode_single, args=agg) + loaderThread.daemon = True + loaderThread.start() -# playlist_menu.add('Paste', append_here, paste_deco) + q += 1 + gui.update += 1 + time.sleep(0.05) + if gui.tc_cancel: + while core_use > 0: + time.sleep(1) + break + if q == len(folder_items) and core_use == 0: + gui.update += 1 + break -def unique_template(string): - return "<t>" in string or \ - "<title>" in string or \ - "<n>" in string or \ - "<number>" in string or \ - "<tracknumber>" in string or \ - "<tn>" in string or \ - "<sn>" in string or \ - "<singlenumber>" in string or \ - "<s>" in string or "%t" in string or "%tn" in string + else: + logging.error("Codec error") + output_dir = prefs.encoder_output / folder_name + if prefs.transcode_inplace: + try: + output_dir.unlink() + except Exception: + logging.exception("Encode folder not removed") + reload_metadata(folder_items[0]) + else: + album_art_gen.save_thumb(pctl.get_track(folder_items[0]), (1080, 1080), str(output_dir / "cover")) -def re_template_word(word, tr): - if word == "aa" or word == "albumartist": + #logging.info(transcode_list[0]) - if tr.album_artist: - return tr.album_artist - return tr.artist + del transcode_list[0] + transcode_state = "" + gui.update += 1 - if word == "a" or word == "artist": - return tr.artist + except Exception: + logging.exception("Transcode failed") + transcode_state = "Transcode Error" + time.sleep(0.2) + show_message(_("Transcode failed."), _("An error was encountered."), mode="error") + gui.update += 1 + time.sleep(0.1) + del transcode_list[0] - if word == "t" or word == "title": - return tr.title + if len(transcode_list) == 0: + if gui.tc_cancel: + gui.tc_cancel = False + show_message( + _("The transcode was canceled before completion."), + _("Incomplete files will remain."), + mode="warning") + else: + line = _("Press F9 to show output.") + if prefs.transcode_codec == "flac": + line = _("Note that any associated output picture is a thumbnail and not an exact copy.") + if not gui.sync_progress: + if not gui.message_box: + show_message(_("Encoding complete."), line, mode="done") + if system == "Linux" and de_notify_support: + g_tc_notify.show() - if word == "n" or word == "number" or word == "tracknumber" or word == "tn": - if len(str(tr.track_number)) < 2: - return "0" + str(tr.track_number) - return str(tr.track_number) + if to_scan: + while to_scan: + track = to_scan[0] + star = star_store.full_get(track) + star_store.remove(track) + pctl.master_library[track] = tag_scan(pctl.master_library[track]) + star_store.merge(track, star) + lastfm.sync_pull_love(pctl.master_library[track]) + del to_scan[0] + gui.update += 1 + album_artist_dict.clear() + pctl.notify_change() + gui.pl_update += 1 - if word == "sn" or word == "singlenumber" or word == "singletracknumber" or word == "s": - return str(tr.track_number) + if loaderCommandReady is True: + for order in load_orders: + if order.stage == 1: + if loaderCommand == LC_Folder: + to_get = 0 + to_got = 0 + loaded_pathes_cache, loaded_cue_cache = cache_paths() + # pre_get(order.target) + if order.force_scan: + gets(order.target, force_scan=True) + else: + gets(order.target) + elif loaderCommand == LC_File: + loaded_pathes_cache, loaded_cue_cache = cache_paths() + add_file(order.target) - if word == "d" or word == "date" or word == "year": - return str(tr.date) + if gui.im_cancel: + gui.im_cancel = False + to_get = 0 + to_got = 0 + load_orders.clear() + added = [] + loaderCommand = LC_Done + loaderCommandReady = False + break - if word == "b" or "album" in word: - return str(tr.album) + loaderCommand = LC_Done + #logging.info("LOAD ORDER") + order.tracks = added - if word == "g" or word == "genre": - return tr.genre + # Double check for cue dupes + for i in reversed(range(len(order.tracks))): + if pctl.master_library[order.tracks[i]].fullpath in cue_list: + if pctl.master_library[order.tracks[i]].is_cue is False: + del order.tracks[i] - if word == "x" or "ext" in word or "file" in word: - return tr.file_ext.lower() + added = [] + order.stage = 2 + loaderCommandReady = False + #logging.info("DONE LOADING") + break - if word == "ux" or "upper" in word: - return tr.file_ext.upper() +def get_album_info(position, pl: int | None = None): + playlist = default_playlist + if pl is not None: + playlist = pctl.multi_playlist[pl].playlist_ids - if word == "c" or "composer" in word: - return tr.composer + global album_info_cache_key - if "comment" in word: - return tr.comment.replace("\n", "").replace("\r", "") + if album_info_cache_key != (pctl.selected_in_playlist, pctl.playing_object()): # Premature optimisation? + album_info_cache.clear() + album_info_cache_key = (pctl.selected_in_playlist, pctl.playing_object()) - return "" + if position in album_info_cache: + return album_info_cache[position] + if album_dex and album_mode and (pl is None or pl == pctl.active_playlist_viewing): + dex = album_dex + else: + dex = reload_albums(custom_list=playlist) -def parse_template2(string: str, track_object: TrackClass, strict: bool = False): - temp = "" - out = "" + end = len(playlist) + start = 0 - mode = 0 + for i, p in enumerate(reversed(dex)): + if p <= position: + start = p + break + end = p - for c in string: + album = list(range(start, end)) - if mode == 0: + playing = 0 + select = False - if c == "<": - mode = 1 - else: - out += c + if pctl.selected_in_playlist in album: + select = True - else: + if len(pctl.track_queue) > 0 and p < len(playlist): + if pctl.track_queue[pctl.queue_step] in playlist[start:end]: + playing = 1 - if c == ">": + album_info_cache[position] = playing, album, select + return playing, album, select - test = re_template_word(temp, track_object) - if strict: - assert test - out += test +def get_folder_list(index: int): + playlist = [] - mode = 0 - temp = "" + for item in default_playlist: + if pctl.master_library[item].parent_folder_name == pctl.master_library[index].parent_folder_name and \ + pctl.master_library[item].album == pctl.master_library[index].album: + playlist.append(item) + return list(set(playlist)) - else: +def gal_jump_select(up=False, num=1): + old_selected = pctl.selected_in_playlist + old_num = num - temp += c + if not default_playlist: + return - if "<und" in string: - out = out.replace(" ", "_") + on = pctl.selected_in_playlist + if on > len(default_playlist) - 1: + on = 0 + pctl.selected_in_playlist = 0 - return parse_template(out, track_object, strict=strict) + if up is False: + while num > 0: + while pctl.master_library[ + default_playlist[on]].parent_folder_name == pctl.master_library[ + default_playlist[pctl.selected_in_playlist]].parent_folder_name: + on += 1 -def parse_template(string, track_object: TrackClass, up_ext: bool = False, strict: bool = False): - set = 0 - underscore = False - output = "" + if on > len(default_playlist) - 1: + pctl.selected_in_playlist = old_selected + return - while set < len(string): - if string[set] == "%" and set < len(string) - 1: - set += 1 - if string[set] == "n": - if len(str(track_object.track_number)) < 2: - output += "0" - if strict: - assert str(track_object.track_number) - output += str(track_object.track_number) - elif string[set] == "a": - if up_ext and track_object.album_artist != "": # Context of renaming a folder - output += track_object.album_artist - else: - if strict: - assert track_object.artist - output += track_object.artist - elif string[set] == "t": - if strict: - assert track_object.title - output += track_object.title - elif string[set] == "c": - if strict: - assert track_object.composer - output += track_object.composer - elif string[set] == "d": - if strict: - assert track_object.date - output += track_object.date - elif string[set] == "b": - if strict: - assert track_object.album - output += track_object.album - elif string[set] == "x": - if up_ext: - output += track_object.file_ext.upper() - else: - output += "." + track_object.file_ext.lower() - elif string[set] == "u": - underscore = True - else: - output += string[set] - set += 1 + pctl.selected_in_playlist = on + num -= 1 + else: + if num > 1: + if pctl.selected_in_playlist > len(default_playlist) - 1: + pctl.selected_in_playlist = old_selected + return - output = output.rstrip(" -").lstrip(" -") + alb = get_album_info(pctl.selected_in_playlist) + if alb[1][0] in album_dex[:num]: + pctl.selected_in_playlist = old_selected + return - if underscore: - output = output.replace(" ", "_") + while num > 0: + alb = get_album_info(pctl.selected_in_playlist) - # Attempt to ensure the output text is filename safe - output = filename_safe(output) + if alb[1][0] > -1: + on = alb[1][0] - 1 - return output + pctl.selected_in_playlist = max(get_album_info(on)[1][0], 0) + num -= 1 +def gen_power2(): + tags = {} # [tag name]: (first position, number of times we saw it) + tag_list = [] -# Create playlist tab menu -tab_menu = Menu(160, show_icons=True) -radio_tab_menu = Menu(160, show_icons=True) + last = "a" + noise = 0 + def key(tag): + return tags[tag][1] -def rename_playlist(index, generator: bool = False) -> None: - gui.rename_playlist_box = True - rename_playlist_box.edit_generator = False - rename_playlist_box.playlist_index = index - rename_playlist_box.x = mouse_position[0] - rename_playlist_box.y = mouse_position[1] + for position in album_dex: - if generator: - rename_playlist_box.y = window_size[1] // 2 - round(200 * gui.scale) - rename_playlist_box.x = window_size[0] // 2 - round(250 * gui.scale) + index = default_playlist[position] + track = pctl.get_track(index) - rename_playlist_box.y = min(rename_playlist_box.y, round(350 * gui.scale)) + crumbs = track.parent_folder_path.split("/") - if rename_playlist_box.y < gui.panelY: - rename_playlist_box.y = gui.panelY + 10 * gui.scale + for i, b in enumerate(crumbs): - if gui.radio_view: - rename_text_area.set_text(pctl.radio_playlists[index]["name"]) - else: - rename_text_area.set_text(pctl.multi_playlist[index].title) - rename_text_area.highlight_all() - gui.gen_code_errors = False + if i > 0 and (track.artist in b and track.artist): + tag = crumbs[i - 1] - if generator: - rename_playlist_box.toggle_edit_gen() + if tag != last: + noise += 1 + last = tag + if tag in tags: + tags[tag][1] += 1 + else: + tags[tag] = [position, 1, "/".join(crumbs[:i])] + tag_list.append(tag) + break -def edit_generator_box(index: int) -> None: - rename_playlist(index, generator=True) + if noise > len(album_dex) / 2: + #logging.info("Playlist is too noisy for power bar.") + return [] + tag_list_sort = sorted(tag_list, key=key, reverse=True) -tab_menu.add(MenuItem(_("Rename"), rename_playlist, pass_ref=True, hint="Ctrl+R")) -radio_tab_menu.add(MenuItem(_("Rename"), rename_playlist, pass_ref=True, hint="Ctrl+R")) + max_tags = round((window_size[1] - gui.panelY - gui.panelBY - 10) // 30 * gui.scale) + tag_list_sort = tag_list_sort[:max_tags] -def pin_playlist_toggle(pl: int) -> None: - pctl.multi_playlist[pl].hidden ^= True + for i in reversed(range(len(tag_list))): + if tag_list[i] not in tag_list_sort: + del tag_list[i] + h = [] -def pl_pin_deco(pl: int): - # if pctl.multi_playlist[pl].hidden == True and tab_menu.pos[1] > + for tag in tag_list: - if pctl.multi_playlist[pl].hidden == True: - return [colours.menu_text, colours.menu_background, _("Pin")] - return [colours.menu_text, colours.menu_background, _("Unpin")] + if tags[tag][1] > 2: + t = PowerTag() + t.path = tags[tag][2] + t.name = tag.upper() + t.position = tags[tag][0] + h.append(t) + cc = random.random() + cj = 0.03 + if len(h) < 5: + cj = 0.11 -tab_menu.add(MenuItem("Pin", pin_playlist_toggle, pl_pin_deco, pass_ref=True, pass_ref_deco=True)) + cj = 0.5 / max(len(h), 2) + for item in h: + item.colour = hsl_to_rgb(cc, 0.8, 0.7) + cc += cj -def pl_lock_deco(pl: int): - if pctl.multi_playlist[pl].locked == True: - return [colours.menu_text, colours.menu_background, _("Unlock")] - return [colours.menu_text, colours.menu_background, _("Lock")] + return h +def reload_albums(quiet: bool = False, return_playlist: int = -1, custom_list=None) -> list[int] | None: + global album_dex + global update_layout + global old_album_pos -def view_pl_is_locked(_) -> bool: - return pctl.multi_playlist[pctl.active_playlist_viewing].locked + if cm_clean_db: + # Doing reload while things are being removed may cause crash + return None + dex = [] + current_folder = "" + current_album = "" + current_artist = "" + current_date = "" + current_title = "" -def pl_is_locked(pl: int) -> bool: - if not pctl.multi_playlist: - return False - return pctl.multi_playlist[pl].locked + if custom_list is not None: + playlist = custom_list + else: + target_pl_no = pctl.active_playlist_viewing + if return_playlist > -1: + target_pl_no = return_playlist + playlist = pctl.multi_playlist[target_pl_no].playlist_ids -def lock_playlist_toggle(pl: int) -> None: - pctl.multi_playlist[pl].locked ^= True + for i in range(len(playlist)): + tr = pctl.master_library[playlist[i]] + split = False + if i == 0: + split = True + elif tr.parent_folder_path != current_folder and tr.date and tr.date != current_date: + split = True + elif prefs.gallery_combine_disc and "Disc" in tr.album and "Disc" in current_album and tr.album.split("Disc")[0].rstrip(" ") == current_album.split("Disc")[0].rstrip(" "): + split = False + elif prefs.gallery_combine_disc and "CD" in tr.album and "CD" in current_album and tr.album.split("CD")[0].rstrip() == current_album.split("CD")[0].rstrip(): + split = False + elif prefs.gallery_combine_disc and "cd" in tr.album and "cd" in current_album and tr.album.split("cd")[0].rstrip() == current_album.split("cd")[0].rstrip(): + split = False + elif tr.album and tr.album == current_album and prefs.gallery_combine_disc: + split = False + elif tr.parent_folder_path != current_folder or current_title != tr.parent_folder_name: + split = True -def lock_colour_callback(): - if pctl.multi_playlist[gui.tab_menu_pl].locked: - if colours.lm: - return [230, 180, 60, 255] - return [240, 190, 10, 255] - return None - + if split: + dex.append(i) + current_folder = tr.parent_folder_path + current_title = tr.parent_folder_name + current_album = tr.album + current_date = tr.date + current_artist = tr.artist -lock_asset = asset_loader(scaled_asset_directory, loaded_asset_dc, "lock.png", True) -lock_icon = MenuIcon(lock_asset) -lock_icon.base_asset_mod = asset_loader(scaled_asset_directory, loaded_asset_dc, "unlock.png", True) -lock_icon.colour = [240, 190, 10, 255] -lock_icon.colour_callback = lock_colour_callback -lock_icon.xoff = 4 -lock_icon.yoff = -1 + if return_playlist > -1 or custom_list: + return dex -tab_menu.add(MenuItem(_("Lock"), lock_playlist_toggle, pl_lock_deco, - pass_ref=True, pass_ref_deco=True, icon=lock_icon, show_test=test_shift)) + album_dex = dex + album_info_cache.clear() + gui.update += 2 + gui.pl_update = 1 + update_layout = True + if not quiet: + goto_album(pctl.playlist_playing_position) -def export_m3u(pl: int, direc: str | None = None, relative: bool = False, show: bool = True) -> int | str: - if len(pctl.multi_playlist[pl].playlist_ids) < 1: - show_message(_("There are no tracks in this playlist. Nothing to export")) - return 1 + # Generate POWER BAR + gui.power_bar = gen_power2() + gui.pt = 0 - if not direc: - direc = str(user_directory / "playlists") - if not os.path.exists(direc): - os.makedirs(direc) - target = os.path.join(direc, pctl.multi_playlist[pl].title + ".m3u") +def star_line_toggle(mode: int= 0) -> bool | None: + if mode == 1: + return gui.star_mode == "line" - f = open(target, "w", encoding="utf-8") - f.write("#EXTM3U") - for number in pctl.multi_playlist[pl].playlist_ids: - track = pctl.master_library[number] - title = track.artist - if title: - title += " - " - title += track.title + if gui.star_mode == "line": + gui.star_mode = "none" + else: + gui.star_mode = "line" - if not track.is_network: - f.write("\n#EXTINF:") - f.write(str(round(track.length))) - if title: - f.write(f",{title}") - path = track.fullpath - if relative: - path = os.path.relpath(path, start=direc) - f.write(f"\n{path}") - f.close() + gui.show_ratings = False - if show: - line = direc - line += "/" - if system == "Windows" or msys: - os.startfile(line) - elif macos: - subprocess.Popen(["open", line]) - else: - subprocess.Popen(["xdg-open", line]) - return target + gui.update += 1 + gui.pl_update = 1 + return None +def star_toggle(mode: int = 0) -> bool | None: + if gui.show_ratings: + if mode == 1: + return prefs.rating_playtime_stars + prefs.rating_playtime_stars ^= True -def export_xspf(pl: int, direc: str | None = None, relative: bool = False, show: bool = True) -> int | str: - if len(pctl.multi_playlist[pl].playlist_ids) < 1: - show_message(_("There are no tracks in this playlist. Nothing to export")) - return 1 + else: + if mode == 1: + return gui.star_mode == "star" - if not direc: - direc = str(user_directory / "playlists") - if not os.path.exists(direc): - os.makedirs(direc) + if gui.star_mode == "star": + gui.star_mode = "none" + else: + gui.star_mode = "star" - target = os.path.join(direc, pctl.multi_playlist[pl].title + ".xspf") + # gui.show_ratings = False + gui.update += 1 + gui.pl_update = 1 + return None - xspf_root = ET.Element("playlist", version="1", xmlns="http://xspf.org/ns/0/") - xspf_tracklist_tag = ET.SubElement(xspf_root, "trackList") +def heart_toggle(mode: int = 0) -> bool | None: + if mode == 1: + return gui.show_hearts - for number in pctl.multi_playlist[pl].playlist_ids: - track = pctl.master_library[number] - path = track.fullpath - if relative: - path = os.path.relpath(path, start=direc) + gui.show_hearts ^= True + # gui.show_ratings = False - xspf_track_tag = ET.SubElement(xspf_tracklist_tag, "track") - if track.title != "": - ET.SubElement(xspf_track_tag, "title").text = track.title - if track.is_cue is False and track.fullpath != "": - ET.SubElement(xspf_track_tag, "location").text = urllib.parse.quote(path) - if track.artist != "": - ET.SubElement(xspf_track_tag, "creator").text = track.artist - if track.album != "": - ET.SubElement(xspf_track_tag, "album").text = track.album - if track.track_number != "": - ET.SubElement(xspf_track_tag, "trackNum").text = str(track.track_number) + gui.update += 1 + gui.pl_update = 1 + return None - ET.SubElement(xspf_track_tag, "duration").text = str(int(track.length * 1000)) +def album_rating_toggle(mode: int = 0) -> bool | None: + if mode == 1: + return gui.show_album_ratings - xspf_tree = ET.ElementTree(xspf_root) - ET.indent(xspf_tree, space=' ', level=0) - xspf_tree.write(target, encoding='UTF-8', xml_declaration=True) + gui.show_album_ratings ^= True - if show: - line = direc - line += "/" - if system == "Windows" or msys: - os.startfile(line) - elif macos: - subprocess.Popen(["open", line]) - else: - subprocess.Popen(["xdg-open", line]) + gui.update += 1 + gui.pl_update = 1 + return None - return target +def rating_toggle(mode: int = 0) -> bool | None: + if mode == 1: + return gui.show_ratings + gui.show_ratings ^= True -def reload(): - if album_mode: - reload_albums(quiet=True) + if gui.show_ratings: + # gui.show_hearts = False + gui.star_mode = "none" + prefs.rating_playtime_stars = True + if not prefs.write_ratings: + show_message(_("Note that ratings are stored in the local database and not written to tags.")) - # tree_view_box.clear_all() - # elif gui.combo_mode: - # reload_albums(quiet=True) - # combo_pl_render.prep() + gui.update += 1 + gui.pl_update = 1 + return None +def toggle_titlebar_line(mode: int = 0) -> bool | None: + global update_title + if mode == 1: + return update_title -def clear_playlist(index: int): - global default_playlist + line = window_title + SDL_SetWindowTitle(t_window, line) + update_title ^= True + if update_title: + update_title_do() + return None - if pl_is_locked(index): - show_message(_("Playlist is locked to prevent accidental erasure")) - return +def toggle_meta_persists_stop(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.meta_persists_stop + prefs.meta_persists_stop ^= True + return None - pctl.multi_playlist[index].last_folder.clear() # clear import folder list # TODO(Martin): This was actually a string not a list wth? +def toggle_side_panel_layout(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.side_panel_layout == 1 - if not pctl.multi_playlist[index].playlist_ids: - logging.info("Playlist is already empty") - return + if prefs.side_panel_layout == 1: + prefs.side_panel_layout = 0 + else: + prefs.side_panel_layout = 1 + return None - li = [] - for i, ref in enumerate(pctl.multi_playlist[index].playlist_ids): - li.append((i, ref)) +def toggle_meta_shows_selected(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.meta_shows_selected_always + prefs.meta_shows_selected_always ^= True + return None - undo.bk_tracks(index, list(reversed(li))) +def scale1(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.ui_scale == 1: + return True + return False - del pctl.multi_playlist[index].playlist_ids[:] - if pctl.active_playlist_viewing == index: - default_playlist = pctl.multi_playlist[index].playlist_ids - reload() + prefs.ui_scale = 1 + pref_box.large_preset() - # pctl.playlist_playing = 0 - pctl.multi_playlist[index].position = 0 - if index == pctl.active_playlist_viewing: - pctl.playlist_view_position = 0 + if prefs.ui_scale != gui.scale: + show_message(_("Change will be applied on restart.")) + return None - gui.pl_update = 1 +def scale125(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.ui_scale == 1.25: + return True + return False + return None + prefs.ui_scale = 1.25 + pref_box.large_preset() -def convert_playlist(pl: int, get_list: bool = False) -> list[list[int]]| None: - global transcode_list + if prefs.ui_scale != gui.scale: + show_message(_("Change will be applied on restart.")) + return None - if not tauon.test_ffmpeg(): - return None - paths: list[str] = [] - folders: list[list[int]] = [] - - for track in pctl.multi_playlist[pl].playlist_ids: - if pctl.master_library[track].parent_folder_path not in paths: - paths.append(pctl.master_library[track].parent_folder_path) +def toggle_use_tray(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.use_tray + prefs.use_tray ^= True + if not prefs.use_tray: + prefs.min_to_tray = False + gnome.hide_indicator() + else: + gnome.show_indicator() + return None - for path in paths: - folder: list[int] = [] - for track in pctl.multi_playlist[pl].playlist_ids: - if pctl.master_library[track].parent_folder_path == path: - folder.append(track) - if prefs.transcode_codec == "flac" and pctl.master_library[track].file_ext.lower() in ( - "mp3", "opus", - "m4a", "mp4", - "ogg", "aac"): - show_message(_("This includes the conversion of a lossy codec to a lossless one!")) +def toggle_text_tray(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.tray_show_title + prefs.tray_show_title ^= True + pctl.notify_update() + return None - folders.append(folder) +def toggle_min_tray(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.min_to_tray + prefs.min_to_tray ^= True + return None - if get_list: - return folders +def scale2(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.ui_scale == 2: + return True + return False - transcode_list.extend(folders) + prefs.ui_scale = 2 + pref_box.large_preset() + if prefs.ui_scale != gui.scale: + show_message(_("Change will be applied on restart.")) + return None -def get_folder_tracks_local(pl_in: int) -> list[int]: - selection = [] - parent = os.path.normpath(pctl.master_library[default_playlist[pl_in]].parent_folder_path) - while pl_in < len(default_playlist) and parent == os.path.normpath( - pctl.master_library[default_playlist[pl_in]].parent_folder_path): - selection.append(pl_in) - pl_in += 1 - return selection +def toggle_borderless(mode: int = 0) -> bool | None: + global draw_border + global update_layout + if mode == 1: + return draw_border -def test_pl_tab_locked(pl: int) -> bool: - if gui.radio_view: - return False - return pctl.multi_playlist[pl].locked + update_layout = True + draw_border ^= True + if draw_border: + SDL_SetWindowBordered(t_window, False) + else: + SDL_SetWindowBordered(t_window, True) + return None -# Clear playlist -tab_menu.add(MenuItem(_("Clear"), clear_playlist, pass_ref=True, disable_test=test_pl_tab_locked, pass_ref_deco=True)) +def toggle_break(mode: int = 0) -> bool | None: + global break_enable + if mode == 1: + return break_enable ^ True + break_enable ^= True + gui.pl_update = 1 + return None +def toggle_scroll(mode: int = 0) -> bool | None: + global scroll_enable + global update_layout -def move_radio_playlist(source, dest): - if dest > source: - dest += 1 - try: - temp = pctl.radio_playlists[source] - pctl.radio_playlists[source] = "old" - pctl.radio_playlists.insert(dest, temp) - pctl.radio_playlists.remove("old") - pctl.radio_playlist_viewing = pctl.radio_playlists.index(temp) - except Exception: - logging.exception("Playlist move error") + if mode == 1: + if scroll_enable: + return False + return True + scroll_enable ^= True + gui.pl_update = 1 + update_layout = True + return None -def move_playlist(source, dest): - global default_playlist - if dest > source: - dest += 1 - try: - active = pctl.multi_playlist[pctl.active_playlist_playing] - view = pctl.multi_playlist[pctl.active_playlist_viewing] +def toggle_hide_bar(mode: int = 0) -> bool | None: + if mode == 1: + return gui.set_bar ^ True + gui.update_layout() + gui.set_bar ^= True + show_message(_("Tip: You can also toggle this from a right-click context menu")) + return None - temp = pctl.multi_playlist[source] - pctl.multi_playlist[source] = "old" - pctl.multi_playlist.insert(dest, temp) - pctl.multi_playlist.remove("old") +def toggle_append_total_time(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.append_total_time + prefs.append_total_time ^= True + gui.pl_update = 1 + gui.update += 1 + return None - pctl.active_playlist_playing = pctl.multi_playlist.index(active) - pctl.active_playlist_viewing = pctl.multi_playlist.index(view) - default_playlist = default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - except Exception: - logging.exception("Playlist move error") +def toggle_append_date(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.append_date + prefs.append_date ^= True + gui.pl_update = 1 + gui.update += 1 + return None +def toggle_true_shuffle(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.true_shuffle + prefs.true_shuffle ^= True + return None -def delete_playlist(index: int, force: bool = False, check_lock: bool = False) -> None: - if gui.radio_view: - del pctl.radio_playlists[index] - if not pctl.radio_playlists: - pctl.radio_playlists = [{"uid": uid_gen(), "name": "Default", "items": []}] - return +def toggle_auto_artist_dl(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.auto_dl_artist_data + prefs.auto_dl_artist_data ^= True + for artist, value in list(artist_list_box.thumb_cache.items()): + if value is None: + del artist_list_box.thumb_cache[artist] + return None - global default_playlist +def toggle_enable_web(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.enable_web - if check_lock and pl_is_locked(index): - show_message(_("Playlist is locked to prevent accidental deletion")) - return + prefs.enable_web ^= True - if not force: - if pl_is_locked(index): - show_message(_("Playlist is locked to prevent accidental deletion")) - return + if prefs.enable_web and not gui.web_running: + webThread = threading.Thread( + target=webserve, args=[pctl, prefs, gui, album_art_gen, str(install_directory), strings, tauon]) + webThread.daemon = True + webThread.start() + show_message(_("Web server starting"), _("External connections will be accepted."), mode="done") - if gui.rename_playlist_box: - return + elif prefs.enable_web is False: + if tauon.radio_server is not None: + tauon.radio_server.shutdown() + gui.web_running = False - # Set screen to be redrawn - gui.pl_update = 1 - gui.update += 1 + time.sleep(0.25) + return None - # Backup the playlist to be deleted - # pctl.playlist_backup.append(pctl.multi_playlist[index]) - # pctl.playlist_backup.append(pctl.multi_playlist[index]) - undo.bk_playlist(index) +def toggle_scrobble_mark(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.scrobble_mark + prefs.scrobble_mark ^= True + return None - # If we're deleting the final playlist, delete it and create a blank one in place - if len(pctl.multi_playlist) == 1: - logging.warning("Deleting final playlist and creating a new Default one") - pctl.multi_playlist.clear() - pctl.multi_playlist.append(pl_gen()) - default_playlist = pctl.multi_playlist[0].playlist_ids - pctl.active_playlist_playing = 0 - return +def toggle_lfm_auto(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.auto_lfm + prefs.auto_lfm ^= True + if prefs.auto_lfm and not last_fm_enable: + show_message(_("Optional module python-pylast not installed"), mode="warning") + prefs.auto_lfm = False + # if prefs.auto_lfm: + # lastfm.hold = False + # else: + # lastfm.hold = True + return None - # Take note of the id of the playing playlist - old_playing_id = pctl.multi_playlist[pctl.active_playlist_playing].uuid_int +def toggle_lb(mode: int = 0) -> bool | None: + if mode == 1: + return lb.enable + if not lb.enable and not prefs.lb_token: + show_message(_("Can't enable this if there's no token."), mode="warning") + return None + lb.enable ^= True + return None - # Take note of the id of the viewed open playlist - old_view_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int +def toggle_maloja(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.maloja_enable + if not prefs.maloja_url or not prefs.maloja_key: + show_message(_("One or more fields is missing."), mode="warning") + return None + prefs.maloja_enable ^= True + return None - # Delete the requested playlist - del pctl.multi_playlist[index] +def toggle_ex_del(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.auto_del_zip + prefs.auto_del_zip ^= True + # if prefs.auto_del_zip is True: + # show_message("Caution! This function deletes things!", mode='info', "This could result in data loss if the process were to malfunction.") + return None - # Re-set the open viewed playlist number by uid - for i, pl in enumerate(pctl.multi_playlist): +def toggle_dl_mon(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.monitor_downloads + prefs.monitor_downloads ^= True + return None - if pl.uuid_int == old_view_id: - pctl.active_playlist_viewing = i - break - else: - # logging.info("Lost the viewed playlist!") - # Try find the playing playlist and make it the viewed playlist - for i, pl in enumerate(pctl.multi_playlist): - if pl.uuid_int == old_playing_id: - pctl.active_playlist_viewing = i - break - else: - # Playing playlist was deleted, lets just move down one playlist - if pctl.active_playlist_viewing > 0: - pctl.active_playlist_viewing -= 1 +def toggle_music_ex(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.extract_to_music + prefs.extract_to_music ^= True + return None - # Re-initiate the now viewed playlist - if old_view_id != pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int: - default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - pctl.playlist_view_position = pctl.multi_playlist[pctl.active_playlist_viewing].position - logging.debug("Position reset by playlist delete") - pctl.selected_in_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].selected - shift_selection = [pctl.selected_in_playlist] +def toggle_extract(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.auto_extract + prefs.auto_extract ^= True + if prefs.auto_extract is False: + prefs.auto_del_zip = False + return None - if album_mode: - reload_albums(True) - goto_album(pctl.playlist_view_position) +def toggle_top_tabs(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.tabs_on_top + prefs.tabs_on_top ^= True + return None - # Re-set the playing playlist number by uid - for i, pl in enumerate(pctl.multi_playlist): +#def toggle_guitar_chords(mode: int = 0) -> bool | None: +# if mode == 1: +# return prefs.guitar_chords +# prefs.guitar_chords ^= True +# return None - if pl.uuid_int == old_playing_id: - pctl.active_playlist_playing = i - break - else: - logging.info("Lost the playing playlist!") - pctl.active_playlist_playing = pctl.active_playlist_viewing - pctl.playlist_playing_position = -1 +# def toggle_auto_lyrics(mode: int = 0) -> bool | None: +# if mode == 1: +# return prefs.auto_lyrics +# prefs.auto_lyrics ^= True - test_show_add_home_music() +def switch_single(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.transcode_mode == "single": + return True + return False + prefs.transcode_mode = "single" + return None - # Cleanup - ids = [] - for p in pctl.multi_playlist: - ids.append(p.uuid_int) +def switch_mp3(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.transcode_codec == "mp3": + return True + return False + prefs.transcode_codec = "mp3" + return None - for key in list(gui.gallery_positions.keys()): - if key not in ids: - del gui.gallery_positions[key] - for key in list(pctl.gen_codes.keys()): - if key not in ids: - del pctl.gen_codes[key] +def switch_ogg(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.transcode_codec == "ogg": + return True + return False + prefs.transcode_codec = "ogg" + return None - pctl.db_inc += 1 +def switch_opus(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.transcode_codec == "opus": + return True + return False + prefs.transcode_codec = "opus" + return None -to_scan = [] +def switch_opus_ogg(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.transcode_opus_as: + return True + return False + prefs.transcode_opus_as ^= True + return None +def toggle_transcode_output(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.transcode_inplace: + return False + return True + prefs.transcode_inplace ^= True + if prefs.transcode_inplace: + transcode_icon.colour = [250, 20, 20, 255] + show_message( + _("DANGER! This will delete the original files. Keeping a backup is recommended in case of malfunction."), + _("For safety, this setting will default to off. Embedded thumbnails are not kept so you may want to extract them first."), + mode="warning") + else: + transcode_icon.colour = [239, 74, 157, 255] + return None -def delete_playlist_force(index: int): - delete_playlist(index, force=True, check_lock=True) +def toggle_transcode_inplace(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.transcode_inplace: + return True + return False + if gui.sync_progress: + prefs.transcode_inplace = False + return None -def delete_playlist_by_id(id: int, force: bool = False, check_lock: bool = False) -> None: - delete_playlist(id_to_pl(id), force=force, check_lock=check_lock) + prefs.transcode_inplace ^= True + if prefs.transcode_inplace: + transcode_icon.colour = [250, 20, 20, 255] + show_message( + _("DANGER! This will delete the original files. Keeping a backup is recommended in case of malfunction."), + _("For safety, this setting will reset on restart. Embedded thumbnails are not kept so you may want to extract them first."), + mode="warning") + else: + transcode_icon.colour = [239, 74, 157, 255] + return None +def switch_flac(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.transcode_codec == "flac": + return True + return False + prefs.transcode_codec = "flac" + return None -def delete_playlist_ask(index: int): - print("ark") - if gui.radio_view: - delete_playlist_force(index) - return - gen = pctl.gen_codes.get(pl_to_id(index), "") - if (gen and not gen.startswith("self ")) or len(pctl.multi_playlist[index].playlist_ids) < 2: - delete_playlist(index) - return +def toggle_sbt(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.prefer_bottom_title + prefs.prefer_bottom_title ^= True + return None - gui.message_box_confirm_callback = delete_playlist_by_id - gui.message_box_confirm_reference = (pl_to_id(index), True, True) - show_message(_("Are you sure you want to delete playlist: {name}?").format(name=pctl.multi_playlist[index].title), mode="confirm") +def toggle_bba(mode: int = 0) -> bool | None: + if mode == 1: + return gui.bb_show_art + gui.bb_show_art ^= True + gui.update_layout() + return None +def toggle_use_title(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.use_title + prefs.use_title ^= True + return None -def rescan_tags(pl: int) -> None: - for track in pctl.multi_playlist[pl].playlist_ids: - if pctl.master_library[track].is_cue is False: - to_scan.append(track) - tauon.thread_manager.ready("worker") +def switch_rg_off(mode: int = 0) -> bool | None: + if mode == 1: + return True if prefs.replay_gain == 0 else False + prefs.replay_gain = 0 + return None +def switch_rg_track(mode: int = 0) -> bool | None: + if mode == 1: + return True if prefs.replay_gain == 1 else False + prefs.replay_gain = 0 if prefs.replay_gain == 1 else 1 + # prefs.replay_gain = 1 + return None -# def re_import(pl: int) -> None: -# -# path = pctl.multi_playlist[pl].last_folder -# if path == "": -# return -# for i in reversed(range(len(pctl.multi_playlist[pl].playlist_ids))): -# if path.replace('\\', '/') in pctl.master_library[pctl.multi_playlist[pl].playlist_ids[i]].parent_folder_path: -# del pctl.multi_playlist[pl].playlist_ids[i] -# -# load_order = LoadClass() -# load_order.replace_stem = True -# load_order.target = path -# load_order.playlist = pctl.multi_playlist[pl].uuid_int -# load_orders.append(copy.deepcopy(load_order)) +def switch_rg_album(mode: int = 0) -> bool | None: + if mode == 1: + return True if prefs.replay_gain == 2 else False + prefs.replay_gain = 0 if prefs.replay_gain == 2 else 2 + return None +def switch_rg_auto(mode: int = 0) -> bool | None: + if mode == 1: + return True if prefs.replay_gain == 3 else False + prefs.replay_gain = 0 if prefs.replay_gain == 3 else 3 + return None -def re_import2(pl: int) -> None: - paths = pctl.multi_playlist[pl].last_folder +def toggle_jump_crossfade(mode: int = 0) -> bool | None: + if mode == 1: + return True if prefs.use_jump_crossfade else False + prefs.use_jump_crossfade ^= True + return None - reduce_paths(paths) +def toggle_pause_fade(mode: int = 0) -> bool | None: + if mode == 1: + return True if prefs.use_pause_fade else False + prefs.use_pause_fade ^= True + return None - for path in paths: - if os.path.isdir(path): - load_order = LoadClass() - load_order.replace_stem = True - load_order.target = path - load_order.notify = True - load_order.playlist = pctl.multi_playlist[pl].uuid_int - load_orders.append(copy.deepcopy(load_order)) +def toggle_transition_crossfade(mode: int = 0) -> bool | None: + if mode == 1: + return True if prefs.use_transition_crossfade else False + prefs.use_transition_crossfade ^= True + return None - if paths: - show_message(_("Rescanning folders..."), mode="info") +def toggle_transition_gapless(mode: int = 0) -> bool | None: + if mode == 1: + return False if prefs.use_transition_crossfade else True + prefs.use_transition_crossfade ^= True + return None +def toggle_eq(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.use_eq + prefs.use_eq ^= True + pctl.playerCommand = "seteq" + pctl.playerCommandReady = True + return None -def rescan_all_folders(): - for i, p in enumerate(pctl.multi_playlist): - re_import2(i) +def reload_backend() -> None: + gui.backend_reloading = True + logging.info("Reload backend...") + wait = 0 + pre_state = pctl.stop(True) -def s_append(index: int): - paste(playlist_no=index) + while pctl.playerCommandReady: + time.sleep(0.01) + wait += 1 + if wait > 20: + break + if tauon.thread_manager.player_lock.locked(): + try: + tauon.thread_manager.player_lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked player_lock") + else: + logging.exception("Unknown RuntimeError trying to release player_lock") + except Exception: + logging.exception("Unknown error trying to release player_lock") + pctl.playerCommand = "unload" + pctl.playerCommandReady = True -def append_playlist(index: int): - global cargo - pctl.multi_playlist[index].playlist_ids += cargo + wait = 0 + while pctl.playerCommand != "done": + time.sleep(0.01) + wait += 1 + if wait > 200: + break - gui.pl_update = 1 - reload() + tauon.thread_manager.ready_playback() -def index_key(index: int): - tr = pctl.master_library[index] - s = str(tr.track_number) - d = str(tr.disc_number) + if pre_state == 1: + pctl.revert() + gui.backend_reloading = False - if "/" in d: - d = d.split("/")[0] +def gen_chart() -> None: + try: + topchart = t_topchart.TopChart(tauon, album_art_gen) - # Make sure the value for disc number is an int, make 1 if 0, otherwise ignore - if d: - try: - dd = int(d) - if dd < 2: - dd = 1 - d = str(dd) - except Exception: - logging.exception("Failed to parse as index as int") - d = "" + tracks = [] + source_tracks = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - # Add the disc number for sorting by CD, make it '1' if theres isnt one - if s or d: - if not d: - s = "1" + "d" + s + if prefs.topchart_sorts_played: + source_tracks = gen_folder_top(0, custom_list=source_tracks) + dex = reload_albums(quiet=True, custom_list=source_tracks) else: - s = d + "d" + s + dex = reload_albums(quiet=True, return_playlist=pctl.active_playlist_viewing) - # Use the filename if we dont have any metadata to sort by, - # since it could likely have the track number in it - else: - s = tr.filename + for item in dex: + tracks.append(pctl.get_track(source_tracks[item])) - if (not tr.disc_number or tr.disc_number == "0") and tr.is_cue: - s = tr.filename + "-" + s + cascade = False + if prefs.chart_cascade: + cascade = ( + (prefs.chart_c1, prefs.chart_c2, prefs.chart_c3), + (prefs.chart_d1, prefs.chart_d2, prefs.chart_d3)) + + path = topchart.generate( + tracks, prefs.chart_bg, prefs.chart_rows, prefs.chart_columns, prefs.chart_text, + prefs.chart_font, prefs.chart_tile, cascade) - # This splits the line by groups of numbers, causing the sorting algorithum to sort - # by those numbers. Should work for filenames, even with the disc number in the name - try: - return [tryint(c) for c in re.split("([0-9]+)", s)] except Exception: - logging.exception("Failed to parse as int, returning 'a'") - return "a" + logging.exception("There was an error generating the chart") + gui.generating_chart = False + show_message(_("There was an error generating the chart"), _("Sorry!"), mode="error") + return + gui.generating_chart = False -def sort_tracK_numbers_album_only(pl: int, custom_list=None): - current_folder = "" - albums = [] - if custom_list is None: - playlist = pctl.multi_playlist[pl].playlist_ids + if path: + open_file(path) else: - playlist = custom_list + show_message(_("There was an error generating the chart"), _("Sorry!"), mode="error") + return - for i in range(len(playlist)): - if i == 0: - albums.append(i) - current_folder = pctl.master_library[playlist[i]].album - elif pctl.master_library[playlist[i]].album != current_folder: - current_folder = pctl.master_library[playlist[i]].album - albums.append(i) + show_message(_("Chart generated"), mode="done") - i = 0 - while i < len(albums) - 1: - playlist[albums[i]:albums[i + 1]] = sorted(playlist[albums[i]:albums[i + 1]], key=index_key) - i += 1 - if len(albums) > 0: - playlist[albums[i]:] = sorted(playlist[albums[i]:], key=index_key) +def update_playlist_call(): + gui.update + 2 + gui.pl_update = 2 - gui.pl_update += 1 +# ---------------------------------------------------------------------------------------- +# ---------------------------------------------------------------------------------------- +def pl_is_mut(pl: int) -> bool: + id = pl_to_id(pl) + if id is None: + return False + return not (pctl.gen_codes.get(id) and "self" not in pctl.gen_codes[id]) +def clear_gen(id: int) -> None: + del pctl.gen_codes[id] + show_message(_("Okay, it's a normal playlist now."), mode="done") -def sort_track_2(pl: int, custom_list: list[int] | None = None) -> None: - current_folder = "" - current_album = "" - current_date = "" - albums = [] - if custom_list is None: - playlist = pctl.multi_playlist[pl].playlist_ids - else: - playlist = custom_list +def clear_gen_ask(id: int) -> None: + if "jelly\"" in pctl.gen_codes.get(id, ""): + return + if "spl\"" in pctl.gen_codes.get(id, ""): + return + if "tpl\"" in pctl.gen_codes.get(id, ""): + return + if "tar\"" in pctl.gen_codes.get(id, ""): + return + if "tmix\"" in pctl.gen_codes.get(id, ""): + return + gui.message_box_confirm_callback = clear_gen + gui.message_box_confirm_reference = (id,) + show_message(_("You added tracks to a generator playlist. Do you want to clear the generator?"), mode="confirm") - for i in range(len(playlist)): - tr = pctl.master_library[playlist[i]] - if i == 0: - albums.append(i) - current_folder = tr.parent_folder_path - current_album = tr.album - current_date = tr.date - elif tr.parent_folder_path != current_folder: - if tr.album == current_album and tr.album and tr.date == current_date and tr.disc_number \ - and os.path.dirname(tr.parent_folder_path) == os.path.dirname(current_folder): - continue - current_folder = tr.parent_folder_path - current_album = tr.album - current_date = tr.date - albums.append(i) +def set_mini_mode(): + if gui.fullscreen: + return - i = 0 - while i < len(albums) - 1: - playlist[albums[i]:albums[i + 1]] = sorted(playlist[albums[i]:albums[i + 1]], key=index_key) - i += 1 - if len(albums) > 0: - playlist[albums[i]:] = sorted(playlist[albums[i]:], key=index_key) + global mouse_down + global mouse_up + global old_window_position + mouse_down = False + mouse_up = False + inp.mouse_click = False - gui.pl_update += 1 + if gui.maximized: + SDL_RestoreWindow(t_window) + update_layout_do() + if gui.mode < 3: + old_window_position = get_window_position() -tauon.sort_track_2 = sort_track_2 + if prefs.mini_mode_on_top: + SDL_SetWindowAlwaysOnTop(t_window, True) + gui.mode = 3 + gui.vis = 0 + gui.turbo = False + gui.draw_vis4_top = False + gui.level_update = False -def key_filepath(index: int): - track = pctl.master_library[index] - return track.parent_folder_path.lower(), track.filename + i_y = pointer(c_int(0)) + i_x = pointer(c_int(0)) + SDL_GetWindowPosition(t_window, i_x, i_y) + gui.save_position = (i_x.contents.value, i_y.contents.value) + mini_mode.was_borderless = draw_border + SDL_SetWindowBordered(t_window, False) -def key_fullpath(index: int): - return pctl.master_library[index].fullpath + size = (350, 429) + if prefs.mini_mode_mode == 1: + size = (330, 330) + if prefs.mini_mode_mode == 2: + size = (420, 499) + if prefs.mini_mode_mode == 3: + size = (430, 430) + if prefs.mini_mode_mode == 4: + size = (330, 80) + if prefs.mini_mode_mode == 5: + size = (350, 545) + style_overlay.flush() + tauon.thread_manager.ready("style") + if logical_size == window_size: + size = (int(size[0] * gui.scale), int(size[1] * gui.scale)) -def key_filename(index: int): - track = pctl.master_library[index] - return track.filename + logical_size[0] = size[0] + logical_size[1] = size[1] + SDL_SetWindowMinimumSize(t_window, 100, 100) -def sort_path_pl(pl: int, custom_list=None): - if custom_list is not None: - target = custom_list - else: - target = pctl.multi_playlist[pl].playlist_ids + SDL_SetWindowResizable(t_window, False) + SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - if use_natsort and False: - target[:] = natsort.os_sorted(target, key=key_fullpath) - else: - target.sort(key=key_filepath) + if mini_mode.save_position: + SDL_SetWindowPosition(t_window, mini_mode.save_position[0], mini_mode.save_position[1]) + i_x = pointer(c_int(0)) + i_y = pointer(c_int(0)) + SDL_GL_GetDrawableSize(t_window, i_x, i_y) + window_size[0] = i_x.contents.value + window_size[1] = i_y.contents.value -def append_current_playing(index: int): - if tauon.spot_ctl.coasting: - tauon.spot_ctl.append_playing(index) - gui.pl_update = 1 - return - - if pctl.playing_state > 0 and len(pctl.track_queue) > 0: - pctl.multi_playlist[index].playlist_ids.append(pctl.track_queue[pctl.queue_step]) - gui.pl_update = 1 + gui.update += 3 +def restore_full_mode(): + logging.info("RESTORE FULL") + i_y = pointer(c_int(0)) + i_x = pointer(c_int(0)) + SDL_GetWindowPosition(t_window, i_x, i_y) + mini_mode.save_position = [i_x.contents.value, i_y.contents.value] -def export_stats(pl: int) -> None: - playlist_time = 0 - play_time = 0 - total_size = 0 - tracks_in_playlist = len(pctl.multi_playlist[pl].playlist_ids) + if not mini_mode.was_borderless: + SDL_SetWindowBordered(t_window, True) - seen_files = {} - seen_types = {} + logical_size[0] = gui.save_size[0] + logical_size[1] = gui.save_size[1] - mp3_bitrates = {} - ogg_bitrates = {} - m4a_bitrates = {} + SDL_SetWindowPosition(t_window, gui.save_position[0], gui.save_position[1]) - are_cue = 0 - for index in pctl.multi_playlist[pl].playlist_ids: - track = pctl.get_track(index) + SDL_SetWindowResizable(t_window, True) + SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) + SDL_SetWindowAlwaysOnTop(t_window, False) - playlist_time += int(track.length) - play_time += star_store.get(index) + # if macos: + # SDL_SetWindowMinimumSize(t_window, 560, 330) + # else: + SDL_SetWindowMinimumSize(t_window, 560, 330) - if track.is_cue: - are_cue += 1 + restore_ignore_timer.set() # Hacky - if track.file_ext == "MP3": - mp3_bitrates[track.bitrate] = mp3_bitrates.get(track.bitrate, 0) + 1 - if track.file_ext == "OGG" or track.file_ext == "OGA": - ogg_bitrates[track.bitrate] = ogg_bitrates.get(track.bitrate, 0) + 1 - if track.file_ext == "M4A": - m4a_bitrates[track.bitrate] = m4a_bitrates.get(track.bitrate, 0) + 1 + gui.mode = 1 - type = track.file_ext - if type == "OGA": - type = "OGG" - seen_types[type] = seen_types.get(type, 0) + 1 + global mouse_down + global mouse_up + mouse_down = False + mouse_up = False + inp.mouse_click = False - if track.fullpath and not track.is_network: - if track.fullpath not in seen_files: - size = track.size - if not size and os.path.isfile(track.fullpath): - size = os.path.getsize(track.fullpath) - seen_files[track.fullpath] = size + if gui.maximized: + SDL_MaximizeWindow(t_window) + time.sleep(0.05) + SDL_PumpEvents() + SDL_GetWindowSize(t_window, i_x, i_y) + logical_size[0] = i_x.contents.value + logical_size[1] = i_y.contents.value - total_size = sum(seen_files.values()) + #logging.info(window_size) - stats_gen.update(pl) - line = _("Playlist:") + "\n" + pctl.multi_playlist[pl].title + "\n\n" - line += _("Generated:") + "\n" + time.strftime("%c") + "\n\n" - line += _("Tracks in playlist:") + "\n" + str(tracks_in_playlist) - line += "\n\n" - line += _("Repeats in playlist:") + "\n" - unique = len(set(pctl.multi_playlist[pl].playlist_ids)) - line += str(tracks_in_playlist - unique) - line += "\n\n" - line += _("Total local size:") + "\n" + get_filesize_string(total_size) + "\n\n" - line += _("Playlist duration:") + "\n" + str(datetime.timedelta(seconds=int(playlist_time))) + "\n\n" - line += _("Total playtime:") + "\n" + str(datetime.timedelta(seconds=int(play_time))) + "\n\n" + SDL_PumpEvents() + SDL_GL_GetDrawableSize(t_window, i_x, i_y) + window_size[0] = i_x.contents.value + window_size[1] = i_y.contents.value - line += _("Track types:") + "\n" - if tracks_in_playlist: - types = sorted(seen_types, key=seen_types.get, reverse=True) - for type in types: - perc = round((seen_types.get(type) / tracks_in_playlist) * 100, 1) - if perc < 0.1: - perc = "<0.1" - if type == "SPOT": - type = "SPOTIFY" - if type == "SUB": - type = "AIRSONIC" - line += f"{type} ({perc}%); " - line = line.rstrip("; ") - line += "\n\n" + gui.update_layout() + if prefs.art_bg: + tauon.thread_manager.ready("style") - if tracks_in_playlist: - line += _("Percent of tracks are CUE type:") + "\n" - perc = are_cue / tracks_in_playlist - if perc == 0: - perc = 0 - if 0 < perc < 0.01: - perc = "<0.01" - else: - perc = round(perc, 2) +def line_render(n_track: TrackClass, p_track: TrackClass, y, this_line_playing, album_fade, start_x, width, style=1, ry=None): + timec = colours.bar_time + titlec = colours.title_text + indexc = colours.index_text + artistc = colours.artist_text + albumc = colours.album_text - line += str(perc) + "%" - line += "\n\n" + if this_line_playing is True: + timec = colours.time_text + titlec = colours.title_playing + indexc = colours.index_playing + artistc = colours.artist_playing + albumc = colours.album_playing - if tracks_in_playlist and mp3_bitrates: - line += _("MP3 bitrates (kbps):") + "\n" - rates = sorted(mp3_bitrates, key=mp3_bitrates.get, reverse=True) - others = 0 - for rate in rates: - perc = round((mp3_bitrates.get(rate) / sum(mp3_bitrates.values())) * 100, 1) - if perc < 1: - others += perc - else: - line += f"{rate} ({perc}%); " + if n_track.found is False: + timec = colours.playlist_text_missing + titlec = colours.playlist_text_missing + indexc = colours.playlist_text_missing + artistc = colours.playlist_text_missing + albumc = colours.playlist_text_missing - if others: - others = round(others, 1) - if others < 0.1: - others = "<0.1" - line += _("Others") + f"({others}%);" - line = line.rstrip("; ") - line += "\n\n" + artistoffset = 0 + indexLine = "" - if tracks_in_playlist and ogg_bitrates: - line += _("OGG bitrates (kbps):") + "\n" - rates = sorted(ogg_bitrates, key=ogg_bitrates.get, reverse=True) - others = 0 - for rate in rates: - perc = round((ogg_bitrates.get(rate) / sum(ogg_bitrates.values())) * 100, 1) - if perc < 1: - others += perc - else: - line += f"{rate} ({perc}%); " + offset_font_extra = 0 + if gui.row_font_size > 14: + offset_font_extra = 8 - if others: - others = round(others, 1) - if others < 0.1: - others = "<0.1" - line += _("Others") + f"({others}%);" - line = line.rstrip("; ") - line += "\n\n" + # In windows (arial?) draws numbers too high (hack fix) + num_y_offset = 0 + # if system == 'Windows': + # num_y_offset = 1 - # if tracks_in_playlist and m4a_bitrates: - # line += "M4A bitrates (kbps):\n" - # rates = sorted(m4a_bitrates, key=m4a_bitrates.get, reverse=True) - # others = 0 - # for rate in rates: - # perc = round((m4a_bitrates.get(rate) / sum(m4a_bitrates.values())) * 100, 1) - # if perc < 1: - # others += perc - # else: - # line += f"{rate} ({perc}%); " - # - # if others: - # others = round(others, 1) - # if others < 0.1: - # others = "<0.1" - # line += f"Others ({others}%);" - # - # line = line.rstrip("; ") - # line += "\n\n" + if True or style == 1: - line += "\n" + f"-------------- {_('Top Artists')} --------------------" + "\n\n" + # if not gui.rsp and not gui.combo_mode: + # width -= 10 * gui.scale - ls = stats_gen.artist_list - for i, item in enumerate(ls[:50]): - line += str(i + 1) + ".\t" + stt2(item[1]) + "\t" + item[0] + "\n" + dash = False + if n_track.artist and colours.artist_text == colours.title_text: + dash = True - line += "\n\n" + f"-------------- {_('Top Albums')} --------------------" + "\n\n" - ls = stats_gen.album_list - for i, item in enumerate(ls[:50]): - line += str(i + 1) + ".\t" + stt2(item[1]) + "\t" + item[0] + "\n" - line += "\n\n" + f"-------------- {_('Top Genres')} --------------------" + "\n\n" - ls = stats_gen.genre_list - for i, item in enumerate(ls[:50]): - line += str(i + 1) + ".\t" + stt2(item[1]) + "\t" + item[0] + "\n" + if n_track.title: - line = line.encode("utf-8") - xport = (user_directory / "stats.txt").open("wb") - xport.write(line) - xport.close() - target = str(user_directory / "stats.txt") - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", target]) - else: - subprocess.call(["xdg-open", target]) + line = track_number_process(n_track.track_number) + indexLine = line -def imported_sort(pl: int) -> None: - if pl_is_locked(pl): - show_message(_("Playlist is locked")) - return + if prefs.use_absolute_track_index and pctl.multi_playlist[pctl.active_playlist_viewing].hide_title: + indexLine = str(p_track) + if len(indexLine) > 3: + indexLine += " " - og = pctl.multi_playlist[pl].playlist_ids - og.sort(key=lambda x: pctl.get_track(x).index) + line = "" - reload_albums() - tree_view_box.clear_target_pl(pl) + if n_track.artist != "" and not dash: + line0 = n_track.artist -def imported_sort_folders(pl: int) -> None: - if pl_is_locked(pl): - show_message(_("Playlist is locked")) - return + artistoffset = ddt.text( + (start_x + 27 * gui.scale, y), + line0, + alpha_mod(artistc, album_fade), + gui.row_font_size, + int(width / 2)) - og = pctl.multi_playlist[pl].playlist_ids - og.sort(key=lambda x: pctl.get_track(x).index) + line = n_track.title + else: + line += n_track.title + else: + line = \ + os.path.splitext(n_track.filename)[ + 0] - first_occurrences = {} - for i, x in enumerate(og): - b = pctl.get_track(x).parent_folder_path - if b not in first_occurrences: - first_occurrences[b] = i + if p_track >= len(default_playlist): + gui.pl_update += 1 + return - og.sort(key=lambda x: first_occurrences[pctl.get_track(x).parent_folder_path]) + index = default_playlist[p_track] + star_x = 0 + total = star_store.get(index) - reload_albums() - tree_view_box.clear_target_pl(pl) + if gui.star_mode == "line" and total > 0 and pctl.master_library[index].length > 0: -def standard_sort(pl: int) -> None: - if pl_is_locked(pl): - show_message(_("Playlist is locked")) - return + ratio = total / pctl.master_library[index].length + if ratio > 0.55: + star_x = int(ratio * 4 * gui.scale) + star_x = min(star_x, 60 * gui.scale) + sp = y - 0 - gui.playlist_text_offset + int(gui.playlist_row_height / 2) + if gui.playlist_row_height > 17 * gui.scale: + sp -= 1 - sort_path_pl(pl) - sort_track_2(pl) - reload_albums() - tree_view_box.clear_target_pl(pl) + lh = 1 + if gui.scale != 1: + lh = 2 + colour = colours.star_line + if this_line_playing and colours.star_line_playing is not None: + colour = colours.star_line_playing -def year_s(plt): - sorted_temp = sorted(plt, key=lambda x: x[1]) - temp = [] + ddt.rect( + [ + width + start_x - star_x - 45 * gui.scale - offset_font_extra, + sp, + star_x + 3 * gui.scale, + lh], + alpha_mod(colour, album_fade)) - for album in sorted_temp: - temp += album[0] - return temp + star_x += 6 * gui.scale + if gui.show_ratings: + sx = round(width + start_x - round(40 * gui.scale) - offset_font_extra) + sy = round(ry + (gui.playlist_row_height // 2) - round(7 * gui.scale)) + sx -= round(68 * gui.scale) -def year_sort(pl: int, custom_list=None): - if custom_list: - playlist = custom_list - else: - playlist = pctl.multi_playlist[pl].playlist_ids - plt = [] - pl2 = [] - artist = "" - album_artist = "" + draw_rating_widget(sx, sy, n_track) - p = 0 - while p < len(playlist): + star_x += round(70 * gui.scale) - track = get_object(playlist[p]) + if gui.star_mode == "star" and total > 0 and pctl.master_library[ + index].length != 0: - if track.artist != artist: - if album_artist and track.album_artist and album_artist == track.album_artist: - pass - elif len(artist) > 5 and artist.lower() in track.parent_folder_name.lower(): - pass - else: - artist = track.artist - pl2 += year_s(plt) - plt = [] + sx = width + start_x - 40 * gui.scale - offset_font_extra + sy = ry + (gui.playlist_row_height // 2) - (6 * gui.scale) + # if gui.scale == 1.25: + # sy += 1 + playtime_stars = star_count(total, pctl.master_library[index].length) - 1 - if track.album_artist: - album_artist = track.album_artist + sx2 = sx + selected_star = -2 + rated_star = -1 - if p > len(playlist) - 1: - break + # if key_ctrl_down: - album = [] - on = get_object(playlist[p]).parent_folder_path - album.append(playlist[p]) - t = 1 + c = 60 + d = 6 - while t + p < len(playlist) - 1 and get_object(playlist[p + t]).parent_folder_path == on: - album.append(playlist[p + t]) - t += 1 + colour = [70, 70, 70, 255] + if colours.lm: + colour = [90, 90, 90, 255] + # colour = alpha_mod(indexc, album_fade) - date = get_object(playlist[p]).date + for count in range(8): - # If date is xx-xx-yyyy format, just grab the year from the end - # so that the M and D don't interfere with the sorter - if len(date) > 4 and date[-4:].isnumeric(): - date = date[-4:] + if selected_star < count and playtime_stars < count and rated_star < count: + break - # If we don't have a date, see if we can grab one from the folder name - # following the format: (XXXX) - if date == "": - pfn = get_object(playlist[p]).parent_folder_name - if len(pfn) > 6 and pfn[-1] == ")" and pfn[-6] == "(": - date = pfn[-5:-1] + if count == 0: + sx -= round(13 * gui.scale) + star_x += round(13 * gui.scale) + elif playtime_stars > 3: + dd = round((13 - (playtime_stars - 3)) * gui.scale) + sx -= dd + star_x += dd + else: + sx -= round(13 * gui.scale) + star_x += round(13 * gui.scale) - plt.append((album, date, artist + " " + get_object(playlist[p]).album)) - p += len(album) - #logging.info(album) + # if playtime_stars > 4: + # colour = [c + d * count, c + d * count, c + d * count, 255] + # if playtime_stars > 6: # and count < 1: + # colour = [230, 220, 60, 255] + if gui.tracklist_bg_is_light: + colour = alpha_blend([0, 0, 0, 200], ddt.text_background_colour) + else: + colour = alpha_blend([255, 255, 255, 50], ddt.text_background_colour) - if plt: - pl2 += year_s(plt) - plt = [] + # if selected_star > -2: + # if selected_star >= count: + # colour = (220, 200, 60, 255) + # else: + # if rated_star >= count: + # colour = (220, 200, 60, 255) - if custom_list is not None: - return pl2 + star_pc_icon.render(sx, sy, colour) - # We can't just assign the playlist because it may disconnect the 'pointer' default_playlist - pctl.multi_playlist[pl].playlist_ids[:] = pl2[:] - reload_albums() - tree_view_box.clear_target_pl(pl) + if gui.show_hearts: + xxx = star_x -def pl_toggle_playlist_break(ref): - pctl.multi_playlist[ref].hide_title ^= 1 - gui.pl_update = 1 + count = 0 + spacing = 6 * gui.scale + yy = ry + (gui.playlist_row_height // 2) - (5 * gui.scale) + if gui.scale == 1.25: + yy += 1 + if xxx > 0: + xxx += 3 * gui.scale -delete_icon.xoff = 3 -delete_icon.colour = [249, 70, 70, 255] + if love(False, index): + count = 1 -tab_menu.add(MenuItem(_("Delete"), - delete_playlist_force, pass_ref=True, hint="Ctrl+W", icon=delete_icon, disable_test=test_pl_tab_locked, pass_ref_deco=True)) -radio_tab_menu.add(MenuItem(_("Delete"), - delete_playlist_force, pass_ref=True, hint="Ctrl+W", icon=delete_icon, disable_test=test_pl_tab_locked, pass_ref_deco=True)) + x = width + start_x - 52 * gui.scale - offset_font_extra - xxx + f_store.store(display_you_heart, (x, yy)) -def gen_unique_pl_title(base: str, extra: str="", start: int = 1) -> str: - ex = start - title = base - while ex < 100: - for playlist in pctl.multi_playlist: - if playlist.title == title: - ex += 1 - if ex == 1: - title = base + " (" + extra.rstrip(" ") + ")" - else: - title = base + " (" + extra + str(ex) + ")" - break - else: - break + star_x += 18 * gui.scale - return title + if "spotify-liked" in pctl.master_library[index].misc: + x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx -def new_playlist(switch: bool = True) -> int | None: - if gui.radio_view: - r = {} - r["uid"] = uid_gen() - r["name"] = _("New Radio List") - r["items"] = [] # copy.copy(prefs.radio_urls) - r["scroll"] = 0 - pctl.radio_playlists.append(r) - return None + f_store.store(display_spot_heart, (x, yy)) - title = gen_unique_pl_title(_("New Playlist")) + star_x += heart_row_icon.w + spacing + 2 - top_panel.prime_side = 1 - top_panel.prime_tab = len(pctl.multi_playlist) + for name in pctl.master_library[index].lfm_friend_likes: - pctl.multi_playlist.append(pl_gen(title=title)) # [title, 0, [], 0, 0, 0]) - if switch: - switch_playlist(len(pctl.multi_playlist) - 1) - return len(pctl.multi_playlist) - 1 + # Limit to number of hears to display + if gui.star_mode == "none": + if count > 6: + break + elif count > 4: + break + x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx -heartx_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-menu.png", True)) -spot_heartx_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-menu.png", True)) -transcode_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "transcode.png", True)) -mod_folder_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "mod_folder.png", True)) -settings_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "settings2.png", True)) -rename_tracks_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "pen.png", True)) -add_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "new.png", True)) -spot_asset = asset_loader(scaled_asset_directory, loaded_asset_dc, "spot.png", True) -spot_icon = MenuIcon(spot_asset) -spot_icon.colour = [30, 215, 96, 255] -spot_icon.xoff = 5 -spot_icon.yoff = 2 + f_store.store(display_friend_heart, (x, yy, name)) -jell_icon = MenuIcon(spot_asset) -jell_icon.colour = [190, 100, 210, 255] -jell_icon.xoff = 5 -jell_icon.yoff = 2 + count += 1 -tab_menu.br() + star_x += heart_row_icon.w + spacing + 2 + # Draw track number/index + display_queue = False -def append_deco(): - if pctl.playing_state > 0: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + if pctl.force_queue: - text = None - if tauon.spot_ctl.coasting: - text = _("Add Spotify Album") + marks = [] + album_type = False + for i, item in enumerate(pctl.force_queue): + if item.track_id == n_track.index and item.position == p_track and item.playlist_id == pl_to_id( + pctl.active_playlist_viewing): + if item.type == 0: # Only show mark if track type + marks.append(i) + # else: + # album_type = True + # marks.append(i) - return [line_colour, colours.menu_background, text] + if marks: + display_queue = True + if display_queue: -def rescan_deco(pl: int): - if pctl.multi_playlist[pl].last_folder: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + li = str(marks[0] + 1) + if li == "1": + li = "N" + # if item.track_id == n_track.index and item.position == p_track and item.playlist_id == pctl.active_playlist_viewing + if pctl.playing_ready() and n_track.index == pctl.track_queue[ + pctl.queue_step] and p_track == pctl.playlist_playing_position: + li = "R" + # if album_type: + # li = "A" - # base = os.path.basename(pctl.multi_playlist[pl].last_folder) + # rect = (start_x + 3 * gui.scale, y - 1 * gui.scale, 5 * gui.scale, 5 * gui.scale) + # ddt.rect_r(rect, [100, 200, 100, 255], True) + if len(marks) > 1: + li += " " + ("." * (len(marks) - 1)) + li = li[:5] - return [line_colour, colours.menu_background, None] + # if album_type: + # li += "🠗" + colour = [244, 200, 66, 255] + if colours.lm: + colour = [220, 40, 40, 255] -def regenerate_deco(pl: int): - id = pl_to_id(pl) - value = pctl.gen_codes.get(id) + ddt.text( + (start_x + 5 * gui.scale, y, 2), + li, colour, gui.row_font_size + 200 - 1) - if value: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + elif len(indexLine) > 2: - return [line_colour, colours.menu_background, None] + ddt.text( + (start_x + 5 * gui.scale, y, 2), indexLine, + alpha_mod(indexc, album_fade), gui.row_font_size) + else: + ddt.text( + (start_x, y), indexLine, + alpha_mod(indexc, album_fade), gui.row_font_size) -column_names = ( - "Artist", - "Album Artist", - "Album", - "Title", - "Composer", - "Time", - "Date", - "Genre", - "#", - "P", - "Starline", - "Rating", - "Comment", - "Codec", - "Lyrics", - "Bitrate", - "S", - "Filename", - "Disc", - "CUE", -) + if dash and n_track.artist and n_track.title: + line = n_track.artist + " - " + n_track.title + ddt.text( + (start_x + 33 * gui.scale + artistoffset, y), + line, + alpha_mod(titlec, album_fade), + gui.row_font_size, + width - 71 * gui.scale - artistoffset - star_x - 20 * gui.scale) -def parse_generator(string: str): - cmds = [] - quotes = [] - current = "" - q_string = "" - inquote = False - for cha in string: - if not inquote and cha == " ": - if current: - cmds.append(current) - quotes.append(q_string) - q_string = "" - current = "" - continue - if cha == "\"": - inquote ^= True + line = get_display_time(n_track.length) - current += cha + ddt.text( + (width + start_x - (round(36 * gui.scale) + offset_font_extra), + y + num_y_offset, 0), line, + alpha_mod(timec, album_fade), gui.row_font_size) - if inquote and cha != "\"": - q_string += cha + f_store.recall_all() - if current: - cmds.append(current) - quotes.append(q_string) +# def visit_radio_site_show_test(p): +# return "website_url" in prefs.radio_urls[p] and prefs.radio_urls[p]["website_url"] - return cmds, quotes, inquote +def visit_radio_site_deco(item): + if "website_url" in item and item["website_url"]: + return [colours.menu_text, colours.menu_background, None] + return [colours.menu_text_disabled, colours.menu_background, None] +def visit_radio_station_site_deco(item): + return visit_radio_site_deco(item[1]) -def upload_spotify_playlist(pl: int): - p_id = pl_to_id(pl) - string = pctl.gen_codes.get(p_id) - id = None - if string: - cmds, quotes, inquote = parse_generator(string) - for i, cm in enumerate(cmds): - if cm.startswith("spl\""): - id = quotes[i] - break +def visit_radio_site(item): + if "website_url" in item and item["website_url"]: + webbrowser.open(item["website_url"], new=2, autoraise=True) - urls = [] - playlist = pctl.multi_playlist[pl].playlist_ids +def visit_radio_station(item): + visit_radio_site(item[1]) - warn = False - for track_id in playlist: - tr = pctl.get_track(track_id) - url = tr.misc.get("spotify-track-url") - if not url: - warn = True - continue - urls.append(url) +def radio_saved_panel_test(_): + return radiobox.tab == 0 - if warn: - show_message(_("Playlist contains non-Spotify tracks"), mode="error") - return +def save_to_radios(item): + pctl.radio_playlists[pctl.radio_playlist_viewing]["items"].append(item) + toast(_("Added station to: ") + pctl.radio_playlists[pctl.radio_playlist_viewing]["name"]) - new = False - if id is None: - name = pctl.multi_playlist[pl].title.split(" by ")[0] - show_message(_("Created new Spotify playlist"), name, mode="done") - id = tauon.spot_ctl.create_playlist(name) - if id: - new = True - pctl.gen_codes[p_id] = "spl\"" + id + "\"" - if id is None: - show_message(_("Error creating Spotify playlist")) - return - if not new: - show_message(_("Updated Spotify playlist"), mode="done") - tauon.spot_ctl.upload_playlist(id, urls) +def create_artist_pl(artist: str, replace: bool = False): + source_pl = pctl.active_playlist_viewing + this_pl = pctl.active_playlist_viewing + if pctl.multi_playlist[source_pl].parent_playlist_id: + if pctl.multi_playlist[source_pl].title.startswith("Artist:"): + new = id_to_pl(pctl.multi_playlist[source_pl].parent_playlist_id) + if new is None: + # The original playlist is now gone + pctl.multi_playlist[source_pl].parent_playlist_id = "" + else: + source_pl = new + # replace = True -def regenerate_playlist(pl: int = -1, silent: bool = False, id: int | None = None) -> None: - if id is None and pl == -1: - return + playlist = [] - if id is None: - id = pl_to_id(pl) + for item in pctl.multi_playlist[source_pl].playlist_ids: + track = pctl.get_track(item) + if track.artist == artist or track.album_artist == artist: + playlist.append(item) - if pl == -1: - pl = id_to_pl(id) - if pl is None: - return + if replace: + pctl.multi_playlist[this_pl].playlist_ids[:] = playlist[:] + pctl.multi_playlist[this_pl].title = _("Artist: ") + artist + if album_mode: + reload_albums() - source_playlist = pctl.multi_playlist[pl].playlist_ids + # Transfer playing track back to original playlist + if pctl.multi_playlist[this_pl].parent_playlist_id: + new = id_to_pl(pctl.multi_playlist[this_pl].parent_playlist_id) + tr = pctl.playing_object() + if new is not None and tr and pctl.active_playlist_playing == this_pl: + if tr.index not in pctl.multi_playlist[this_pl].playlist_ids and tr.index in pctl.multi_playlist[source_pl].playlist_ids: + logging.info("Transfer back playing") + pctl.active_playlist_playing = source_pl + pctl.playlist_playing_position = pctl.multi_playlist[source_pl].playlist_ids.index(tr.index) - string = pctl.gen_codes.get(id) - if not string: - if not silent: - show_message(_("This playlist has no generator")) - return + pctl.gen_codes[pl_to_id(this_pl)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" - cmds, quotes, inquote = parse_generator(string) + else: - if inquote: - gui.gen_code_errors = "close" - return + pctl.multi_playlist.append( + pl_gen( + title=_("Artist: ") + artist, + playlist_ids=playlist, + hide_title=False, + parent=pl_to_id(source_pl))) - playlist = [] - selections = [] - errors = False - selections_searched = 0 + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" - def is_source_type(code: str | None) -> bool: - return \ - code is None or \ - code == "" or \ - code.startswith(("self", "jelly", "plex", "koel", "tau", "air", "sal")) + switch_playlist(len(pctl.multi_playlist) - 1) - #logging.info(cmds) - #logging.info(quotes) +def aa_sort_alpha(): + prefs.artist_list_sort_mode = "alpha" + artist_list_box.saves.clear() - pctl.regen_in_progress = True +def aa_sort_popular(): + prefs.artist_list_sort_mode = "popular" + artist_list_box.saves.clear() - for i, cm in enumerate(cmds): +def aa_sort_play(): + prefs.artist_list_sort_mode = "play" + artist_list_box.saves.clear() - quote = quotes[i] +def toggle_artist_list_style(): + if prefs.artist_list_style == 1: + prefs.artist_list_style = 2 + else: + prefs.artist_list_style = 1 - if cm.startswith("\"") and (cm.endswith((">", "<"))): - cm_found = False +def toggle_artist_list_threshold(): + if prefs.artist_list_threshold > 0: + prefs.artist_list_threshold = 0 + else: + prefs.artist_list_threshold = 4 + artist_list_box.saves.clear() - for col in column_names: +def toggle_artist_list_threshold_deco(): + if prefs.artist_list_threshold == 0: + return [colours.menu_text, colours.menu_background, _("Filter Small Artists")] + save = artist_list_box.saves.get(pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int) + if save and save[5] == 0: + return [colours.menu_text_disabled, colours.menu_background, _("Include All Artists")] + return [colours.menu_text, colours.menu_background, _("Include All Artists")] - if quote.lower() == col.lower() or _(quote).lower() == col.lower(): - cm_found = True +def verify_discogs(): + return len(prefs.discogs_pat) == 40 - if cm[-1] == ">": - sort_ass(0, invert=False, custom_list=playlist, custom_name=col) - elif cm[-1] == "<": - sort_ass(0, invert=True, custom_list=playlist, custom_name=col) - break - if cm_found: - continue +def save_discogs_artist_thumb(artist, filepath): + logging.info("Searching discogs for artist image...") - elif cm == "self": - selections.append(pctl.multi_playlist[pl].playlist_ids) + # Make artist name url safe + artist = artist.replace("/", "").replace("\\", "").replace(":", "") - elif cm == "auto": - pass - - elif cm.startswith("spl\""): - playlist.extend(tauon.spot_ctl.playlist(quote, return_list=True)) + # Search for Discogs artist id + url = "https://api.discogs.com/database/search" + r = requests.get(url, params={"query": artist, "type": "artist", "token": prefs.discogs_pat}, headers={"User-Agent": t_agent}, timeout=10) + id = r.json()["results"][0]["id"] - elif cm.startswith("tpl\""): - playlist.extend(tauon.tidal.playlist(quote, return_list=True)) + # Search artist info, get images + url = "https://api.discogs.com/artists/" + str(id) + r = requests.get(url, headers={"User-Agent": t_agent}, params={"token": prefs.discogs_pat}, timeout=10) + images = r.json()["images"] - elif cm == "tfa": - playlist.extend(tauon.tidal.fav_albums(return_list=True)) + # Respect rate limit + rate_remaining = r.headers["X-Discogs-Ratelimit-Remaining"] + if int(rate_remaining) < 30: + time.sleep(5) - elif cm == "tft": - playlist.extend(tauon.tidal.fav_tracks(return_list=True)) + # Find a square image in list of images + for image in images: + if image["height"] == image["width"]: + logging.info("Found square") + url = image["uri"] + break + else: + url = images[0]["uri"] - elif cm.startswith("tar\""): - playlist.extend(tauon.tidal.artist(quote, return_list=True)) + response = urllib.request.urlopen(url, context=ssl_context) + im = Image.open(response) - elif cm.startswith("tmix\""): - playlist.extend(tauon.tidal.mix(quote, return_list=True)) + width, height = im.size + if width > height: + delta = width - height + left = int(delta / 2) + upper = 0 + right = height + left + lower = height + else: + delta = height - width + left = 0 + upper = int(delta / 2) + right = width + lower = width + upper - elif cm == "sal": - playlist.extend(tauon.spot_ctl.get_library_albums(return_list=True)) + im = im.crop((left, upper, right, lower)) + im.save(filepath, "JPEG", quality=90) + im.close() + logging.info("Found artist image from Discogs") - elif cm == "slt": - playlist.extend(tauon.spot_ctl.get_library_likes(return_list=True)) +def save_fanart_artist_thumb(mbid, filepath, preview=False): + logging.info("Searching fanart.tv for image...") + #logging.info("mbid is " + mbid) + r = requests.get("https://webservice.fanart.tv/v3/music/" + mbid + "?api_key=" + prefs.fatvap, timeout=5) + #logging.info(r.json()) + thumblink = r.json()["artistthumb"][0]["url"] + if preview: + thumblink = thumblink.replace("/fanart/music", "/preview/music") - elif cm == "plex": - if not plex.scanning: - playlist.extend(plex.get_albums(return_list=True)) + response = urllib.request.urlopen(thumblink, timeout=10, context=ssl_context) + info = response.info() - elif cm.startswith("jelly\""): - if not jellyfin.scanning: - playlist.extend(jellyfin.get_playlist(quote, return_list=True)) + t = io.BytesIO() + t.seek(0) + t.write(response.read()) + l = 0 + t.seek(0, 2) + l = t.tell() + t.seek(0) - elif cm == "jelly": - if not jellyfin.scanning: - playlist.extend(jellyfin.ingest_library(return_list=True)) + if info.get_content_maintype() == "image" and l > 1000: + f = open(filepath, "wb") + f.write(t.read()) + f.close() - elif cm == "koel": - if not koel.scanning: - playlist.extend(koel.get_albums(return_list=True)) + if prefs.fanart_notify: + prefs.fanart_notify = False + show_message( + _("Notice: Artist image sourced from fanart.tv"), + _("They encourage you to contribute at {link}").format(link="https://fanart.tv"), mode="link") + logging.info("Found artist thumbnail from fanart.tv") - elif cm == "tau": - if not tau.processing: - playlist.extend(tau.get_playlist(pctl.multi_playlist[pl].title, return_list=True)) +def queue_pause_deco(): + if pctl.pause_queue: + return [colours.menu_text, colours.menu_background, _("Resume Queue")] + return [colours.menu_text, colours.menu_background, _("Pause Queue")] - elif cm == "air": - if not subsonic.scanning: - playlist.extend(subsonic.get_music3(return_list=True)) +# def finish_current_deco(): +# +# colour = colours.menu_text +# line = "Finish Playing Album" +# +# if pctl.playing_object() is None: +# colour = colours.menu_text_disabled +# if pctl.force_queue and pctl.force_queue[0].album_stage == 1: +# colour = colours.menu_text_disabled +# +# return [colour, colours.menu_background, line] - elif cm == "a": - if not selections and not selections_searched: - for plist in pctl.multi_playlist: - code = pctl.gen_codes.get(plist.uuid_int) - if is_source_type(code): - selections.append(plist.playlist_ids) +def art_metadata_overlay(right, bottom, showc): + if not showc: + return - temp = [] - for selection in selections: - temp += selection + padding = 6 * gui.scale - playlist += list(OrderedDict.fromkeys(temp)) - selections.clear() + if not key_shift_down: - elif cm == "cue": + line = "" + if showc[0] == 1: + line += "E " + elif showc[0] == 2: + line += "N " + else: + line += "F " - for i in reversed(range(len(playlist))): - tr = pctl.get_track(playlist[i]) - if not tr.is_cue: - del playlist[i] - playlist = list(OrderedDict.fromkeys(playlist)) + line += str(showc[2] + 1) + "/" + str(showc[1]) - elif cm == "today": - d = datetime.date.today() - for i in reversed(range(len(playlist))): - tr = pctl.get_track(playlist[i]) - if tr.date[5:7] != f"{d:%m}" or tr.date[8:10] != f"{d:%d}": - del playlist[i] - playlist = list(OrderedDict.fromkeys(playlist)) + y = bottom - 40 * gui.scale - elif cm.startswith("com\""): - for i in reversed(range(len(playlist))): - tr = pctl.get_track(playlist[i]) - if quote not in tr.comment: - del playlist[i] - playlist = list(OrderedDict.fromkeys(playlist)) + tag_width = ddt.get_text_w(line, 12) + 12 * gui.scale + ddt.rect_a((right - (tag_width + padding), y), (tag_width, 18 * gui.scale), [8, 8, 8, 255]) + ddt.text(((right) - (6 * gui.scale + padding), y, 1), line, [200, 200, 200, 255], 12, bg=[30, 30, 30, 255]) - elif cm.startswith("ext"): - value = quote.upper() - if value: - if not selections: - for plist in pctl.multi_playlist: - selections.append(plist.playlist_ids) + else: # Extended metadata - temp = [] - for selection in selections: - for track in selection: - tr = pctl.get_track(track) - if tr.file_ext == value: - temp.append(track) + line = "" + if showc[0] == 1: + line += "Embedded" + elif showc[0] == 2: + line += "Network" + else: + line += "File" - playlist += list(OrderedDict.fromkeys(temp)) + y = bottom - 76 * gui.scale - elif cm == "ypa": - playlist = year_sort(0, playlist) + tag_width = ddt.get_text_w(line, 12) + 12 * gui.scale + ddt.rect_a((right - (tag_width + padding), y), (tag_width, 18 * gui.scale), [8, 8, 8, 255]) + ddt.text(((right) - (6 * gui.scale + padding), y, 1), line, [200, 200, 200, 255], 12, bg=[30, 30, 30, 255]) - elif cm == "tn": - sort_track_2(0, playlist) + y += 18 * gui.scale - elif cm == "ia>": - playlist = gen_last_imported_folders(0, playlist) + line = "" + line += showc[4] + line += " " + str(showc[3][0]) + "×" + str(showc[3][1]) - elif cm == "ia<": - playlist = gen_last_imported_folders(0, playlist, reverse=True) + tag_width = ddt.get_text_w(line, 12) + 12 * gui.scale + ddt.rect_a((right - (tag_width + padding), y), (tag_width, 18 * gui.scale), [8, 8, 8, 255]) + ddt.text(((right) - (6 * gui.scale + padding), y, 1), line, [200, 200, 200, 255], 12, bg=[30, 30, 30, 255]) - elif cm == "m>": - playlist = gen_last_modified(0, playlist) + y += 18 * gui.scale - elif cm == "m<": - playlist = gen_last_modified(0, playlist, reverse=False) + line = "" + line += str(showc[2] + 1) + "/" + str(showc[1]) - elif cm == "ly" or cm == "lyrics": - playlist = gen_lyrics(0, playlist) + tag_width = ddt.get_text_w(line, 12) + 12 * gui.scale + ddt.rect_a((right - (tag_width + padding), y), (tag_width, 18 * gui.scale), [8, 8, 8, 255]) + ddt.text(((right) - (6 * gui.scale + padding), y, 1), line, [200, 200, 200, 255], 12, bg=[30, 30, 30, 255]) - elif cm == "l" or cm == "love" or cm == "loved": - playlist = gen_love(0, playlist) +def artist_dl_deco(): + if artist_info_box.status == "Ready": + return [colours.menu_text_disabled, colours.menu_background, None] + return [colours.menu_text, colours.menu_background, None] - elif cm == "clr": - selections.clear() +def station_browse(): + radiobox.active = True + radiobox.edit_mode = False + radiobox.add_mode = False + radiobox.center = True + radiobox.tab = 1 - elif cm == "rv" or cm == "reverse": - playlist = gen_reverse(0, playlist) +def add_station(): + radiobox.active = True + radiobox.edit_mode = True + radiobox.add_mode = True + radiobox.radio_field.text = "" + radiobox.radio_field_title.text = "" + radiobox.station_editing = None + radiobox.center = True - elif cm == "rva": - playlist = gen_folder_reverse(0, playlist) +def rename_station(item): + station = item[1] + radiobox.active = True + radiobox.center = False + radiobox.edit_mode = True + radiobox.add_mode = False + radiobox.radio_field.text = station["stream_url"] + radiobox.radio_field_title.text = station.get("title", "") + radiobox.station_editing = station - elif cm == "rata>": +def remove_station(item): + index = item[0] + del pctl.radio_playlists[pctl.radio_playlist_viewing]["items"][index] - playlist = gen_folder_top_rating(0, custom_list=playlist) +def dismiss_dl(): + dl_mon.ready.clear() + dl_mon.done.update(dl_mon.watching) + dl_mon.watching.clear() - elif cm == "rat>": +def download_img(link: str, target_folder: str, track: TrackClass) -> None: + try: + response = urllib.request.urlopen(link, context=ssl_context) + info = response.info() + if info.get_content_maintype() == "image": + if info.get_content_subtype() == "jpeg": + save_target = os.path.join(target_dir, "image.jpg") + with open(save_target, "wb") as f: + f.write(response.read()) + # clear_img_cache() + clear_track_image_cache(track) - def rat_key(track_id): - return star_store.get_rating(track_id) + elif info.get_content_subtype() == "png": + save_target = os.path.join(target_dir, "image.png") + with open(save_target, "wb") as f: + f.write(response.read()) + # clear_img_cache() + clear_track_image_cache(track) + else: + show_message(_("Image types other than PNG or JPEG are currently not supported"), mode="warning") + else: + show_message(_("The link does not appear to refer to an image file."), mode="warning") + gui.image_downloading = False - playlist = sorted(playlist, key=rat_key, reverse=True) + except Exception as e: + logging.exception("Image download failed") + show_message(_("Image download failed."), str(e), mode="warning") + gui.image_downloading = False - elif cm == "rat<": +def display_you_heart(x: int, yy: int, just: int = 0) -> None: + rect = [x - 1 * gui.scale, yy - 4 * gui.scale, 15 * gui.scale, 17 * gui.scale] + gui.heart_fields.append(rect) + fields.add(rect, update_playlist_call) + if coll(rect) and not track_box: + gui.pl_update += 1 + w = ddt.get_text_w(_("You"), 13) + xx = (x - w) - 5 * gui.scale - def rat_key(track_id): - return star_store.get_rating(track_id) + if just == 1: + xx += w + 15 * gui.scale - playlist = sorted(playlist, key=rat_key) + ty = yy - 28 * gui.scale + tx = xx + if ty < gui.panelY + 5 * gui.scale: + ty = gui.panelY + 5 * gui.scale + tx -= 20 * gui.scale - elif cm[:4] == "rat=": - value = cm[4:] - try: - value = float(value) * 2 - temp = [] - for item in playlist: - if value == star_store.get_rating(item): - temp.append(item) - playlist = temp - except Exception: - logging.exception("Failed to get rating") - errors = True + # ddt.rect_r((xx - 1 * gui.scale, yy - 26 * gui.scale - 1 * gui.scale, w + 10 * gui.scale + 2 * gui.scale, 19 * gui.scale + 2 * gui.scale), [50, 50, 50, 255], True) + ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [15, 15, 15, 255]) + ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [35, 35, 35, 255]) + ddt.text((tx + 5 * gui.scale, ty + 4 * gui.scale), _("You"), [250, 250, 250, 255], 13, bg=[15, 15, 15, 255]) - elif cm[:4] == "rat<": - value = cm[4:] - try: - value = float(value) * 2 - temp = [] - for item in playlist: - if value > star_store.get_rating(item): - temp.append(item) - playlist = temp - except Exception: - logging.exception("Failed to get rating") - errors = True + heart_row_icon.render(x, yy, [244, 100, 100, 255]) - elif cm[:4] == "rat>": - value = cm[4:] - try: - value = float(value) * 2 - temp = [] - for item in playlist: - if value < star_store.get_rating(item): - temp.append(item) - playlist = temp - except Exception: - logging.exception("Failed to get rating") - errors = True +def display_spot_heart(x: int, yy: int, just: int = 0) -> None: + rect = [x - 1 * gui.scale, yy - 4 * gui.scale, 15 * gui.scale, 17 * gui.scale] + gui.heart_fields.append(rect) + fields.add(rect, update_playlist_call) + if coll(rect) and not track_box: + gui.pl_update += 1 + w = ddt.get_text_w(_("Liked on Spotify"), 13) + xx = (x - w) - 5 * gui.scale - elif cm == "rat": - temp = [] - for item in playlist: - # tr = pctl.get_track(item) - if star_store.get_rating(item) > 0: - temp.append(item) - playlist = temp + if just == 1: + xx += w + 15 * gui.scale - elif cm == "norat": - temp = [] - for item in playlist: - if star_store.get_rating(item) == 0: - temp.append(item) - playlist = temp + ty = yy - 28 * gui.scale + tx = xx + if ty < gui.panelY + 5 * gui.scale: + ty = gui.panelY + 5 * gui.scale + tx -= 20 * gui.scale - elif cm == "d>": - playlist = gen_sort_len(0, custom_list=playlist) + # ddt.rect_r((xx - 1 * gui.scale, yy - 26 * gui.scale - 1 * gui.scale, w + 10 * gui.scale + 2 * gui.scale, 19 * gui.scale + 2 * gui.scale), [50, 50, 50, 255], True) + ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [15, 15, 15, 255]) + ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [35, 35, 35, 255]) + ddt.text((tx + 5 * gui.scale, ty + 4 * gui.scale), _("Liked on Spotify"), [250, 250, 250, 255], 13, bg=[15, 15, 15, 255]) - elif cm == "d<": - playlist = gen_sort_len(0, custom_list=playlist) - playlist = list(reversed(playlist)) + heart_row_icon.render(x, yy, [100, 244, 100, 255]) - elif cm[:2] == "d<": - value = cm[2:] - if value and value.isdigit(): - value = int(value) - for i in reversed(range(len(playlist))): - tr = pctl.get_track(playlist[i]) - if not value > tr.length: - del playlist[i] +def display_friend_heart(x: int, yy: int, name: str, just: int = 0) -> None: + heart_row_icon.render(x, yy, heart_colours.get(name)) - elif cm[:2] == "d>": - value = cm[2:] - if value and value.isdigit(): - value = int(value) - for i in reversed(range(len(playlist))): - tr = pctl.get_track(playlist[i]) - if not value < tr.length: - del playlist[i] + rect = [x - 1, yy - 4, 15 * gui.scale, 17 * gui.scale] + gui.heart_fields.append(rect) + fields.add(rect, update_playlist_call) + if coll(rect) and not track_box: + gui.pl_update += 1 + w = ddt.get_text_w(name, 13) + xx = (x - w) - 5 * gui.scale - elif cm == "path": - sort_path_pl(0, custom_list=playlist) + if just == 1: + xx += w + 15 * gui.scale - elif cm == "pa>": - playlist = gen_folder_top(0, custom_list=playlist) + ty = yy - 28 * gui.scale + tx = xx + if ty < gui.panelY + 5 * gui.scale: + ty = gui.panelY + 5 * gui.scale + tx -= 20 * gui.scale - elif cm == "pa<": - playlist = gen_folder_top(0, custom_list=playlist) - playlist = gen_folder_reverse(0, playlist) + ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [15, 15, 15, 255]) + ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [35, 35, 35, 255]) + ddt.text((tx + 5 * gui.scale, ty + 4 * gui.scale), name, [250, 250, 250, 255], 13, bg=[15, 15, 15, 255]) - elif cm == "pt>" or cm == "pc>": - playlist = gen_top_100(0, custom_list=playlist) +# Set SDL window drag areas +# if system != 'windows': - elif cm == "pt<" or cm == "pc<": - playlist = gen_top_100(0, custom_list=playlist) - playlist = list(reversed(playlist)) +def hit_callback(win, point, data): + x = point.contents.x / logical_size[0] * window_size[0] + y = point.contents.y / logical_size[0] * window_size[0] - elif cm[:3] == "pt>": - value = cm[3:] - if value and value.isdigit(): - value = int(value) - for i in reversed(range(len(playlist))): - t_time = star_store.get(playlist[i]) - if t_time < value: - del playlist[i] + # Special layout modes + if gui.mode == 3: - elif cm[:3] == "pt<": - value = cm[3:] - if value and value.isdigit(): - value = int(value) - for i in reversed(range(len(playlist))): - t_time = star_store.get(playlist[i]) - if t_time > value: - del playlist[i] + if key_shift_down or key_shiftr_down: + return SDL_HITTEST_NORMAL - elif cm[:3] == "pc>": - value = cm[3:] - if value and value.isdigit(): - value = int(value) - for i in reversed(range(len(playlist))): - t_time = star_store.get(playlist[i]) - tr = pctl.get_track(playlist[i]) - if tr.length > 0: - if not value < t_time / tr.length: - del playlist[i] + # if prefs.mini_mode_mode == 5: + # return SDL_HITTEST_NORMAL - elif cm[:3] == "pc<": - value = cm[3:] - if value and value.isdigit(): - value = int(value) - for i in reversed(range(len(playlist))): - t_time = star_store.get(playlist[i]) - tr = pctl.get_track(playlist[i]) - if tr.length > 0: - if not value > t_time / tr.length: - del playlist[i] + if prefs.mini_mode_mode in (4, 5) and x > window_size[1] - 5 * gui.scale and y > window_size[1] - 12 * gui.scale: + return SDL_HITTEST_NORMAL - elif cm == "y<": - playlist = gen_sort_date(0, False, playlist) + if y < gui.window_control_hit_area_h and x > window_size[ + 0] - gui.window_control_hit_area_w: + return SDL_HITTEST_NORMAL - elif cm == "y>": - playlist = gen_sort_date(0, True, playlist) + # Square modes + y1 = window_size[0] + # if prefs.mini_mode_mode == 5: + # y1 = window_size[1] + y0 = 0 + if macos: + y0 = round(35 * gui.scale) + if window_size[0] == window_size[1]: + y1 = window_size[1] - 79 * gui.scale + if y0 < y < y1 and not search_over.active: + return SDL_HITTEST_DRAGGABLE - elif cm[:2] == "y=": - value = cm[2:] - if value: - temp = [] - for item in playlist: - if value in pctl.master_library[item].date: - temp.append(item) - playlist = temp + return SDL_HITTEST_NORMAL - elif cm[:3] == "y>=": - value = cm[3:] - if value and value.isdigit(): - value = int(value) - temp = [] - for item in playlist: - if pctl.master_library[item].date[:4].isdigit() and int( - pctl.master_library[item].date[:4]) >= value: - temp.append(item) - playlist = temp - - elif cm[:3] == "y<=": - value = cm[3:] - if value and value.isdigit(): - value = int(value) - temp = [] - for item in playlist: - if pctl.master_library[item].date[:4].isdigit() and int( - pctl.master_library[item].date[:4]) <= value: - temp.append(item) - playlist = temp + # Standard player mode + if not gui.maximized: + if y < 0 and x > window_size[0]: + return SDL_HITTEST_RESIZE_TOPRIGHT - elif cm[:2] == "y>": - value = cm[2:] - if value and value.isdigit(): - value = int(value) - temp = [] - for item in playlist: - if pctl.master_library[item].date[:4].isdigit() and int(pctl.master_library[item].date[:4]) > value: - temp.append(item) - playlist = temp + if y < 0 and x < 1: + return SDL_HITTEST_RESIZE_TOPLEFT - elif cm[:2] == "y<": - value = cm[2:] - if value and value.isdigit: - value = int(value) - temp = [] - for item in playlist: - if pctl.master_library[item].date[:4].isdigit() and int(pctl.master_library[item].date[:4]) < value: - temp.append(item) - playlist = temp + # if draw_border and y < 3 * gui.scale and x < window_size[0] - 40 * gui.scale and not gui.maximized: + # return SDL_HITTEST_RESIZE_TOP - elif cm == "st" or cm == "rt" or cm == "r": - random.shuffle(playlist) + if y < gui.panelY: - elif cm == "sf" or cm == "rf" or cm == "ra" or cm == "sa": - playlist = gen_folder_shuffle(0, custom_list=playlist) + if gui.top_bar_mode2: - elif cm.startswith("n"): - value = cm[1:] - if value.isdigit(): - playlist = playlist[:int(value)] + if y < gui.panelY - gui.panelY2: + if prefs.left_window_control and x < 100 * gui.scale: + return SDL_HITTEST_NORMAL - # SEARCH FOLDER - elif cm.startswith("p\"") and len(cm) > 3: + if x > window_size[0] - 100 * gui.scale and y < 30 * gui.scale: + return SDL_HITTEST_NORMAL + return SDL_HITTEST_DRAGGABLE + if top_panel.drag_zone_start_x > x or tab_menu.active: + return SDL_HITTEST_NORMAL + return SDL_HITTEST_DRAGGABLE - if not selections: - for plist in pctl.multi_playlist: - code = pctl.gen_codes.get(plist.uuid_int) - if is_source_type(code): - selections.append(plist.playlist_ids) + if top_panel.drag_zone_start_x < x < window_size[0] - (gui.offset_extra + 5): - search = quote - search_over.all_folders = True - search_over.sip = True - search_over.search_text.text = search - if worker2_lock.locked(): - try: - worker2_lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked worker2_lock") - else: - logging.exception("Unknown RuntimeError trying to release worker2_lock") - except Exception: - logging.exception("Unknown error trying to release worker2_lock") - while search_over.sip: - time.sleep(0.01) + if tab_menu.active or mouse_up or mouse_down: # mouse up/down is workaround for Wayland + return SDL_HITTEST_NORMAL - found_name = "" + if (prefs.left_window_control and x > window_size[0] - (100 * gui.scale) and ( + macos or system == "Windows" or msys)) or (not prefs.left_window_control and x > window_size[0] - (160 * gui.scale) and ( + macos or system == "Windows" or msys)): + return SDL_HITTEST_NORMAL - for result in search_over.results: - if result[0] == 5: - found_name = result[1] - break - else: - logging.info("No folder search result found") - continue + return SDL_HITTEST_DRAGGABLE - search_over.clear() + if not gui.maximized: + if x > window_size[0] - 20 * gui.scale and y > window_size[1] - 20 * gui.scale: + return SDL_HITTEST_RESIZE_BOTTOMRIGHT + if x < 5 and y > window_size[1] - 5: + return SDL_HITTEST_RESIZE_BOTTOMLEFT + if y > window_size[1] - 5 * gui.scale: + return SDL_HITTEST_RESIZE_BOTTOM - playlist += search_over.click_meta(found_name, get_list=True, search_lists=selections) + if x > window_size[0] - 3 * gui.scale and y > 20 * gui.scale: + return SDL_HITTEST_RESIZE_RIGHT + if x < 5 * gui.scale and y > 10 * gui.scale: + return SDL_HITTEST_RESIZE_LEFT + return SDL_HITTEST_NORMAL + return SDL_HITTEST_NORMAL - # SEARCH GENRE - elif (cm.startswith(('g"', 'gm"', 'g="'))) and len(cm) > 3: +def reload_scale(prefs: Prefs): + auto_scale(prefs) - if not selections: - for plist in pctl.multi_playlist: - code = pctl.gen_codes.get(plist.uuid_int) - if is_source_type(code): - selections.append(plist.playlist_ids) + scale = prefs.scale_want - g_search = quote.lower().replace("-", "") # .replace(" ", "") + gui.scale = scale + ddt.scale = gui.scale + prime_fonts(prefs) + ddt.clear_text_cache() + scale_assets(scale_want=scale, force=True) + img_slide_update_gall(album_mode_art_size) - search = g_search - search_over.sip = True - search_over.search_text.text = search - if worker2_lock.locked(): - try: - worker2_lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked worker2_lock") - else: - logging.exception("Unknown RuntimeError trying to release worker2_lock") - except Exception: - logging.exception("Unknown error trying to release worker2_lock") - while search_over.sip: - time.sleep(0.01) + for item in WhiteModImageAsset.assets: + item.reload() + for item in LoadImageAsset.assets: + item.reload() + for menu in Menu.instances: + menu.rescale() + bottom_bar1.__init__() + bottom_bar_ao1.__init__() + top_panel.__init__() + view_box.__init__(reload=True) + queue_box.recalc() + playlist_box.recalc() - found_name = "" +def update_layout_do(prefs: Prefs): + if prefs.scale_want != gui.scale: + reload_scale(prefs) - if cm.startswith("g=\""): - for result in search_over.results: - if result[0] == 3 and result[1].lower().replace("-", "").replace(" ", "") == g_search: - found_name = result[1] - break - elif cm.startswith("g\"") or not prefs.sep_genre_multi: - for result in search_over.results: - if result[0] == 3: - found_name = result[1] - break - elif cm.startswith("gm\""): - for result in search_over.results: - if result[0] == 3 and result[1].endswith("+"): - found_name = result[1] - break + w = window_size[0] + h = window_size[1] - if not found_name: - logging.warning("No genre search result found") - continue + if gui.switch_showcase_off: + ddt.force_gray = False + gui.switch_showcase_off = False + exit_combo(restore=True) - search_over.clear() + global draw_max_button + if draw_max_button and prefs.force_hide_max_button: + draw_max_button = False - playlist += search_over.click_genre(found_name, get_list=True, search_lists=selections) + if gui.theme_name != prefs.theme_name: + gui.reload_theme = True + global theme + theme = get_theme_number(prefs.theme_name) + #logging.info("Config reload theme...") - # SEARCH ARTIST - elif cm.startswith("a\"") and len(cm) > 3 and cm != "auto": - if not selections: - for plist in pctl.multi_playlist: - code = pctl.gen_codes.get(plist.uuid_int) - if is_source_type(code): - selections.append(plist.playlist_ids) + # Restore in case of error + if gui.rspw < 30 * gui.scale: - search = quote - search_over.sip = True - search_over.search_text.text = "artist " + search - if worker2_lock.locked(): - try: - worker2_lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked worker2_lock") - else: - logging.exception("Unknown RuntimeError trying to release worker2_lock") - except Exception: - logging.exception("Unknown error trying to release worker2_lock") - while search_over.sip: - time.sleep(0.01) + gui.rspw = 100 * gui.scale - found_name = "" + # Lock right side panel to full size if fully extended ----- + if prefs.side_panel_layout == 0 and not album_mode: + max_w = round( + ((window_size[1] - gui.panelY - gui.panelBY - 17 * gui.scale) * gui.art_max_ratio_lock) + 17 * gui.scale) + # 17 here is the art box inset value - for result in search_over.results: - if result[0] == 0: - found_name = result[1] - break - else: - logging.warning("No artist search result found") - continue + if not album_mode and gui.rspw > max_w - 12 * gui.scale and side_drag: + gui.rsp_full_lock = True + # ---------------------------------------------------------- - search_over.clear() - # for item in search_over.click_artist(found_name, get_list=True, search_lists=selections): - # playlist.append(item) - playlist += search_over.click_artist(found_name, get_list=True, search_lists=selections) + # Auto shrink left side panel -------------- + pl_width = window_size[0] + pl_width_a = pl_width + if gui.rsp: + pl_width_a = pl_width - gui.rspw + pl_width -= gui.rspw - 300 * gui.scale # More sensitivity for compact with rsp for better visual balancing - elif cm.startswith("ff\""): + if pl_width < 900 * gui.scale and not gui.hide_tracklist_in_gallery: + gui.lspw = 180 * gui.scale - for i in reversed(range(len(playlist))): - tr = pctl.get_track(playlist[i]) - line = " ".join([tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() + if pl_width < 700 * gui.scale: + gui.lspw = 150 * gui.scale - if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): - line = str(unidecode(line)) + if prefs.left_panel_mode == "artist list" and prefs.artist_list_style == 1: + gui.compact_artist_list = True + gui.lspw = 75 * gui.scale + if gui.force_side_on_drag: + gui.lspw = 180 * gui.scale + else: + gui.lspw = 220 * gui.scale + gui.compact_artist_list = False + if prefs.left_panel_mode == "artist list": + gui.lspw = 230 * gui.scale - if not search_magic(quote.lower(), line): - del playlist[i] + if gui.lsp and prefs.left_panel_mode == "folder view": + gui.lspw = 260 * gui.scale + max_insets = 0 + for item in tree_view_box.rows: + max_insets = max(item[2], max_insets) - playlist = list(OrderedDict.fromkeys(playlist)) + p = (pl_width_a * 0.15) - round(200 * gui.scale) + if gui.hide_tracklist_in_gallery: + p = ((window_size[0] - gui.lspw) * 0.15) - round(170 * gui.scale) - elif cm.startswith("fx\""): + p = min(round(200 * gui.scale), p) + if p > 0: + gui.lspw += p + if max_insets > 1: + gui.lspw = max(gui.lspw, 260 * gui.scale + round(15 * gui.scale) * max_insets) - for i in reversed(range(len(playlist))): - tr = pctl.get_track(playlist[i]) - line = " ".join( - [tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() - if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): - line = str(unidecode(line)) + # ----- - if search_magic(quote.lower(), line): - del playlist[i] + # Set bg art strength according to setting ---- + if prefs.art_bg_stronger == 3: + prefs.art_bg_opacity = 29 + elif prefs.art_bg_stronger == 2: + prefs.art_bg_opacity = 19 + else: + prefs.art_bg_opacity = 10 + if prefs.bg_showcase_only: + prefs.art_bg_opacity += 21 - elif cm.startswith(('find"', 'f"', 'fs"')): + # ----- - if not selections: - for plist in pctl.multi_playlist: - code = pctl.gen_codes.get(plist.uuid_int) - if is_source_type(code): - selections.append(plist.playlist_ids) + # Adjust for for compact window sizes ---- + if (prefs.always_art_header or (w < 600 * gui.scale and not gui.rsp and prefs.art_in_top_panel)) and not album_mode: + gui.top_bar_mode2 = True + gui.panelY = round(100 * gui.scale) + gui.playlist_top = gui.panelY + (8 * gui.scale) + gui.playlist_top_bk = gui.playlist_top - cooldown = 0 - dones = {} - for selection in selections: - for track_id in selection: - if track_id not in dones: - tr = pctl.get_track(track_id) + else: + gui.top_bar_mode2 = False + gui.panelY = round(30 * gui.scale) + gui.playlist_top = gui.panelY + (8 * gui.scale) + gui.playlist_top_bk = gui.playlist_top - if cm.startswith("fs\""): - line = "|".join([tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() - if quote.lower() in line: - playlist.append(track_id) + gui.show_playlist = True + if w < 750 * gui.scale and album_mode: + gui.show_playlist = False - else: - line = " ".join([tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() + # Set bio panel size according to setting + if prefs.bio_large: + gui.artist_panel_height = 320 * gui.scale + if window_size[0] < 600 * gui.scale: + gui.artist_panel_height = 200 * gui.scale - # if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): - # line = str(unidecode(line)) + else: + gui.artist_panel_height = 200 * gui.scale + if window_size[0] < 600 * gui.scale: + gui.artist_panel_height = 150 * gui.scale - if search_magic(quote.lower(), line): - playlist.append(track_id) + # Trigger artist bio reload if panel size has changed + if gui.artist_info_panel: + if gui.last_artist_panel_height != gui.artist_panel_height: + artist_info_box.get_data(artist_info_box.artist_on) + gui.last_artist_panel_height = gui.artist_panel_height - cooldown += 1 - if cooldown > 300: - time.sleep(0.005) - cooldown = 0 + # prefs.art_bg_blur = 9 + # if prefs.bg_showcase_only: + # prefs.art_bg_blur = 15 + # + # if w / h == 16 / 9: + # logging.info("YEP") + # elif w / h < 16 / 9: + # logging.info("too low") + # else: + # logging.info("too high") + #logging.info((w, h)) - dones[track_id] = None + # input.mouse_click = False - playlist = list(OrderedDict.fromkeys(playlist)) + global renderer + if prefs.spec2_colour_mode == 0: + prefs.spec2_base = [10, 10, 100] + prefs.spec2_multiply = [0.5, 1, 1] + elif prefs.spec2_colour_mode == 1: + prefs.spec2_base = [10, 10, 10] + prefs.spec2_multiply = [2, 1.2, 5] + # elif prefs.spec2_colour_mode == 2: + # prefs.spec2_base = [10, 100, 10] + # prefs.spec2_multiply = [1, -1, 0.4] - elif cm.startswith(('s"', 'px"')): - pl_name = quote - target = None - for p in pctl.multi_playlist: - if p.title.lower() == pl_name.lower(): - target = p.playlist_ids - break - else: - for p in pctl.multi_playlist: - #logging.info(p.title.lower()) - #logging.info(pl_name.lower()) - if p.title.lower().startswith(pl_name.lower()): - target = p.playlist_ids - break - if target is None: - logging.warning(f"not found: {pl_name}") - logging.warning("Target playlist not found") - if cm.startswith("s\""): - selections_searched += 1 - errors = "playlist" - continue + gui.draw_vis4_top = False - if cm.startswith("s\""): - selections_searched += 1 - selections.append(target) - elif cm.startswith("px\""): - playlist[:] = [x for x in playlist if x not in target] + if gui.combo_mode and gui.showcase_mode and prefs.showcase_vis and gui.mode != 3 and prefs.backend == 4: + gui.vis = 4 + gui.turbo = True + elif gui.vis_want == 0: + gui.turbo = False + gui.vis = 0 + else: + gui.vis = gui.vis_want + if gui.vis > 0: + gui.turbo = True - else: - errors = True + # Disable vis when in compact view + if gui.mode == 3 or gui.top_bar_mode2: # or prefs.backend == 2: + if not gui.combo_mode: + gui.vis = 0 + gui.turbo = False - gui.gen_code_errors = errors - if not playlist and not errors: - gui.gen_code_errors = "empty" + if gui.mode == 1: + if not gui.maximized and not gui.lowered and gui.mode != 3: + gui.save_size[0] = logical_size[0] + gui.save_size[1] = logical_size[1] - if gui.rename_playlist_box and (not playlist or cmds.count("a") > 1): - pass - else: - source_playlist[:] = playlist[:] + bottom_bar1.update() - tree_view_box.clear_target_pl(0, id) - pctl.regen_in_progress = False - gui.pl_update = 1 - reload() - pctl.notify_change() + # if system != 'windows': + # if draw_border: + # gui.panelY = 30 * gui.scale + 3 * gui.scale + # top_panel.ty = 3 * gui.scale + # else: + # gui.panelY = 30 * gui.scale + # top_panel.ty = 0 - #logging.info(cmds) + if gui.set_bar and gui.set_mode: + gui.playlist_top = gui.playlist_top_bk + gui.set_height - 6 * gui.scale + else: + gui.playlist_top = gui.playlist_top_bk + if gui.artist_info_panel: + gui.playlist_top += gui.artist_panel_height -def make_auto_sorting(pl: int) -> None: - pctl.gen_codes[pl_to_id(pl)] = "self a path tn ypa auto" - show_message( - _("OK. This playlist will automatically sort on import from now on"), - _("You remove or edit this behavior by going \"Misc...\" > \"Edit generator...\""), mode="done") + gui.offset_extra = 0 + if draw_border and not prefs.left_window_control: + offset = 61 * gui.scale + if not draw_min_button: + offset -= 35 * gui.scale + if draw_max_button: + offset += 33 * gui.scale + if gui.macstyle: + offset = 24 + if draw_min_button: + offset += 20 + if draw_max_button: + offset += 20 + offset = round(offset * gui.scale) + gui.offset_extra = offset -extra_tab_menu = Menu(155, show_icons=True) + global album_v_gap + global album_h_gap + global album_v_slide_value -extra_tab_menu.add(MenuItem(_("New Playlist"), new_playlist, icon=add_icon)) + album_v_slide_value = round(50 * gui.scale) + if gui.gallery_show_text: + album_h_gap = 30 * gui.scale + album_v_gap = 66 * gui.scale + else: + album_h_gap = 30 * gui.scale + album_v_gap = 25 * gui.scale + if prefs.thin_gallery_borders: -def spotify_show_test(_): - return prefs.spot_mode + if gui.gallery_show_text: + album_h_gap = 20 * gui.scale + album_v_gap = 55 * gui.scale + else: + album_h_gap = 17 * gui.scale + album_v_gap = 15 * gui.scale -def jellyfin_show_test(_): - return prefs.jelly_password and prefs.jelly_username + album_v_slide_value = round(45 * gui.scale) + if prefs.increase_gallery_row_spacing: + album_v_gap = round(album_v_gap * 1.3) -tab_menu.add(MenuItem(_("Upload"), - upload_spotify_playlist, pass_ref=True, pass_ref_deco=True, icon=jell_icon, show_test=spotify_show_test)) + gui.gallery_scroll_field_left = window_size[0] - round(40 * gui.scale) -def upload_jellyfin_playlist(pl: TauonPlaylist) -> None: - if jellyfin.scanning: - return - shooter(jellyfin.upload_playlist, [pl]) + # gui.spec_rect[0] = window_size[0] - gui.offset_extra - 90 + gui.spec1_rec.x = int(round(window_size[0] - gui.offset_extra - 90 * gui.scale)) -tab_menu.add(MenuItem(_("Upload"), - upload_jellyfin_playlist, pass_ref=True, pass_ref_deco=True, icon=spot_icon, show_test=jellyfin_show_test)) + # gui.spec_x = window_size[0] - gui.offset_extra - 90 + gui.spec2_rec.x = int(round(window_size[0] - gui.spec2_rec.w - 10 * gui.scale - gui.offset_extra)) -def regen_playlist_async(pl: int) -> None: - if pctl.regen_in_progress: - show_message(_("A regen is already in progress...")) - return - shoot_dl = threading.Thread(target=regenerate_playlist, args=([pl])) - shoot_dl.daemon = True - shoot_dl.start() + gui.scroll_hide_box = (1, gui.panelY, 28 * gui.scale, window_size[1] - gui.panelBY - gui.panelY) + # Tracklist row size and text positioning --------------------------------- + gui.playlist_row_height = prefs.playlist_row_height + gui.row_font_size = prefs.playlist_font_size # 13 -tab_menu.add(MenuItem(_("Regenerate"), regen_playlist_async, regenerate_deco, pass_ref=True, pass_ref_deco=True, hint="Alt+R")) -tab_menu.add_sub(_("Generate…"), 150) -tab_menu.add_sub(_("Sort…"), 170) -extra_tab_menu.add_sub(_("From Current…"), 133) -# tab_menu.add(_("Sort by Filepath"), standard_sort, pass_ref=True, disable_test=test_pl_tab_locked, pass_ref_deco=True) -# tab_menu.add(_("Sort Track Numbers"), sort_track_2, pass_ref=True) -# tab_menu.add(_("Sort Year per Artist"), year_sort, pass_ref=True) + gui.playlist_text_offset = round(gui.playlist_row_height * 0.55) + 4 - 13 * gui.scale -tab_menu.add_to_sub(1, MenuItem(_("Sort by Imported Tracks"), imported_sort, pass_ref=True)) -tab_menu.add_to_sub(1, MenuItem(_("Sort by Imported Folders"), imported_sort_folders, pass_ref=True)) -tab_menu.add_to_sub(1, MenuItem(_("Sort by Filepath"), standard_sort, pass_ref=True)) -tab_menu.add_to_sub(1, MenuItem(_("Sort Track Numbers"), sort_track_2, pass_ref=True)) -tab_menu.add_to_sub(1, MenuItem(_("Sort Year per Artist"), year_sort, pass_ref=True)) -tab_menu.add_to_sub(1, MenuItem(_("Make Playlist Auto-Sorting"), make_auto_sorting, pass_ref=True)) + if gui.scale != 1: + real_font_px = ddt.f_dict[gui.row_font_size][2] + # gui.playlist_text_offset = (round(gui.playlist_row_height - real_font_px) / 2) - ddt.get_y_offset("AbcD", gui.row_font_size, 100) + round(1.3 * gui.scale) -tab_menu.br() + if gui.scale < 1.3: + gui.playlist_text_offset = round(((gui.playlist_row_height - real_font_px) / 2) - 1.9 * gui.scale) + elif gui.scale < 1.5: + gui.playlist_text_offset = round(((gui.playlist_row_height - real_font_px) / 2) - 1.3 * gui.scale) + elif gui.scale < 1.75: + gui.playlist_text_offset = round(((gui.playlist_row_height - real_font_px) / 2) - 1.1 * gui.scale) + elif gui.scale < 2.3: + gui.playlist_text_offset = round(((gui.playlist_row_height - real_font_px) / 2) - 1.5 * gui.scale) + else: + gui.playlist_text_offset = round(((gui.playlist_row_height - real_font_px) / 2) - 1.8 * gui.scale) -tab_menu.add(MenuItem(_("Rescan Folder"), re_import2, rescan_deco, pass_ref=True, pass_ref_deco=True)) + gui.playlist_text_offset += prefs.tracklist_y_text_offset -tab_menu.add(MenuItem(_("Paste"), s_append, paste_deco, pass_ref=True)) -tab_menu.add(MenuItem(_("Append Playing"), append_current_playing, append_deco, pass_ref=True)) -tab_menu.br() + gui.pl_title_real_height = round(gui.playlist_row_height * 0.55) + 4 - 12 -# tab_menu.add("Sort By Filepath", sort_path_pl, pass_ref=True) + # ------------------------------------------------------------------------- + gui.playlist_view_length = int( + (window_size[1] - gui.panelBY - gui.playlist_top - 12 * gui.scale) // gui.playlist_row_height) -tab_menu.add(MenuItem(_("Export…"), export_playlist_box.activate, pass_ref=True)) + box_r = gui.rspw / (window_size[1] - gui.panelBY - gui.panelY) -tab_menu.add_sub(_("Misc…"), 175) + if gui.art_aspect_ratio > 1.01: + gui.art_unlock_ratio = True + gui.art_max_ratio_lock = max(gui.art_aspect_ratio, gui.art_max_ratio_lock) -def forget_pl_import_folder(pl: int) -> None: - pctl.multi_playlist[pl].last_folder = [] + #logging.info("Avaliabe: " + str(box_r)) + elif box_r <= 1: + gui.art_unlock_ratio = False + gui.art_max_ratio_lock = 1 + if side_drag and key_shift_down: + gui.art_unlock_ratio = True + gui.art_max_ratio_lock = 5 -def remove_duplicates(pl: int) -> None: - playlist = [] + gui.rspw = gui.pref_rspw + if album_mode: + gui.rspw = gui.pref_gallery_w - for item in pctl.multi_playlist[pl].playlist_ids: - if item not in playlist: - playlist.append(item) + # Limit the right side panel width to height of area + if gui.rsp and prefs.side_panel_layout == 0: + if album_mode: + pass + else: - removed = len(pctl.multi_playlist[pl].playlist_ids) - len(playlist) - if not removed: - show_message(_("No duplicates were found")) - else: - show_message(_("{N} duplicates removed").format(N=removed), mode="done") + if not gui.art_unlock_ratio: - pctl.multi_playlist[pl].playlist_ids[:] = playlist[:] + if gui.rsp_full_lock and not side_drag: + gui.rspw = window_size[0] + gui.rspw = min(gui.rspw, window_size[1] - gui.panelY - gui.panelBY) -def start_quick_add(pl: int) -> None: - pctl.quick_add_target = pl_to_id(pl) - show_message( - _("You can now add/remove albums to this playlist by right clicking in gallery of any playlist"), - _("To exit this mode, click \"Disengage\" from main MENU")) + # Determine how wide the playlist need to be + gui.plw = window_size[0] + gui.playlist_left = 0 + if gui.lsp: + # if gui.plw > gui.lspw: + gui.plw -= gui.lspw + gui.playlist_left = gui.lspw + if gui.rsp: + gui.plw -= gui.rspw + # Shrink side panel if playlist gets too small + if window_size[0] > 100 and not gui.hide_tracklist_in_gallery: -def auto_get_sync_targets(): - search_paths = [ - "/run/user/*/gvfs/*/*/[Mm]usic", - "/run/media/*/*/[Mm]usic"] - result_paths = [] - for item in search_paths: - result_paths.extend(glob.glob(item)) - return result_paths + if gui.plw < 300: + if gui.rsp: + l = 0 + if gui.lsp: + l = gui.lspw -def auto_sync_thread(pl: int) -> None: - if prefs.transcode_inplace: - show_message(_("Cannot sync when in transcode inplace mode")) - return + gui.rspw = max(window_size[0] - l - 300, 110) + # if album_mode and window_size[0] > 750 * gui.scale: + # gui.pref_gallery_w = gui.rspw - # Find target path - gui.sync_progress = "Starting Sync..." - gui.update += 1 + # Determine how wide the playlist need to be (again) + gui.plw = window_size[0] + gui.playlist_left = 0 + if gui.lsp: + # if gui.plw > gui.lspw: + gui.plw -= gui.lspw + gui.playlist_left = gui.lspw + if gui.rsp: + gui.plw -= gui.rspw - path = Path(sync_target.text.strip().rstrip("/").rstrip("\\").replace("\n", "").replace("\r", "")) - logging.debug(f"sync_path: {path}") - if not path: - show_message(_("No target folder selected")) - gui.sync_progress = "" - gui.stop_sync = False - gui.update += 1 - return - if not path.is_dir(): - show_message(_("Target folder could not be found")) - gui.sync_progress = "" - gui.stop_sync = False - gui.update += 1 - return + if window_size[0] < 630 * gui.scale: + gui.compact_bar = True + else: + gui.compact_bar = False - prefs.sync_target = str(path) + gui.pl_update = 1 - # Get list of folder names on device - logging.info("Getting folder list from device...") - d_folder_names = path.iterdir() - logging.info("Got list") + # Tracklist sizing ---------------------------------------------------- + left = gui.playlist_left + width = gui.plw - # Get list of folders we want - folders = convert_playlist(pl, get_list=True) - folder_names: list[str] = [] - folder_dict = {} + center_mode = True + if gui.lsp or gui.rsp or gui.set_mode: + center_mode = False - if gui.stop_sync: - gui.sync_progress = "" - gui.stop_sync = False - gui.update += 1 + if gui.set_mode and window_size[0] < 600: + center_mode = False - # Find the folder names the transcode function would name them - for folder in folders: - name = encode_folder_name(pctl.get_track(folder[0])) - for item in folder: - if pctl.get_track(item).album != pctl.get_track(folder[0]).album: - name = os.path.basename(pctl.get_track(folder[0]).parent_folder_path) - break - folder_names.append(name) - folder_dict[name] = folder + highlight_left = 0 + highlight_width = width - # ------ - # Find deletes - if prefs.sync_deletes: - for d_folder in d_folder_names: - d_folder = d_folder.name - if gui.stop_sync: - break - if d_folder not in folder_names: - gui.sync_progress = _("Deleting folders...") - gui.update += 1 - logging.warning(f"DELETING: {d_folder}") - shutil.rmtree(path / d_folder) + inset_left = highlight_left + 23 * gui.scale + inset_width = highlight_width - 32 * gui.scale - # ------- - # Find todos - todos: list[str] = [] - for folder in folder_names: - if folder not in d_folder_names: - todos.append(folder) - logging.info(f"Want to add: {folder}") - else: - logging.error(f"Already exists: {folder}") + if gui.lsp and not gui.rsp: + inset_width -= 10 * gui.scale - gui.update += 1 - # ----- - # Prepare and copy - for i, item in enumerate(todos): - gui.sync_progress = _("Copying files to device") - if gui.stop_sync: - break + if gui.lsp: + inset_left -= 10 * gui.scale + inset_width += 10 * gui.scale - free_space = shutil.disk_usage(path)[2] / 8 / 100000000 # in GB - if free_space < 0.6: - show_message(_("Sync aborted! Low disk space on target device"), mode="warning") - break + if center_mode: + if gui.set_mode: + highlight_left = int(pow((window_size[0] / gui.scale * 0.005), 2) * gui.scale) + else: + highlight_left = int(pow((window_size[0] / gui.scale * 0.01), 2) * gui.scale) - if prefs.bypass_transcode or (prefs.smart_bypass and 0 < pctl.get_track(folder_dict[item][0]).bitrate <= 128): - logging.info("Smart bypass...") + if window_size[0] < 600 * gui.scale: + highlight_left = 3 * gui.scale - source_parent = Path(pctl.get_track(folder_dict[item][0]).parent_folder_path) - if source_parent.exists(): - if (path / item).exists(): - show_message( - _("Sync warning"), _("One or more folders to sync has the same name. Skipping."), mode="warning") - continue + highlight_width -= highlight_left * 2 + inset_left = highlight_left + 18 * gui.scale + inset_width = highlight_width - 25 * gui.scale - (path / item).mkdir() - encode_done = source_parent - else: - show_message(_("One or more folders is missing")) - continue + if window_size[0] < 600 and gui.lsp: + inset_width = highlight_width - 18 * gui.scale - else: + gui.tracklist_center_mode = center_mode + gui.tracklist_inset_left = inset_left + gui.tracklist_inset_width = inset_width + gui.tracklist_highlight_left = highlight_left + gui.tracklist_highlight_width = highlight_width - encode_done = prefs.encoder_output / item - # TODO(Martin): We should make sure that the length of the source and target matches or is greater, not just that the dir exists and is not empty! - if not encode_done.exists() or not any(encode_done.iterdir()): - logging.info("Need to transcode") - remain = len(todos) - i - if remain > 1: - gui.sync_progress = _("{N} Folders Remaining").format(N=str(remain)) - else: - gui.sync_progress = _("{N} Folder Remaining").format(N=str(remain)) - transcode_list.append(folder_dict[item]) - tauon.thread_manager.ready("worker") - while transcode_list: - time.sleep(1) - if gui.stop_sync: - break - else: - logging.warning("A transcode is already done") + if album_mode and gui.hide_tracklist_in_gallery: + gui.show_playlist = False + gui.rspw = window_size[0] - 20 * gui.scale + if gui.lsp: + gui.rspw -= gui.lspw - if encode_done.exists(): + # -------------------------------------------------------------------- - if (path / item).exists(): - show_message( - _("Sync warning"), _("One or more folders to sync has the same name. Skipping."), mode="warning") - continue - - (path / item).mkdir() - - for file in encode_done.iterdir(): - file = file.name - logging.info(f"Copy file {file} to {path / item}…") - # gui.sync_progress += "." - gui.update += 1 - - if (encode_done / file).is_file(): - size = os.path.getsize(encode_done / file) - sync_file_timer.set() - try: - shutil.copyfile(encode_done / file, path / item / file) - except OSError as e: - if str(e).startswith("[Errno 22] Invalid argument: "): - sanitized_file = re.sub(r'[<>:"/\\|?*]', '_', file) - if sanitized_file == file: - logging.exception("Unknown OSError trying to copy file, maybe FS does not support the name?") - else: - shutil.copyfile(encode_done / file, path / item / sanitized_file) - logging.warning(f"Had to rename {file} to {sanitized_file} on the output! Probably a FS limitation!") - else: - logging.exception("Unknown OSError trying to copy file") - except Exception: - logging.exception("Unknown error trying to copy file") + if window_size[0] > gui.max_window_tex or window_size[1] > gui.max_window_tex: - if gui.sync_speed == 0 or (sync_file_update_timer.get() > 1 and not file.endswith(".jpg")): - sync_file_update_timer.set() - gui.sync_speed = size / sync_file_timer.get() - gui.sync_progress = _("Copying files to device") + " @ " + get_filesize_string_rounded( - gui.sync_speed) + "/s" - if gui.stop_sync: - gui.sync_progress = _("Aborting Sync") + " @ " + get_filesize_string_rounded(gui.sync_speed) + "/s" + while window_size[0] > gui.max_window_tex: + gui.max_window_tex += 1000 + while window_size[1] > gui.max_window_tex: + gui.max_window_tex += 1000 - logging.info("Finished copying folder") + gui.tracklist_texture_rect = SDL_Rect(0, 0, gui.max_window_tex, gui.max_window_tex) - gui.sync_speed = 0 - gui.sync_progress = "" - gui.stop_sync = False - gui.update += 1 - show_message(_("Sync completed"), mode="done") + SDL_DestroyTexture(gui.tracklist_texture) + SDL_RenderClear(renderer) + gui.tracklist_texture = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, + gui.max_window_tex, + gui.max_window_tex) + SDL_SetRenderTarget(renderer, gui.tracklist_texture) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_RenderClear(renderer) + SDL_SetTextureBlendMode(gui.tracklist_texture, SDL_BLENDMODE_BLEND) -def auto_sync(pl: int) -> None: - shoot_dl = threading.Thread(target=auto_sync_thread, args=([pl])) - shoot_dl.daemon = True - shoot_dl.start() + # SDL_SetRenderTarget(renderer, gui.main_texture) + # SDL_RenderClear(renderer) + SDL_DestroyTexture(gui.main_texture) + gui.main_texture = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, + gui.max_window_tex, + gui.max_window_tex) + SDL_SetTextureBlendMode(gui.main_texture, SDL_BLENDMODE_BLEND) + SDL_SetRenderTarget(renderer, gui.main_texture) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_SetRenderTarget(renderer, gui.main_texture) + SDL_RenderClear(renderer) -def set_sync_playlist(pl: int) -> None: - id = pl_to_id(pl) - if prefs.sync_playlist == id: - prefs.sync_playlist = None - else: - prefs.sync_playlist = pl_to_id(pl) + SDL_DestroyTexture(gui.main_texture_overlay_temp) + gui.main_texture_overlay_temp = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_TARGET, gui.max_window_tex, + gui.max_window_tex) + SDL_SetTextureBlendMode(gui.main_texture_overlay_temp, SDL_BLENDMODE_BLEND) + SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) + SDL_RenderClear(renderer) + update_set() -def sync_playlist_deco(pl: int): - text = _("Set as Sync Playlist") - id = pl_to_id(pl) - if id == prefs.sync_playlist: - text = _("Un-set as Sync Playlist") - return [colours.menu_text, colours.menu_background, text] + if prefs.art_bg: + tauon.thread_manager.ready("style") +def window_is_focused() -> bool: + """Thread safe?""" + if SDL_GetWindowFlags(t_window) & SDL_WINDOW_INPUT_FOCUS: + return True + return False -def set_download_playlist(pl: int) -> None: - id = pl_to_id(pl) - if prefs.download_playlist == id: - prefs.download_playlist = None +def save_state() -> None: + if should_save_state: + logging.info("Writing database to disk... ") else: - prefs.download_playlist = pl_to_id(pl) - -def set_podcast_playlist(pl: int) -> None: - pctl.multi_playlist[pl].persist_time_positioning ^= True - - -def set_download_deco(pl: int): - text = _("Set as Downloads Playlist") - if id == prefs.download_playlist: - text = _("Un-set as Downloads Playlist") - return [colours.menu_text, colours.menu_background, text] - -def set_podcast_deco(pl: int): - text = _("Set Use Persistent Time") - if pctl.multi_playlist[pl].persist_time_positioning: - text = _("Un-set Use Persistent Time") - return [colours.menu_text, colours.menu_background, text] - - -def csv_string(item): - item = str(item) - item.replace("\"", "\"\"") - return f"\"{item}\"" - - -def export_playlist_albums(pl: int) -> None: - p = pctl.multi_playlist[pl] - name = p.title - playlist = p.playlist_ids - - albums = [] - playtimes = {} - last_folder = None - for i, id in enumerate(playlist): - track = pctl.get_track(id) - if last_folder != track.parent_folder_path: - last_folder = track.parent_folder_path - if id not in albums: - albums.append(id) - - playtimes[last_folder] = playtimes.get(last_folder, 0) + int(star_store.get(id)) - - filename = f"{user_directory}/{name}.csv" - xport = open(filename, "w") - - xport.write("Album name;Artist;Release date;Genre;Rating;Playtime;Folder path") - - for id in albums: - track = pctl.get_track(id) - artist = track.album_artist - if not artist: - artist = track.artist - - xport.write("\n") - xport.write(csv_string(track.album) + ",") - xport.write(csv_string(artist) + ",") - xport.write(csv_string(track.date) + ",") - xport.write(csv_string(track.genre) + ",") - xport.write(str(int(album_star_store.get_rating(track)))) - xport.write(",") - xport.write(str(round(playtimes[track.parent_folder_path]))) - xport.write(",") - xport.write(csv_string(track.parent_folder_path)) - - xport.close() - show_message(_("Export complete."), _("Saved as: ") + filename, mode="done") - - -tab_menu.add_to_sub(2, MenuItem(_("Export Playlist Stats"), export_stats, pass_ref=True)) -tab_menu.add_to_sub(2, MenuItem(_("Export Albums CSV"), export_playlist_albums, pass_ref=True)) -tab_menu.add_to_sub(2, MenuItem(_("Transcode All"), convert_playlist, pass_ref=True)) -tab_menu.add_to_sub(2, MenuItem(_("Rescan Tags"), rescan_tags, pass_ref=True)) -# tab_menu.add_to_sub(_('Forget Import Folder'), 2, forget_pl_import_folder, rescan_deco, pass_ref=True, pass_ref_deco=True) -# tab_menu.add_to_sub(_('Re-Import Last Folder'), 1, re_import, pass_ref=True) -# tab_menu.add_to_sub(_('Quick Export XSPF'), 2, export_xspf, pass_ref=True) -# tab_menu.add_to_sub(_('Quick Export M3U'), 2, export_m3u, pass_ref=True) -tab_menu.add_to_sub(2, MenuItem(_("Toggle Breaks"), pl_toggle_playlist_break, pass_ref=True)) -tab_menu.add_to_sub(2, MenuItem(_("Edit Generator..."), edit_generator_box, pass_ref=True)) -tab_menu.add_to_sub(2, MenuItem(_("Engage Gallery Quick Add"), start_quick_add, pass_ref=True)) -tab_menu.add_to_sub(2, MenuItem(_("Set as Sync Playlist"), set_sync_playlist, sync_playlist_deco, pass_ref_deco=True, pass_ref=True)) -tab_menu.add_to_sub(2, MenuItem(_("Set as Downloads Playlist"), set_download_playlist, set_download_deco, pass_ref_deco=True, pass_ref=True)) -tab_menu.add_to_sub(2, MenuItem(_("Set podcast mode"), set_podcast_playlist, set_podcast_deco, pass_ref_deco=True, pass_ref=True)) -tab_menu.add_to_sub(2, MenuItem(_("Remove Duplicates"), remove_duplicates, pass_ref=True)) -tab_menu.add_to_sub(2, MenuItem(_("Toggle Console"), console.toggle)) - - -# tab_menu.add_to_sub("Empty Playlist", 0, new_playlist) - -def best(index: int): - # key = pctl.master_library[index].title + pctl.master_library[index].filename - if pctl.master_library[index].length < 1: - return 0 - return int(star_store.get(index)) - - -def key_rating(index: int): - return star_store.get_rating(index) - -def key_scrobbles(index: int): - return pctl.get_track(index).lfm_scrobbles - -def key_disc(index: int): - return pctl.get_track(index).disc_number - -def key_cue(index: int): - return pctl.get_track(index).is_cue - -def key_playcount(index: int): - # key = pctl.master_library[index].title + pctl.master_library[index].filename - if pctl.master_library[index].length < 1: - return 0 - return star_store.get(index) / pctl.master_library[index].length - # if key in pctl.star_library: - # return pctl.star_library[key] / pctl.master_library[index].length - # else: - # return 0 - - -def add_pl_tag(text): - return f" <{text}>" - + logging.warning("Dev mode, not saving state... ") + return -def gen_top_rating(index, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids - playlist = copy.deepcopy(source) - playlist = sorted(playlist, key=key_rating, reverse=True) + # view_prefs['star-lines'] = star_lines + view_prefs["update-title"] = update_title + view_prefs["side-panel"] = prefs.prefer_side + view_prefs["dim-art"] = prefs.dim_art + #view_prefs['level-meter'] = gui.turbo + # view_prefs['pl-follow'] = pl_follow + view_prefs["scroll-enable"] = scroll_enable + view_prefs["break-enable"] = break_enable + # view_prefs['dd-index'] = dd_index + view_prefs["append-date"] = prefs.append_date - if custom_list is not None: - return playlist + tauonplaylist_jar = [] + tauonqueueitem_jar = [] +# if db_version > 68: + for v in pctl.multi_playlist: +# logging.warning(f"Playlist: {v}") + tauonplaylist_jar.append(v.__dict__) + for v in pctl.force_queue: +# logging.warning(f"Queue: {v}") + tauonqueueitem_jar.append(v.__dict__) +# else: +# tauonplaylist_jar = pctl.multi_playlist +# tauonqueueitem_jar = pctl.track_queue - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Top Rated Tracks")), - playlist_ids=copy.deepcopy(playlist), - hide_title=True)) + trackclass_jar = [] + for v in pctl.master_library.values(): + trackclass_jar.append(v.__dict__) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rat>" - - -def gen_top_100(index, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids - playlist = copy.deepcopy(source) - playlist = sorted(playlist, key=best, reverse=True) - - if custom_list is not None: - return playlist - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Top Played Tracks")), - playlist_ids=copy.deepcopy(playlist), - hide_title=True)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a pt>" - - -tab_menu.add_to_sub(0, MenuItem(_("Top Played Tracks"), gen_top_100, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Top Played Tracks"), gen_top_100, pass_ref=True)) - - -def gen_folder_top(pl: int, get_sets: bool = False, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[pl].playlist_ids - - if len(source) < 3: - return [] - - sets = [] - se = [] - tr = pctl.get_track(source[0]) - last = tr.parent_folder_path - last_al = tr.album - for track in source: - if last != pctl.master_library[track].parent_folder_path or last_al != pctl.master_library[track].album: - last = pctl.master_library[track].parent_folder_path - last_al = pctl.master_library[track].album - sets.append(copy.deepcopy(se)) - se = [] - se.append(track) - sets.append(copy.deepcopy(se)) - - def best(folder): - #logging.info(folder) - total_star = 0 - for item in folder: - # key = pctl.master_library[item].title + pctl.master_library[item].filename - # if key in pctl.star_library: - # total_star += int(pctl.star_library[key]) - total_star += int(star_store.get(item)) - #logging.info(total_star) - return total_star - - if get_sets: - r = [] - for item in sets: - r.append((item, best(item))) - return r - - sets = sorted(sets, key=best, reverse=True) - - playlist = [] - - for se in sets: - playlist += se - - # pctl.multi_playlist.append( - # [pctl.multi_playlist[pl].title + " <Most Played Albums>", 0, copy.deepcopy(playlist), 0, 0, 0]) - if custom_list is not None: - return playlist - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[pl].title + add_pl_tag(_("Top Played Albums")), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a pa>" - - -tab_menu.add_to_sub(0, MenuItem(_("Top Played Albums"), gen_folder_top, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Top Played Albums"), gen_folder_top, pass_ref=True)) - -tab_menu.add_to_sub(0, MenuItem(_("Top Rated Tracks"), gen_top_rating, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Top Rated Tracks"), gen_top_rating, pass_ref=True)) - - -def gen_folder_top_rating(pl: int, get_sets: bool = False, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[pl].playlist_ids - - if len(source) < 3: - return [] - - sets = [] - se = [] - tr = pctl.get_track(source[0]) - last = tr.parent_folder_path - last_al = tr.album - for track in source: - if last != pctl.master_library[track].parent_folder_path or last_al != pctl.master_library[track].album: - last = pctl.master_library[track].parent_folder_path - last_al = pctl.master_library[track].album - sets.append(copy.deepcopy(se)) - se = [] - se.append(track) - sets.append(copy.deepcopy(se)) - - def best(folder): - return album_star_store.get_rating(pctl.get_track(folder[0])) - - if get_sets: - r = [] - for item in sets: - r.append((item, best(item))) - return r - - sets = sorted(sets, key=best, reverse=True) - - playlist = [] - - for se in sets: - playlist += se - - if custom_list is not None: - return playlist - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[pl].title + add_pl_tag(_("Top Rated Albums")), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a rata>" - - -def gen_lyrics(plpl: int, custom_list=None): - playlist = [] - - source = custom_list - if source is None: - source = pctl.multi_playlist[pl].playlist_ids - - for item in source: - if pctl.master_library[item].lyrics != "": - playlist.append(item) - - if custom_list is not None: - return playlist - - if len(playlist) > 0: - pctl.multi_playlist.append( - pl_gen( - title=_("Tracks with lyrics"), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a ly" - - else: - show_message(_("No tracks with lyrics were found.")) - - -tab_menu.add_to_sub(0, MenuItem(_("Top Rated Albums"), gen_folder_top_rating, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Top Rated Albums"), gen_folder_top_rating, pass_ref=True)) - - -def gen_incomplete(plpl: int, custom_list=None): - playlist = [] - - source = custom_list - if source is None: - source = pctl.multi_playlist[pl].playlist_ids - - albums = {} - nums = {} - for id in source: - track = pctl.get_track(id) - if track.album and track.track_number: - - if type(track.track_number) is str and not track.track_number.isdigit(): - continue - - if track.album not in albums: - albums[track.album] = [] - nums[track.album] = [] - - if track not in albums[track.album]: - albums[track.album].append(track) - nums[track.album].append(int(track.track_number)) - - for album, tracks in albums.items(): - numbers = nums[album] - if len(numbers) > 2: - mi = min(numbers) - mx = max(numbers) - for track in tracks: - if type(track.track_total) is int or (type(track.track_total) is str and track.track_total.isdigit()): - mx = max(mx, int(track.track_total)) - r = list(range(int(mi), int(mx))) - for track in tracks: - if int(track.track_number) in r: - r.remove(int(track.track_number)) - if r or mi > 1: - for tr in tracks: - playlist.append(tr.index) - - if custom_list is not None: - return playlist - - if len(playlist) > 0: - show_message(_("Note this may include albums that simply have tracks missing an album tag")) - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[pl].title + add_pl_tag(_("Incomplete Albums")), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - # pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a ly" - - else: - show_message(_("No incomplete albums were found.")) - - -def gen_codec_pl(codec): - playlist = [] - - for pl in pctl.multi_playlist: - for item in pl.playlist_ids: - if pctl.master_library[item].file_ext == codec and item not in playlist: - playlist.append(item) - - if len(playlist) > 0: - pctl.multi_playlist.append( - pl_gen( - title=_("Codec: ") + codec, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - -def gen_last_imported_folders(index, custom_list=None, reverse=True): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids - - a_cache = {} - - def key_import(index: int): - - track = pctl.master_library[index] - cached = a_cache.get((track.album, track.parent_folder_name)) - if cached is not None: - return cached - - if track.album: - a_cache[(track.album, track.parent_folder_name)] = index - return index - - playlist = copy.deepcopy(source) - playlist = sorted(playlist, key=key_import, reverse=reverse) - sort_track_2(0, playlist) - - if custom_list is not None: - return playlist - - -def gen_last_modified(index, custom_list=None, reverse=True): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids - - a_cache = {} - - def key_modified(index: int): - - track = pctl.master_library[index] - cached = a_cache.get((track.album, track.parent_folder_name)) - if cached is not None: - return cached - - if track.album: - a_cache[(track.album, track.parent_folder_name)] = pctl.master_library[index].modified_time - return pctl.master_library[index].modified_time - - playlist = copy.deepcopy(source) - playlist = sorted(playlist, key=key_modified, reverse=reverse) - sort_track_2(0, playlist) - - if custom_list is not None: - return playlist - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("File Modified")), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a m>" - - -tab_menu.add_to_sub(0, MenuItem(_("File Modified"), gen_last_modified, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("File Modified"), gen_last_modified, pass_ref=True)) - - -# tab_menu.add_to_sub(_("File Path"), 0, standard_sort, pass_ref=True) -# extra_tab_menu.add_to_sub(_("File Path"), 0, standard_sort, pass_ref=True) - - -def gen_love(pl: int, custom_list=None): - playlist = [] - - source = custom_list - if source is None: - source = pctl.multi_playlist[pl].playlist_ids - - for item in source: - if get_love_index(item): - playlist.append(item) - - playlist.sort(key=lambda x: get_love_timestamp_index(x), reverse=True) - - if custom_list is not None: - return playlist - - if len(playlist) > 0: - # pctl.multi_playlist.append(["Interesting Comments", 0, copy.deepcopy(playlist), 0, 0, 0]) - pctl.multi_playlist.append( - pl_gen( - title=_("Loved"), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a l" - else: - show_message(_("No loved tracks were found.")) - - -def gen_comment(pl: int) -> None: - playlist = [] - - for item in pctl.multi_playlist[pl].playlist_ids: - cm = pctl.master_library[item].comment - if len(cm) > 20 and \ - cm[0] != "0" and \ - "http://" not in cm and \ - "www." not in cm and \ - "Release" not in cm and \ - "EAC" not in cm and \ - "@" not in cm and \ - ".com" not in cm and \ - "ipped" not in cm and \ - "ncoded" not in cm and \ - "ExactA" not in cm and \ - "WWW." not in cm and \ - cm[2] != "+" and \ - cm[1] != "+": - playlist.append(item) - - if len(playlist) > 0: - # pctl.multi_playlist.append(["Interesting Comments", 0, copy.deepcopy(playlist), 0, 0, 0]) - pctl.multi_playlist.append( - pl_gen( - title=_("Interesting Comments"), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - else: - show_message(_("Nothing of interest was found.")) - - -def gen_replay(pl: int) -> None: - playlist = [] - - for item in pctl.multi_playlist[pl].playlist_ids: - if pctl.master_library[item].misc.get("replaygain_track_gain"): - playlist.append(item) - - if len(playlist) > 0: - pctl.multi_playlist.append( - pl_gen( - title=_("ReplayGain Tracks"), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - else: - show_message(_("No replay gain tags were found.")) - - -def gen_sort_len(index: int, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids - - def length(index: int) -> int: - - if pctl.master_library[index].length < 1: - return 0 - return int(pctl.master_library[index].length) - - playlist = copy.deepcopy(source) - playlist = sorted(playlist, key=length, reverse=True) - - if custom_list is not None: - return playlist - - # pctl.multi_playlist.append( - # [pctl.multi_playlist[index].title + " <Duration Sorted>", 0, copy.deepcopy(playlist), 0, 1, 0]) - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Duration Sorted")), - playlist_ids=copy.deepcopy(playlist), - hide_title=True)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a d>" - - -tab_menu.add_to_sub(0, MenuItem(_("Longest Tracks"), gen_sort_len, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Longest Tracks"), gen_sort_len, pass_ref=True)) - - -def gen_folder_duration(pl: int, get_sets: bool = False): - if len(pctl.multi_playlist[pl].playlist_ids) < 3: - return None - - sets = [] - se = [] - last = pctl.master_library[pctl.multi_playlist[pl].playlist_ids[0]].parent_folder_path - last_al = pctl.master_library[pctl.multi_playlist[pl].playlist_ids[0]].album - for track in pctl.multi_playlist[pl].playlist_ids: - if last != pctl.master_library[track].parent_folder_path or last_al != pctl.master_library[track].album: - last = pctl.master_library[track].parent_folder_path - last_al = pctl.master_library[track].album - sets.append(copy.deepcopy(se)) - se = [] - se.append(track) - sets.append(copy.deepcopy(se)) - - def best(folder): - total_duration = 0 - for item in folder: - total_duration += pctl.master_library[item].length - return total_duration - - if get_sets: - r = [] - for item in sets: - r.append((item, best(item))) - return r - - sets = sorted(sets, key=best, reverse=True) - playlist = [] - - for se in sets: - playlist += se - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[pl].title + add_pl_tag(_("Longest Albums")), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - -tab_menu.add_to_sub(0, MenuItem(_("Longest Albums"), gen_folder_duration, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Longest Albums"), gen_folder_duration, pass_ref=True)) - - -def gen_sort_date(index: int, rev: bool = False, custom_list=None): - def g_date(index: int): - - if pctl.master_library[index].date != "": - return str(pctl.master_library[index].date) - return "z" - - playlist = [] - lowest = 0 - highest = 0 - first = True - - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids - - for item in source: - date = pctl.master_library[item].date - if date != "": - playlist.append(item) - if len(date) > 4 and date[:4].isdigit(): - date = date[:4] - if len(date) == 4 and date.isdigit(): - year = int(date) - if first: - lowest = year - highest = year - first = False - lowest = min(year, lowest) - highest = max(year, highest) - - playlist = sorted(playlist, key=g_date, reverse=rev) - - if custom_list is not None: - return playlist - - line = add_pl_tag(_("Year Sorted")) - if lowest != highest and lowest != 0 and highest != 0: - if rev: - line = " <" + str(highest) + "-" + str(lowest) + ">" - else: - line = " <" + str(lowest) + "-" + str(highest) + ">" - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + line, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - if rev: - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a y>" - else: - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a y<" - - -tab_menu.add_to_sub(0, MenuItem(_("Year by Oldest"), gen_sort_date, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Year by Oldest"), gen_sort_date, pass_ref=True)) - - -def gen_sort_date_new(index: int): - gen_sort_date(index, True) - - -tab_menu.add_to_sub(0, MenuItem(_("Year by Latest"), gen_sort_date_new, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Year by Latest"), gen_sort_date_new, pass_ref=True)) - - -# tab_menu.add_to_sub(_("Year by Artist"), 0, year_sort, pass_ref=True) -# extra_tab_menu.add_to_sub(_("Year by Artist"), 0, year_sort, pass_ref=True) - -def gen_500_random(index: int): - playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - - random.shuffle(playlist) - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Shuffled Tracks")), - playlist_ids=copy.deepcopy(playlist), - hide_title=True)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a st" - - -tab_menu.add_to_sub(0, MenuItem(_("Shuffled Tracks"), gen_500_random, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Shuffled Tracks"), gen_500_random, pass_ref=True)) - - -def gen_folder_shuffle(index, custom_list=None): - folders = [] - dick = {} - - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids - - for track in source: - parent = pctl.master_library[track].parent_folder_path - if parent not in folders: - folders.append(parent) - if parent not in dick: - dick[parent] = [] - dick[parent].append(track) - - random.shuffle(folders) - playlist = [] - - for folder in folders: - playlist += dick[folder] - - if custom_list is not None: - return playlist - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Shuffled Albums")), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a ra" - - -tab_menu.add_to_sub(0, MenuItem(_("Shuffled Albums"), gen_folder_shuffle, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Shuffled Albums"), gen_folder_shuffle, pass_ref=True)) - - -def gen_best_random(index: int): - playlist = [] - - for p in pctl.multi_playlist[index].playlist_ids: - time = star_store.get(p) - - if time > 300: - playlist.append(p) - - random.shuffle(playlist) - - if len(playlist) > 0: - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Lucky Random")), - playlist_ids=copy.deepcopy(playlist), - hide_title=True)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a pt>300 rt" - - -tab_menu.add_to_sub(0, MenuItem(_("Lucky Random"), gen_best_random, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Lucky Random"), gen_best_random, pass_ref=True)) - - -def gen_reverse(index, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids - - playlist = list(reversed(source)) - - if custom_list is not None: - return playlist - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Reversed")), - playlist_ids=copy.deepcopy(playlist), - hide_title=pctl.multi_playlist[index].hide_title)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rv" - - -tab_menu.add_to_sub(0, MenuItem(_("Reverse Tracks"), gen_reverse, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Reverse Tracks"), gen_reverse, pass_ref=True)) - - -def gen_folder_reverse(index: int, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids - - folders = [] - dick = {} - for track in source: - parent = pctl.master_library[track].parent_folder_path - if parent not in folders: - folders.append(parent) - if parent not in dick: - dick[parent] = [] - dick[parent].append(track) - - folders = list(reversed(folders)) - playlist = [] - - for folder in folders: - playlist += dick[folder] - - if custom_list is not None: - return playlist - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Reversed Albums")), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rva" - - -tab_menu.add_to_sub(0, MenuItem(_("Reverse Albums"), gen_folder_reverse, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Reverse Albums"), gen_folder_reverse, pass_ref=True)) - - -def gen_dupe(index: int) -> None: - playlist = pctl.multi_playlist[index].playlist_ids - - pctl.multi_playlist.append( - pl_gen( - title=gen_unique_pl_title(pctl.multi_playlist[index].title, _("Duplicate") + " ", 0), - playing=pctl.multi_playlist[index].playing, - playlist_ids=copy.deepcopy(playlist), - position=pctl.multi_playlist[index].position, - hide_title=pctl.multi_playlist[index].hide_title, - selected=pctl.multi_playlist[index].selected)) - - -tab_menu.add_to_sub(0, MenuItem(_("Duplicate"), gen_dupe, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Duplicate"), gen_dupe, pass_ref=True)) - - -def gen_sort_path(index: int) -> None: - def path(index: int) -> str: - return pctl.master_library[index].fullpath - - playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - playlist = sorted(playlist, key=path) - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Filepath Sorted")), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - -# tab_menu.add_to_sub("Filepath", 1, gen_sort_path, pass_ref=True) - - -def gen_sort_artist(index: int) -> None: - def artist(index: int) -> str: - return pctl.master_library[index].artist - - playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - playlist = sorted(playlist, key=artist) - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Artist Sorted")), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - -# tab_menu.add_to_sub("Artist → gui.abc", 0, gen_sort_artist, pass_ref=True) - - -def gen_sort_album(index: int) -> None: - def album(index: int) -> None: - return pctl.master_library[index].album - - playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - playlist = sorted(playlist, key=album) - - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Album Sorted")), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - -# tab_menu.add_to_sub("Album → gui.abc", 0, gen_sort_album, pass_ref=True) -tab_menu.add_to_sub(0, MenuItem(_("Loved"), gen_love, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Loved"), gen_love, pass_ref=True)) -tab_menu.add_to_sub(0, MenuItem(_("Has Comment"), gen_comment, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Has Comment"), gen_comment, pass_ref=True)) -tab_menu.add_to_sub(0, MenuItem(_("Has Lyrics"), gen_lyrics, pass_ref=True)) -extra_tab_menu.add_to_sub(0, MenuItem(_("Has Lyrics"), gen_lyrics, pass_ref=True)) - - -def get_playing_line() -> str: - if 3 > pctl.playing_state > 0: - title = pctl.master_library[pctl.track_queue[pctl.queue_step]].title - artist = pctl.master_library[pctl.track_queue[pctl.queue_step]].artist - return artist + " - " + title - return "Stopped" - - - -def reload_config_file(): - if transcode_list: - show_message(_("Cannot reload while a transcode is in progress!"), mode="error") - return - - load_prefs() - gui.opened_config_file = False - - ddt.force_subpixel_text = prefs.force_subpixel_text - ddt.clear_text_cache() - pctl.playerCommand = "reload" - pctl.playerCommandReady = True - show_message(_("Configuration reloaded"), mode="done") - gui.update_layout() - - -def open_config_file(): - save_prefs() - target = str(config_directory / "tauon.conf") - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", "-t", target]) - else: - subprocess.call(["xdg-open", target]) - show_message(_("Config file opened."), _('Click "Reload" if you made any changes'), mode="arrow") - # reload_config_file() - # gui.message_box = False - gui.opened_config_file = True - - -def open_keymap_file(): - target = str(config_directory / "input.txt") - - if not os.path.isfile(target): - show_message(_("Input file missing")) - return - - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", target]) - else: - subprocess.call(["xdg-open", target]) - - -def open_file(target): - if not os.path.isfile(target): - show_message(_("Input file missing")) - return - - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", target]) - else: - subprocess.call(["xdg-open", target]) - - -def open_data_directory(): - target = str(user_directory) - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", target]) - else: - subprocess.call(["xdg-open", target]) - - -def remove_folder(index: int): - global default_playlist - - for b in range(len(default_playlist) - 1, -1, -1): - r_folder = pctl.master_library[index].parent_folder_name - if pctl.master_library[default_playlist[b]].parent_folder_name == r_folder: - del default_playlist[b] - - reload() - - -def convert_folder(index: int): - global default_playlist - global transcode_list - - if not tauon.test_ffmpeg(): - return - - folder = [] - if key_shift_down or key_shiftr_down: - track_object = pctl.get_track(index) - if track_object.is_network: - show_message(_("Transcoding tracks from network locations is not supported")) - return - folder = [index] - - if prefs.transcode_codec == "flac" and track_object.file_ext.lower() in ( - "mp3", "opus", - "mp4", "ogg", - "aac"): - show_message(_("NO! Bad user!"), _("Im not going to let you transcode a lossy codec to a lossless one!"), - mode="warning") - - return - folder = [index] - - else: - r_folder = pctl.master_library[index].parent_folder_path - for item in default_playlist: - if r_folder == pctl.master_library[item].parent_folder_path: - - track_object = pctl.get_track(item) - if track_object.file_ext == "SPOT": # track_object.is_network: - show_message(_("Transcoding spotify tracks not possible")) - return - - if item not in folder: - folder.append(item) - #logging.info(prefs.transcode_codec) - #logging.info(track_object.file_ext) - if prefs.transcode_codec == "flac" and track_object.file_ext.lower() in ( - "mp3", "opus", - "mp4", "ogg", - "aac"): - show_message(_("NO! Bad user!"), _("Im not going to let you transcode a lossy codec to a lossless one!"), - mode="warning") - - return - - #logging.info(folder) - transcode_list.append(folder) - tauon.thread_manager.ready("worker") - - -def transfer(index: int, args) -> None: - global cargo - global default_playlist - old_cargo = copy.deepcopy(cargo) - - if args[0] == 1 or args[0] == 0: # copy - if args[1] == 1: # single track - cargo.append(index) - if args[0] == 0: # cut - del default_playlist[pctl.selected_in_playlist] - - elif args[1] == 2: # folder - for b in range(len(default_playlist)): - if pctl.master_library[default_playlist[b]].parent_folder_name == pctl.master_library[ - index].parent_folder_name: - cargo.append(default_playlist[b]) - if args[0] == 0: # cut - for b in reversed(range(len(default_playlist))): - if pctl.master_library[default_playlist[b]].parent_folder_name == pctl.master_library[ - index].parent_folder_name: - del default_playlist[b] - - elif args[1] == 3: # playlist - cargo += default_playlist - if args[0] == 0: # cut - default_playlist = [] - - elif args[0] == 2: # Drop - if args[1] == 1: # Before - - insert = pctl.selected_in_playlist - while insert > 0 and pctl.master_library[default_playlist[insert]].parent_folder_name == \ - pctl.master_library[index].parent_folder_name: - insert -= 1 - if insert == 0: - break - else: - insert += 1 - - while len(cargo) > 0: - default_playlist.insert(insert, cargo.pop()) - - elif args[1] == 2: # After - insert = pctl.selected_in_playlist - - while insert < len(default_playlist) and pctl.master_library[default_playlist[insert]].parent_folder_name == \ - pctl.master_library[index].parent_folder_name: - insert += 1 - - while len(cargo) > 0: - default_playlist.insert(insert, cargo.pop()) - elif args[1] == 3: # End - default_playlist += cargo - # cargo = [] - - cargo = old_cargo - - reload() - - -def temp_copy_folder(ref): - global cargo - cargo = [] - transfer(ref, args=[1, 2]) - - -def activate_track_box(index: int): - global track_box - global r_menu_index - r_menu_index = index - track_box = True - track_box_path_tool_timer.set() - - -def menu_paste(position): - paste(None, position) - - -def s_copy(): - # Copy tracks to internal clipboard - # gui.lightning_copy = False - # if key_shift_down: - gui.lightning_copy = True - - clip = copy_from_clipboard() - if "file://" in clip: - copy_to_clipboard("") - - global cargo - cargo = [] - if default_playlist: - for item in shift_selection: - cargo.append(default_playlist[item]) - - if not cargo and -1 < pctl.selected_in_playlist < len(default_playlist): - cargo.append(default_playlist[pctl.selected_in_playlist]) - - tauon.copied_track = None - - if len(cargo) == 1: - tauon.copied_track = cargo[0] - - -def directory_size(path: str) -> int: - total = 0 - for dirpath, dirname, filenames in os.walk(path): - for file in filenames: - path = os.path.join(dirpath, file) - total += os.path.getsize(path) - return total - - -def lightning_paste(): - move = True - # if not key_shift_down: - # move = False - - move_track = pctl.get_track(cargo[0]) - move_path = move_track.parent_folder_path - - for item in cargo: - if move_path != pctl.get_track(item).parent_folder_path: - show_message( - _("More than one folder is in the clipboard"), - _("This function can only move one folder at a time."), mode="info") - return - - match_track = pctl.get_track(default_playlist[shift_selection[0]]) - match_path = match_track.parent_folder_path - - if pctl.playing_state > 0 and move: - if pctl.playing_object().parent_folder_path == move_path: - pctl.stop(True) - - p = Path(match_path) - s = list(p.parts) - base = s[0] - c = base - del s[0] - - to_move = [] - for pl in pctl.multi_playlist: - for i in reversed(range(len(pl.playlist_ids))): - if pctl.get_track(pl.playlist_ids[i]).parent_folder_path == move_track.parent_folder_path: - to_move.append(pl.playlist_ids[i]) - - to_move = list(set(to_move)) - - for level in s: - upper = c - c = os.path.join(c, level) - - t_artist = match_track.artist - ta_artist = match_track.album_artist - - t_artist = filename_safe(t_artist) - ta_artist = filename_safe(ta_artist) - - if (len(t_artist) > 0 and t_artist in level) or \ - (len(ta_artist) > 0 and ta_artist in level): - - logging.info("found target artist level") - logging.info(t_artist) - logging.info("Upper folder is: " + upper) - - if len(move_path) < 4: - show_message(_("Safety interupt! The source path seems oddly short."), move_path, mode="error") - return - - if not os.path.isdir(upper): - show_message(_("The target directory is missing!"), upper, mode="warning") - return - - if not os.path.isdir(move_path): - show_message(_("The source directory is missing!"), move_path, mode="warning") - return - - protect = ("", "Documents", "Music", "Desktop", "Downloads") - for fo in protect: - if move_path.strip("\\/") == os.path.join(os.path.expanduser("~"), fo).strip("\\/"): - show_message(_("Better not do anything to that folder!"), os.path.join(os.path.expanduser("~"), fo), - mode="warning") - return - - if directory_size(move_path) > 3000000000: - show_message(_("Folder size safety limit reached! (3GB)"), move_path, mode="warning") - return - - if len(next(os.walk(move_path))[2]) > max(20, len(to_move) * 2): - show_message(_("Safety interupt! The source folder seems to have many files."), move_path, mode="warning") - return - - artist = move_track.artist - if move_track.album_artist != "": - artist = move_track.album_artist - - artist = filename_safe(artist) - - if artist == "": - show_message(_("The track needs to have an artist name.")) - return - - artist_folder = os.path.join(upper, artist) - - logging.info("Target will be: " + artist_folder) - - if os.path.isdir(artist_folder): - logging.info("The target artist folder already exists") - else: - logging.info("Need to make artist folder") - os.makedirs(artist_folder) - - logging.info("The folder to be moved is: " + move_path) - load_order = LoadClass() - load_order.target = os.path.join(artist_folder, move_track.parent_folder_name) - load_order.playlist = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - - insert = shift_selection[0] - old_insert = insert - while insert < len(default_playlist) and pctl.master_library[ - pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids[insert]].parent_folder_name == \ - pctl.master_library[ - pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids[old_insert]].parent_folder_name: - insert += 1 - - load_order.playlist_position = insert - - move_jobs.append( - (move_path, os.path.join(artist_folder, move_track.parent_folder_name), move, - move_track.parent_folder_name, load_order)) - tauon.thread_manager.ready("worker") - # Remove all tracks with the old paths - for pl in pctl.multi_playlist: - for i in reversed(range(len(pl.playlist_ids))): - if pctl.get_track(pl.playlist_ids[i]).parent_folder_path == move_track.parent_folder_path: - del pl.playlist_ids[i] - - break - else: - show_message(_("Could not find a folder with the artist's name to match level at.")) - return - - # for file in os.listdir(artist_folder): - # - - if album_mode: - prep_gal() - reload_albums(True) - - cargo.clear() - gui.lightning_copy = False - - -def paste(playlist_no=None, track_id=None): - clip = copy_from_clipboard() - logging.info(clip) - if "tidal.com/album/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - if num and num.isnumeric(): - logging.info(num) - tauon.tidal.append_album(num) - clip = False - - elif "tidal.com/playlist/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.playlist(num) - clip = False - - elif "tidal.com/mix/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.mix(num) - clip = False - - elif "tidal.com/browse/track/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.track(num) - clip = False - - elif "tidal.com/browse/artist/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.artist(num) - clip = False - - elif "spotify" in clip: - cargo.clear() - for link in clip.split("\n"): - logging.info(link) - link = link.strip() - if clip.startswith(("https://open.spotify.com/track/", "spotify:track:")): - tauon.spot_ctl.append_track(link) - elif clip.startswith(("https://open.spotify.com/album/", "spotify:album:")): - l = tauon.spot_ctl.append_album(link, return_list=True) - if l: - cargo.extend(l) - elif clip.startswith("https://open.spotify.com/playlist/"): - tauon.spot_ctl.playlist(link) - if album_mode: - reload_albums() - gui.pl_update += 1 - clip = False - - found = False - if clip: - clip = clip.split("\n") - for i, line in enumerate(clip): - if line.startswith(("file://", "/")): - target = str(urllib.parse.unquote(line)).replace("file://", "").replace("\r", "") - load_order = LoadClass() - load_order.target = target - load_order.playlist = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - - if playlist_no is not None: - load_order.playlist = pl_to_id(playlist_no) - if track_id is not None: - load_order.playlist_position = r_menu_position - - load_orders.append(copy.deepcopy(load_order)) - found = True - - if not found: - - if playlist_no is None: - if track_id is None: - transfer(0, (2, 3)) - else: - transfer(track_id, (2, 2)) - else: - append_playlist(playlist_no) - - gui.pl_update += 1 - - -def s_cut(): - s_copy() - del_selected() - - -playlist_menu.add(MenuItem("Paste", paste, paste_deco)) - - -def paste_playlist_coast_fire(): - url = None - if tauon.spot_ctl.coasting and pctl.playing_state == 3: - url = tauon.spot_ctl.get_album_url_from_local(pctl.playing_object()) - elif pctl.playing_ready() and "spotify-album-url" in pctl.playing_object().misc: - url = pctl.playing_object().misc["spotify-album-url"] - if url: - default_playlist.extend(tauon.spot_ctl.append_album(url, return_list=True)) - gui.pl_update += 1 - -def paste_playlist_track_coast_fire(): - url = None - # if tauon.spot_ctl.coasting and pctl.playing_state == 3: - # url = tauon.spot_ctl.get_album_url_from_local(pctl.playing_object()) - if pctl.playing_ready() and "spotify-track-url" in pctl.playing_object().misc: - url = pctl.playing_object().misc["spotify-track-url"] - if url: - tauon.spot_ctl.append_track(url) - gui.pl_update += 1 - - -def paste_playlist_coast_album(): - shoot_dl = threading.Thread(target=paste_playlist_coast_fire) - shoot_dl.daemon = True - shoot_dl.start() -def paste_playlist_coast_track(): - shoot_dl = threading.Thread(target=paste_playlist_track_coast_fire) - shoot_dl.daemon = True - shoot_dl.start() - -def paste_playlist_coast_album_deco(): - if tauon.spot_ctl.coasting or tauon.spot_ctl.playing: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled - - return [line_colour, colours.menu_background, None] - - -playlist_menu.add(MenuItem(_("Add Playing Spotify Album"), paste_playlist_coast_album, paste_playlist_coast_album_deco, - show_test=spotify_show_test)) -playlist_menu.add(MenuItem(_("Add Playing Spotify Track"), paste_playlist_coast_track, paste_playlist_coast_album_deco, - show_test=spotify_show_test)) - -def refind_playing(): - # Refind playing index - if pctl.playing_ready(): - for i, n in enumerate(default_playlist): - if pctl.track_queue[pctl.queue_step] == n: - pctl.playlist_playing_position = i - break - - -def del_selected(force_delete=False): - global shift_selection - - gui.update += 1 - gui.pl_update = 1 - - if not shift_selection: - shift_selection = [pctl.selected_in_playlist] - - if not default_playlist: - return - - li = [] - - for item in reversed(shift_selection): - if item > len(default_playlist) - 1: - return - - li.append((item, default_playlist[item])) # take note for force delete - - # Correct track playing position - if pctl.active_playlist_playing == pctl.active_playlist_viewing: - if 0 < pctl.playlist_playing_position + 1 > item: - pctl.playlist_playing_position -= 1 - - del default_playlist[item] - - if force_delete: - for item in li: - - tr = pctl.get_track(item[1]) - if not tr.is_network: - try: - send2trash(tr.fullpath) - show_message(_("Tracks sent to trash")) - except Exception: - logging.exception("One or more tracks could not be sent to trash") - show_message(_("One or more tracks could not be sent to trash")) - - if force_delete: - try: - os.remove(tr.fullpath) - show_message(_("Files deleted"), mode="info") - except Exception: - logging.exception("Error deleting one or more files") - show_message(_("Error deleting one or more files"), mode="error") - - else: - undo.bk_tracks(pctl.active_playlist_viewing, li) - - reload() - tree_view_box.clear_target_pl(pctl.active_playlist_viewing) - - pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(default_playlist) - 1) - - shift_selection = [pctl.selected_in_playlist] - gui.pl_update += 1 - refind_playing() - pctl.notify_change() - - -def force_del_selected(): - del_selected(force_delete=True) - - -def test_show(dummy): - return album_mode - - -def show_in_gal(track: TrackClass, silent: bool = False): - # goto_album(pctl.playlist_selected) - gui.gallery_animate_highlight_on = goto_album(pctl.selected_in_playlist) - if not silent: - gallery_select_animate_timer.set() - - -# Create track context menu -track_menu = Menu(195, show_icons=True) - -track_menu.add(MenuItem(_("Open Folder"), open_folder, pass_ref=True, pass_ref_deco=True, icon=folder_icon, disable_test=open_folder_disable_test)) -track_menu.add(MenuItem(_("Track Info…"), activate_track_box, pass_ref=True, icon=info_icon)) - - -def last_fm_test(ignore): - if lastfm.connected: - return True - return False - - -def heart_xmenu_colour(): - global r_menu_index - if love(False, r_menu_index): - return [245, 60, 60, 255] - if colours.lm: - return [255, 150, 180, 255] - return None - - -heartx_icon.colour = [55, 55, 55, 255] -heartx_icon.xoff = 1 -heartx_icon.yoff = 0 -heartx_icon.colour_callback = heart_xmenu_colour - - -def spot_heart_xmenu_colour(): - if not (pctl.playing_state == 1 or pctl.playing_state == 2): - return None - tr = pctl.playing_object() - if tr and "spotify-liked" in tr.misc: - return [30, 215, 96, 255] - return None - - -spot_heartx_icon.colour = [30, 215, 96, 255] -spot_heartx_icon.xoff = 3 -spot_heartx_icon.yoff = 0 -spot_heartx_icon.colour_callback = spot_heart_xmenu_colour - - -def love_decox(): - global r_menu_index - - if love(False, r_menu_index): - return [colours.menu_text, colours.menu_background, _("Un-Love Track")] - return [colours.menu_text, colours.menu_background, _("Love Track")] - - -def love_index(): - global r_menu_index - - notify = False - if not gui.show_hearts: - notify = True - - # love(True, r_menu_index) - shoot_love = threading.Thread(target=love, args=[True, r_menu_index, False, notify]) - shoot_love.daemon = True - shoot_love.start() - - -# Mark track as 'liked' -track_menu.add(MenuItem("Love", love_index, love_decox, icon=heartx_icon)) - -def toggle_spotify_like_ref(): - tr = pctl.get_track(r_menu_index) - if tr: - shoot_dl = threading.Thread(target=toggle_spotify_like_active2, args=([tr])) - shoot_dl.daemon = True - shoot_dl.start() - -def toggle_spotify_like3(): - toggle_spotify_like_active2(pctl.get_track(r_menu_index)) - -def toggle_spotify_like_row_deco(): - tr = pctl.get_track(r_menu_index) - text = _("Spotify Like Track") - - # if pctl.playing_state == 0 or not tr or not "spotify-track-url" in tr.misc: - # return [colours.menu_text_disabled, colours.menu_background, text] - if "spotify-liked" in tr.misc: - text = _("Un-like Spotify Track") - - return [colours.menu_text, colours.menu_background, text] - -def spot_like_show_test(x): - - return spotify_show_test and pctl.get_track(r_menu_index).file_ext == "SPTY" - -def spot_heart_menu_colour(): - tr = pctl.get_track(r_menu_index) - if tr and "spotify-liked" in tr.misc: - return [30, 215, 96, 255] - return None - -heart_spot_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-menu.png", True)) -heart_spot_icon.colour = [30, 215, 96, 255] -heart_spot_icon.xoff = 1 -heart_spot_icon.yoff = 0 -heart_spot_icon.colour_callback = spot_heart_menu_colour - -track_menu.add(MenuItem("Spotify Like Track", toggle_spotify_like_ref, toggle_spotify_like_row_deco, show_test=spot_like_show_test, icon=heart_spot_icon)) - - -def add_to_queue(ref): - pctl.force_queue.append(queue_item_gen(ref, r_menu_position, pl_to_id(pctl.active_playlist_viewing))) - queue_timer_set() - if prefs.stop_end_queue: - pctl.auto_stop = False - - -def add_selected_to_queue(): - gui.pl_update += 1 - if prefs.stop_end_queue: - pctl.auto_stop = False - if gui.album_tab_mode: - add_album_to_queue(default_playlist[get_album_info(pctl.selected_in_playlist)[1][0]], pctl.selected_in_playlist) - queue_timer_set() - else: - pctl.force_queue.append( - queue_item_gen(default_playlist[pctl.selected_in_playlist], - pctl.selected_in_playlist, - pl_to_id(pctl.active_playlist_viewing))) - queue_timer_set() - - -def add_selected_to_queue_multi(): - if prefs.stop_end_queue: - pctl.auto_stop = False - for index in shift_selection: - pctl.force_queue.append( - queue_item_gen(default_playlist[index], - index, - pl_to_id(pctl.active_playlist_viewing))) - - -def queue_timer_set(plural: bool = False, queue_object: TauonQueueItem | None = None) -> None: - queue_add_timer.set() - gui.frame_callback_list.append(TestTimer(2.51)) - gui.queue_toast_plural = plural - if queue_object: - gui.toast_queue_object = queue_object - elif pctl.force_queue: - gui.toast_queue_object = pctl.force_queue[-1] - - -def split_queue_album(id: int) -> int | None: - item = pctl.force_queue[0] - - pl = id_to_pl(item.playlist_id) - if pl is None: - return None - - playlist = pctl.multi_playlist[pl].playlist_ids - - i = pctl.playlist_playing_position + 1 - parts = [] - album_parent_path = pctl.get_track(item.track_id).parent_folder_path - - while i < len(playlist): - if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: - break - - parts.append((playlist[i], i)) - i += 1 - - del pctl.force_queue[0] - - for part in reversed(parts): - pctl.force_queue.insert(0, queue_item_gen(part[0], part[1], item.type)) - return (len(parts)) - - -def add_to_queue_next(ref: int) -> None: - if pctl.force_queue and pctl.force_queue[0].album_stage == 1: - split_queue_album(None) - - pctl.force_queue.insert(0, queue_item_gen(ref, r_menu_position, pl_to_id(pctl.active_playlist_viewing))) - - -# def toggle_queue(mode: int = 0) -> bool: -# if mode == 1: -# return prefs.show_queue -# prefs.show_queue ^= True -# prefs.show_queue ^= True - - -track_menu.add(MenuItem(_("Add to Queue"), add_to_queue, pass_ref=True, hint="MB3")) - -track_menu.add(MenuItem(_("↳ After Current Track"), add_to_queue_next, pass_ref=True, show_test=test_shift)) - -track_menu.add(MenuItem(_("Show in Gallery"), show_in_gal, pass_ref=True, show_test=test_show)) - -track_menu.add_sub(_("Meta…"), 160) - -track_menu.br() -# track_menu.add('Cut', s_cut, pass_ref=False) -# track_menu.add('Remove', del_selected) -track_menu.add(MenuItem(_("Copy"), s_copy, pass_ref=False)) - -# track_menu.add(_('Paste + Transfer Folder'), lightning_paste, pass_ref=False, show_test=lightning_move_test) - -track_menu.add(MenuItem(_("Paste"), menu_paste, paste_deco, pass_ref=True)) - - -def delete_track(track_ref): - tr = pctl.get_track(track_ref) - fullpath = tr.fullpath - - if system == "Windows" or msys: - fullpath = fullpath.replace("/", "\\") - - if tr.is_network: - show_message(_("Cannot delete a network track")) - return - - while track_ref in default_playlist: - default_playlist.remove(track_ref) - - try: - send2trash(fullpath) - - if os.path.exists(fullpath): - try: - os.remove(fullpath) - show_message(_("File deleted"), fullpath, mode="info") - except Exception: - logging.exception("Error deleting file") - show_message(_("Error deleting file"), fullpath, mode="error") - else: - show_message(_("File moved to trash")) - - except Exception: - try: - os.remove(fullpath) - show_message(_("File deleted"), fullpath, mode="info") - except Exception: - logging.exception("Error deleting file") - show_message(_("Error deleting file"), fullpath, mode="error") - - reload() - refind_playing() - pctl.notify_change() - - -track_menu.add(MenuItem(_("Delete Track File"), delete_track, pass_ref=True, icon=delete_icon, show_test=test_shift)) - -track_menu.br() - - -def rename_tracks_deco(track_id: int): - if key_shift_down or key_shiftr_down: - return [colours.menu_text, colours.menu_background, _("Rename (Single track)")] - return [colours.menu_text, colours.menu_background, _("Rename Tracks…")] - - -# rename_tracks_icon.colour = [244, 241, 66, 255] -# rename_tracks_icon.colour = [204, 255, 66, 255] -rename_tracks_icon.colour = [204, 100, 205, 255] -rename_tracks_icon.xoff = 1 -track_menu.add_to_sub(0, MenuItem(_("Rename Tracks…"), rename_track_box.activate, rename_tracks_deco, pass_ref=True, - pass_ref_deco=True, icon=rename_tracks_icon, disable_test=rename_track_box.disable_test)) - - -def activate_trans_editor(): - trans_edit_box.active = True - - -track_menu.add_to_sub(0, MenuItem(_("Edit fields…"), activate_trans_editor)) - - -def delete_folder(index, force=False): - track = pctl.master_library[index] - - if track.is_network: - show_message(_("Cannot physically delete"), _("One or more tracks is from a network location!"), mode="info") - return - - old = track.parent_folder_path - - if len(old) < 5: - show_message(_("This folder path seems short, I don't wanna try delete that"), mode="warning") - return - - if not os.path.exists(old): - show_message(_("Error deleting folder. The folder seems to be missing."), _("It's gone! Just gone!"), mode="error") - return - - protect = ("", "Documents", "Music", "Desktop", "Downloads") - - for fo in protect: - if old.strip("\\/") == os.path.join(os.path.expanduser("~"), fo).strip("\\/"): - show_message(_("Woah, careful there!"), _("I don't think we should delete that folder."), mode="warning") - return - - if directory_size(old) > 1500000000: - show_message(_("Delete size safety limit reached! (1.5GB)"), old, mode="warning") - return - - try: - - if pctl.playing_state > 0 and os.path.normpath( - pctl.master_library[pctl.track_queue[pctl.queue_step]].parent_folder_path) == os.path.normpath(old): - pctl.stop(True) - - if force: - shutil.rmtree(old) - elif system == "Windows" or msys: - send2trash(old.replace("/", "\\")) - else: - send2trash(old) - - for i in reversed(range(len(default_playlist))): - - if old == pctl.master_library[default_playlist[i]].parent_folder_path: - del default_playlist[i] - - if not os.path.exists(old): - if force: - show_message(_("Folder deleted."), old, mode="done") - else: - show_message(_("Folder sent to trash."), old, mode="done") - else: - show_message(_("Hmm, its still there"), old, mode="error") - - if album_mode: - prep_gal() - reload_albums() - - except Exception: - if force: - logging.exception("Unable to comply, could not delete folder. Try checking permissions.") - show_message(_("Unable to comply."), _("Could not delete folder. Try checking permissions."), mode="error") - else: - logging.exception("Folder could not be trashed, try again while holding shift to force delete.") - show_message(_("Folder could not be trashed."), _("Try again while holding shift to force delete."), - mode="error") - - tree_view_box.clear_target_pl(pctl.active_playlist_viewing) - gui.pl_update += 1 - pctl.notify_change() - - -def rename_parent(index: int, template: str) -> None: - # template = prefs.rename_folder_template - template = template.strip("/\\") - track = pctl.master_library[index] - - if track.is_network: - show_message(_("Cannot rename"), _("One or more tracks is from a network location!"), mode="info") - return - - old = track.parent_folder_path - #logging.info(old) - - new = parse_template2(template, track) - - if len(new) < 1: - show_message(_("Rename error."), _("The generated name is too short"), mode="warning") - return - - if len(old) < 5: - show_message(_("Rename error."), _("This folder path seems short, I don't wanna try rename that"), mode="warning") - return - - if not os.path.exists(old): - show_message(_("Rename Failed. The original folder is missing."), mode="warning") - return - - protect = ("", "Documents", "Music", "Desktop", "Downloads") - - for fo in protect: - if os.path.normpath(old) == os.path.normpath(os.path.join(os.path.expanduser("~"), fo)): - show_message(_("Woah, careful there!"), _("I don't think we should rename that folder."), mode="warning") - return - - logging.info(track.parent_folder_path) - re = os.path.dirname(track.parent_folder_path.rstrip("/\\")) - logging.info(re) - new_parent_path = os.path.join(re, new) - logging.info(new_parent_path) - - pre_state = 0 - - for key, object in pctl.master_library.items(): - - if object.fullpath == "": - continue - - if old == object.parent_folder_path: - - new_fullpath = os.path.join(new_parent_path, object.filename) - - if os.path.normpath(new_parent_path) == os.path.normpath(old): - show_message(_("The folder already has that name.")) - return - - if os.path.exists(new_parent_path): - show_message(_("Rename Failed."), _("A folder with that name already exists"), mode="warning") - return - - if key == pctl.track_queue[pctl.queue_step] and pctl.playing_state > 0: - pre_state = pctl.stop(True) - - object.parent_folder_name = new - object.parent_folder_path = new_parent_path - object.fullpath = new_fullpath - - search_string_cache.pop(object.index, None) - search_dia_string_cache.pop(object.index, None) - - # Fix any other tracks paths that contain the old path - if os.path.normpath(object.fullpath)[:len(old)] == os.path.normpath(old) \ - and os.path.normpath(object.fullpath)[len(old)] in ("/", "\\"): - object.fullpath = os.path.join(new_parent_path, object.fullpath[len(old):].lstrip("\\/")) - object.parent_folder_path = os.path.join(new_parent_path, object.parent_folder_path[len(old):].lstrip("\\/")) - - search_string_cache.pop(object.index, None) - search_dia_string_cache.pop(object.index, None) - - if new_parent_path is not None: - try: - os.rename(old, new_parent_path) - logging.info(new_parent_path) - except Exception: - logging.exception("Rename failed, something went wrong!") - show_message(_("Rename Failed!"), _("Something went wrong, sorry."), mode="error") - return - - show_message(_("Folder renamed."), _("Renamed to: {name}").format(name=new), mode="done") - - if pre_state == 1: - pctl.revert() - - tree_view_box.clear_target_pl(pctl.active_playlist_viewing) - pctl.notify_change() - - -def rename_folders_disable_test(index: int) -> bool: - return pctl.get_track(index).is_network - -def rename_folders(index: int): - global track_box - global rename_index - global input_text - - track_box = False - rename_index = index - - if rename_folders_disable_test(index): - show_message(_("Not applicable for a network track.")) - return - - gui.rename_folder_box = True - input_text = "" - shift_selection.clear() - - global quick_drag - global playlist_hold - quick_drag = False - playlist_hold = False - - -mod_folder_icon.colour = [229, 98, 98, 255] -track_menu.add_to_sub(0, MenuItem(_("Modify Folder…"), rename_folders, pass_ref=True, pass_ref_deco=True, icon=mod_folder_icon, disable_test=rename_folders_disable_test)) - - -def move_folder_up(index: int, do: bool = False) -> bool | None: - track = pctl.master_library[index] - - if track.is_network: - show_message(_("Cannot move"), _("One or more tracks is from a network location!"), mode="info") - return None - - parent_folder = os.path.dirname(track.parent_folder_path) - folder_name = track.parent_folder_name - move_target = track.parent_folder_path - upper_folder = os.path.dirname(parent_folder) - - if not os.path.exists(track.parent_folder_path): - if do: - show_message(_("Error shifting directory"), _("The directory does not appear to exist"), mode="warning") - return False - - if len(os.listdir(parent_folder)) > 1: - return False - - if do is False: - return True - - pre_state = 0 - if pctl.playing_state > 0 and track.parent_folder_path in pctl.playing_object().parent_folder_path: - pre_state = pctl.stop(True) - - try: - - # Rename the track folder to something temporary - os.rename(move_target, os.path.join(parent_folder, "RMTEMP000")) - - # Move the temporary folder up 2 levels - shutil.move(os.path.join(parent_folder, "RMTEMP000"), upper_folder) - - # Delete the old directory that contained the original folder - shutil.rmtree(parent_folder) - - # Rename the moved folder back to its original name - os.rename(os.path.join(upper_folder, "RMTEMP000"), os.path.join(upper_folder, folder_name)) - - except Exception as e: - logging.exception("System Error!") - show_message(_("System Error!"), str(e), mode="error") - - # Fix any other tracks paths that contain the old path - old = track.parent_folder_path - new_parent_path = os.path.join(upper_folder, folder_name) - for key, object in pctl.master_library.items(): - - if os.path.normpath(object.fullpath)[:len(old)] == os.path.normpath(old) \ - and os.path.normpath(object.fullpath)[len(old)] in ("/", "\\"): - object.fullpath = os.path.join(new_parent_path, object.fullpath[len(old):].lstrip("\\/")) - object.parent_folder_path = os.path.join( - new_parent_path, object.parent_folder_path[len(old):].lstrip("\\/")) - - search_string_cache.pop(object.index, None) - search_dia_string_cache.pop(object.index, None) - - logging.info(object.fullpath) - logging.info(object.parent_folder_path) - - if pre_state == 1: - pctl.revert() - - -def clean_folder(index: int, do: bool = False) -> int | None: - track = pctl.master_library[index] - - if track.is_network: - show_message(_("Cannot clean"), _("One or more tracks is from a network location!"), mode="info") - return None - - folder = track.parent_folder_path - found = 0 - to_purge = [] - if not os.path.isdir(folder): - return 0 - try: - for item in os.listdir(folder): - if (item[:8] == "AlbumArt" and ".jpg" in item.lower()) \ - or item == "desktop.ini" \ - or item == "Thumbs.db" \ - or item == ".DS_Store": - - to_purge.append(item) - found += 1 - elif item == "__MACOSX" and os.path.isdir(os.path.join(folder, item)): - found += 1 - found += 1 - if do: - logging.info("Deleting Folder: " + os.path.join(folder, item)) - shutil.rmtree(os.path.join(folder, item)) - - if do: - for item in to_purge: - if os.path.isfile(os.path.join(folder, item)): - logging.info("Deleting File: " + os.path.join(folder, item)) - os.remove(os.path.join(folder, item)) - # clear_img_cache() - - for track_id in default_playlist: - if pctl.get_track(track_id).parent_folder_path == folder: - clear_track_image_cache(pctl.get_track(track_id)) - - except Exception: - logging.exception("Error deleting files, may not have permission or file may be set to read-only") - show_message(_("Error deleting files."), _("May not have permission or file may be set to read-only"), mode="warning") - return 0 - - return found - - -def reset_play_count(index: int): - star_store.remove(index) - - -# track_menu.add_to_sub("Reset Track Play Count", 0, reset_play_count, pass_ref=True) - - -def vacuum_playtimes(index: int): - todo = [] - for k in default_playlist: - if pctl.master_library[index].parent_folder_name == pctl.master_library[k].parent_folder_name: - todo.append(k) - - for track in todo: - - tr = pctl.get_track(track) - - total_playtime = 0 - flags = "" - - to_del = [] - - for key, value in star_store.db.items(): - if key[0].lower() == tr.artist.lower() and tr.artist and key[1].lower().replace( - " ", "") == tr.title.lower().replace( - " ", "") and tr.title: - to_del.append(key) - total_playtime += value[0] - flags = "".join(set(flags + value[1])) - - for key in to_del: - del star_store.db[key] - - key = star_store.object_key(tr) - value = [total_playtime, flags, 0] - if key not in star_store.db: - logging.info("Saving value") - star_store.db[key] = value - else: - logging.error("ERROR KEY ALREADY HERE?") - - -def reload_metadata(input, keep_star: bool = True) -> None: - global todo - - # vacuum_playtimes(index) - # return - todo = [] - - if isinstance(input, list): - todo = input - - else: - for k in default_playlist: - if pctl.master_library[input].parent_folder_path == pctl.master_library[k].parent_folder_path: - todo.append(pctl.master_library[k]) - - for i in reversed(range(len(todo))): - if todo[i].is_cue: - del todo[i] - - for track in todo: - - search_string_cache.pop(track.index, None) - search_dia_string_cache.pop(track.index, None) - - #logging.info('Reloading Metadata for ' + track.filename) - if keep_star: - to_scan.append(track.index) - else: - # if keep_star: - # star = star_store.full_get(track.index) - # star_store.remove(track.index) - - pctl.master_library[track.index] = tag_scan(track) - - # if keep_star: - # if star is not None and (star[0] > 0 or star[1] or star[2] > 0): - # star_store.merge(track.index, star) - - pctl.notify_change() - - gui.pl_update += 1 - tauon.thread_manager.ready("worker") - - -def reload_metadata_selection() -> None: - cargo = [] - for item in shift_selection: - cargo.append(default_playlist[item]) - - for k in cargo: - if pctl.master_library[k].is_cue == False: - to_scan.append(k) - tauon.thread_manager.ready("worker") - - - -def editor(index: int | None) -> None: - todo = [] - obs = [] - - if key_shift_down and index is not None: - todo = [index] - obs = [pctl.master_library[index]] - elif index is None: - for item in shift_selection: - todo.append(default_playlist[item]) - obs.append(pctl.master_library[default_playlist[item]]) - if len(todo) > 0: - index = todo[0] - else: - for k in default_playlist: - if pctl.master_library[index].parent_folder_path == pctl.master_library[k].parent_folder_path: - if pctl.master_library[k].is_cue == False: - todo.append(k) - obs.append(pctl.master_library[k]) - - # Keep copy of play times - old_stars = [] - for track in todo: - item = [] - item.append(pctl.get_track(track)) - item.append(star_store.key(track)) - item.append(star_store.full_get(track)) - old_stars.append(item) - - file_line = "" - for track in todo: - file_line += ' "' - file_line += pctl.master_library[track].fullpath - file_line += '"' - - if system == "Windows" or msys: - file_line = file_line.replace("/", "\\") - - prefix = "" - app = prefs.tag_editor_target - - if (system == "Windows" or msys) and app: - if app[0] != '"': - app = '"' + app - if app[-1] != '"': - app = app + '"' - - app_switch = "" - - ok = False - - prefix = launch_prefix - - if system == "Linux": - ok = whicher(prefs.tag_editor_target) - else: - - if not os.path.isfile(prefs.tag_editor_target.strip('"')): - logging.info(prefs.tag_editor_target) - show_message(_("Application not found"), prefs.tag_editor_target, mode="info") - return - - ok = True - - if not ok: - show_message(_("Tag editor app does not appear to be installed."), mode="warning") - - if flatpak_mode: - show_message( - _("App not found on host OR insufficient Flatpak permissions."), - _(" For details, see {link}").format(link="https://github.com/Taiko2k/Tauon/wiki/Flatpak-Extra-Steps"), - mode="bubble") - - return - - if "picard" in prefs.tag_editor_target: - app_switch = " --d " - - line = prefix + app + app_switch + file_line - - show_message( - prefs.tag_editor_name + " launched.", "Fields will be updated once application is closed.", mode="arrow") - gui.update = 1 - - complete = subprocess.run(shlex.split(line), stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) - - if "picard" in prefs.tag_editor_target: - r = complete.stderr.decode() - for line in r.split("\n"): - if "file._rename" in line and " Moving file " in line: - a, b = line.split(" Moving file ")[1].split(" => ") - a = a.strip("'").strip('"') - b = b.strip("'").strip('"') - - for track in todo: - if pctl.master_library[track].fullpath == a: - pctl.master_library[track].fullpath = b - pctl.master_library[track].filename = os.path.basename(b) - logging.info("External Edit: File rename detected.") - logging.info(" Renaming: " + a) - logging.info(" To: " + b) - break - else: - logging.warning("External Edit: A file rename was detected but track was not found.") - - gui.message_box = False - reload_metadata(obs, keep_star=False) - - # Re apply playtime data in case file names change - for item in old_stars: - - old_key = item[1] - old_value = item[2] - - if not old_value: # ignore if there was no old playcount metadata - continue - - new_key = star_store.object_key(item[0]) - new_value = star_store.full_get(item[0].index) - - if old_key == new_key: - continue - - if new_value is None: - new_value = [0, "", 0] - - new_value[0] += old_value[0] - new_value[1] = "".join(set(new_value[1] + old_value[1])) - - if old_key in star_store.db: - del star_store.db[old_key] - - star_store.db[new_key] = new_value - - gui.pl_update = 1 - gui.update = 1 - pctl.notify_change() - - -def launch_editor(index: int): - if snap_mode: - show_message(_("Sorry, this feature isn't (yet) available with Snap.")) - return - - if launch_editor_disable_test(index): - show_message(_("Cannot edit tags of a network track.")) - return - - mini_t = threading.Thread(target=editor, args=[index]) - mini_t.daemon = True - mini_t.start() - -def launch_editor_selection_disable_test(index: int): - for position in shift_selection: - if pctl.get_track(default_playlist[position]).is_network: - return True - return False - -def launch_editor_selection(index: int): - if launch_editor_selection_disable_test(index): - show_message(_("Cannot edit tags of a network track.")) - return - - mini_t = threading.Thread(target=editor, args=[None]) - mini_t.daemon = True - mini_t.start() - - -# track_menu.add('Reload Metadata', reload_metadata, pass_ref=True) -track_menu.add_to_sub(0, MenuItem(_("Rescan Tags"), reload_metadata, pass_ref=True)) - -mbp_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "mbp-g.png")) -mbp_icon.base_asset = asset_loader(scaled_asset_directory, loaded_asset_dc, "mbp-gs.png") - -mbp_icon.xoff = 2 -mbp_icon.yoff = -1 - -if gui.scale == 1.25: - mbp_icon.yoff = 0 - -edit_icon = None -if prefs.tag_editor_name == "Picard": - edit_icon = mbp_icon - - -def edit_deco(index: int): - if key_shift_down or key_shiftr_down: - return [colours.menu_text, colours.menu_background, prefs.tag_editor_name + " (Single track)"] - return [colours.menu_text, colours.menu_background, _("Edit with ") + prefs.tag_editor_name] - -def launch_editor_disable_test(index: int): - return pctl.get_track(index).is_network - -track_menu.add_to_sub(0, MenuItem(_("Edit with"), launch_editor, pass_ref=True, pass_ref_deco=True, icon=edit_icon, render_func=edit_deco, disable_test=launch_editor_disable_test)) - - -def show_lyrics_menu(index: int): - global track_box - track_box = False - enter_showcase_view(track_id=r_menu_index) - inp.mouse_click = False - - -track_menu.add_to_sub(0, MenuItem(_("Lyrics..."), show_lyrics_menu, pass_ref=True)) - - -def recode(text, enc): - return text.encode("Latin-1", "ignore").decode(enc, "ignore") - - -def intel_moji(index: int): - gui.pl_update += 1 - gui.update += 1 - - track = pctl.master_library[index] - - lot = [] - - for item in default_playlist: - - if track.album == pctl.master_library[item].album and \ - track.parent_folder_name == pctl.master_library[item].parent_folder_name: - lot.append(item) - - lot = set(lot) - - l_artist = track.artist.encode("Latin-1", "ignore") - l_album = track.album.encode("Latin-1", "ignore") - detect = None - - if track.artist not in track.parent_folder_path: - for enc in encodings: - try: - q_artist = l_artist.decode(enc) - if q_artist.strip(" ") in track.parent_folder_path.strip(" "): - detect = enc - break - except Exception: - logging.exception("Error decoding artist") - continue - - if detect is None and track.album not in track.parent_folder_path: - for enc in encodings: - try: - q_album = l_album.decode(enc) - if q_album in track.parent_folder_path: - detect = enc - break - except Exception: - logging.exception("Error decoding album") - continue - - for item in lot: - t_track = pctl.master_library[item] - - if detect is None: - for enc in encodings: - test = recode(t_track.artist, enc) - for cha in test: - if cha in j_chars: - detect = enc - logging.info("This looks like Japanese: " + test) - break - if detect is not None: - break - - if detect is None: - for enc in encodings: - test = recode(t_track.title, enc) - for cha in test: - if cha in j_chars: - detect = enc - logging.info("This looks like Japanese: " + test) - break - if detect is not None: - break - if detect is not None: - break - - if detect is not None: - logging.info("Fix Mojibake: Detected encoding as: " + detect) - for item in lot: - track = pctl.master_library[item] - # key = pctl.master_library[item].title + pctl.master_library[item].filename - key = star_store.full_get(item) - star_store.remove(item) - - track.title = recode(track.title, detect) - track.album = recode(track.album, detect) - track.artist = recode(track.artist, detect) - track.album_artist = recode(track.album_artist, detect) - track.genre = recode(track.genre, detect) - track.comment = recode(track.comment, detect) - track.lyrics = recode(track.lyrics, detect) - - if key != None: - star_store.insert(item, key) - - search_string_cache.pop(track.index, None) - search_dia_string_cache.pop(track.index, None) - - else: - show_message(_("Autodetect failed")) - - -track_menu.add_to_sub(0, MenuItem(_("Fix Mojibake"), intel_moji, pass_ref=True)) - - -def sel_to_car(): - global default_playlist - cargo = [] - - for item in shift_selection: - cargo.append(default_playlist[item]) - - -# track_menu.add_to_sub("Copy Playlist", 1, transfer, pass_ref=True, args=[1, 3]) -def cut_selection(): - sel_to_car() - del_selected() - - -def clip_ar_al(index: int): - line = pctl.master_library[index].artist + " - " + pctl.master_library[index].album - SDL_SetClipboardText(line.encode("utf-8")) - - -def clip_ar(index: int): - if pctl.master_library[index].album_artist != "": - line = pctl.master_library[index].album_artist - else: - line = pctl.master_library[index].artist - SDL_SetClipboardText(line.encode("utf-8")) - - -def clip_title(index: int): - n_track = pctl.master_library[index] - - if not prefs.use_title and n_track.album_artist != "" and n_track.album != "": - line = n_track.album_artist + " - " + n_track.album - else: - line = n_track.parent_folder_name - - SDL_SetClipboardText(line.encode("utf-8")) - - -selection_menu = Menu(200, show_icons=False) -folder_menu = Menu(193, show_icons=True) - -folder_menu.add(MenuItem(_("Open Folder"), open_folder, pass_ref=True, pass_ref_deco=True, icon=folder_icon, disable_test=open_folder_disable_test)) - -folder_menu.add(MenuItem(_("Modify Folder…"), rename_folders, pass_ref=True, pass_ref_deco=True, icon=mod_folder_icon, disable_test=rename_folders_disable_test)) -folder_tree_menu.add(MenuItem(_("Modify Folder…"), rename_folders, pass_ref=True, pass_ref_deco=True, icon=mod_folder_icon, disable_test=rename_folders_disable_test)) -# folder_menu.add(_("Add Album to Queue"), add_album_to_queue, pass_ref=True) -folder_menu.add(MenuItem(_("Add Album to Queue"), add_album_to_queue, pass_ref=True)) -folder_menu.add(MenuItem(_("Enqueue Album Next"), add_album_to_queue_fc, pass_ref=True)) - -gallery_menu.add(MenuItem(_("Modify Folder…"), rename_folders, pass_ref=True, pass_ref_deco=True, icon=mod_folder_icon, disable_test=rename_folders_disable_test)) - -folder_menu.add(MenuItem(_("Rename Tracks…"), rename_track_box.activate, rename_tracks_deco, - pass_ref=True, pass_ref_deco=True, icon=rename_tracks_icon, disable_test=rename_track_box.disable_test)) -folder_tree_menu.add(MenuItem(_("Rename Tracks…"), rename_track_box.activate, pass_ref=True, pass_ref_deco=True, icon=rename_tracks_icon, disable_test=rename_track_box.disable_test)) - -if not snap_mode: - folder_menu.add(MenuItem("Edit with", launch_editor_selection, pass_ref=True, - pass_ref_deco=True, icon=edit_icon, render_func=edit_deco, disable_test=launch_editor_selection_disable_test)) - -folder_tree_menu.add(MenuItem(_("Add Album to Queue"), add_album_to_queue, pass_ref=True)) -folder_tree_menu.add(MenuItem(_("Enqueue Album Next"), add_album_to_queue_fc, pass_ref=True)) - -folder_tree_menu.br() -folder_tree_menu.add(MenuItem(_("Collapse All"), collapse_tree, collapse_tree_deco)) -folder_tree_menu.add(MenuItem("lock", lock_folder_tree, lock_folder_tree_deco)) - - -def lightning_copy(): - s_copy() - gui.lightning_copy = True - - -# selection_menu.br() - -def toggle_transcode(mode: int = 0) -> bool: - if mode == 1: - return prefs.enable_transcode - prefs.enable_transcode ^= True - return None - - -def toggle_chromecast(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_chromecast - prefs.show_chromecast ^= True - return None - - -def toggle_transfer(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_transfer - prefs.show_transfer ^= True - - if prefs.show_transfer: - show_message( - _("Warning! Using this function moves physical folders."), - _("This menu entry appears after selecting 'copy'. See manual (github wiki) for more info."), - mode="info") - return None - - -transcode_icon.colour = [239, 74, 157, 255] - - -def transcode_deco(): - if key_shift_down or key_shiftr_down: - return [colours.menu_text, colours.menu_background, _("Transcode Single")] - return [colours.menu_text, colours.menu_background, _("Transcode Folder")] - - -folder_menu.add(MenuItem(_("Rescan Tags"), reload_metadata, pass_ref=True)) -folder_menu.add(MenuItem(_("Edit fields…"), activate_trans_editor)) -folder_menu.add(MenuItem(_("Vacuum Playtimes"), vacuum_playtimes, pass_ref=True, show_test=test_shift)) -folder_menu.add(MenuItem(_("Transcode Folder"), convert_folder, transcode_deco, pass_ref=True, icon=transcode_icon, - show_test=toggle_transcode)) -gallery_menu.add(MenuItem(_("Transcode Folder"), convert_folder, transcode_deco, pass_ref=True, icon=transcode_icon, - show_test=toggle_transcode)) -folder_menu.br() - -tauon.spot_ctl.cache_saved_albums = spot_cache_saved_albums - -# Copy album title text to clipboard -folder_menu.add(MenuItem(_('Copy "Artist - Album"'), clip_title, pass_ref=True)) - - -def get_album_spot_url(track_id: int): - track_object = pctl.get_track(track_id) - url = tauon.spot_ctl.get_album_url_from_local(track_object) - if url: - copy_to_clipboard(url) - show_message(_("URL copied to clipboard"), mode="done") - else: - show_message(_("No results found")) - - -def get_album_spot_url_deco(track_id: int): - track_object = pctl.get_track(track_id) - if "spotify-album-url" in track_object.misc: - text = _("Copy Spotify Album URL") - else: - text = _("Lookup Spotify Album URL") - return [colours.menu_text, colours.menu_background, text] - - -folder_menu.add(MenuItem("Lookup Spotify Album URL", get_album_spot_url, get_album_spot_url_deco, pass_ref=True, - pass_ref_deco=True, show_test=spotify_show_test, icon=spot_icon)) - - -def add_to_spotify_library_deco(track_id: int): - track_object = pctl.get_track(track_id) - text = _("Save Album to Spotify") - if track_object.file_ext != "SPTY": - return (colours.menu_text_disabled, colours.menu_background, text) - - album_url = track_object.misc.get("spotify-album-url") - if album_url and album_url in tauon.spot_ctl.cache_saved_albums: - text = _("Un-save Spotify Album") - - return (colours.menu_text, colours.menu_background, text) - - -def add_to_spotify_library2(album_url: str) -> None: - if album_url in tauon.spot_ctl.cache_saved_albums: - tauon.spot_ctl.remove_album_from_library(album_url) - else: - tauon.spot_ctl.add_album_to_library(album_url) - - for i, p in enumerate(pctl.multi_playlist): - code = pctl.gen_codes.get(p.uuid_int) - if code and code.startswith("sal"): - logging.info("Fetching Spotify Library...") - regenerate_playlist(i, silent=True) - - -def add_to_spotify_library(track_id: int) -> None: - track_object = pctl.get_track(track_id) - album_url = track_object.misc.get("spotify-album-url") - if track_object.file_ext != "SPTY" or not album_url: - return - - shoot_dl = threading.Thread(target=add_to_spotify_library2, args=([album_url])) - shoot_dl.daemon = True - shoot_dl.start() - - -folder_menu.add(MenuItem("Add to Spotify Library", add_to_spotify_library, add_to_spotify_library_deco, pass_ref=True, - pass_ref_deco=True, show_test=spotify_show_test, icon=spot_icon)) - - -# Copy artist name text to clipboard -# folder_menu.add(_('Copy "Artist"'), clip_ar, pass_ref=True) - -def selection_queue_deco(): - total = 0 - for item in shift_selection: - total += pctl.get_track(default_playlist[item]).length - - total = get_hms_time(total) - - text = (_("Queue {N}").format(N=len(shift_selection))) + f" [{total}]" - - return [colours.menu_text, colours.menu_background, text] - - -selection_menu.add(MenuItem(_("Add to queue"), add_selected_to_queue_multi, selection_queue_deco)) - -selection_menu.br() - -selection_menu.add(MenuItem(_("Rescan Tags"), reload_metadata_selection)) - -selection_menu.add(MenuItem(_("Edit fields…"), activate_trans_editor)) - -selection_menu.add(MenuItem(_("Edit with "), launch_editor_selection, pass_ref=True, pass_ref_deco=True, icon=edit_icon, render_func=edit_deco, disable_test=launch_editor_selection_disable_test)) - -selection_menu.br() -folder_menu.br() - -# It's complicated -# folder_menu.add(_('Copy Folder From Library'), lightning_copy) - -selection_menu.add(MenuItem(_("Copy"), s_copy)) -selection_menu.add(MenuItem(_("Cut"), s_cut)) -selection_menu.add(MenuItem(_("Remove"), del_selected)) -selection_menu.add(MenuItem(_("Delete Files"), force_del_selected, show_test=test_shift, icon=delete_icon)) - -folder_menu.add(MenuItem(_("Copy"), s_copy)) -gallery_menu.add(MenuItem(_("Copy"), s_copy)) -# folder_menu.add(_('Cut'), s_cut) -# folder_menu.add(_('Paste + Transfer Folder'), lightning_paste, pass_ref=False, show_test=lightning_move_test) -# gallery_menu.add(_('Paste + Transfer Folder'), lightning_paste, pass_ref=False, show_test=lightning_move_test) -folder_menu.add(MenuItem(_("Remove"), del_selected)) -gallery_menu.add(MenuItem(_("Remove"), del_selected)) - - -def toggle_rym(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_rym - prefs.show_rym ^= True - return None - - -def toggle_band(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_band - prefs.show_band ^= True - return None - - -def toggle_wiki(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_wiki - prefs.show_wiki ^= True - return None - - -# def toggle_show_discord(mode: int = 0) -> bool: -# if mode == 1: -# return prefs.discord_show -# if prefs.discord_show is False and discord_allow is False: -# show_message(_("Warning: pypresence package not installed")) -# prefs.discord_show ^= True - -def toggle_gen(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_gen - prefs.show_gen ^= True - return None - - -def ser_band_done(result: str) -> None: - if result: - webbrowser.open(result, new=2, autoraise=True) - gui.message_box = False - gui.update += 1 - else: - show_message(_("No matching artist result found")) - - -def ser_band(track_id: int) -> None: - tr = pctl.get_track(track_id) - if tr.artist: - shoot_dl = threading.Thread(target=bandcamp_search, args=([tr.artist, ser_band_done])) - shoot_dl.daemon = True - shoot_dl.start() - show_message(_("Searching...")) - - -def ser_rym(index: int) -> None: - if len(pctl.master_library[index].artist) < 2: - return - line = "https://rateyourmusic.com/search?searchtype=a&searchterm=" + urllib.parse.quote( - pctl.master_library[index].artist) - webbrowser.open(line, new=2, autoraise=True) - - -def copy_to_clipboard(text: str) -> None: - SDL_SetClipboardText(text.encode(errors="surrogateescape")) - - -def copy_from_clipboard(): - return SDL_GetClipboardText().decode() - - -def clip_aar_al(index: int): - if pctl.master_library[index].album_artist == "": - line = pctl.master_library[index].artist + " - " + pctl.master_library[index].album - else: - line = pctl.master_library[index].album_artist + " - " + pctl.master_library[index].album - SDL_SetClipboardText(line.encode("utf-8")) - - -def ser_gen_thread(tr): - s_artist = tr.artist - s_title = tr.title - - if s_artist in prefs.lyrics_subs: - s_artist = prefs.lyrics_subs[s_artist] - if s_title in prefs.lyrics_subs: - s_title = prefs.lyrics_subs[s_title] - - line = genius(s_artist, s_title, return_url=True) - - r = requests.head(line, timeout=10) - - if r.status_code != 404: - webbrowser.open(line, new=2, autoraise=True) - gui.message_box = False - else: - line = "https://genius.com/search?q=" + urllib.parse.quote(f"{s_artist} {s_title}") - webbrowser.open(line, new=2, autoraise=True) - gui.message_box = False - - -def ser_gen(track_id, get_lyrics=False): - tr = pctl.master_library[track_id] - if len(tr.title) < 1: - return - - show_message(_("Searching...")) - - shoot = threading.Thread(target=ser_gen_thread, args=[tr]) - shoot.daemon = True - shoot.start() - - -def ser_wiki(index: int) -> None: - if len(pctl.master_library[index].artist) < 2: - return - line = "https://en.wikipedia.org/wiki/Special:Search?search=" + urllib.parse.quote(pctl.master_library[index].artist) - webbrowser.open(line, new=2, autoraise=True) - - -track_menu.add(MenuItem(_("Search Artist on Wikipedia"), ser_wiki, pass_ref=True, show_test=toggle_wiki)) - -track_menu.add(MenuItem(_("Search Track on Genius"), ser_gen, pass_ref=True, show_test=toggle_gen)) - -son_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "sonemic-g.png")) -son_icon.base_asset = asset_loader(scaled_asset_directory, loaded_asset_dc, "sonemic-gs.png") - -son_icon.xoff = 1 -track_menu.add(MenuItem(_("Search Artist on Sonemic"), ser_rym, pass_ref=True, icon=son_icon, show_test=toggle_rym)) - -band_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "band.png", True)) -band_icon.xoff = 0 -band_icon.yoff = 1 -band_icon.colour = [96, 147, 158, 255] - -track_menu.add(MenuItem(_("Search Artist on Bandcamp"), ser_band, pass_ref=True, icon=band_icon, show_test=toggle_band)) - - -def clip_ar_tr(index: int) -> None: - line = pctl.master_library[index].artist + " - " + pctl.master_library[index].title - - SDL_SetClipboardText(line.encode("utf-8")) - - -# Copy metadata to clipboard -# track_menu.add(_('Copy "Artist - Album"'), clip_aar_al, pass_ref=True) -# Copy metadata to clipboard -track_menu.add(MenuItem(_('Copy "Artist - Track"'), clip_ar_tr, pass_ref=True)) - -def tidal_copy_album(index: int) -> None: - t = pctl.master_library.get(index) - if t and t.file_ext == "TIDAL": - id = t.misc.get("tidal_album") - if id: - url = "https://listen.tidal.com/album/" + str(id) - copy_to_clipboard(url) - -def is_tidal_track(_) -> bool: - return pctl.master_library[r_menu_index].file_ext == "TIDAL" - - -track_menu.add(MenuItem(_("Copy TIDAL Album URL"), tidal_copy_album, show_test=is_tidal_track, pass_ref=True)) - -# def get_track_spot_url_show_test(_): -# if pctl.get_track(r_menu_index).misc.get("spotify-track-url"): -# return True -# return False - - -def get_track_spot_url(track_id: int) -> None: - track_object = pctl.get_track(track_id) - url = track_object.misc.get("spotify-track-url") - if url: - copy_to_clipboard(url) - show_message(_("Url copied to clipboard"), mode="done") - else: - show_message(_("No results found")) - -def get_track_spot_url_deco(): - if pctl.get_track(r_menu_index).misc.get("spotify-track-url"): - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled - - return [line_colour, colours.menu_background, None] - -track_menu.add_sub(_("Spotify…"), 190, show_test=spotify_show_test) - -def get_spot_artist_track(index: int) -> None: - get_artist_spot(pctl.get_track(index)) - -track_menu.add_to_sub(1, MenuItem(_("Show Full Artist"), get_spot_artist_track, pass_ref=True, icon=spot_icon)) - -def get_album_spot_active(tr: TrackClass | None = None) -> None: - if tr is None: - tr = pctl.playing_object() - if not tr: - return - url = tauon.spot_ctl.get_album_url_from_local(tr) - if not url: - show_message(_("No results found")) - return - l = tauon.spot_ctl.append_album(url, return_list=True) - if len(l) < 2: - show_message(_("Looks like that's the only track in the album")) - return - pctl.multi_playlist.append( - pl_gen( - title=f"{pctl.get_track(l[0]).artist} - {pctl.get_track(l[0]).album}", - playlist_ids=l, - hide_title=False)) - switch_playlist(len(pctl.multi_playlist) - 1) - - -def get_spot_album_track(index: int): - get_album_spot_active(pctl.get_track(index)) - -track_menu.add_to_sub(1, MenuItem(_("Show Full Album"), get_spot_album_track, pass_ref=True, icon=spot_icon)) - - - -track_menu.add_to_sub(1, MenuItem(_("Copy Track URL"), get_track_spot_url, get_track_spot_url_deco, pass_ref=True, - icon=spot_icon)) - -# def get_spot_recs(tr: TrackClass | None = None) -> None: -# if not tr: -# tr = pctl.playing_object() -# if not tr: -# return -# url = tauon.spot_ctl.get_artist_url_from_local(tr) -# if not url: -# show_message(_("No results found")) -# return -# track_url = tr.misc.get("spotify-track-url") -# -# show_message(_("Fetching...")) -# shooter(tauon.spot_ctl.rec_playlist, (url, track_url)) -# -# def get_spot_recs_track(index: int): -# get_spot_recs(pctl.get_track(index)) -# -# track_menu.add_to_sub(1, MenuItem(_("Get Recommended"), get_spot_recs_track, pass_ref=True, icon=spot_icon)) - - -def drop_tracks_to_new_playlist(track_list: list[int], hidden: bool = False) -> None: - pl = new_playlist(switch=False) - albums = [] - artists = [] - for item in track_list: - albums.append(pctl.get_track(default_playlist[item]).album) - artists.append(pctl.get_track(default_playlist[item]).artist) - pctl.multi_playlist[pl].playlist_ids.append(default_playlist[item]) - - if len(track_list) > 1: - if len(albums) > 0 and albums.count(albums[0]) == len(albums): - track = pctl.get_track(default_playlist[track_list[0]]) - artist = track.artist - if track.album_artist != "": - artist = track.album_artist - pctl.multi_playlist[pl].title = artist + " - " + albums[0][:50] - - elif len(track_list) == 1 and artists: - pctl.multi_playlist[pl].title = artists[0] - - if tree_view_box.dragging_name: - pctl.multi_playlist[pl].title = tree_view_box.dragging_name - - pctl.notify_change() - - -def queue_deco(): - if len(pctl.force_queue) > 0: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled - - return [line_colour, colours.menu_background, None] - - -track_menu.br() -track_menu.add(MenuItem(_("Transcode Folder"), convert_folder, transcode_deco, pass_ref=True, icon=transcode_icon, - show_test=toggle_transcode)) - - -def bass_test(_) -> bool: - # return True - return prefs.backend == 1 - - -def gstreamer_test(_) -> bool: - # return True - return prefs.backend == 2 - - -# Create top menu -x_menu: Menu = Menu(190, show_icons=True) -view_menu = Menu(170) -set_menu = Menu(150) -set_menu_hidden = Menu(100) -vis_menu = Menu(140) -window_menu = Menu(140) -field_menu = Menu(140) -dl_menu = Menu(90) - -window_menu = Menu(140) -window_menu.add(MenuItem(_("Minimize"), do_minimize_button)) -window_menu.add(MenuItem(_("Maximize"), do_maximize_button)) -window_menu.add(MenuItem(_("Exit"), do_exit_button)) - -def field_copy(text_field) -> None: - text_field.copy() - - -def field_paste(text_field) -> None: - text_field.paste() - - -def field_clear(text_field) -> None: - text_field.clear() - - -# Copy text -field_menu.add(MenuItem(_("Copy"), field_copy, pass_ref=True)) -# Paste text -field_menu.add(MenuItem(_("Paste"), field_paste, pass_ref=True)) -# Clear text -field_menu.add(MenuItem(_("Clear"), field_clear, pass_ref=True)) - - -def vis_off() -> None: - gui.vis_want = 0 - gui.update_layout() - # gui.turbo = False - - -vis_menu.add(MenuItem(_("Off"), vis_off)) - - -def level_on() -> None: - if gui.vis_want == 1 and gui.turbo is True: - gui.level_meter_colour_mode += 1 - if gui.level_meter_colour_mode > 4: - gui.level_meter_colour_mode = 0 - - gui.vis_want = 1 - gui.update_layout() - # if prefs.backend == 2: - # show_message("Visualisers not implemented in GStreamer mode") - # gui.turbo = True - - -vis_menu.add(MenuItem(_("Level Meter"), level_on)) - - -def spec_on() -> None: - gui.vis_want = 2 - # if prefs.backend == 2: - # show_message("Not implemented") - gui.update_layout() - - -vis_menu.add(MenuItem(_("Spectrum Visualizer"), spec_on)) - - -def spec2_def() -> None: - if gui.vis_want == 3: - prefs.spec2_colour_mode += 1 - if prefs.spec2_colour_mode > 1: - prefs.spec2_colour_mode = 0 - - gui.vis_want = 3 - if prefs.backend == 2: - show_message(_("Not implemented")) - # gui.turbo = True - prefs.spec2_colour_setting = "custom" - gui.update_layout() - - -# vis_menu.add(_("Spectrogram"), spec2_def) - -def sa_remove(h: int) -> None: - if len(gui.pl_st) > 1: - del gui.pl_st[h] - gui.update_layout() - else: - show_message(_("Cannot remove the only column.")) - - -def sa_artist() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Artist", 220, False]) - gui.update_layout() - - -def sa_album_artist() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Album Artist", 220, False]) - gui.update_layout() - - -def sa_composer() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Composer", 220, False]) - gui.update_layout() - - -def sa_title() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Title", 220, False]) - gui.update_layout() - - -def sa_album() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Album", 220, False]) - gui.update_layout() - - -def sa_comment() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Comment", 300, False]) - gui.update_layout() - - -def sa_track() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["#", 25, True]) - gui.update_layout() - - -def sa_count() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["P", 25, True]) - gui.update_layout() - - -def sa_scrobbles() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["S", 25, True]) - gui.update_layout() - - -def sa_time() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Time", 55, True]) - gui.update_layout() - - -def sa_date() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Date", 55, True]) - gui.update_layout() - - -def sa_genre() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Genre", 150, False]) - gui.update_layout() - - -def sa_file() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Filepath", 350, False]) - gui.update_layout() - - -def sa_filename() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Filename", 300, False]) - gui.update_layout() - - -def sa_codec() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Codec", 65, True]) - gui.update_layout() - - -def sa_bitrate() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Bitrate", 65, True]) - gui.update_layout() - - -def sa_lyrics() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Lyrics", 50, True]) - gui.update_layout() - -def sa_cue() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["CUE", 50, True]) - gui.update_layout() - -def sa_star() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Starline", 80, True]) - gui.update_layout() - -def sa_disc() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Disc", 50, True]) - gui.update_layout() - -def sa_rating() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Rating", 80, True]) - gui.update_layout() - - -def sa_love() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["❤", 25, True]) - # gui.pl_st.append(["❤", 25, True]) - gui.update_layout() - - -def key_love(index: int) -> bool: - return get_love_index(index) - - -def key_artist(index: int) -> str: - return pctl.master_library[index].artist.lower() - - -def key_album_artist(index: int) -> str: - return pctl.master_library[index].album_artist.lower() - - -def key_composer(index: int) -> str: - return pctl.master_library[index].composer.lower() - - -def key_comment(index: int) -> str: - return pctl.master_library[index].comment - - -def key_title(index: int) -> str: - return pctl.master_library[index].title.lower() - - -def key_album(index: int) -> str: - return pctl.master_library[index].album.lower() - - -def key_duration(index: int) -> int: - return pctl.master_library[index].length - - -def key_date(index: int) -> str: - return pctl.master_library[index].date - - -def key_genre(index: int) -> str: - return pctl.master_library[index].genre.lower() - - -def key_t(index: int): - # return str(pctl.master_library[index].track_number) - return index_key(index) - - -def key_codec(index: int) -> str: - return pctl.master_library[index].file_ext - - -def key_bitrate(index: int) -> int: - return pctl.master_library[index].bitrate - -def key_hl(index: int) -> int: - if len(pctl.master_library[index].lyrics) > 5: - return 0 - return 1 - - -def sort_ass(h, invert=False, custom_list=None, custom_name=""): - global default_playlist - - if custom_list is None: - if pl_is_locked(pctl.active_playlist_viewing): - show_message(_("Playlist is locked")) - return - - name = gui.pl_st[h][0] - playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - else: - name = custom_name - playlist = custom_list - - key = None - ns = False - - if name == "Filepath": - key = key_filepath - if use_natsort: - key = key_fullpath - ns = True - if name == "Filename": - key = key_filepath # key_filename - if use_natsort: - key = key_fullpath - ns = True - if name == "Artist": - key = key_artist - if name == "Album Artist": - key = key_album_artist - if name == "Title": - key = key_title - if name == "Album": - key = key_album - if name == "Composer": - key = key_composer - if name == "Time": - key = key_duration - if name == "Date": - key = key_date - if name == "Genre": - key = key_genre - if name == "#": - key = key_t - if name == "S": - key = key_scrobbles - if name == "P": - key = key_playcount - if name == "Starline": - key = best - if name == "Rating": - key = key_rating - if name == "Comment": - key = key_comment - if name == "Codec": - key = key_codec - if name == "Bitrate": - key = key_bitrate - if name == "Lyrics": - key = key_hl - if name == "❤": - key = key_love - if name == "Disc": - key = key_disc - if name == "CUE": - key = key_cue - - if custom_list is None: - if key is not None: - - if ns: - key = natsort.natsort_keygen(key=key, alg=natsort.PATH) - - playlist.sort(key=key, reverse=invert) - - pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids = playlist - default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - - pctl.playlist_view_position = 0 - logging.debug("Position changed by sort") - gui.pl_update = 1 - - elif custom_list is not None: - playlist.sort(key=key, reverse=invert) - - reload() - - -def sort_dec(h): - sort_ass(h, True) - - -def hide_set_bar(): - gui.set_bar = False - gui.update_layout() - gui.pl_update = 1 - - -def show_set_bar(): - gui.set_bar = True - gui.update_layout() - gui.pl_update = 1 - - -# Mark for translation -_("Time") -_("Filepath") - -# -# set_menu.add(_("Sort Ascending"), sort_ass, pass_ref=True, disable_test=view_pl_is_locked, pass_ref_deco=True) -# set_menu.add(_("Sort Decending"), sort_dec, pass_ref=True, disable_test=view_pl_is_locked, pass_ref_deco=True) -# set_menu.br() -set_menu.add(MenuItem(_("Auto Resize"), auto_size_columns)) -set_menu.add(MenuItem(_("Hide bar"), hide_set_bar)) -set_menu_hidden.add(MenuItem(_("Show bar"), show_set_bar)) -set_menu.br() -set_menu.add(MenuItem("- " + _("Remove This"), sa_remove, pass_ref=True)) -set_menu.br() -set_menu.add(MenuItem("+ " + _("Artist"), sa_artist)) -set_menu.add(MenuItem("+ " + _("Title"), sa_title)) -set_menu.add(MenuItem("+ " + _("Album"), sa_album)) -set_menu.add(MenuItem("+ " + _("Duration"), sa_time)) -set_menu.add(MenuItem("+ " + _("Date"), sa_date)) -set_menu.add(MenuItem("+ " + _("Genre"), sa_genre)) -set_menu.add(MenuItem("+ " + _("Track Number"), sa_track)) -set_menu.add(MenuItem("+ " + _("Play Count"), sa_count)) -set_menu.add(MenuItem("+ " + _("Codec"), sa_codec)) -set_menu.add(MenuItem("+ " + _("Bitrate"), sa_bitrate)) -set_menu.add(MenuItem("+ " + _("Filename"), sa_filename)) -set_menu.add(MenuItem("+ " + _("Starline"), sa_star)) -set_menu.add(MenuItem("+ " + _("Rating"), sa_rating)) -set_menu.add(MenuItem("+ " + _("Loved"), sa_love)) - -set_menu.add_sub("+ " + _("More…"), 150) - -set_menu.add_to_sub(0, MenuItem("+ " + _("Album Artist"), sa_album_artist)) -set_menu.add_to_sub(0, MenuItem("+ " + _("Comment"), sa_comment)) -set_menu.add_to_sub(0, MenuItem("+ " + _("Filepath"), sa_file)) -set_menu.add_to_sub(0, MenuItem("+ " + _("Scrobble Count"), sa_scrobbles)) -set_menu.add_to_sub(0, MenuItem("+ " + _("Composer"), sa_composer)) -set_menu.add_to_sub(0, MenuItem("+ " + _("Disc Number"), sa_disc)) -set_menu.add_to_sub(0, MenuItem("+ " + _("Has Lyrics"), sa_lyrics)) -set_menu.add_to_sub(0, MenuItem("+ " + _("Is CUE Sheet"), sa_cue)) - -def bass_features_deco(): - line_colour = colours.menu_text - if prefs.backend != 1: - line_colour = colours.menu_text_disabled - return [line_colour, colours.menu_background, None] - - -def toggle_dim_albums(mode: int = 0) -> bool: - if mode == 1: - return prefs.dim_art - - prefs.dim_art ^= True - gui.pl_update = 1 - gui.update += 1 - - -def toggle_gallery_combine(mode: int = 0) -> bool: - if mode == 1: - return prefs.gallery_combine_disc - - prefs.gallery_combine_disc ^= True - reload_albums() -def toggle_gallery_click(mode: int = 0) -> bool: - if mode == 1: - return prefs.gallery_single_click - - prefs.gallery_single_click ^= True - - -def toggle_gallery_thin(mode: int = 0) -> bool: - if mode == 1: - return prefs.thin_gallery_borders - - prefs.thin_gallery_borders ^= True - gui.update += 1 - update_layout_do() - - -def toggle_gallery_row_space(mode: int = 0) -> bool: - if mode == 1: - return prefs.increase_gallery_row_spacing - - prefs.increase_gallery_row_spacing ^= True - gui.update += 1 - update_layout_do() - - -def toggle_galler_text(mode: int = 0) -> bool: - if mode == 1: - return gui.gallery_show_text - - gui.gallery_show_text ^= True - gui.update += 1 - update_layout_do() - - # Jump to playing album - if album_mode and gui.first_in_grid is not None: - - if gui.first_in_grid < len(default_playlist): - goto_album(gui.first_in_grid, force=True) - - -def toggle_card_style(mode: int = 0) -> bool: - if mode == 1: - return prefs.use_card_style - - prefs.use_card_style ^= True - gui.update += 1 - - -def toggle_side_panel(mode: int = 0) -> bool: - global update_layout - global album_mode - - if mode == 1: - return prefs.prefer_side - - prefs.prefer_side ^= True - update_layout = True - - if album_mode or prefs.prefer_side is True: - gui.rsp = True - else: - gui.rsp = False - - if prefs.prefer_side: - gui.rspw = gui.pref_rspw - - -def force_album_view(): - toggle_album_mode(True) - - -def enter_combo(): - if not gui.combo_mode: - gui.combo_was_album = album_mode - gui.showcase_mode = False - gui.radio_view = False - if album_mode: - toggle_album_mode() - if gui.rsp: - gui.rsp = False - gui.combo_mode = True - gui.update_layout() - - -def exit_combo(restore=False): - if gui.combo_mode: - if gui.combo_was_album and restore: - force_album_view() - gui.showcase_mode = False - gui.radio_view = False - if prefs.prefer_side: - gui.rsp = True - gui.update_layout() - gui.combo_mode = False - gui.was_radio = False - - -def enter_showcase_view(track_id=None): - if not gui.combo_mode: - enter_combo() - gui.was_radio = False - gui.showcase_mode = True - gui.radio_view = False - if track_id is None or pctl.playing_object() is None or pctl.playing_object().index == track_id: - pass - else: - gui.force_showcase_index = track_id - inp.mouse_click = False - gui.update_layout() - - -def enter_radio_view(): - if not gui.combo_mode: - enter_combo() - gui.showcase_mode = False - gui.radio_view = True - inp.mouse_click = False - gui.update_layout() - - -def standard_size(): - global album_mode - global window_size - global update_layout - - global album_mode_art_size - - album_mode = False - gui.rsp = True - window_size = window_default_size - SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - - gui.rspw = 80 + int(window_size[0] * 0.18) - update_layout = True - album_mode_art_size = 130 - # clear_img_cache() - - -def path_stem_to_playlist(path: str, title: str) -> None: - """Used with gallery power bar""" - playlist = [] - - # Hack for networked tracks - if path.lstrip("/") == title: - for item in pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids: - if title == os.path.basename(pctl.master_library[item].parent_folder_path): - playlist.append(item) - - else: - for item in pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids: - if path in pctl.master_library[item].parent_folder_path: - playlist.append(item) - - pctl.multi_playlist.append(pl_gen( - title=os.path.basename(title).upper(), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pctl.active_playlist_viewing].title + "\" f\"" + path + "\"" - - switch_playlist(len(pctl.multi_playlist) - 1) - - -def goto_album(playlist_no: int, down: bool = False, force: bool = False) -> list | int | None: - logging.debug("Postion set by album locate") - - if core_timer.get() < 0.5: - return None - - global album_dex - - # ---- - w = gui.rspw - if window_size[0] < 750 * gui.scale: - w = window_size[0] - 20 * gui.scale - if gui.lsp: - w -= gui.lspw - area_x = w + 38 * gui.scale - row_len = int((area_x - album_h_gap) / (album_mode_art_size + album_h_gap)) - global last_row - last_row = row_len - # ---- - - px = 0 - row = 0 - re = 0 - - for i in range(len(album_dex)): - if i == len(album_dex) - 1: - re = i - break - if album_dex[i + 1] - 1 > playlist_no - 1: - re = i - break - row += 1 - if row > row_len - 1: - row = 0 - px += album_mode_art_size + album_v_gap - - # If the album is within the view port already, dont jump to it - # (unless we really want to with force) - if not force and gui.album_scroll_px + album_v_slide_value < px < gui.album_scroll_px + window_size[1]: - - # Dont chance the view since its alread in the view port - # But if the album is just out of view on the bottom, bring it into view on to bottom row - if window_size[1] > (album_mode_art_size + album_v_gap) * 2: - while not gui.album_scroll_px - 20 < px + (album_mode_art_size + album_v_gap + 3) < gui.album_scroll_px + \ - window_size[1] - 40: - gui.album_scroll_px += 1 - - else: - # Set the view to the calculated position - gui.album_scroll_px = px - gui.album_scroll_px -= album_v_slide_value - - gui.album_scroll_px = max(gui.album_scroll_px, 0 - album_v_slide_value) - - if len(album_dex) > 0: - return album_dex[re] - return 0 - - gui.update += 1 - - -def toggle_album_mode(force_on=False): - global album_mode - global window_size - global update_layout - global album_playlist_width - global old_album_pos - - gui.gall_tab_enter = False - - if album_mode is True: - - album_mode = False - # album_playlist_width = gui.playlist_width - # old_album_pos = gui.album_scroll_px - gui.rspw = gui.pref_rspw - gui.rsp = prefs.prefer_side - gui.album_tab_mode = False - else: - album_mode = True - if gui.combo_mode: - exit_combo() - - gui.rsp = True - - gui.rspw = gui.pref_gallery_w - - space = window_size[0] - gui.rspw - if gui.lsp: - space -= gui.lspw - - if album_mode and gui.set_mode and len(gui.pl_st) > 6 and space < 600 * gui.scale: - gui.set_mode = False - gui.pl_update = True - gui.update_layout() - - reload_albums(quiet=True) - - # if pctl.active_playlist_playing == pctl.active_playlist_viewing: - # goto_album(pctl.playlist_playing_position) - - if album_mode: - if pctl.selected_in_playlist < len(pctl.playing_playlist()): - goto_album(pctl.selected_in_playlist) - - -def toggle_gallery_keycontrol(always_exit=False): - if is_level_zero(): - if not album_mode: - toggle_album_mode() - gui.gall_tab_enter = True - gui.album_tab_mode = True - show_in_gal(pctl.selected_in_playlist, silent=True) - elif gui.gall_tab_enter or always_exit: - # Exit gallery and tab mode - toggle_album_mode() - else: - gui.album_tab_mode ^= True - if gui.album_tab_mode: - show_in_gal(pctl.selected_in_playlist, silent=True) - - -def check_auto_update_okay(code, pl=None): - try: - cmds = shlex.split(code) - except Exception: - logging.exception("Malformed generator code!") - return False - return "auto" in cmds or ( - prefs.always_auto_update_playlists and - pctl.active_playlist_playing != pl and - "sf" not in cmds and - "rf" not in cmds and - "ra" not in cmds and - "sa" not in cmds and - "st" not in cmds and - "rt" not in cmds and - "plex" not in cmds and - "jelly" not in cmds and - "koel" not in cmds and - "tau" not in cmds and - "air" not in cmds and - "sal" not in cmds and - "slt" not in cmds and - "spl\"" not in code and - "tpl\"" not in code and - "tar\"" not in code and - "tmix\"" not in code and - "r" not in cmds) - - -def switch_playlist(number, cycle=False, quiet=False): - global default_playlist - - global search_index - global shift_selection - - # Close any active menus - # for instance in Menu.instances: - # instance.active = False - close_all_menus() - if gui.radio_view: - if cycle: - pctl.radio_playlist_viewing += number - else: - pctl.radio_playlist_viewing = number - if pctl.radio_playlist_viewing > len(pctl.radio_playlists) - 1: - pctl.radio_playlist_viewing = 0 - return - - gui.previous_playlist_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - - gui.pl_update = 1 - search_index = 0 - gui.column_d_click_on = -1 - gui.search_error = False - if quick_search_mode: - gui.force_search = True - - # if pl_follow: - # pctl.multi_playlist[pctl.playlist_active][1] = copy.deepcopy(pctl.playlist_playing) - - if gui.showcase_mode and gui.combo_mode and not quiet: - view_standard() - - pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids = default_playlist - pctl.multi_playlist[pctl.active_playlist_viewing].position = pctl.playlist_view_position - pctl.multi_playlist[pctl.active_playlist_viewing].selected = pctl.selected_in_playlist - - if gall_pl_switch_timer.get() > 240: - gui.gallery_positions.clear() - gall_pl_switch_timer.set() - - gui.gallery_positions[gui.previous_playlist_id] = gui.album_scroll_px - - if cycle: - pctl.active_playlist_viewing += number - else: - pctl.active_playlist_viewing = number - - while pctl.active_playlist_viewing > len(pctl.multi_playlist) - 1: - pctl.active_playlist_viewing -= len(pctl.multi_playlist) - while pctl.active_playlist_viewing < 0: - pctl.active_playlist_viewing += len(pctl.multi_playlist) - - default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - pctl.playlist_view_position = pctl.multi_playlist[pctl.active_playlist_viewing].position - pctl.selected_in_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].selected - logging.debug("Position changed by playlist change") - shift_selection = [pctl.selected_in_playlist] - - id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - - code = pctl.gen_codes.get(id) - if code is not None and check_auto_update_okay(code, pctl.active_playlist_viewing): - gui.regen_single_id = id - tauon.thread_manager.ready("worker") - - if album_mode: - reload_albums(True) - if id in gui.gallery_positions: - gui.album_scroll_px = gui.gallery_positions[id] - else: - goto_album(pctl.playlist_view_position) - - if prefs.auto_goto_playing: - pctl.show_current(this_only=True, playing=False, highlight=True, no_switch=True) - - if prefs.shuffle_lock: - view_box.lyrics(hit=True) - if pctl.active_playlist_viewing: - pctl.active_playlist_playing = pctl.active_playlist_viewing - random_track() - - -def cycle_playlist_pinned(step): - if gui.radio_view: - - pctl.radio_playlist_viewing += step * -1 - if pctl.radio_playlist_viewing > len(pctl.radio_playlists) - 1: - pctl.radio_playlist_viewing = 0 - if pctl.radio_playlist_viewing < 0: - pctl.radio_playlist_viewing = len(pctl.radio_playlists) - 1 - return - - if step > 0: - p = pctl.active_playlist_viewing - le = len(pctl.multi_playlist) - on = p - on -= 1 - while True: - if on < 0: - on = le - 1 - if on == p: - break - if pctl.multi_playlist[on].hidden is False or not prefs.tabs_on_top or ( - gui.lsp and prefs.left_panel_mode == "playlist"): - switch_playlist(on) - break - on -= 1 - - elif step < 0: - p = pctl.active_playlist_viewing - le = len(pctl.multi_playlist) - on = p - on += 1 - while True: - if on == le: - on = 0 - if on == p: - break - if pctl.multi_playlist[on].hidden is False or not prefs.tabs_on_top or ( - gui.lsp and prefs.left_panel_mode == "playlist"): - switch_playlist(on) - break - on += 1 - - -def activate_info_box(): - fader.rise() - pref_box.enabled = True - - -def activate_radio_box(): - radiobox.active = True - radiobox.radio_field.clear() - radiobox.radio_field_title.clear() - - -def new_playlist_colour_callback(): - if gui.radio_view: - return [120, 90, 245, 255] - return [237, 80, 221, 255] - - -add_icon.xoff = 3 -add_icon.yoff = 0 -add_icon.colour = [237, 80, 221, 255] -add_icon.colour_callback = new_playlist_colour_callback - - -def new_playlist_deco(): - if gui.radio_view: - text = _("New Radio List") - else: - text = _("New Playlist") - return [colours.menu_text, colours.menu_background, text] - - -x_menu.add(MenuItem(_("New Playlist"), new_playlist, new_playlist_deco, icon=add_icon)) - - -def clean_db_show_test(_): - return gui.suggest_clean_db - - -def clean_db_fast(): - keys = set(pctl.master_library.keys()) - for pl in pctl.multi_playlist: - keys -= set(pl.playlist_ids) - for item in keys: - pctl.purge_track(item, fast=True) - gui.show_message(_("Done! {N} old items were removed.").format(N=len(keys)), mode="done") - gui.suggest_clean_db = False - - -def clean_db_deco(): - return [colours.menu_text, [30, 150, 120, 255], _("Clean Database!")] - - -x_menu.add(MenuItem(_("Clean Database!"), clean_db_fast, clean_db_deco, show_test=clean_db_show_test)) - -# x_menu.add(_("Internet Radio…"), activate_radio_box) - -tauon.switch_playlist = switch_playlist - - -def import_spotify_playlist() -> None: - clip = copy_from_clipboard() - for line in clip.split("\n"): - if line.startswith(("https://open.spotify.com/playlist/", "spotify:playlist:")): - clip = clip.strip() - tauon.spot_ctl.playlist(line) - - if album_mode: - reload_albums() - gui.pl_update += 1 - - -def import_spotify_playlist_deco(): - clip = copy_from_clipboard() - if clip.startswith(("https://open.spotify.com/playlist/", "spotify:playlist:")): - return [colours.menu_text, colours.menu_background, None] - return [colours.menu_text_disabled, colours.menu_background, None] - - -x_menu.add(MenuItem(_("Paste Spotify Playlist"), import_spotify_playlist, import_spotify_playlist_deco, icon=spot_icon, - show_test=spotify_show_test)) - - -def show_import_music(_): - return gui.add_music_folder_ready - - -def import_music(): - pl = pl_gen(_("Music")) - pl.last_folder = [str(music_directory)] - pctl.multi_playlist.append(pl) - load_order = LoadClass() - load_order.target = str(music_directory) - load_order.playlist = pl.uuid_int - load_orders.append(load_order) - switch_playlist(len(pctl.multi_playlist) - 1) - gui.add_music_folder_ready = False - - -x_menu.add(MenuItem(_("Import Music Folder"), import_music, show_test=show_import_music)) - -x_menu.br() - -settings_icon.xoff = 0 -settings_icon.yoff = 2 -settings_icon.colour = [232, 200, 96, 255] # [230, 152, 118, 255]#[173, 255, 47, 255] #[198, 237, 56, 255] -# settings_icon.colour = [180, 140, 255, 255] -x_menu.add(MenuItem(_("Settings"), activate_info_box, icon=settings_icon)) -x_menu.add_sub(_("Database…"), 190) -if dev_mode: - def dev_mode_enable_save_state() -> None: - global should_save_state - should_save_state = True - show_message(_("Enabled saving state")) - - def dev_mode_disable_save_state() -> None: - global should_save_state - should_save_state = False - show_message(_("Disabled saving state")) - - x_menu.add_sub(_("Dev Mode"), 190) - x_menu.add_to_sub(1, MenuItem(_("Enable Saving State"), dev_mode_enable_save_state)) - x_menu.add_to_sub(1, MenuItem(_("Disable Saving State"), dev_mode_disable_save_state)) -x_menu.br() - - -# x_menu.add('Toggle Side panel', toggle_combo_view, combo_deco) - -def stt2(sec): - days, rem = divmod(sec, 86400) - hours, rem = divmod(rem, 3600) - min, sec = divmod(rem, 60) - - s_day = str(days) + "d" - if s_day == "0d": - s_day = " " - - s_hours = str(hours) + "h" - if s_hours == "0h" and s_day == " ": - s_hours = " " - - s_min = str(min) + "m" - - return s_day.rjust(3) + " " + s_hours.rjust(3) + " " + s_min.rjust(3) - - -def export_database(): - path = str(user_directory / "DatabaseExport.csv") - xport = open(path, "w") - - xport.write("Artist;Title;Album;Album artist;Track number;Type;Duration;Release date;Genre;Playtime;File path") - - for index, track in pctl.master_library.items(): - - xport.write("\n") - - xport.write(csv_string(track.artist) + ",") - xport.write(csv_string(track.title) + ",") - xport.write(csv_string(track.album) + ",") - xport.write(csv_string(track.album_artist) + ",") - xport.write(csv_string(track.track_number) + ",") - type = "File" - if track.is_network: - type = "Network" - elif track.is_cue: - type = "CUE File" - xport.write(type + ",") - xport.write(str(track.length) + ",") - xport.write(csv_string(track.date) + ",") - xport.write(csv_string(track.genre) + ",") - xport.write(str(int(star_store.get_by_object(track))) + ",") - xport.write(csv_string(track.fullpath)) - - xport.close() - show_message(_("Export complete."), _("Saved as: ") + path, mode="done") - - -def q_to_playlist(): - pctl.multi_playlist.append(pl_gen( - title=_("Play History"), - playing=0, - playlist_ids=list(reversed(copy.deepcopy(pctl.track_queue))), - position=0, - hide_title=True, - selected=0)) - - -x_menu.add_to_sub(0, MenuItem(_("Export as CSV"), export_database)) -x_menu.add_to_sub(0, MenuItem(_("Rescan All Folders"), rescan_all_folders)) -x_menu.add_to_sub(0, MenuItem(_("Play History to Playlist"), q_to_playlist)) -x_menu.add_to_sub(0, MenuItem(_("Reset Image Cache"), clear_img_cache)) - -cm_clean_db = False - - -def clean_db() -> None: - global cm_clean_db - prefs.remove_network_tracks = False - cm_clean_db = True - tauon.thread_manager.ready("worker") - - -def clean_db2() -> None: - global cm_clean_db - prefs.remove_network_tracks = True - cm_clean_db = True - tauon.thread_manager.ready("worker") - - -x_menu.add_to_sub(0, MenuItem(_("Remove Network Tracks"), clean_db2)) -x_menu.add_to_sub(0, MenuItem(_("Remove Missing Tracks"), clean_db)) - - - -def import_fmps() -> None: - unique = set() - for playlist in pctl.multi_playlist: - for id in playlist.playlist_ids: - tr = pctl.get_track(id) - if "FMPS_Rating" in tr.misc: - rating = round(tr.misc["FMPS_Rating"] * 10) - star_store.set_rating(tr.index, rating) - unique.add(tr.index) - - show_message(_("{N} ratings imported").format(N=str(len(unique))), mode="done") - - gui.pl_update += 1 - -x_menu.add_to_sub(0, MenuItem(_("Import FMPS Ratings"), import_fmps)) - - -def import_popm(): - unique = set() - skipped = set() - for playlist in pctl.multi_playlist: - for id in playlist.playlist_ids: - tr = pctl.get_track(id) - if "POPM" in tr.misc: - rating = tr.misc["POPM"] - t_rating = 0 - if rating <= 1: - t_rating = 2 - elif rating <= 64: - t_rating = 4 - elif rating <= 128: - t_rating = 6 - elif rating <= 196: - t_rating = 8 - elif rating <= 255: - t_rating = 10 - - if star_store.get_rating(tr.index) == 0: - star_store.set_rating(tr.index, t_rating) - unique.add(tr.index) - else: - logging.info("Won't import POPM because track is already rated") - skipped.add(tr.index) - - s = str(len(unique)) + " ratings imported" - if len(skipped) > 0: - s += f", {len(skipped)} skipped" - show_message(s, mode="done") - - gui.pl_update += 1 - -x_menu.add_to_sub(0, MenuItem(_("Import POPM Ratings"), import_popm)) - - -def clear_ratings() -> None: - if not key_shift_down: - show_message( - _("This will delete all track and album ratings from the local database!"), - _("Press button again while holding shift key if you're sure you want to do that."), - mode="warning") - return - for key, star in star_store.db.items(): - star[2] = 0 - album_star_store.db.clear() - gui.pl_update += 1 - - -x_menu.add_to_sub(0, MenuItem(_("Reset User Ratings"), clear_ratings)) - - -def find_incomplete() -> None: - gen_incomplete(pctl.active_playlist_viewing) - - -x_menu.add_to_sub(0, MenuItem(_("Find Incomplete Albums"), find_incomplete)) -x_menu.add_to_sub(0, MenuItem(_("Mark Missing as Found"), pctl.reset_missing_flags, show_test=test_shift)) - - -def cast_deco(): - line_colour = colours.menu_text - if tauon.chrome_mode: - return [line_colour, colours.menu_background, _("Stop Cast")] # [24, 25, 60, 255] - return [line_colour, colours.menu_background, None] - - -def cast_search2() -> None: - chrome.rescan() - -def cast_search() -> None: - - if tauon.chrome_mode: - pctl.stop() - chrome.end() - else: - if not chrome: - show_message(_("pychromecast not found")) - return - show_message(_("Searching for Chomecasts...")) - shooter(cast_search2) - - -if chrome: - x_menu.add_sub(_("Chromecast…"), 220) - shooter(cast_search2) - -tauon.chrome_menu = x_menu - -#x_menu.add(_("Cast…"), cast_search, cast_deco) - - -def clear_queue() -> None: - pctl.force_queue = [] - gui.pl_update = 1 - pctl.pause_queue = False - - -mode_menu = Menu(175) - - -def set_mini_mode_A1() -> None: - prefs.mini_mode_mode = 0 - set_mini_mode() - - -def set_mini_mode_B1() -> None: - prefs.mini_mode_mode = 1 - set_mini_mode() - - -def set_mini_mode_A2() -> None: - prefs.mini_mode_mode = 2 - set_mini_mode() - - -def set_mini_mode_C1() -> None: - prefs.mini_mode_mode = 5 - set_mini_mode() - -def set_mini_mode_B2() -> None: - prefs.mini_mode_mode = 3 - set_mini_mode() - - -def set_mini_mode_D() -> None: - prefs.mini_mode_mode = 4 - set_mini_mode() - - -mode_menu.add(MenuItem(_("Tab"), set_mini_mode_D)) -mode_menu.add(MenuItem(_("Mini"), set_mini_mode_A1)) -# mode_menu.add(_('Mini Mode Large'), set_mini_mode_A2) -mode_menu.add(MenuItem(_("Slate"), set_mini_mode_C1)) -mode_menu.add(MenuItem(_("Square"), set_mini_mode_B1)) -mode_menu.add(MenuItem(_("Square Large"), set_mini_mode_B2)) - - -def copy_bb_metadata() -> str | None: - tr = pctl.playing_object() - if tr is None: - return None - if not tr.title and not tr.artist and pctl.playing_state == 3: - return pctl.tag_meta - text = f"{tr.artist} - {tr.title}".strip(" -") - if text: - copy_to_clipboard(text) - else: - show_message(_("No metadata available to copy")) - return None - - -mode_menu.br() -mode_menu.add(MenuItem(_("Copy Title to Clipboard"), copy_bb_metadata)) - -extra_menu = Menu(175, show_icons=True) - - -def stop() -> None: - pctl.stop() - - -def random_track() -> None: - playlist = pctl.multi_playlist[pctl.active_playlist_playing].playlist_ids - if playlist: - random_position = random.randrange(0, len(playlist)) - track_id = playlist[random_position] - pctl.jump(track_id, random_position) - pctl.show_current() - - -extra_menu.add(MenuItem(_("Random Track"), random_track, hint=";")) - - -def random_album() -> None: - folders = {} - playlist = pctl.multi_playlist[pctl.active_playlist_playing].playlist_ids - if playlist: - for i, id in enumerate(playlist): - track = pctl.get_track(id) - if track.parent_folder_path not in folders: - folders[track.parent_folder_path] = (id, i) - - key = random.choice(list(folders.keys())) - result = folders[key] - pctl.jump(*result) - pctl.show_current() - - -def radio_random() -> None: - pctl.advance(rr=True) - - -radiorandom_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "radiorandom.png", True)) -revert_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "revert.png", True)) - -radiorandom_icon.xoff = 1 -radiorandom_icon.yoff = 0 -radiorandom_icon.colour = [153, 229, 133, 255] -extra_menu.add(MenuItem(_("Radio Random"), radio_random, hint="/", icon=radiorandom_icon)) - -revert_icon.xoff = 1 -revert_icon.yoff = 0 -revert_icon.colour = [229, 102, 59, 255] -extra_menu.add(MenuItem(_("Revert"), pctl.revert, hint="Shift+/", icon=revert_icon)) - -# extra_menu.add('Toggle Repeat', toggle_repeat, hint='COMMA') - - -# extra_menu.add('Toggle Random', toggle_random, hint='PERIOD') -extra_menu.add(MenuItem(_("Clear Queue"), clear_queue, queue_deco, hint="Alt+Shift+Q")) - - -def heart_menu_colour() -> list[int] | None: - if not (pctl.playing_state == 1 or pctl.playing_state == 2): - if colours.lm: - return [255, 150, 180, 255] - return None - if love(False): - return [245, 60, 60, 255] - if colours.lm: - return [255, 150, 180, 255] - return None - - -heart_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-menu.png", True)) -heart_row_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-track.png", True) -heart_notify_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-notify.png", True) -heart_notify_break_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-notify-break.png", True) -# spotify_row_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "spotify-row.png", True) -star_pc_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "star-pc.png", True) -star_row_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "star.png", True) -star_half_row_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "star-half.png", True) - - -def draw_rating_widget(x: int, y: int, n_track: TrackClass, album: bool = False): - if album: - rat = album_star_store.get_rating(n_track) - else: - rat = star_store.get_rating(n_track.index) - - rect = (x - round(5 * gui.scale), y - round(4 * gui.scale), round(80 * gui.scale), round(16 * gui.scale)) - gui.heart_fields.append(rect) - - if coll(rect) and (inp.mouse_click or (is_level_zero() and not quick_drag)): - gui.pl_update = 2 - pp = mouse_position[0] - x - - if pp < 5 * gui.scale: - rat = 0 - elif pp > 70 * gui.scale: - rat = 10 - else: - rat = pp // (star_row_icon.w // 2) - - if inp.mouse_click: - rat = min(rat, 10) - if album: - album_star_store.set_rating(n_track, rat) - else: - star_store.set_rating(n_track.index, rat, write=True) - - # bg = colours.grey(40) - bg = [255, 255, 255, 17] - fg = colours.grey(210) - - if gui.tracklist_bg_is_light: - bg = [0, 0, 0, 25] - fg = colours.grey(70) - - playtime_stars = 0 - if prefs.rating_playtime_stars and rat == 0 and not album: - playtime_stars = star_count3(star_store.get(n_track.index), n_track.length) - if gui.tracklist_bg_is_light: - fg2 = alpha_blend([0, 0, 0, 70], ddt.text_background_colour) - else: - fg2 = alpha_blend([255, 255, 255, 50], ddt.text_background_colour) - - for ss in range(5): - - xx = x + ss * star_row_icon.w - - if playtime_stars: - if playtime_stars - 1 < ss * 2: - star_row_icon.render(xx, y, bg) - elif playtime_stars - 1 == ss * 2: - star_row_icon.render(xx, y, bg) - star_half_row_icon.render(xx, y, fg2) - else: - star_row_icon.render(xx, y, fg2) - else: - - if rat - 1 < ss * 2: - star_row_icon.render(xx, y, bg) - elif rat - 1 == ss * 2: - star_row_icon.render(xx, y, bg) - star_half_row_icon.render(xx, y, fg) - else: - star_row_icon.render(xx, y, fg) - - -heart_colours = ColourGenCache(0.7, 0.7) - -heart_icon.colour = [245, 60, 60, 255] -heart_icon.xoff = 3 -heart_icon.yoff = 0 - - - -if gui.scale == 1.25: - heart_icon.yoff = 1 - -heart_icon.colour_callback = heart_menu_colour - - -def love_deco(): - if love(False): - return [colours.menu_text, colours.menu_background, _("Un-Love Track")] - if pctl.playing_state == 1 or pctl.playing_state == 2: - return [colours.menu_text, colours.menu_background, _("Love Track")] - return [colours.menu_text_disabled, colours.menu_background, _("Love Track")] - - -def bar_love(notify: bool = False) -> None: - shoot_love = threading.Thread(target=love, args=[True, None, False, notify]) - shoot_love.daemon = True - shoot_love.start() - - -def bar_love_notify() -> None: - bar_love(notify=True) - - -def select_love(notify: bool = False) -> None: - selected = pctl.selected_in_playlist - playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - if -1 < selected < len(playlist): - track_id = playlist[selected] - - shoot_love = threading.Thread(target=love, args=[True, track_id, False, notify]) - shoot_love.daemon = True - shoot_love.start() - - -extra_menu.add(MenuItem("Love", bar_love_notify, love_deco, icon=heart_icon)) - -def toggle_spotify_like_active2(tr: TrackClass) -> None: - if "spotify-track-url" in tr.misc: - if "spotify-liked" in tr.misc: - tauon.spot_ctl.unlike_track(tr) - else: - tauon.spot_ctl.like_track(tr) - gui.pl_update += 1 - for i, p in enumerate(pctl.multi_playlist): - code = pctl.gen_codes.get(p.uuid_int) - if code and code.startswith("slt"): - logging.info("Fetching Spotify likes...") - regenerate_playlist(i, silent=True) - gui.pl_update += 1 - -def toggle_spotify_like_active() -> None: - tr = pctl.playing_object() - if tr: - shoot_dl = threading.Thread(target=toggle_spotify_like_active2, args=([tr])) - shoot_dl.daemon = True - shoot_dl.start() - - -def toggle_spotify_like_active_deco(): - tr = pctl.playing_object() - text = _("Spotify Like Track") - - if pctl.playing_state == 0 or not tr or "spotify-track-url" not in tr.misc: - return [colours.menu_text_disabled, colours.menu_background, text] - if "spotify-liked" in tr.misc: - text = _("Un-like Spotify Track") - - return [colours.menu_text, colours.menu_background, text] - - -def locate_artist() -> None: - track = pctl.playing_object() - if not track: - return - - artist = track.artist - if track.album_artist: - artist = track.album_artist - - block_starts = [] - current = False - for i in range(len(default_playlist)): - track = pctl.get_track(default_playlist[i]) - if current is False: - if track.artist == artist or track.album_artist == artist or ( - "artists" in track.misc and artist in track.misc["artists"]): - block_starts.append(i) - current = True - elif (track.artist != artist and track.album_artist != artist) or ( - "artists" in track.misc and artist in track.misc["artists"]): - current = False - - if block_starts: - - next = False - for start in block_starts: - - if next: - pctl.selected_in_playlist = start - pctl.playlist_view_position = start - shift_selection.clear() - break - - if pctl.selected_in_playlist == start: - next = True - continue - - else: - pctl.selected_in_playlist = block_starts[0] - pctl.playlist_view_position = block_starts[0] - shift_selection.clear() - - tree_view_box.show_track(pctl.get_track(default_playlist[pctl.selected_in_playlist])) - else: - show_message(_("No exact matching artist could be found in this playlist")) - - logging.debug("Position changed by artist locate") - - gui.pl_update += 1 - - -def activate_search_overlay() -> None: - if cm_clean_db: - show_message(_("Please wait for cleaning process to finish")) - return - search_over.active = True - search_over.delay_enter = False - search_over.search_text.selection = 0 - search_over.search_text.cursor_position = 0 - search_over.spotify_mode = False - - -extra_menu.add(MenuItem(_("Global Search"), activate_search_overlay, hint="Ctrl+G")) - - -def get_album_spot_url_active() -> None: - tr = pctl.playing_object() - if tr: - url = tauon.spot_ctl.get_album_url_from_local(tr) - - if url: - copy_to_clipboard(url) - show_message(_("URL copied to clipboard"), mode="done") - else: - show_message(_("No results found")) - - -def get_album_spot_url_actove_deco(): - tr = pctl.playing_object() - text = _("Copy Album URL") - if not tr: - return [colours.menu_text_disabled, colours.menu_background, text] - if "spotify-album-url" not in tr.misc: - text = _("Lookup Spotify Album") - - return [colours.menu_text, colours.menu_background, text] - - - -def goto_playing_extra() -> None: - pctl.show_current(highlight=True) - - -extra_menu.add(MenuItem(_("Locate Artist"), locate_artist)) - -extra_menu.add(MenuItem(_("Go To Playing"), goto_playing_extra, hint="'")) - -def show_spot_playing_deco(): - if not (tauon.spot_ctl.coasting or tauon.spot_ctl.playing): - return [colours.menu_text, colours.menu_background, None] - return [colours.menu_text_disabled, colours.menu_background, None] - -def show_spot_coasting_deco(): - if tauon.spot_ctl.coasting: - return [colours.menu_text, colours.menu_background, None] - return [colours.menu_text_disabled, colours.menu_background, None] - - -def show_spot_playing() -> None: - if pctl.playing_state != 0 and pctl.playing_state != 3 and not tauon.spot_ctl.coasting and not tauon.spot_ctl.playing: - pctl.stop() - tauon.spot_ctl.update(start=True) - - -def spot_transfer_playback_here() -> None: - tauon.spot_ctl.preparing_spotify = True - if not (tauon.spot_ctl.playing or tauon.spot_ctl.coasting): - tauon.spot_ctl.update(start=True) - pctl.playerCommand = "spotcon" - pctl.playerCommandReady = True - pctl.playing_state = 3 - shooter(tauon.spot_ctl.transfer_to_tauon) - - -extra_menu.br() -extra_menu.add(MenuItem("Spotify Like Track", toggle_spotify_like_active, toggle_spotify_like_active_deco, - show_test=spotify_show_test, icon=spot_heartx_icon)) - -def spot_import_albums() -> None: - if not tauon.spot_ctl.spotify_com: - tauon.spot_ctl.spotify_com = True - shoot = threading.Thread(target=tauon.spot_ctl.get_library_albums) - shoot.daemon = True - shoot.start() - else: - show_message(_("Please wait until current job is finished")) - -extra_menu.add_sub(_("Import Spotify…"), 140, show_test=spotify_show_test) - -extra_menu.add_to_sub(0, MenuItem(_("Liked Albums"), spot_import_albums, show_test=spotify_show_test, icon=spot_icon)) - -def spot_import_tracks() -> None: - if not tauon.spot_ctl.spotify_com: - tauon.spot_ctl.spotify_com = True - shoot = threading.Thread(target=tauon.spot_ctl.get_library_likes) - shoot.daemon = True - shoot.start() - else: - show_message(_("Please wait until current job is finished")) - -extra_menu.add_to_sub(0, MenuItem(_("Liked Tracks"), spot_import_tracks, show_test=spotify_show_test, icon=spot_icon)) - -def spot_import_playlists() -> None: - if not tauon.spot_ctl.spotify_com: - show_message(_("Importing Spotify playlists...")) - shoot_dl = threading.Thread(target=tauon.spot_ctl.import_all_playlists) - shoot_dl.daemon = True - shoot_dl.start() - else: - show_message(_("Please wait until current job is finished")) - - -#extra_menu.add_to_sub(_("Import All Playlists"), 0, spot_import_playlists, show_test=spotify_show_test, icon=spot_icon) - -def spot_import_playlist_menu() -> None: - if not tauon.spot_ctl.spotify_com: - playlists = tauon.spot_ctl.get_playlist_list() - spotify_playlist_menu.items.clear() - if playlists: - for item in playlists: - spotify_playlist_menu.add(MenuItem(item[0], tauon.spot_ctl.playlist, pass_ref=True, set_ref=item[1])) - - spotify_playlist_menu.add(MenuItem(_("> Import All Playlists"), spot_import_playlists)) - spotify_playlist_menu.activate(position=(extra_menu.pos[0], window_size[1] - gui.panelBY)) - else: - show_message(_("Please wait until current job is finished")) - -extra_menu.add_to_sub(0, MenuItem(_("Playlist…"), spot_import_playlist_menu, show_test=spotify_show_test, icon=spot_icon)) - - -def spot_import_context() -> None: - shooter(tauon.spot_ctl.import_context) - -extra_menu.add_to_sub(0, MenuItem(_("Current Context"), spot_import_context, show_spot_coasting_deco, show_test=spotify_show_test, icon=spot_icon)) - - -def get_album_spot_deco(): - tr = pctl.playing_object() - text = _("Show Full Album") - if not tr: - return [colours.menu_text_disabled, colours.menu_background, text] - if "spotify-album-url" not in tr.misc: - text = _("Lookup Spotify Album") - - return [colours.menu_text, colours.menu_background, text] - - -extra_menu.add(MenuItem("Show Full Album", get_album_spot_active, get_album_spot_deco, - show_test=spotify_show_test, icon=spot_icon)) - - -def get_artist_spot(tr: TrackClass = None) -> None: - if not tr: - tr = pctl.playing_object() - if not tr: - return - url = tauon.spot_ctl.get_artist_url_from_local(tr) - if not url: - show_message(_("No results found")) - return - show_message(_("Fetching...")) - shooter(tauon.spot_ctl.artist_playlist, (url,)) - -extra_menu.add(MenuItem(_("Show Full Artist"), get_artist_spot, - show_test=spotify_show_test, icon=spot_icon)) - -extra_menu.add(MenuItem(_("Start Spotify Remote"), show_spot_playing, show_spot_playing_deco, show_test=spotify_show_test, - icon=spot_icon)) - -# def spot_transfer_playback_here_deco(): -# tr = pctl.playing_state == 3: -# text = _("Show Full Album") -# if not tr: -# return [colours.menu_text_disabled, colours.menu_background, text] -# if not "spotify-album-url" in tr.misc: -# text = _("Lookup Spotify Album") -# -# return [colours.menu_text, colours.menu_background, text] - - -extra_menu.add(MenuItem("Transfer audio here", spot_transfer_playback_here, show_test=lambda x:spotify_show_test(0) and tauon.enable_librespot and prefs.launch_spotify_local and not pctl.spot_playing and (tauon.spot_ctl.coasting or tauon.spot_ctl.playing), - icon=spot_icon)) - -def toggle_auto_theme(mode: int = 0) -> None: - if mode == 1: - return prefs.colour_from_image - - prefs.colour_from_image ^= True - gui.theme_temp_current = -1 - - gui.reload_theme = True - - # if prefs.colour_from_image and prefs.art_bg and not key_shift_down: - # toggle_auto_bg() - - -def toggle_auto_bg(mode: int= 0) -> bool | None: - if mode == 1: - return prefs.art_bg - prefs.art_bg ^= True - - if prefs.art_bg: - gui.update = 60 - - style_overlay.flush() - tauon.thread_manager.ready("style") - # if prefs.colour_from_image and prefs.art_bg and not key_shift_down: - # toggle_auto_theme() - return None - - -def toggle_auto_bg_strong(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.art_bg_stronger == 2 - - if prefs.art_bg_stronger == 2: - prefs.art_bg_stronger = 1 - else: - prefs.art_bg_stronger = 2 - gui.update_layout() - return None - -def toggle_auto_bg_strong1(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.art_bg_stronger == 1 - prefs.art_bg_stronger = 1 - gui.update_layout() - return None - - -def toggle_auto_bg_strong2(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.art_bg_stronger == 2 - prefs.art_bg_stronger = 2 - gui.update_layout() - if prefs.art_bg: - gui.update = 60 - return None - - -def toggle_auto_bg_strong3(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.art_bg_stronger == 3 - prefs.art_bg_stronger = 3 - gui.update_layout() - if prefs.art_bg: - gui.update = 60 - return None - - -def toggle_auto_bg_blur(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.art_bg_always_blur - prefs.art_bg_always_blur ^= True - style_overlay.flush() - tauon.thread_manager.ready("style") - return None - - -def toggle_auto_bg_showcase(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.bg_showcase_only - prefs.bg_showcase_only ^= True - gui.update_layout() - return None - - -def toggle_notifications(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.show_notifications - - prefs.show_notifications ^= True - - if prefs.show_notifications: - if not de_notify_support: - show_message(_("Notifications for this DE not supported"), "", mode="warning") - return None - - -# def toggle_al_pref_album_artist(mode: int = 0) -> bool: -# -# if mode == 1: -# return prefs.artist_list_prefer_album_artist -# -# prefs.artist_list_prefer_album_artist ^= True -# artist_list_box.saves.clear() -# return None - -def toggle_mini_lyrics(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.show_lyrics_side - prefs.show_lyrics_side ^= True - return None - - -def toggle_showcase_vis(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.showcase_vis - - prefs.showcase_vis ^= True - gui.update_layout() - return None - - -def toggle_level_meter(mode: int = 0) -> bool | None: - if mode == 1: - return gui.vis_want != 0 - - if gui.vis_want == 0: - gui.vis_want = 1 - else: - gui.vis_want = 0 - - gui.update_layout() - return None - - -# def toggle_force_subpixel(mode: int = 0) -> bool | None: -# -# if mode == 1: -# return prefs.force_subpixel_text != 0 -# -# prefs.force_subpixel_text ^= True -# ddt.force_subpixel_text = prefs.force_subpixel_text -# ddt.clear_text_cache() - - -def level_meter_special_2(): - gui.level_meter_colour_mode = 2 - - -theme_files = os.listdir(str(install_directory / "theme")) -theme_files.sort() - - -def last_fm_menu_deco(): - if prefs.scrobble_hold: - if not prefs.auto_lfm and lb.enable: - line = _("ListenBrainz is Paused") - else: - line = _("Scrobbling is Paused") - bg = colours.menu_background - else: - if not prefs.auto_lfm and lb.enable: - line = _("ListenBrainz is Active") - else: - line = _("Scrobbling is Active") - - bg = colours.menu_background - - return [colours.menu_text, bg, line] - - -def lastfm_colour() -> list[int] | None: - if not prefs.scrobble_hold: - return [250, 50, 50, 255] - return None - - -last_fm_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "as.png", True) -lastfm_icon = MenuIcon(last_fm_icon) - -if gui.scale == 2 or gui.scale == 1.25: - lastfm_icon.xoff = 0 -else: - lastfm_icon.xoff = -1 - -lastfm_icon.yoff = 1 - -lastfm_icon.colour = [249, 70, 70, 255] -lastfm_icon.colour_callback = lastfm_colour - - -def lastfm_menu_test(a) -> bool: - if (prefs.auto_lfm and prefs.last_fm_token is not None) or prefs.enable_lb or prefs.maloja_enable: - return True - return False - - -lb_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "lb-g.png")) -lb_icon.base_asset = asset_loader(scaled_asset_directory, loaded_asset_dc, "lb-gs.png") - - -def lb_mode() -> bool: - return prefs.enable_lb - - -lb_icon.mode_callback = lb_mode - -lb_icon.xoff = 3 -lb_icon.yoff = -1 - -if gui.scale == 1.25: - lb_icon.yoff = 0 - -if prefs.auto_lfm: - listen_icon = lastfm_icon -elif lb.enable: - listen_icon = lb_icon -else: - listen_icon = None - -x_menu.add(MenuItem("LFM", lastfm.toggle, last_fm_menu_deco, icon=listen_icon, show_test=lastfm_menu_test)) - - - -def get_album_art_url(tr: TrackClass): - - artist = tr.album_artist - if not tr.album: - return None - if not artist: - artist = tr.artist - if not artist: - return None - - release_id = None - release_group_id = None - if (artist, tr.album) in pctl.album_mbid_release_cache or (artist, tr.album) in pctl.album_mbid_release_group_cache: - release_id = pctl.album_mbid_release_cache[(artist, tr.album)] - release_group_id = pctl.album_mbid_release_group_cache[(artist, tr.album)] - if release_id is None and release_group_id is None: - return None - - if not release_group_id: - release_group_id = tr.misc.get("musicbrainz_releasegroupid") - - if not release_id: - release_id = tr.misc.get("musicbrainz_albumid") - - if not release_group_id: - try: - #logging.info("lookup release group id") - s = musicbrainzngs.search_release_groups(tr.album, artist=artist, limit=1) - release_group_id = s["release-group-list"][0]["id"] - tr.misc["musicbrainz_releasegroupid"] = release_group_id - #logging.info("got release group id") - except Exception: - logging.exception("Error lookup mbid for discord") - pctl.album_mbid_release_group_cache[(artist, tr.album)] = None - - if not release_id: - try: - #logging.info("lookup release id") - s = musicbrainzngs.search_releases(tr.album, artist=artist, limit=1) - release_id = s["release-list"][0]["id"] - tr.misc["musicbrainz_albumid"] = release_id - #logging.info("got release group id") - except Exception: - logging.exception("Error lookup mbid for discord") - pctl.album_mbid_release_cache[(artist, tr.album)] = None - - image_data = None - final_id = None - if release_group_id: - url = pctl.mbid_image_url_cache.get(release_group_id) - if url: - return url - - base_url = "https://coverartarchive.org/release-group/" - url = f"{base_url}{release_group_id}" - - try: - #logging.info("lookup image url from release group") - response = requests.get(url, timeout=10) - response.raise_for_status() - image_data = response.json() - final_id = release_group_id - except (requests.RequestException, ValueError): - logging.exception("No image found for release group") - pctl.album_mbid_release_group_cache[(artist, tr.album)] = None - except Exception: - logging.exception("Unknown error finding image for release group") - - if release_id and not image_data: - url = pctl.mbid_image_url_cache.get(release_id) - if url: - return url - - base_url = "https://coverartarchive.org/release/" - url = f"{base_url}{release_id}" - - try: - #logging.print("lookup image url from album id") - response = requests.get(url, timeout=10) - response.raise_for_status() - image_data = response.json() - final_id = release_id - except (requests.RequestException, ValueError): - logging.exception("No image found for album id") - pctl.album_mbid_release_cache[(artist, tr.album)] = None - except Exception: - logging.exception("Unknown error getting image found for album id") - - if image_data: - for image in image_data["images"]: - if image.get("front") and ("250" in image["thumbnails"] or "small" in image["thumbnails"]): - pctl.album_mbid_release_cache[(artist, tr.album)] = release_id - pctl.album_mbid_release_group_cache[(artist, tr.album)] = release_group_id - - url = image["thumbnails"].get("250") - if url is None: - url = image["thumbnails"].get("small") - - if url: - logging.info("got mb image url for discord") - pctl.mbid_image_url_cache[final_id] = url - return url - - pctl.album_mbid_release_cache[(artist, tr.album)] = None - pctl.album_mbid_release_group_cache[(artist, tr.album)] = None - - return None - - -def discord_loop() -> None: - prefs.discord_active = True - - try: - if not pctl.playing_ready(): - return - asyncio.set_event_loop(asyncio.new_event_loop()) - - # logging.info("Attempting to connect to Discord...") - client_id = "954253873160286278" - RPC = Presence(client_id) - RPC.connect() - - logging.info("Discord RPC connection successful.") - time.sleep(1) - start_time = time.time() - idle_time = Timer() - - state = 0 - index = -1 - br = False - gui.discord_status = "Connected" - gui.update += 1 - current_state = 0 - - while True: - while True: - - current_index = pctl.playing_object().index - if pctl.playing_state == 3: - current_index = radiobox.song_key - - if current_state == 0 and pctl.playing_state in (1, 3): - current_state = 1 - elif current_state == 1 and pctl.playing_state not in (1, 3): - current_state = 0 - idle_time.set() - - if state != current_state or index != current_index: - if pctl.a_time > 4 or current_state != 1: - state = current_state - index = current_index - start_time = time.time() - pctl.playing_time - - break - - if current_state == 0 and idle_time.get() > 13: - logging.info("Pause discord RPC...") - gui.discord_status = "Idle" - RPC.clear(pid) - # RPC.close() - - while True: - if prefs.disconnect_discord: - break - if pctl.playing_state == 1: - logging.info("Reconnect discord...") - RPC.connect() - gui.discord_status = "Connected" - break - time.sleep(2) - - if not prefs.disconnect_discord: - continue - - time.sleep(2) - - if prefs.disconnect_discord: - RPC.clear(pid) - RPC.close() - prefs.disconnect_discord = False - gui.discord_status = "Not connected" - br = True - break - - if br: - break - - title = _("Unknown Track") - tr = pctl.playing_object() - if tr.artist != "" and tr.title != "": - title = tr.title + " | " + tr.artist - if len(title) > 150: - title = _("Unknown Track") - - if tr.album: - album = tr.album - else: - album = _("Unknown Album") - if pctl.playing_state == 3: - album = radiobox.loaded_station["title"] - - if len(album) == 1: - album += " " - - if state == 1: - #logging.info("PLAYING: " + title) - #logging.info(start_time) - url = get_album_art_url(pctl.playing_object()) - - large_image = "tauon-standard" - small_image = None - if url: - large_image = url - small_image = "tauon-standard" - RPC.update( - pid=pid, - state=album, - details=title, - start=int(start_time), - large_image=large_image, - small_image=small_image) - - else: - #logging.info("Discord RPC - Stop") - RPC.update( - pid=pid, - state="Idle", - large_image="tauon-standard") - - time.sleep(5) - - if prefs.disconnect_discord: - RPC.clear(pid) - RPC.close() - prefs.disconnect_discord = False - break - - except Exception: - logging.exception("Error connecting to Discord - is Discord running?") - # show_message(_("Error connecting to Discord", mode='error') - gui.discord_status = _("Error - Discord not running?") - prefs.disconnect_discord = False - - finally: - loop = asyncio.get_event_loop() - if not loop.is_closed(): - loop.close() - prefs.discord_active = False - - -def hit_discord() -> None: - if prefs.discord_enable and prefs.discord_allow and not prefs.discord_active: - discord_t = threading.Thread(target=discord_loop) - discord_t.daemon = True - discord_t.start() - - - -x_menu.add(MenuItem(_("Exit Shuffle Lockdown"), toggle_shuffle_layout, show_test=exit_shuffle_layout)) - -def open_donate_link() -> None: - webbrowser.open("https://github.com/sponsors/Taiko2k", new=2, autoraise=True) - - -x_menu.add(MenuItem(_("Donate"), open_donate_link)) - -x_menu.add(MenuItem(_("Exit"), tauon.exit, hint="Alt+F4", set_ref="User clicked menu exit button", pass_ref=+True)) - - -def stop_quick_add() -> None: - pctl.quick_add_target = None - - -def show_stop_quick_add(_) -> bool: - return pctl.quick_add_target is not None - - -x_menu.add(MenuItem(_("Disengage Quick Add"), stop_quick_add, show_test=show_stop_quick_add)) - - -def view_tracks() -> None: - # if gui.show_playlist is False: - # gui.show_playlist = True - if album_mode: - toggle_album_mode() - if gui.combo_mode: - exit_combo() - if gui.rsp: - toggle_side_panel() - - -# -# def view_standard_full(): -# # if gui.show_playlist is False: -# # gui.show_playlist = True -# -# if album_mode: -# toggle_album_mode() -# if gui.combo_mode: -# toggle_combo_view(off=True) -# if not gui.rsp: -# toggle_side_panel() -# global update_layout -# update_layout = True -# gui.rspw = window_size[0] - - -def view_standard_meta() -> None: - # if gui.show_playlist is False: - # gui.show_playlist = True - if album_mode: - toggle_album_mode() - - if gui.combo_mode: - exit_combo() - - if not gui.rsp: - toggle_side_panel() - - global update_layout - update_layout = True - # gui.rspw = 80 + int(window_size[0] * 0.18) - - -def view_standard() -> None: - # if gui.show_playlist is False: - # gui.show_playlist = True - if album_mode: - toggle_album_mode() - if gui.combo_mode: - exit_combo() - if not gui.rsp: - toggle_side_panel() - - -def standard_view_deco(): - if album_mode or gui.combo_mode or not gui.rsp: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled - return [line_colour, colours.menu_background, None] - - -# def gallery_only_view(): -# if gui.show_playlist is False: -# return -# if not album_mode: -# toggle_album_mode() -# gui.show_playlist = False -# global album_playlist_width -# global update_layout -# update_layout = True -# gui.rspw = window_size[0] -# album_playlist_width = gui.playlist_width -# #gui.playlist_width = -19 - - -def toggle_library_mode() -> None: - if gui.set_mode: - gui.set_mode = False - # gui.set_bar = False - else: - gui.set_mode = True - # gui.set_bar = True - gui.update_layout() - - -def library_deco(): - tc = colours.menu_text - if gui.combo_mode or (gui.show_playlist is False and album_mode): - tc = colours.menu_text_disabled - - if gui.set_mode: - return [tc, colours.menu_background, _("Disable Columns")] - return [tc, colours.menu_background, _("Enable Columns")] - - -def break_deco(): - tex = colours.menu_text - if gui.combo_mode or (gui.show_playlist is False and album_mode): - tex = colours.menu_text_disabled - if not break_enable: - tex = colours.menu_text_disabled - - if not pctl.multi_playlist[pctl.active_playlist_viewing].hide_title: - return [tex, colours.menu_background, _("Disable Title Breaks")] - return [tex, colours.menu_background, _("Enable Title Breaks")] - - -def toggle_playlist_break() -> None: - pctl.multi_playlist[pctl.active_playlist_viewing].hide_title ^= 1 - gui.pl_update = 1 - - -# --------------------------------------------------------------------------------------- - - -def transcode_single(item: list[tuple[int, str]], manual_directory: str | None = None, manual_name: str | None = None): - global core_use - global dl_use - - if manual_directory != None: - codec = "opus" - output = manual_directory - track = item - core_use += 1 - bitrate = 48 - else: - track = item[0] - codec = prefs.transcode_codec - output = prefs.encoder_output / item[1] - bitrate = prefs.transcode_bitrate - - t = pctl.master_library[track] - - path = t.fullpath - cleanup = False - - if t.is_network: - while dl_use > 1: - time.sleep(0.2) - dl_use += 1 - try: - url, params = pctl.get_url(t) - assert url - path = os.path.join(tmp_cache_dir(), str(t.index)) - if os.path.exists(path): - os.remove(path) - logging.info("Downloading file...") - with requests.get(url, params=params, timeout=60) as response, open(path, "wb") as out_file: - out_file.write(response.content) - logging.info("Download complete") - cleanup = True - except Exception: - logging.exception("Error downloading file") - dl_use -= 1 - - if not os.path.isfile(path): - show_message(_("Encoding warning: Missing one or more files")) - core_use -= 1 - return - - out_line = encode_track_name(t) - - if not (output / _("output")).exists(): - (output / _("output")).mkdir() - target_out = str(output / _("output") / (str(track) + "." + codec)) - - command = tauon.get_ffmpeg() + " " - - if not t.is_cue: - command += '-i "' - else: - command += "-ss " + str(t.start_time) - command += " -t " + str(t.length) - - command += ' -i "' - - command += path.replace('"', '\\"') - - command += '" ' - if pctl.master_library[track].is_cue: - if t.title != "": - command += '-metadata title="' + t.title.replace('"', "").replace("'", "") + '" ' - if t.artist != "": - command += '-metadata artist="' + t.artist.replace('"', "").replace("'", "") + '" ' - if t.album != "": - command += '-metadata album="' + t.album.replace('"', "").replace("'", "") + '" ' - if t.track_number != "": - command += '-metadata track="' + str(t.track_number).replace('"', "").replace("'", "") + '" ' - if t.date != "": - command += '-metadata year="' + str(t.date).replace('"', "").replace("'", "") + '" ' - - if codec != "flac": - command += " -b:a " + str(bitrate) + "k -vn " - - command += '"' + target_out.replace('"', '\\"') + '"' - - # logging.info(shlex.split(command)) - startupinfo = None - if system == "Windows" or msys: - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - - if not msys: - command = shlex.split(command) - - subprocess.call(command, stdout=subprocess.PIPE, shell=False, startupinfo=startupinfo) - - logging.info("FFmpeg finished") - if codec == "opus" and prefs.transcode_opus_as: - codec = "ogg" - - # logging.info(target_out) - - if manual_name is None: - final_out = output / (out_line + "." + codec) - final_name = out_line + "." + codec - os.rename(target_out, final_out) - else: - final_out = output / (manual_name + "." + codec) - final_name = manual_name + "." + codec - os.rename(target_out, final_out) - - if prefs.transcode_inplace and not t.is_network and not t.is_cue: - logging.info("MOVE AND REPLACE!") - if os.path.isfile(final_out) and os.path.getsize(final_out) > 1000: - new_name = os.path.join(t.parent_folder_path, final_name) - logging.info(new_name) - shutil.move(final_out, new_name) - - old_key = star_store.key(track) - old_star = star_store.full_get(track) - - try: - send2trash(pctl.master_library[track].fullpath) - except Exception: - logging.exception("File trash error") - - if os.path.isfile(pctl.master_library[track].fullpath): - try: - os.remove(pctl.master_library[track].fullpath) - except Exception: - logging.exception("File delete error") - - pctl.master_library[track].fullpath = new_name - pctl.master_library[track].file_ext = codec.upper() - - # Update and merge playtimes - new_key = star_store.key(track) - if old_star and (new_key != old_key): - - new_star = star_store.full_get(track) - if new_star is None: - new_star = star_store.new_object() - - new_star[0] += old_star[0] - if old_star[2] > 0 and new_star[2] == 0: - new_star[2] = old_star[2] - new_star[1] = "".join(set(new_star[1] + old_star[1])) - - if old_key in star_store.db: - del star_store.db[old_key] - - star_store.db[new_key] = new_star - - gui.transcoding_bach_done += 1 - if cleanup: - os.remove(path) - core_use -= 1 - gui.update += 1 - - -# --------------------- -added = [] - - -def cue_scan(content: str, tn: TrackClass) -> int | None: - # Get length from backend - - lasttime = tn.length - - content = content.replace("\r", "") - content = content.split("\n") - - #logging.info(content) - - global added - - cued = [] - - LENGTH = 0 - PERFORMER = "" - TITLE = "" - START = 0 - DATE = "" - ALBUM = "" - GENRE = "" - MAIN_PERFORMER = "" - - for LINE in content: - if 'TITLE "' in LINE: - ALBUM = LINE[7:len(LINE) - 2] - - if 'PERFORMER "' in LINE: - while LINE[0] != "P": - LINE = LINE[1:] - - MAIN_PERFORMER = LINE[11:len(LINE) - 2] - - if "REM DATE" in LINE: - DATE = LINE[9:len(LINE) - 1] - - if "REM GENRE" in LINE: - GENRE = LINE[10:len(LINE) - 1] - - if "TRACK " in LINE: - break - - for LINE in reversed(content): - if len(LINE) > 100: - return 1 - if "INDEX 01 " in LINE: - temp = "" - pos = len(LINE) - pos -= 1 - while LINE[pos] != ":": - pos -= 1 - if pos < 8: - break - - START = int(LINE[pos - 2:pos]) + (int(LINE[pos - 5:pos - 3]) * 60) - LENGTH = int(lasttime) - START - lasttime = START - - elif 'PERFORMER "' in LINE: - switch = 0 - for i in range(len(LINE)): - if switch == 1 and LINE[i] == '"': - break - if switch == 1: - PERFORMER += LINE[i] - if LINE[i] == '"': - switch = 1 - - elif 'TITLE "' in LINE: - - switch = 0 - for i in range(len(LINE)): - if switch == 1 and LINE[i] == '"': - break - if switch == 1: - TITLE += LINE[i] - if LINE[i] == '"': - switch = 1 - - elif "TRACK " in LINE: - - pos = 0 - while LINE[pos] != "K": - pos += 1 - if pos > 15: - return 1 - TN = LINE[pos + 2:pos + 4] - - TN = int(TN) - - # try: - # bitrate = audio.info.bitrate - # except Exception: - # logging.exception("Failed to set audio bitrate") - # bitrate = 0 - - if PERFORMER == "": - PERFORMER = MAIN_PERFORMER - - nt = copy.deepcopy(tn) - - nt.cue_sheet = "" - nt.is_embed_cue = True - - nt.index = pctl.master_count - # nt.fullpath = filepath.replace('\\', '/') - # nt.filename = filename - # nt.parent_folder_path = os.path.dirname(filepath.replace('\\', '/')) - # nt.parent_folder_name = os.path.splitext(os.path.basename(filepath))[0] - # nt.file_ext = os.path.splitext(os.path.basename(filepath))[1][1:].upper() - if MAIN_PERFORMER: - nt.album_artist = MAIN_PERFORMER - if PERFORMER: - nt.artist = PERFORMER - if GENRE: - nt.genre = GENRE - nt.title = TITLE - nt.length = LENGTH - # nt.bitrate = source_track.bitrate - if ALBUM: - nt.album = ALBUM - if DATE: - nt.date = DATE.replace('"', "") - nt.track_number = TN - nt.start_time = START - nt.is_cue = True - nt.size = 0 # source_track.size - # nt.samplerate = source_track.samplerate - if TN == 1: - nt.size = os.path.getsize(nt.fullpath) - - pctl.master_library[pctl.master_count] = nt - - cued.append(pctl.master_count) - # loaded_pathes_cache[filepath.replace('\\', '/')] = pctl.master_count - # added.append(pctl.master_count) - - pctl.master_count += 1 - LENGTH = 0 - PERFORMER = "" - TITLE = "" - START = 0 - TN = 0 - - added += reversed(cued) - - # cue_list.append(filepath) - - -def get_album_from_first_track(track_position, track_id=None, pl_number=None, pl_id: int | None = None): - if pl_number is None: - - if pl_id: - pl_number = id_to_pl(pl_id) - else: - pl_number = pctl.active_playlist_viewing - - playlist = pctl.multi_playlist[pl_number].playlist_ids - - if track_id is None: - track_id = playlist[track_position] - - if playlist[track_position] != track_id: - return [] - - tracks = [] - album_parent_path = pctl.get_track(track_id).parent_folder_path - - i = track_position - - while i < len(playlist): - if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: - break - - tracks.append(playlist[i]) - i += 1 - - return tracks - - -class SearchOverlay: - - def __init__(self): - - self.active = False - self.search_text = TextBox() - - self.results = [] - self.searched_text = "" - self.on = 0 - self.force_select = -1 - self.old_mouse = [0, 0] - self.sip = False - self.delay_enter = False - self.last_animate_time = 0 - self.animate_timer = Timer(100) - self.input_timer = Timer(100) - self.all_folders = False - self.spotify_mode = False - - def clear(self): - self.search_text.text = "" - self.results.clear() - self.searched_text = "" - self.on = 0 - self.all_folders = False - - def click_artist(self, name, get_list=False, search_lists=None): - - playlist = [] - - if search_lists is None: - search_lists = [] - for pl in pctl.multi_playlist: - search_lists.append(pl.playlist_ids) - - for pl in search_lists: - for item in pl: - tr = pctl.master_library[item] - n = name.lower() - if tr.artist.lower() == n \ - or tr.album_artist.lower() == n \ - or ("artists" in tr.misc and name in tr.misc["artists"]): - if item not in playlist: - playlist.append(item) - - if get_list: - return playlist - - pctl.multi_playlist.append(pl_gen( - title=_("Artist: ") + name, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - if gui.combo_mode: - exit_combo() - switch_playlist(len(pctl.multi_playlist) - 1) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "a\"" + name + "\"" - - inp.key_return_press = False - - def click_year(self, name, get_list: bool = False): - - playlist = [] - for pl in pctl.multi_playlist: - for item in pl.playlist_ids: - if name in pctl.master_library[item].date: - if item not in playlist: - playlist.append(item) - - if get_list: - return playlist - - pctl.multi_playlist.append(pl_gen( - title=_("Year: ") + name, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - if gui.combo_mode: - exit_combo() - - switch_playlist(len(pctl.multi_playlist) - 1) - - inp.key_return_press = False - - def click_composer(self, name: str, get_list: bool = False): - - playlist = [] - for pl in pctl.multi_playlist: - for item in pl.playlist_ids: - if pctl.master_library[item].composer.lower() == name.lower(): - if item not in playlist: - playlist.append(item) - - if get_list: - return playlist - - pctl.multi_playlist.append(pl_gen( - title=_("Composer: ") + name, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - if gui.combo_mode: - exit_combo() - - switch_playlist(len(pctl.multi_playlist) - 1) - - inp.key_return_press = False - - def click_meta(self, name: str, get_list: bool = False, search_lists=None): - - if search_lists is None: - search_lists = [] - for pl in pctl.multi_playlist: - search_lists.append(pl.playlist_ids) - - playlist = [] - for pl in search_lists: - for item in pl: - if name in pctl.master_library[item].parent_folder_path: - if item not in playlist: - playlist.append(item) - - if get_list: - return playlist - - pctl.multi_playlist.append(pl_gen( - title=os.path.basename(name).upper(), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - if gui.combo_mode: - exit_combo() - - switch_playlist(len(pctl.multi_playlist) - 1) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "p\"" + name + "\"" - - inp.key_return_press = False - - def click_genre(self, name: str, get_list: bool = False, search_lists=None): - - playlist = [] - - if search_lists is None: - search_lists = [] - for pl in pctl.multi_playlist: - search_lists.append(pl.playlist_ids) - - include_multi = False - if name.endswith("+") or not prefs.sep_genre_multi: - name = name.rstrip("+") - include_multi = True - - for pl in search_lists: - for item in pl: - track = pctl.master_library[item] - if track.genre.lower().replace("-", "") == name.lower().replace("-", ""): - if item not in playlist: - playlist.append(item) - elif include_multi and ("/" in track.genre or "," in track.genre or ";" in track.genre): - for split in track.genre.replace(",", "/").replace(";", "/").split("/"): - split = split.strip() - if name.lower().replace("-", "") == split.lower().replace("-", ""): - if item not in playlist: - playlist.append(item) - - if get_list: - return playlist - - pctl.multi_playlist.append(pl_gen( - title=_("Genre: ") + name, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - if gui.combo_mode: - exit_combo() - - switch_playlist(len(pctl.multi_playlist) - 1) - - if include_multi: - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "gm\"" + name + "\"" - else: - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "g=\"" + name + "\"" - - inp.key_return_press = False - - def click_album(self, index): - - pctl.jump(index) - if gui.combo_mode: - exit_combo() - - pctl.show_current() - - inp.key_return_press = False - - def render(self): - global input_text - if self.active is False: - - # Activate search overlay on key presses - if prefs.search_on_letter and input_text != "" and gui.layer_focus == 0 and \ - not key_lalt and not key_ralt and \ - not key_ctrl_down and not radiobox.active and not rename_track_box.active and \ - not quick_search_mode and not pref_box.enabled and not gui.rename_playlist_box \ - and not gui.rename_folder_box and input_text.isalnum() and not gui.box_over \ - and not trans_edit_box.active: - - # Divert to artist list if mouse over - if gui.lsp and prefs.left_panel_mode == "artist list" and 2 < mouse_position[0] < gui.lspw \ - and gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY: - artist_list_box.locate_artist_letter(input_text) - return - - activate_search_overlay() - self.old_mouse = copy.deepcopy(mouse_position) - - if self.active: - - x = 0 - y = 0 - w = window_size[0] - h = window_size[1] - - if keymaps.test("add-to-queue"): - input_text = "" - - if inp.backspace_press: - # self.searched_text = "" - # self.results.clear() - - if len(self.search_text.text) - inp.backspace_press < 1: - self.active = False - self.search_text.text = "" - self.results.clear() - self.searched_text = "" - return - - if key_esc_press: - if self.delay_enter: - self.delay_enter = False - else: - self.active = False - self.search_text.text = "" - self.results.clear() - self.searched_text = "" - return - - if gui.level_2_click and mouse_position[0] > 350 * gui.scale: - self.active = False - self.search_text.text = "" - - mouse_change = False - if not point_proximity_test(self.old_mouse, mouse_position, 25): - mouse_change = True - # mouse_change = True - - ddt.rect((x, y, w, h), [3, 3, 3, 235]) - ddt.text_background_colour = [12, 12, 12, 255] - - - input_text_x = 80 * gui.scale - highlight_x = 30 * gui.scale - thumbnail_rx = 100 * gui.scale - text_lx = 120 * gui.scale - - s_font = 15 - s_b_font = 214 - b_font = 215 - - if window_size[0] < 400 * gui.scale: - input_text_x = 30 * gui.scale - highlight_x = 4 * gui.scale - thumbnail_rx = 65 * gui.scale - text_lx = 80 * gui.scale - s_font = 415 - s_b_font = 514 - d_font = 515 - - #album_art_size_s = 0 * gui.scale - - # Search active animation - if self.sip: - x = round(15 * gui.scale) - y = x - s = round(7 * gui.scale) - g = round(4 * gui.scale) - - t = self.animate_timer.get() - if abs(t - self.last_animate_time) > 0.3: - self.animate_timer.set() - t = 0 - - self.last_animate_time = t - - for item in range(4): - a = 100 - if round(t * 14) % 4 == item: - a = 255 - if self.spotify_mode: - colour = (145, 245, 78, a) - else: - colour = (140, 100, 255, a) - - ddt.rect((x, y, s, s), colour) - x += g + s - - gui.update += 1 - - # No results found message - elif not self.results and len(self.search_text.text) > 1: - if self.input_timer.get() > 0.5 and not self.sip: - ddt.text((window_size[0] // 2, 200 * gui.scale, 2), _("No results found"), [250, 250, 250, 255], 216, - bg=[12, 12, 12, 255]) - - # Spotify search text - if prefs.spot_mode and not self.spotify_mode: - text = _("Press Tab key to switch to Spotify search") - ddt.text((window_size[0] // 2, window_size[1] - 30 * gui.scale, 2), text, [250, 250, 250, 255], 212, - bg=[12, 12, 12, 255]) - - self.search_text.draw(input_text_x, 60 * gui.scale, [230, 230, 230, 255], True, False, 30, - window_size[0] - 100, big=True, click=gui.level_2_click, selection_height=30) - - if inp.key_tab_press: - search_over.spotify_mode ^= True - self.sip = True - search_over.searched_text = search_over.search_text.text - if worker2_lock.locked(): - try: - worker2_lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked worker2_lock") - else: - logging.exception("Unknown RuntimeError trying to release worker2_lock") - except Exception: - logging.exception("Unknown error trying to release worker2_lock") - - if input_text or key_backspace_press: - self.input_timer.set() - - gui.update += 1 - elif self.input_timer.get() >= 0.20 and \ - (len(search_over.search_text.text) > 1 or (len(search_over.search_text.text) == 1 and ord(search_over.search_text.text) > 128)) \ - and search_over.search_text.text != search_over.searched_text: - self.sip = True - if worker2_lock.locked(): - try: - worker2_lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked worker2_lock") - else: - logging.exception("Unknown RuntimeError trying to release worker2_lock") - except Exception: - logging.exception("Unknown error trying to release worker2_lock") - - if self.input_timer.get() < 10: - gui.frame_callback_list.append(TestTimer(0.1)) - - yy = 110 * gui.scale - - if key_down_press: - - self.force_select += 1 - if self.force_select > 4: - self.on = self.force_select - 4 - self.force_select = min(self.force_select, len(self.results) - 1) - self.old_mouse = copy.deepcopy(mouse_position) - - if key_up_press: - - if self.force_select > -1: - self.force_select -= 1 - self.force_select = max(self.force_select, 0) - - if self.force_select < self.on + 4: - self.on = self.force_select - 4 - self.on = max(self.on, 0) - - self.old_mouse = copy.deepcopy(mouse_position) - - if mouse_wheel == -1: - self.on += 1 - self.force_select += 1 - if mouse_wheel == 1 and self.on > -1: - self.on -= 1 - self.force_select -= 1 - - enter = False - - if self.delay_enter and not self.sip and self.search_text.text == self.searched_text: - enter = True - self.delay_enter = False - - elif inp.key_return_press: - if self.results: - enter = True - self.delay_enter = False - elif self.sip or self.input_timer.get() < 0.25: - self.delay_enter = True - else: - enter = True - self.delay_enter = False - - inp.key_return_press = False - - bar_colour = [140, 80, 240, 255] - track_in_bar_colour = [244, 209, 66, 255] - - self.on = max(self.on, 0) - self.on = min(len(self.results) - 1, self.on) - - full_count = 0 - - sec = False - - p = -1 - - if self.on > 4: - p += self.on - 4 - p = self.on - 1 - clear = False - - for i, item in enumerate(self.results): - - p += 1 - - if p > len(self.results) - 1: - break - - item: list[int] = self.results[p] - - fade = 1 - selected = self.on - if self.force_select > -1: - selected = self.force_select - - #logging.info(selected) - - if selected != p: - fade = 0.8 - - start = yy - - n = item[0] - - names = { - 0: "Artist", - 1: "Album", - 2: "Track", - 3: "Genre", - 5: "Folder", - 6: "Composer", - 7: "Year", - 8: "Playlist", - 10: "Artist", - 11: "Album", - 12: "Track", - } - type_colours = { - 0: [250, 140, 190, 255], # Artist - 1: [250, 140, 190, 255], # Album - 2: [250, 220, 190, 255], # Track - 3: [240, 240, 160, 255], # Genre - 5: [250, 100, 50, 255], # Folder - 6: [180, 250, 190, 255], # Composer - 7: [250, 50, 140, 255], # Year - 8: [100, 210, 250, 255], # Playlist - 10: [145, 245, 78, 255], # Spotify Artist - 11: [130, 237, 69, 255], # Spotify Album - 12: [200, 255, 150, 255], # Spotify Track - } - if n not in names: - name = "NYI" - colour = [255, 255, 255, 255] - else: - name = names[n] - colour = type_colours[n] - colour[3] = int(colour[3] * fade) - - pad = round(4 * gui.scale) - height = round(25 * gui.scale) - if n in (1, 11): - height = round(50 * gui.scale) - album_art_size = height - - - # Selection bar - s_rect = (highlight_x, yy, 600 * gui.scale, height + pad + pad - 1) - fields.add(s_rect) - if fade == 1: - ddt.rect((highlight_x, yy + pad, 4 * gui.scale, height), bar_colour) - if n in (2,): - if key_ctrl_down and item[2] in default_playlist: - ddt.rect((highlight_x + round(5 * gui.scale), yy + pad, 4 * gui.scale, height), track_in_bar_colour) - - # Type text - if n in (0, 3, 5, 6, 7, 8, 10, 12): - ddt.text((thumbnail_rx, yy + pad + round(3 * gui.scale), 1), names[n], type_colours[n], 214) - - # Thumbnail - if n in (1, 2): - thl = thumbnail_rx - album_art_size - ddt.rect((thl, yy + pad, album_art_size, album_art_size), [50, 50, 50, 150]) - tauon.gall_ren.render(pctl.get_track(item[2]), (thl, yy + pad), album_art_size) - if fade != 1: - ddt.rect((thl, yy + pad, album_art_size, album_art_size), [0, 0, 0, 70]) - if n in (11,): - thl = thumbnail_rx - album_art_size - ddt.rect((thl, yy + pad, album_art_size, album_art_size), [50, 50, 50, 150]) - # tauon.gall_ren.render(pctl.get_track(item[2]), (50 * gui.scale, yy + 5), 50 * gui.scale) - if not item[5].draw(thumbnail_rx - album_art_size, yy + pad): - if tauon.gall_ren.lock.locked(): - try: - tauon.gall_ren.lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked gall_ren_lock") - else: - logging.exception("Unknown RuntimeError trying to release gall_ren_lock") - except Exception: - logging.exception("Unknown error trying to release gall_ren_lock") - - # Result text - if n in (0, 5, 6, 7, 8, 10): # Bold - xx = ddt.text((text_lx, yy + pad + round(3 * gui.scale)), item[1], [255, 255, 255, int(255 * fade)], b_font) - if n in (3,): # Genre - xx = ddt.text((text_lx, yy + pad + round(3 * gui.scale)), item[1].rstrip("+"), [255, 255, 255, int(255 * fade)], b_font) - if item[1].endswith("+"): - ddt.text( - (xx + text_lx + 13 * gui.scale, yy + pad + round(3 * gui.scale)), _("(Include multi-tag results)"), - [255, 255, 255, int(255 * fade) // 2], 313) - if n == 11: # Spotify Album - xx = ddt.text((text_lx, yy + round(5 * gui.scale)), item[1][0], [255, 255, 255, int(255 * fade)], s_b_font) - artist = item[1][1] - ddt.text((text_lx + 5 * gui.scale, yy + 30 * gui.scale), _("BY"), [250, 240, 110, int(255 * fade)], 212) - xx += 8 * gui.scale - xx += ddt.text((text_lx + 30 * gui.scale, yy + 30 * gui.scale), artist, [250, 250, 250, int(255 * fade)], s_font) - if n in (12,): # Spotify Track - yyy = yy - yyy += round(6 * gui.scale) - xx = ddt.text((text_lx, yyy), item[1][0], [255, 255, 255, int(255 * fade)], s_font) - xx += 9 * gui.scale - ddt.text((xx + text_lx, yyy), _("BY"), [250, 160, 110, int(255 * fade)], 212) - xx += 25 * gui.scale - xx += ddt.text((xx + text_lx, yyy), item[1][1], [255, 255, 255, int(255 * fade)], s_b_font) - if n in (2, ): # Track - yyy = yy - yyy += round(6 * gui.scale) - track = pctl.master_library[item[2]] - if track.artist == track.title == "": - text = os.path.splitext(track.filename)[0] - xx = ddt.text((text_lx, yyy + pad), text, [255, 255, 255, int(255 * fade)], s_font) - else: - xx = ddt.text((text_lx, yyy), item[1], [255, 255, 255, int(255 * fade)], s_font) - xx += 9 * gui.scale - ddt.text((xx + text_lx, yyy), _("BY"), [250, 160, 110, int(255 * fade)], 212) - xx += 25 * gui.scale - artist = track.artist - xx += ddt.text((xx + text_lx, yyy), artist, [255, 255, 255, int(255 * fade)], s_b_font) - if track.album: - xx += 9 * gui.scale - xx += ddt.text((xx + text_lx, yyy), _("FROM"), [120, 120, 120, int(255 * fade)], 212) - xx += 8 * gui.scale - xx += ddt.text((xx + text_lx, yyy), track.album, [80, 80, 80, int(255 * fade)], 212) - - if n in (1,): # Two line album - track = pctl.master_library[item[2]] - artist = track.album_artist - if not artist: - artist = track.artist - - xx = ddt.text((text_lx, yy + pad + round(5 * gui.scale)), item[1], [255, 255, 255, int(255 * fade)], s_b_font) - - ddt.text((text_lx + 5 * gui.scale, yy + 30 * gui.scale), _("BY"), [250, 240, 110, int(255 * fade)], 212) - xx += 8 * gui.scale - xx += ddt.text((text_lx + 30 * gui.scale, yy + 30 * gui.scale), artist, [250, 250, 250, int(255 * fade)], s_font) - - - yy += height + pad + pad - - show = False - go = False - extend = False - if coll(s_rect) and mouse_change: - if self.force_select != p: - self.force_select = p - gui.update = 2 - - if gui.level_2_click: - if key_ctrl_down: - extend = True - else: - go = True - clear = True - - - if level_2_right_click: - show = True - clear = True - - if enter and key_shift_down and fade == 1: - show = True - clear = True - - elif enter and fade == 1: - if key_shift_down or key_shiftr_down: - show = True - clear = True - else: - go = True - clear = True - - if extend: - match n: - case 0: - default_playlist.extend(self.click_artist(item[1], get_list=True)) - case 1: - for k, pl in enumerate(pctl.multi_playlist): - if item[2] in pl.playlist_ids: - default_playlist.extend( - get_album_from_first_track(pl.playlist_ids.index(item[2]), item[2], k)) - break - case 2: - default_playlist.append(item[2]) - case 3: - default_playlist.extend(self.click_genre(item[1], get_list=True)) - case 5: - default_playlist.extend(self.click_meta(item[1], get_list=True)) - case 6: - default_playlist.extend(self.click_composer(item[1], get_list=True)) - case 7: - default_playlist.extend(self.click_year(item[1], get_list=True)) - case 8: - default_playlist.extend(pctl.multi_playlist[pl].playlist_ids) - case 12: - tauon.spot_ctl.append_track(item[2]) - reload_albums() - - gui.pl_update += 1 - elif show: - match n: - case 0 | 1 | 2 | 3 | 5 | 6 | 7 | 10: - pctl.show_current(index=item[2], playing=False) - if album_mode: - show_in_gal(0) - case 8: - pl = id_to_pl(item[3]) - if pl: - switch_playlist(pl) - - elif go: - match n: - case 0: - self.click_artist(item[1]) - case 10: - show_message(_("Searching for albums by artist: ") + item[1], _("This may take a moment")) - shoot = threading.Thread(target=tauon.spot_ctl.artist_playlist, args=([item[2]])) - shoot.daemon = True - shoot.start() - case 1 | 2: - self.click_album(item[2]) - pctl.show_current(index=item[2]) - pctl.playlist_view_position = pctl.selected_in_playlist - case 3: - self.click_genre(item[1]) - case 5: - self.click_meta(item[1]) - case 6: - self.click_composer(item[1]) - case 7: - self.click_year(item[1]) - case 8: - pl = id_to_pl(item[3]) - if pl: - switch_playlist(pl) - case 11: - tauon.spot_ctl.album_playlist(item[2]) - reload_albums() - case 12: - tauon.spot_ctl.append_track(item[2]) - reload_albums() - - if n in (2,) and keymaps.test("add-to-queue") and fade == 1: - queue_object = queue_item_gen( - item[2], - pctl.multi_playlist[id_to_pl(item[3])].playlist_ids.index(item[2]), - item[3]) - pctl.force_queue.append(queue_object) - queue_timer_set(queue_object=queue_object) - - # ---- - - # --- - if i > 40: - break - if yy > window_size[1] - (100 * gui.scale): - break - - continue - - if clear: - self.active = False - self.search_text.text = "" - self.results.clear() - self.searched_text = "" - - - -search_over = SearchOverlay() - - -class MessageBox: - - def __init__(self): - pass - - def get_rect(self): - - w1 = ddt.get_text_w(gui.message_text, 15) + 74 * gui.scale - w2 = ddt.get_text_w(gui.message_subtext, 12) + 74 * gui.scale - w3 = ddt.get_text_w(gui.message_subtext2, 12) + 74 * gui.scale - w = max(w1, w2, w3) - - w = max(w, 210 * gui.scale) - - h = round(60 * gui.scale) - if gui.message_subtext2: - h += round(15 * gui.scale) - - x = int(window_size[0] / 2) - int(w / 2) - y = int(window_size[1] / 2) - int(h / 2) - - return x, y, w, h - - def render(self): - - if inp.mouse_click or inp.key_return_press or right_click or key_esc_press or inp.backspace_press \ - or keymaps.test("quick-find") or (k_input and message_box_min_timer.get() > 1.2): - - if not key_focused and message_box_min_timer.get() > 0.4: - gui.message_box = False - gui.update += 1 - inp.key_return_press = False - - x, y, w, h = self.get_rect() - - ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), - colours.box_text_border) - ddt.rect_a((x, y), (w, h), colours.message_box_bg) - - ddt.text_background_colour = colours.message_box_bg - - if gui.message_mode == "info": - message_info_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) - elif gui.message_mode == "warning": - message_warning_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) - elif gui.message_mode == "done": - message_tick_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) - elif gui.message_mode == "arrow": - message_arrow_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) - elif gui.message_mode == "download": - message_download_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) - elif gui.message_mode == "error": - message_error_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_error_icon.h / 2) - 1) - elif gui.message_mode == "bubble": - message_bubble_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_bubble_icon.h / 2) - 1) - elif gui.message_mode == "link": - message_info_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_bubble_icon.h / 2) - 1) - elif gui.message_mode == "confirm": - message_info_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) - ddt.text((x + 62 * gui.scale, y + 9 * gui.scale), gui.message_text, colours.message_box_text, 15) - if draw.button("Yes", (w // 2 + x) - 70 * gui.scale, y + 32 * gui.scale, w=60*gui.scale): - gui.message_box_confirm_callback(*gui.message_box_confirm_reference) - if draw.button("No", (w // 2 + x) + 25 * gui.scale, y + 32 * gui.scale, w=60*gui.scale): - gui.message_box = False - return - - if gui.message_subtext: - ddt.text((x + 62 * gui.scale, y + 11 * gui.scale), gui.message_text, colours.message_box_text, 15) - if gui.message_mode == "bubble" or gui.message_mode == "link": - link_pa = draw_linked_text((x + 63 * gui.scale, y + (9 + 22) * gui.scale), gui.message_subtext, - colours.message_box_text, 12) - link_activate(x + 63 * gui.scale, y + (9 + 22) * gui.scale, link_pa) - else: - ddt.text((x + 63 * gui.scale, y + (9 + 22) * gui.scale), gui.message_subtext, colours.message_box_text, - 12) - - if gui.message_subtext2: - ddt.text((x + 63 * gui.scale, y + (9 + 42) * gui.scale), gui.message_subtext2, colours.message_box_text, - 12) - - else: - ddt.text((x + 62 * gui.scale, y + 20 * gui.scale), gui.message_text, colours.message_box_text, 15) - - -message_box = MessageBox() - - -class NagBox: - def __init__(self): - self.wiggle_timer = Timer(10) - - def draw(self): - w = 485 * gui.scale - h = 165 * gui.scale - x = int(window_size[0] / 2) - int(w / 2) - # if self.wiggle_timer.get() < 0.5: - # gui.update += 1 - # x += math.sin(core_timer.get() * 40) * 4 - y = int(window_size[1] / 2) - int(h / 2) - - # xx = x - round(8 * gui.scale) - # hh = 0.0 #349 / 360 - # while xx < x + w + round(8 * gui.scale): - # re = [xx, y - round(8 * gui.scale), 3, h + round(8 * gui.scale) + round(8 * gui.scale)] - # hh -= 0.0007 - # c = hsl_to_rgb(hh, 0.9, 0.7) - # #c = hsl_to_rgb(hh, 0.63, 0.43) - # ddt.rect(re, c) - # xx += 3 - - ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), - colours.box_text_border) - ddt.rect_a((x, y), (w, h), colours.message_box_bg) - - # if gui.level_2_click and not coll((x, y, w, h)): - # if core_timer.get() < 2: - # self.wiggle_timer.set() - # else: - # prefs.show_nag = False - # - # gui.update += 1 - - ddt.text_background_colour = colours.message_box_bg - - x += round(10 * gui.scale) - y += round(13 * gui.scale) - ddt.text((x, y), _("Welcome to v7.2.0!"), colours.message_box_text, 212) - y += round(20 * gui.scale) - - link_pa = draw_linked_text( - (x, y), - _("You can check out the release notes on the https://") + "github.com/Taiko2k/TauonMusicBox/releases", - colours.message_box_text, 12, replace=_("Github release page.")) - link_activate(x, y, link_pa, click=gui.level_2_click) - - heart_notify_icon.render(x + round(425 * gui.scale), y + round(80 * gui.scale), [255, 90, 90, 255]) - - y += round(30 * gui.scale) - ddt.text((x, y), _("New supporter bonuses!"), colours.message_box_text, 212) - - y += round(20 * gui.scale) - - ddt.text((x, y), _("A new supporter bonus theme is now available! Check it out at the above link!"), - colours.message_box_text, 12) - # link_activate(x, y, link_pa, click=gui.level_2_click) - - y += round(20 * gui.scale) - ddt.text((x, y), _("Your support means a lot! Love you!"), colours.message_box_text, 12) - - y += round(30 * gui.scale) - - if draw.button("Close", x, y, press=gui.level_2_click): - prefs.show_nag = False - # show_message("Oh... :( 💔") - # if draw.button("Show supporter page", x + round(304 * gui.scale), y, background_colour=[60, 140, 60, 255], background_highlight_colour=[60, 150, 60, 255], press=gui.level_2_click): - # webbrowser.open("https://github.com/sponsors/Taiko2k", new=2, autoraise=True) - # prefs.show_nag = False - # if draw.button("I already am!", x + round(360), y, press=gui.level_2_click): - # show_message("Oh hey, thanks! :)") - # prefs.show_nag = False - - -nagbox = NagBox() - - -def worker3(): - while True: - # time.sleep(0.04) - - # if tauon.thread_manager.exit_worker3: - # tauon.thread_manager.exit_worker3 = False - # return - # time.sleep(1) - - tauon.gall_ren.worker_render() - - -def worker4(): - gui.style_worker_timer.set() - while True: - if prefs.art_bg or (gui.mode == 3 and prefs.mini_mode_mode == 5): - style_overlay.worker() - - time.sleep(0.01) - if pctl.playing_state > 0 and pctl.playing_time < 5: - gui.style_worker_timer.set() - if gui.style_worker_timer.get() > 5: - return - - -worker2_lock = threading.Lock() -spot_search_rate_timer = Timer() - - -def worker2(): - while True: - worker2_lock.acquire() - - if search_over.search_text.text and not (len(search_over.search_text.text) == 1 and ord(search_over.search_text.text[0]) < 128): - - if search_over.spotify_mode: - t = spot_search_rate_timer.get() - if t < 1: - time.sleep(1 - t) - spot_search_rate_timer.set() - logging.info("Spotify search") - search_over.results.clear() - results = tauon.spot_ctl.search(search_over.search_text.text) - if results is not None: - search_over.results = results - else: - search_over.active = False - gui.show_message(_( - "Global search + Tab triggers Spotify search but Spotify is not enabled in settings!"), - mode="warning") - search_over.searched_text = search_over.search_text.text - search_over.sip = False - - elif True: - # perf_timer.set() - - temp_results = [] - - search_over.searched_text = search_over.search_text.text - - artists = {} - albums = {} - genres = {} - metas = {} - composers = {} - years = {} - - tracks = set() - - br = 0 - - if search_over.searched_text in ("the", "and"): - continue - - search_over.sip = True - gui.update += 1 - - o_text = search_over.search_text.text.lower().replace("-", "") - - dia_mode = False - if all([ord(c) < 128 for c in o_text]): - dia_mode = True - - artist_mode = False - if o_text.startswith("artist "): - o_text = o_text[7:] - artist_mode = True - - album_mode = False - if o_text.startswith("album "): - o_text = o_text[6:] - album_mode = True - - composer_mode = False - if o_text.startswith("composer "): - o_text = o_text[9:] - composer_mode = True - - year_mode = False - if o_text.startswith("year "): - o_text = o_text[5:] - year_mode = True - - cn_mode = False - if use_cc and re.search(r"[\u4e00-\u9fff\u3400-\u4dbf\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2b820-\u2ceaf\uf900-\ufaff\u2f800-\u2fa1f]", o_text): - t_cn = s2t.convert(o_text) - s_cn = t2s.convert(o_text) - cn_mode = True - - s_text = o_text - - searched = set() - - for playlist in pctl.multi_playlist: - - # if "<" in playlist.title: - # #logging.info("Skipping search on derivative playlist: " + playlist.title) - # continue - - for track in playlist.playlist_ids: - - if track in searched: - continue - searched.add(track) - - - if cn_mode: - s_text = o_text - cache_string = search_string_cache.get(track) - if cache_string: - if search_magic_any(s_text, cache_string): - pass - elif search_magic_any(t_cn, cache_string): - s_text = t_cn - elif search_magic_any(s_cn, cache_string): - s_text = s_cn - - if dia_mode: - cache_string = search_dia_string_cache.get(track) - if cache_string is not None: - if not search_magic_any(s_text, cache_string): - continue - # if s_text not in cache_string: - # continue - else: - cache_string = search_string_cache.get(track) - if cache_string is not None: - if not search_magic_any(s_text, cache_string): - continue - - t = pctl.master_library[track] - - title = t.title.lower().replace("-", "") - artist = t.artist.lower().replace("-", "") - album_artist = t.album_artist.lower().replace("-", "") - composer = t.composer.lower().replace("-", "") - date = t.date.lower().replace("-", "") - album = t.album.lower().replace("-", "") - genre = t.genre.lower().replace("-", "") - filename = t.filename.lower().replace("-", "") - stem = os.path.dirname(t.parent_folder_path).lower().replace("-", "") - sartist = t.misc.get("artist_sort", "").lower() - - if cache_string is None: - if not dia_mode: - search_string_cache[ - track] = title + artist + album_artist + composer + date + album + genre + sartist + filename + stem - - if cn_mode: - cache_string = search_string_cache.get(track) - if cache_string: - if search_magic_any(s_text, cache_string): - pass - elif search_magic_any(t_cn, cache_string): - s_text = t_cn - elif search_magic_any(s_cn, cache_string): - s_text = s_cn - - if dia_mode: - title = unidecode(title) - - artist = unidecode(artist) - album_artist = unidecode(album_artist) - composer = unidecode(composer) - album = unidecode(album) - filename = unidecode(filename) - sartist = unidecode(sartist) - - if cache_string is None: - search_dia_string_cache[ - track] = title + artist + album_artist + composer + date + album + genre + sartist + filename + stem - - stem = os.path.dirname(t.parent_folder_path) - - if len(s_text) > 2 and s_text in stem.replace("-", "").lower(): - # if search_over.all_folders or (artist not in stem.lower() and album not in stem.lower()): - - if stem in metas: - metas[stem] += 2 - else: - temp_results.append([5, stem, track, playlist.uuid_int, 0]) - metas[stem] = 2 - - if s_text in genre: - - if "/" in genre or "," in genre or ";" in genre: - - for split in genre.replace(";", "/").replace(",", "/").split("/"): - if s_text in split: - - split = genre_correct(split) - if prefs.sep_genre_multi: - split += "+" - if split in genres: - genres[split] += 3 - else: - temp_results.append([3, split, track, playlist.uuid_int, 0]) - genres[split] = 1 - else: - name = genre_correct(t.genre) - if name in genres: - genres[name] += 3 - else: - temp_results.append([3, name, track, playlist.uuid_int, 0]) - genres[name] = 1 - - if s_text in composer: - - if t.composer in composers: - composers[t.composer] += 2 - else: - temp_results.append([6, t.composer, track, playlist.uuid_int, 0]) - composers[t.composer] = 2 - - if s_text in date: - - year = get_year_from_string(date) - if year: - - if year in years: - years[year] += 1 - else: - temp_results.append([7, year, track, playlist.uuid_int, 0]) - years[year] = 1000 - - if search_magic(s_text, title + artist + filename + album + sartist + album_artist): - - if "artists" in t.misc and t.misc["artists"]: - for a in t.misc["artists"]: - if search_magic(s_text, a.lower()): - - value = 1 - if a.lower().startswith(s_text): - value = 5 - - # Add artist - if a in artists: - artists[a] += value - else: - temp_results.append([0, a, track, playlist.uuid_int, 0]) - artists[a] = value - - if t.album in albums: - albums[t.album] += 1 - else: - temp_results.append([1, t.album, track, playlist.uuid_int, 0]) - albums[t.album] = 1 - - elif search_magic(s_text, artist + sartist): - - value = 1 - if artist.startswith(s_text): - value = 10 - - # Add artist - if t.artist in artists: - artists[t.artist] += value - else: - temp_results.append([0, t.artist, track, playlist.uuid_int, 0]) - artists[t.artist] = value - - if t.album in albums: - albums[t.album] += 1 - else: - temp_results.append([1, t.album, track, playlist.uuid_int, 0]) - albums[t.album] = 1 - - elif search_magic(s_text, album_artist): - - # Add album artist - value = 1 - if t.album_artist.startswith(s_text): - value = 5 - - if t.album_artist in artists: - artists[t.album_artist] += value - else: - temp_results.append([0, t.album_artist, track, playlist.uuid_int, 0]) - artists[t.album_artist] = value - - if t.album in albums: - albums[t.album] += 1 - else: - temp_results.append([1, t.album, track, playlist.uuid_int, 0]) - albums[t.album] = 1 - - if s_text in album: - - value = 1 - if s_text == album: - value = 3 - - if t.album in albums: - albums[t.album] += value - else: - temp_results.append([1, t.album, track, playlist.uuid_int, 0]) - albums[t.album] = value - - if search_magic(s_text, artist + sartist) or search_magic(s_text, album): - - if t.album in albums: - albums[t.album] += 3 - else: - temp_results.append([1, t.album, track, playlist.uuid_int, 0]) - albums[t.album] = 3 - - elif search_magic_any(s_text, artist + sartist) and search_magic_any(s_text, album): - - if t.album in albums: - albums[t.album] += 3 - else: - temp_results.append([1, t.album, track, playlist.uuid_int, 0]) - albums[t.album] = 3 - - if s_text in title: - - if t not in tracks: - - value = 50 - if s_text == title: - value = 200 - - temp_results.append([2, t.title, track, playlist.uuid_int, value]) - - tracks.add(t) - - elif t not in tracks: - temp_results.append([2, t.title, track, playlist.uuid_int, 1]) - - tracks.add(t) - - br += 1 - if br > 800: - time.sleep(0.005) # Throttle thread - br = 0 - if search_over.searched_text != search_over.search_text.text: - break - - search_over.sip = False - search_over.on = 0 - gui.update += 1 - - # Remove results not matching any filter keyword - - if artist_mode: - for i in reversed(range(len(temp_results))): - if temp_results[i][0] != 0: - del temp_results[i] - - elif album_mode: - for i in reversed(range(len(temp_results))): - if temp_results[i][0] != 1: - del temp_results[i] - - elif composer_mode: - for i in reversed(range(len(temp_results))): - if temp_results[i][0] != 6: - del temp_results[i] - - elif year_mode: - for i in reversed(range(len(temp_results))): - if temp_results[i][0] != 7: - del temp_results[i] - - # Sort results by weightings - for i, item in enumerate(temp_results): - if item[0] == 0: - temp_results[i][4] = artists[item[1]] - if item[0] == 1: - temp_results[i][4] = albums[item[1]] - if item[0] == 3: - temp_results[i][4] = genres[item[1]] - if item[0] == 5: - temp_results[i][4] = metas[item[1]] - if not search_over.all_folders: - if metas[item[1]] < 42: - temp_results[i] = None - if item[0] == 6: - temp_results[i][4] = composers[item[1]] - if item[0] == 7: - temp_results[i][4] = years[item[1]] - # 8 is playlists - - temp_results[:] = [item for item in temp_results if item is not None] - search_over.results = sorted(temp_results, key=lambda x: x[4], reverse=True) - #logging.info(search_over.results) - - i = 0 - for playlist in pctl.multi_playlist: - if search_magic(s_text, playlist.title.lower()): - item = [8, playlist.title, None, playlist.uuid_int, 100000] - search_over.results.insert(0, item) - i += 1 - if i > 3: - break - - search_over.on = 0 - search_over.force_select = 0 - #logging.info(perf_timer.get()) - - -def worker1(): - global cue_list - global loaderCommand - global loaderCommandReady - global DA_Formats - global home - global loading_in_progress - global added - global to_get - global to_got - - loaded_pathes_cache = {} - loaded_cue_cache = {} - added = [] - - def get_quoted_from_line(line): - - # Extract quoted or unquoted string from a line - # e.g., 'FILE "01 - Track01.wav" WAVE' or 'TITLE Track01' or "PERFORMER 'Artist Name'" - - parts = line.split(None, 1) - if len(parts) < 2: - return "" - - content = parts[1].strip() - - if content.startswith('"'): - end = content.find('"', 1) - return content[1:end] if end != -1 else content[1:] - if content.startswith("'"): - end = content.find("'", 1) - return content[1:end] if end != -1 else content[1:] - # If not quoted, return the first word - return content.split()[0] - - def add_from_cue(path): - - global added - - if not msys: # Windows terminal doesn't like unicode - logging.info("Reading CUE file: " + path) - - try: - - try: - with open(path, encoding="utf_8") as f: - content = f.readlines() - logging.info("-- Reading as UTF-8") - except Exception: - logging.exception("Failed opening file as UTF-8") - try: - with open(path, encoding="utf_16") as f: - content = f.readlines() - logging.info("-- Reading as UTF-16") - except Exception: - logging.exception("Failed opening file as UTF-16") - try: - j = False - try: - with open(path, encoding="shiftjis") as f: - content = f.readlines() - for line in content: - for c in j_chars: - if c in line: - j = True - logging.info("-- Reading as SHIFT-JIS") - break - except Exception: - logging.exception("Failed opening file as shiftjis") - if not j: - with open(path, encoding="windows-1251") as f: - content = f.readlines() - logging.info("-- Fallback encoding read as windows-1251") - - except Exception: - logging.exception("Abort: Can't detect encoding of CUE file") - return 1 - - f.close() - - # We want to detect if this is a cue sheet that points to either a single file with subtracks, or multiple - # files with mutiple subtracks, but not multiple files that are individual tracks - # i.e, is there really any splitting going on - - files = 0 - files_with_subtracks = 0 - subtrack_count = 0 - for line in content: - if line.startswith("FILE "): - files += 1 - if subtrack_count > 2: # A hack way to avoid non-compliant EAC CUE sheet - files_with_subtracks += 1 - subtrack_count = 0 - elif line.strip().startswith("TRACK "): - subtrack_count += 1 - if subtrack_count > 2: - files_with_subtracks += 1 - - if files == 1: - pass - elif files_with_subtracks > 1: - pass - else: - return 1 - - cue_performer = "" - cue_date = "" - cue_album = "" - cue_genre = "" - cue_main_performer = "" - cue_songwriter = "" - cue_disc = 0 - cue_disc_total = 0 - - cd = [] - cds = [] - - file_name = "" - file_path = "" - - in_header = True - - i = -1 - while True: - i += 1 - - if i > len(content) - 1: - break - - line = content[i].strip() - - if in_header: - if line.startswith("REM "): - line = line[4:] - - if line.startswith("TITLE "): - cue_album = get_quoted_from_line(line) - if line.startswith("PERFORMER "): - cue_performer = get_quoted_from_line(line) - if line.startswith("MAIN PERFORMER "): - cue_main_performer = get_quoted_from_line(line) - if line.startswith("SONGWRITER "): - cue_songwriter = get_quoted_from_line(line) - if line.startswith("GENRE "): - cue_genre = get_quoted_from_line(line) - if line.startswith("DATE "): - cue_date = get_quoted_from_line(line) - if line.startswith("DISCNUMBER "): - cue_disc = get_quoted_from_line(line) - if line.startswith("TOTALDISCS "): - cue_disc_total = get_quoted_from_line(line) - - if line.startswith("FILE "): - in_header = False - else: - continue - - if line.startswith("FILE "): - - if cd: - cds.append(cd) - cd = [] - - file_name = get_quoted_from_line(line) - file_path = os.path.join(os.path.dirname(path), file_name) - - if not os.path.isfile(file_path): - if files == 1: - logging.info("-- The referenced source file wasn't found. Searching for matching file name...") - for item in os.listdir(os.path.dirname(path)): - if os.path.splitext(item)[0] == os.path.splitext(os.path.basename(path))[0]: - if ".cue" not in item.lower() and item.split(".")[-1].lower() in DA_Formats: - file_name = item - file_path = os.path.join(os.path.dirname(path), file_name) - logging.info("-- Source found at: " + file_path) - break - else: - logging.error("-- Abort: Source file not found") - return 1 - else: - logging.error("-- Abort: Source file not found") - return 1 - - if line.startswith("TRACK "): - line = line[6:] - if line.endswith("AUDIO"): - line = line[:-5] - - c = loaded_cue_cache.get((file_path.replace("\\", "/"), int(line.strip()))) - if c is not None: - nt = c - else: - nt = TrackClass() - nt.index = pctl.master_count - pctl.master_count += 1 - - nt.fullpath = file_path - nt.filename = file_name - nt.parent_folder_path = os.path.dirname(file_path.replace("\\", "/")) - nt.parent_folder_name = os.path.splitext(os.path.basename(file_path))[0] - nt.file_ext = os.path.splitext(file_name)[1][1:].upper() - nt.is_cue = True - - nt.album_artist = cue_main_performer - if not cue_main_performer: - nt.album_artist = cue_performer - nt.artist = cue_performer - nt.composer = cue_songwriter - nt.genre = cue_genre - nt.album = cue_album - nt.date = cue_date.replace('"', "") - nt.track_number = int(line.strip()) - if nt.track_number == 1: - nt.size = os.path.getsize(nt.fullpath) - nt.misc["parent-size"] = os.path.getsize(nt.fullpath) - - while True: - i += 1 - if i > len(content) - 1 or content[i].startswith("FILE ") or content[i].strip().startswith( - "TRACK"): - break - - line = content[i] - line = line.strip() - - if line.startswith("TITLE"): - nt.title = get_quoted_from_line(line) - if line.startswith("PERFORMER"): - nt.artist = get_quoted_from_line(line) - if line.startswith("SONGWRITER"): - nt.composer = get_quoted_from_line(line) - if line.startswith("INDEX 01 ") and ":" in line: - line = line[9:] - times = line.split(":") - nt.start_time = int(times[0]) * 60 + int(times[1]) + int(times[2]) / 100 - - i -= 1 - cd.append(nt) - - if cd: - cds.append(cd) - - for cdn, cd in enumerate(cds): - - last_end = None - end_track = TrackClass() - end_track.fullpath = cd[-1].fullpath - tag_scan(end_track) - - # Remove target track if already imported - for i in reversed(range(len(added))): - if pctl.get_track(added[i]).fullpath == end_track.fullpath: - del added[i] - - # Update with proper length - for track in reversed(cd): - - if last_end == None: - last_end = end_track.length - - track.length = last_end - track.start_time - track.samplerate = end_track.samplerate - track.bitrate = end_track.bitrate - track.bit_depth = end_track.bit_depth - track.misc["parent-length"] = end_track.length - last_end = track.start_time - - # inherit missing metadata - if not track.date: - track.date = end_track.date - if not track.album_artist: - track.album_artist = end_track.album_artist - if not track.album: - track.album = end_track.album - if not track.artist: - track.artist = end_track.artist - if not track.genre: - track.genre = end_track.genre - if not track.comment: - track.comment = end_track.comment - if not track.composer: - track.composer = end_track.composer - - if cue_disc: - track.disc_number = cue_disc - elif len(cds) == 0: - track.disc_number = "" - else: - track.disc_number = str(cdn) - - if cue_disc_total: - track.disc_total = cue_disc_total - elif len(cds) == 0: - track.disc_total = "" - else: - track.disc_total = str(len(cds)) - - - # Add all tracks for import to playlist - for cd in cds: - for track in cd: - pctl.master_library[track.index] = track - if track.fullpath not in cue_list: - cue_list.append(track.fullpath) - loaded_pathes_cache[track.fullpath] = track.index - added.append(track.index) - - except Exception: - logging.exception("Internal error processing CUE file") - - def add_file(path, force_scan: bool = False) -> int | None: - # bm.get("add file start") - global DA_Formats - global to_got - - if not os.path.isfile(path): - logging.error("File to import missing") - return 0 - - if os.path.splitext(path)[1][1:] in {"CUE", "cue"}: - add_from_cue(path) - return 0 - - if path.lower().endswith(".xspf"): - logging.info("Found XSPF file at: " + path) - load_xspf(path) - return 0 - - if path.lower().endswith(".m3u") or path.lower().endswith(".m3u8"): - load_m3u(path) - return 0 - - if path.endswith(".pls"): - load_pls(path) - return 0 - - if os.path.splitext(path)[1][1:].lower() not in DA_Formats: - if os.path.splitext(path)[1][1:].lower() in Archive_Formats: - if not prefs.auto_extract: - show_message( - _("You attempted to drop an archive."), - _('However the "extract archive" function is not enabled.'), mode="info") - else: - type = os.path.splitext(path)[1][1:].lower() - split = os.path.splitext(path) - target_dir = split[0] - if prefs.extract_to_music and music_directory is not None: - target_dir = os.path.join(str(music_directory), os.path.basename(target_dir)) - #logging.info(os.path.getsize(path)) - if os.path.getsize(path) > 4e+9: - logging.warning("Archive file is large!") - show_message(_("Skipping oversize zip file (>4GB)")) - return 1 - if not os.path.isdir(target_dir) and not os.path.isfile(target_dir): - if type == "zip": - try: - b = to_got - to_got = "ex" - gui.update += 1 - zip_ref = zipfile.ZipFile(path, "r") - - zip_ref.extractall(target_dir) - zip_ref.close() - except RuntimeError as e: - logging.exception("Zip error") - to_got = b - if "encrypted" in e: - show_message( - _("Failed to extract zip archive."), - _("The archive is encrypted. You'll need to extract it manually with the password."), - mode="warning") - else: - show_message( - _("Failed to extract zip archive."), - _("Maybe archive is corrupted? Does disk have enough space and have write permission?"), - mode="warning") - return 1 - except Exception: - logging.exception("Zip error 2") - to_got = b - show_message( - _("Failed to extract zip archive."), - _("Maybe archive is corrupted? Does disk have enough space and have write permission?"), - mode="warning") - return 1 - - elif type == "rar": - b = to_got - try: - to_got = "ex" - gui.update += 1 - line = launch_prefix + "unrar x -y -p- " + shlex.quote(path) + " " + shlex.quote( - target_dir) + os.sep - result = subprocess.run(shlex.split(line), check=True) - logging.info(result) - except Exception: - logging.exception("Failed to extract rar archive.") - to_got = b - show_message(_("Failed to extract rar archive."), mode="warning") - - return 1 - - elif type == "7z": - b = to_got - try: - to_got = "ex" - gui.update += 1 - line = launch_prefix + "7z x -y " + shlex.quote(path) + " -o" + shlex.quote( - target_dir) + os.sep - result = subprocess.run(shlex.split(line), check=True) - logging.info(result) - except Exception: - logging.exception("Failed to extract 7z archive.") - to_got = b - show_message(_("Failed to extract 7z archive."), mode="warning") - - return 1 - - upper = os.path.dirname(target_dir) - cont = os.listdir(target_dir) - new = upper + "/temporaryfolderd" - error = False - if len(cont) == 1 and os.path.isdir(split[0] + "/" + cont[0]): - logging.info("one thing") - os.rename(target_dir, new) - try: - shutil.move(new + "/" + cont[0], upper) - except Exception: - logging.exception("Could not move file") - error = True - shutil.rmtree(new) - logging.info(new) - target_dir = upper + "/" + cont[0] - if not os.path.isdir(target_dir): - logging.error("Extract error, expected directory not found") - - if True and not error and prefs.auto_del_zip: - logging.info("Moving archive file to trash: " + path) - try: - send2trash(path) - except Exception: - logging.exception("Could not move archive to trash") - show_message(_("Could not move archive to trash"), path, mode="info") - - to_got = b - gets(target_dir) - quick_import_done.append(target_dir) - # gets(target_dir) - - return 1 - - to_got += 1 - gui.update = 1 - - path = path.replace("\\", "/") - - if path in loaded_pathes_cache: - de = loaded_pathes_cache[path] - - if pctl.master_library[de].fullpath in cue_list: - logging.info("File has an associated .cue file... Skipping") - return None - - if pctl.master_library[de].file_ext.lower() in GME_Formats: - # Skip cache for subtrack formats - pass - else: - added.append(de) - return None - - time.sleep(0.002) - - # audio = auto.File(path) - - nt = TrackClass() - - nt.index = pctl.master_count - set_path(nt, path) - - def commit_track(nt): - pctl.master_library[pctl.master_count] = nt - added.append(pctl.master_count) - - if prefs.auto_sort or force_scan: - tag_scan(nt) - else: - after_scan.append(nt) - tauon.thread_manager.ready("worker") - - pctl.master_count += 1 - - # nt = tag_scan(nt) - if nt.cue_sheet != "": - tag_scan(nt) - cue_scan(nt.cue_sheet, nt) - del nt - - elif nt.file_ext.lower() in GME_Formats and gme: - - emu = ctypes.c_void_p() - err = gme.gme_open_file(nt.fullpath.encode("utf-8"), ctypes.byref(emu), -1) - if not err: - n = gme.gme_track_count(emu) - for i in range(n): - nt = TrackClass() - set_path(nt, path) - nt.index = pctl.master_count - nt.subtrack = i - commit_track(nt) - - gme.gme_delete(emu) - - else: - - commit_track(nt) - - # bm.get("fill entry") - if gui.auto_play_import: - pctl.jump(pctl.master_count - 1) - gui.auto_play_import = False - - # Count the approx number of files to be imported - def pre_get(direc): - - global to_get - - to_get = 0 - for root, dirs, files in os.walk(direc): - to_get += len(files) - if gui.im_cancel: - return - gui.update = 3 - - def gets(direc, force_scan=False): - - global DA_Formats - - if os.path.basename(direc) == "__MACOSX": - return - - try: - items_in_dir = os.listdir(direc) - if use_natsort: - items_in_dir = natsort.os_sorted(items_in_dir) - else: - items_in_dir.sort() - except PermissionError: - logging.exception("Permission error accessing one or more files") - if snap_mode: - show_message( - _("Permission error accessing one or more files."), - _("If this location is on external media, see https://") + "github.com/Taiko2k/TauonMusicBox/wiki/Snap-Permissions", - mode="bubble") - else: - show_message(_("Permission error accessing one or more files"), mode="warning") - - return - except Exception: - logging.exception("Unknown error accessing one or more files") - return - - for q in range(len(items_in_dir)): - if items_in_dir[q][0] == ".": - continue - if os.path.isdir(os.path.join(direc, items_in_dir[q])): - gets(os.path.join(direc, items_in_dir[q])) - if gui.im_cancel: - return - - for q in range(len(items_in_dir)): - if items_in_dir[q][0] == ".": - continue - if os.path.isdir(os.path.join(direc, items_in_dir[q])) is False: - - if os.path.splitext(items_in_dir[q])[1][1:].lower() in DA_Formats: - - if len(items_in_dir[q]) > 2 and items_in_dir[q][0:2] == "._": - continue - - add_file(os.path.join(direc, items_in_dir[q]).replace("\\", "/"), force_scan) - - elif os.path.splitext(items_in_dir[q])[1][1:] in {"CUE", "cue"}: - add_from_cue(os.path.join(direc, items_in_dir[q]).replace("\\", "/")) - - if gui.im_cancel: - return - - def cache_paths(): - dic = {} - dic2 = {} - for key, value in pctl.master_library.items(): - if value.is_network: - continue - dic[value.fullpath.replace("\\", "/")] = key - if value.is_cue: - dic2[(value.fullpath.replace("\\", "/"), value.track_number)] = value - return dic, dic2 - - - #logging.info(pctl.master_library) - - global transcode_list - global transcode_state - global album_art_gen - global cm_clean_db - global to_got - global to_get - global move_in_progress - - active_timer = Timer() - while True: - - if not after_scan: - time.sleep(0.1) - - if after_scan or load_orders or \ - artist_list_box.load or \ - artist_list_box.to_fetch or \ - gui.regen_single_id or \ - gui.regen_single > -1 or \ - pctl.after_import_flag or \ - tauon.worker_save_state or \ - move_jobs or \ - cm_clean_db or \ - transcode_list or \ - to_scan or \ - loaderCommandReady: - active_timer.set() - elif active_timer.get() > 5: - return - - if after_scan: - i = 0 - while after_scan: - i += 1 - - if i > 123: - break - - tag_scan(after_scan[0]) - - gui.update = 2 - gui.pl_update = 1 - # time.sleep(0.001) - if pctl.running: - del after_scan[0] - else: - break - - album_artist_dict.clear() - - artist_list_box.worker() - - # Update smart playlists - if gui.regen_single_id is not None: - regenerate_playlist(pl=-1, silent=True, id=gui.regen_single_id) - gui.regen_single_id = None - - # Update smart playlists - if gui.regen_single > -1: - target = gui.regen_single - gui.regen_single = -1 - regenerate_playlist(target, silent=True) - - if pctl.after_import_flag and not after_scan and not search_over.active and not loading_in_progress: - pctl.after_import_flag = False - - for i, plist in enumerate(pctl.multi_playlist): - if pl_to_id(i) in pctl.gen_codes: - code = pctl.gen_codes[pl_to_id(i)] - try: - if check_auto_update_okay(code, pl=i): - if not pl_is_locked(i): - logging.info("Reloading smart playlist: " + plist.title) - regenerate_playlist(i, silent=True) - time.sleep(0.02) - except Exception: - logging.exception("Failed to handle playlist") - - tree_view_box.clear_all() - - if tauon.worker_save_state and \ - not gui.pl_pulse and \ - not loading_in_progress and \ - not to_scan and not after_scan and \ - not plex.scanning and \ - not jellyfin.scanning and \ - not cm_clean_db and \ - not lastfm.scanning_friends and \ - not move_in_progress and \ - (gui.lowered or not window_is_focused() or not gui.mouse_in_window): - save_state() - cue_list.clear() - tauon.worker_save_state = False - - # Folder moving - if len(move_jobs) > 0: - gui.update += 1 - move_in_progress = True - job = move_jobs[0] - del move_jobs[0] - - if job[0].strip("\\/") == job[1].strip("\\/"): - show_message(_("Folder copy error."), _("The target and source are the same."), mode="info") - gui.update += 1 - move_in_progress = False - continue - - try: - shutil.copytree(job[0], job[1]) - except Exception: - logging.exception("Failed to copy directory") - move_in_progress = False - gui.update += 1 - show_message(_("The folder copy has failed!"), _("Some files may have been written."), mode="warning") - continue - - if job[2] == True: - try: - shutil.rmtree(job[0]) - - except Exception: - logging.exception("Failed to delete directory") - show_message(_("Something has gone horribly wrong!"), _("Could not delete {name}").format(name=job[0]), mode="error") - gui.update += 1 - move_in_progress = False - return - - show_message(_("Folder move complete."), _("Folder name: {name}").format(name=job[3]), mode="done") - else: - show_message(_("Folder copy complete."), _("Folder name: {name}").format(name=job[3]), mode="done") - - move_in_progress = False - load_orders.append(job[4]) - gui.update += 1 - - # Clean database - if cm_clean_db is True: - items_removed = 0 - - # old_db = copy.deepcopy(pctl.master_library) - to_got = 0 - to_get = len(pctl.master_library) - search_over.results.clear() - - keys = set(pctl.master_library.keys()) - for index in keys: - time.sleep(0.0001) - track = pctl.master_library[index] - to_got += 1 - - if to_got % 100 == 0: - gui.update = 1 - - if not prefs.remove_network_tracks and track.file_ext == "SPTY": - - for playlist in pctl.multi_playlist: - if index in playlist.playlist_ids: - break - else: - pctl.purge_track(index) - items_removed += 1 - - continue - - if (prefs.remove_network_tracks is False and not track.is_network and not os.path.isfile( - track.fullpath)) or \ - (prefs.remove_network_tracks is True and track.is_network): - - if track.is_network and track.file_ext == "SPTY": - continue - - pctl.purge_track(index) - items_removed += 1 - - cm_clean_db = False - show_message( - _("Cleaning complete."), - _("{N} items were removed from the database.").format(N=str(items_removed)), mode="done") - if album_mode: - reload_albums(True) - if gui.combo_mode: - reload_albums() - - gui.update = 1 - gui.pl_update = 1 - pctl.notify_change() - - search_dia_string_cache.clear() - search_string_cache.clear() - search_over.results.clear() - - pctl.notify_change() - - # FOLDER ENC - if transcode_list: - - try: - transcode_state = "" - gui.update += 1 - - folder_items = transcode_list[0] - - ref_track_object = pctl.master_library[folder_items[0]] - ref_album = ref_track_object.album - - # Generate a folder name based on artist and album of first track in batch - folder_name = encode_folder_name(ref_track_object) - - # If folder contains tracks from multiple albums, use original folder name instead - for item in folder_items: - test_object = pctl.master_library[item] - if test_object.album != ref_album: - folder_name = ref_track_object.parent_folder_name - break - - logging.info("Transcoding folder: " + folder_name) - - # Remove any existing matching folder - if (prefs.encoder_output / folder_name).is_dir(): - shutil.rmtree(prefs.encoder_output / folder_name) - - # Create new empty folder to output tracks to - (prefs.encoder_output / folder_name).mkdir(parents=True) - - full_wav_out_p = prefs.encoder_output / "output.wav" - full_target_out_p = prefs.encoder_output / ("output." + prefs.transcode_codec) - if full_wav_out_p.is_file(): - full_wav_out_p.unlink() - if full_target_out_p.is_file(): - full_target_out_p.unlink() - - cache_dir = tmp_cache_dir() - if not os.path.isdir(cache_dir): - os.makedirs(cache_dir) - - if prefs.transcode_codec in ("opus", "ogg", "flac", "mp3"): - global core_use - cores = os.cpu_count() - - total = len(folder_items) - gui.transcoding_batch_total = total - gui.transcoding_bach_done = 0 - dones = [] - - q = 0 - while True: - if core_use < cores and q < len(folder_items): - agg = [[folder_items[q], folder_name]] - if agg not in dones: - core_use += 1 - dones.append(agg) - loaderThread = threading.Thread(target=transcode_single, args=agg) - loaderThread.daemon = True - loaderThread.start() - - q += 1 - gui.update += 1 - time.sleep(0.05) - if gui.tc_cancel: - while core_use > 0: - time.sleep(1) - break - if q == len(folder_items) and core_use == 0: - gui.update += 1 - break - - else: - logging.error("Codec error") - - output_dir = prefs.encoder_output / folder_name - if prefs.transcode_inplace: - try: - output_dir.unlink() - except Exception: - logging.exception("Encode folder not removed") - reload_metadata(folder_items[0]) - else: - album_art_gen.save_thumb(pctl.get_track(folder_items[0]), (1080, 1080), str(output_dir / "cover")) - - #logging.info(transcode_list[0]) - - del transcode_list[0] - transcode_state = "" - gui.update += 1 - - except Exception: - logging.exception("Transcode failed") - transcode_state = "Transcode Error" - time.sleep(0.2) - show_message(_("Transcode failed."), _("An error was encountered."), mode="error") - gui.update += 1 - time.sleep(0.1) - del transcode_list[0] - - if len(transcode_list) == 0: - if gui.tc_cancel: - gui.tc_cancel = False - show_message( - _("The transcode was canceled before completion."), - _("Incomplete files will remain."), - mode="warning") - else: - line = _("Press F9 to show output.") - if prefs.transcode_codec == "flac": - line = _("Note that any associated output picture is a thumbnail and not an exact copy.") - if not gui.sync_progress: - if not gui.message_box: - show_message(_("Encoding complete."), line, mode="done") - if system == "Linux" and de_notify_support: - g_tc_notify.show() - - if to_scan: - while to_scan: - track = to_scan[0] - star = star_store.full_get(track) - star_store.remove(track) - pctl.master_library[track] = tag_scan(pctl.master_library[track]) - star_store.merge(track, star) - lastfm.sync_pull_love(pctl.master_library[track]) - del to_scan[0] - gui.update += 1 - album_artist_dict.clear() - pctl.notify_change() - gui.pl_update += 1 - - if loaderCommandReady is True: - for order in load_orders: - if order.stage == 1: - if loaderCommand == LC_Folder: - to_get = 0 - to_got = 0 - loaded_pathes_cache, loaded_cue_cache = cache_paths() - # pre_get(order.target) - if order.force_scan: - gets(order.target, force_scan=True) - else: - gets(order.target) - elif loaderCommand == LC_File: - loaded_pathes_cache, loaded_cue_cache = cache_paths() - add_file(order.target) - - if gui.im_cancel: - gui.im_cancel = False - to_get = 0 - to_got = 0 - load_orders.clear() - added = [] - loaderCommand = LC_Done - loaderCommandReady = False - break - - loaderCommand = LC_Done - #logging.info("LOAD ORDER") - order.tracks = added - - # Double check for cue dupes - for i in reversed(range(len(order.tracks))): - if pctl.master_library[order.tracks[i]].fullpath in cue_list: - if pctl.master_library[order.tracks[i]].is_cue is False: - del order.tracks[i] - - added = [] - order.stage = 2 - loaderCommandReady = False - #logging.info("DONE LOADING") - break - - -album_info_cache = {} -perfs = [] -album_info_cache_key = (-1, -1) - - -def get_album_info(position, pl: int | None = None): - - playlist = default_playlist - if pl is not None: - playlist = pctl.multi_playlist[pl].playlist_ids - - global album_info_cache_key - - if album_info_cache_key != (pctl.selected_in_playlist, pctl.playing_object()): # Premature optimisation? - album_info_cache.clear() - album_info_cache_key = (pctl.selected_in_playlist, pctl.playing_object()) - - if position in album_info_cache: - return album_info_cache[position] - - if album_dex and album_mode and (pl is None or pl == pctl.active_playlist_viewing): - dex = album_dex - else: - dex = reload_albums(custom_list=playlist) - - end = len(playlist) - start = 0 - - for i, p in enumerate(reversed(dex)): - if p <= position: - start = p - break - end = p - - album = list(range(start, end)) - - playing = 0 - select = False - - if pctl.selected_in_playlist in album: - select = True - - if len(pctl.track_queue) > 0 and p < len(playlist): - if pctl.track_queue[pctl.queue_step] in playlist[start:end]: - playing = 1 - - album_info_cache[position] = playing, album, select - return playing, album, select - - -tauon.get_album_info = get_album_info - - -def get_folder_list(index: int): - playlist = [] - - for item in default_playlist: - if pctl.master_library[item].parent_folder_name == pctl.master_library[index].parent_folder_name and \ - pctl.master_library[item].album == pctl.master_library[index].album: - playlist.append(item) - return list(set(playlist)) - - -def gal_jump_select(up=False, num=1): - - old_selected = pctl.selected_in_playlist - old_num = num - - if not default_playlist: - return - - on = pctl.selected_in_playlist - if on > len(default_playlist) - 1: - on = 0 - pctl.selected_in_playlist = 0 - - if up is False: - - while num > 0: - while pctl.master_library[ - default_playlist[on]].parent_folder_name == pctl.master_library[ - default_playlist[pctl.selected_in_playlist]].parent_folder_name: - on += 1 - - if on > len(default_playlist) - 1: - pctl.selected_in_playlist = old_selected - return - - pctl.selected_in_playlist = on - num -= 1 - else: - - if num > 1: - if pctl.selected_in_playlist > len(default_playlist) - 1: - pctl.selected_in_playlist = old_selected - return - - alb = get_album_info(pctl.selected_in_playlist) - if alb[1][0] in album_dex[:num]: - pctl.selected_in_playlist = old_selected - return - - while num > 0: - alb = get_album_info(pctl.selected_in_playlist) - - if alb[1][0] > -1: - on = alb[1][0] - 1 - - pctl.selected_in_playlist = max(get_album_info(on)[1][0], 0) - num -= 1 - - -power_tag_colours = ColourGenCache(0.5, 0.8) - - -class PowerTag: - - def __init__(self): - self.name = "BLANK" - self.path = "" - self.position = 0 - self.colour = None - - self.peak_x = 0 - self.ani_timer = Timer() - self.ani_timer.force_set(10) - - -gui.pt_on = Timer() -gui.pt_off = Timer() -gui.pt = 0 - - -def gen_power2(): - tags = {} # [tag name]: (first position, number of times we saw it) - tag_list = [] - - last = "a" - noise = 0 - - def key(tag): - return tags[tag][1] - - for position in album_dex: - - index = default_playlist[position] - track = pctl.get_track(index) - - crumbs = track.parent_folder_path.split("/") - - for i, b in enumerate(crumbs): - - if i > 0 and (track.artist in b and track.artist): - tag = crumbs[i - 1] - - if tag != last: - noise += 1 - last = tag - - if tag in tags: - tags[tag][1] += 1 - else: - tags[tag] = [position, 1, "/".join(crumbs[:i])] - tag_list.append(tag) - break - - if noise > len(album_dex) / 2: - #logging.info("Playlist is too noisy for power bar.") - return [] - - tag_list_sort = sorted(tag_list, key=key, reverse=True) - - max_tags = round((window_size[1] - gui.panelY - gui.panelBY - 10) // 30 * gui.scale) - - tag_list_sort = tag_list_sort[:max_tags] - - for i in reversed(range(len(tag_list))): - if tag_list[i] not in tag_list_sort: - del tag_list[i] - - h = [] - - for tag in tag_list: - - if tags[tag][1] > 2: - t = PowerTag() - t.path = tags[tag][2] - t.name = tag.upper() - t.position = tags[tag][0] - h.append(t) - - cc = random.random() - cj = 0.03 - if len(h) < 5: - cj = 0.11 - - cj = 0.5 / max(len(h), 2) - - for item in h: - item.colour = hsl_to_rgb(cc, 0.8, 0.7) - cc += cj - - return h - - -def reload_albums(quiet: bool = False, return_playlist: int = -1, custom_list=None) -> list[int] | None: - global album_dex - global update_layout - global old_album_pos - - if cm_clean_db: - # Doing reload while things are being removed may cause crash - return None - - dex = [] - current_folder = "" - current_album = "" - current_artist = "" - current_date = "" - current_title = "" - - if custom_list is not None: - playlist = custom_list - else: - target_pl_no = pctl.active_playlist_viewing - if return_playlist > -1: - target_pl_no = return_playlist - - playlist = pctl.multi_playlist[target_pl_no].playlist_ids - - for i in range(len(playlist)): - tr = pctl.master_library[playlist[i]] - - split = False - if i == 0: - split = True - elif tr.parent_folder_path != current_folder and tr.date and tr.date != current_date: - split = True - elif prefs.gallery_combine_disc and "Disc" in tr.album and "Disc" in current_album and tr.album.split("Disc")[0].rstrip(" ") == current_album.split("Disc")[0].rstrip(" "): - split = False - elif prefs.gallery_combine_disc and "CD" in tr.album and "CD" in current_album and tr.album.split("CD")[0].rstrip() == current_album.split("CD")[0].rstrip(): - split = False - elif prefs.gallery_combine_disc and "cd" in tr.album and "cd" in current_album and tr.album.split("cd")[0].rstrip() == current_album.split("cd")[0].rstrip(): - split = False - elif tr.album and tr.album == current_album and prefs.gallery_combine_disc: - split = False - elif tr.parent_folder_path != current_folder or current_title != tr.parent_folder_name: - split = True - - if split: - dex.append(i) - current_folder = tr.parent_folder_path - current_title = tr.parent_folder_name - current_album = tr.album - current_date = tr.date - current_artist = tr.artist - - if return_playlist > -1 or custom_list: - return dex - - album_dex = dex - album_info_cache.clear() - gui.update += 2 - gui.pl_update = 1 - update_layout = True - - if not quiet: - goto_album(pctl.playlist_playing_position) - - # Generate POWER BAR - gui.power_bar = gen_power2() - gui.pt = 0 - - -tauon.reload_albums = reload_albums - -# ------------------------------------------------------------------------------------ -# WEBSERVER -if prefs.enable_web is True: - webThread = threading.Thread( - target=webserve, args=[pctl, prefs, gui, album_art_gen, str(install_directory), strings, tauon]) - webThread.daemon = True - webThread.start() - -ctlThread = threading.Thread(target=controller, args=[tauon]) -ctlThread.daemon = True -ctlThread.start() - -if prefs.enable_remote: - tauon.start_remote() - tauon.remote_limited = False - - -# -------------------------------------------------------------- - -def star_line_toggle(mode: int= 0) -> bool | None: - if mode == 1: - return gui.star_mode == "line" - - if gui.star_mode == "line": - gui.star_mode = "none" - else: - gui.star_mode = "line" - - gui.show_ratings = False - - gui.update += 1 - gui.pl_update = 1 - return None - - -def star_toggle(mode: int = 0) -> bool | None: - if gui.show_ratings: - if mode == 1: - return prefs.rating_playtime_stars - prefs.rating_playtime_stars ^= True - - else: - if mode == 1: - return gui.star_mode == "star" - - if gui.star_mode == "star": - gui.star_mode = "none" - else: - gui.star_mode = "star" - - # gui.show_ratings = False - gui.update += 1 - gui.pl_update = 1 - return None - -def heart_toggle(mode: int = 0) -> bool | None: - if mode == 1: - return gui.show_hearts - - gui.show_hearts ^= True - # gui.show_ratings = False - - gui.update += 1 - gui.pl_update = 1 - return None - - -def album_rating_toggle(mode: int = 0) -> bool | None: - if mode == 1: - return gui.show_album_ratings - - gui.show_album_ratings ^= True - - gui.update += 1 - gui.pl_update = 1 - return None - - -def rating_toggle(mode: int = 0) -> bool | None: - if mode == 1: - return gui.show_ratings - - gui.show_ratings ^= True - - if gui.show_ratings: - # gui.show_hearts = False - gui.star_mode = "none" - prefs.rating_playtime_stars = True - if not prefs.write_ratings: - show_message(_("Note that ratings are stored in the local database and not written to tags.")) - - gui.update += 1 - gui.pl_update = 1 - return None - - -def toggle_titlebar_line(mode: int = 0) -> bool | None: - global update_title - if mode == 1: - return update_title - - line = window_title - SDL_SetWindowTitle(t_window, line) - update_title ^= True - if update_title: - update_title_do() - return None - - -def toggle_meta_persists_stop(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.meta_persists_stop - prefs.meta_persists_stop ^= True - return None - - -def toggle_side_panel_layout(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.side_panel_layout == 1 - - if prefs.side_panel_layout == 1: - prefs.side_panel_layout = 0 - else: - prefs.side_panel_layout = 1 - return None - - -def toggle_meta_shows_selected(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.meta_shows_selected_always - prefs.meta_shows_selected_always ^= True - return None - - -def scale1(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.ui_scale == 1: - return True - return False - - prefs.ui_scale = 1 - pref_box.large_preset() - - if prefs.ui_scale != gui.scale: - show_message(_("Change will be applied on restart.")) - return None - - -def scale125(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.ui_scale == 1.25: - return True - return False - return None - - prefs.ui_scale = 1.25 - pref_box.large_preset() - - if prefs.ui_scale != gui.scale: - show_message(_("Change will be applied on restart.")) - return None - - -def toggle_use_tray(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.use_tray - prefs.use_tray ^= True - if not prefs.use_tray: - prefs.min_to_tray = False - gnome.hide_indicator() - else: - gnome.show_indicator() - return None - - -def toggle_text_tray(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.tray_show_title - prefs.tray_show_title ^= True - pctl.notify_update() - return None - - -def toggle_min_tray(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.min_to_tray - prefs.min_to_tray ^= True - return None - - -def scale2(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.ui_scale == 2: - return True - return False - - prefs.ui_scale = 2 - pref_box.large_preset() - - if prefs.ui_scale != gui.scale: - show_message(_("Change will be applied on restart.")) - return None - - -def toggle_borderless(mode: int = 0) -> bool | None: - global draw_border - global update_layout - - if mode == 1: - return draw_border - - update_layout = True - draw_border ^= True - - if draw_border: - SDL_SetWindowBordered(t_window, False) - else: - SDL_SetWindowBordered(t_window, True) - return None - - -def toggle_break(mode: int = 0) -> bool | None: - global break_enable - if mode == 1: - return break_enable ^ True - break_enable ^= True - gui.pl_update = 1 - return None - - -def toggle_scroll(mode: int = 0) -> bool | None: - global scroll_enable - global update_layout - - if mode == 1: - if scroll_enable: - return False - return True - - scroll_enable ^= True - gui.pl_update = 1 - update_layout = True - return None - - -def toggle_hide_bar(mode: int = 0) -> bool | None: - if mode == 1: - return gui.set_bar ^ True - gui.update_layout() - gui.set_bar ^= True - show_message(_("Tip: You can also toggle this from a right-click context menu")) - return None - - -def toggle_append_total_time(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.append_total_time - prefs.append_total_time ^= True - gui.pl_update = 1 - gui.update += 1 - return None - - -def toggle_append_date(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.append_date - prefs.append_date ^= True - gui.pl_update = 1 - gui.update += 1 - return None - - -def toggle_true_shuffle(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.true_shuffle - prefs.true_shuffle ^= True - return None - - -def toggle_auto_artist_dl(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.auto_dl_artist_data - prefs.auto_dl_artist_data ^= True - for artist, value in list(artist_list_box.thumb_cache.items()): - if value is None: - del artist_list_box.thumb_cache[artist] - return None - - -def toggle_enable_web(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.enable_web - - prefs.enable_web ^= True - - if prefs.enable_web and not gui.web_running: - webThread = threading.Thread( - target=webserve, args=[pctl, prefs, gui, album_art_gen, str(install_directory), strings, tauon]) - webThread.daemon = True - webThread.start() - show_message(_("Web server starting"), _("External connections will be accepted."), mode="done") - - elif prefs.enable_web is False: - if tauon.radio_server is not None: - tauon.radio_server.shutdown() - gui.web_running = False - - time.sleep(0.25) - return None - - -def toggle_scrobble_mark(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.scrobble_mark - prefs.scrobble_mark ^= True - return None - - -def toggle_lfm_auto(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.auto_lfm - prefs.auto_lfm ^= True - if prefs.auto_lfm and not last_fm_enable: - show_message(_("Optional module python-pylast not installed"), mode="warning") - prefs.auto_lfm = False - # if prefs.auto_lfm: - # lastfm.hold = False - # else: - # lastfm.hold = True - return None - - -def toggle_lb(mode: int = 0) -> bool | None: - if mode == 1: - return lb.enable - if not lb.enable and not prefs.lb_token: - show_message(_("Can't enable this if there's no token."), mode="warning") - return None - lb.enable ^= True - return None - - -def toggle_maloja(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.maloja_enable - if not prefs.maloja_url or not prefs.maloja_key: - show_message(_("One or more fields is missing."), mode="warning") - return None - prefs.maloja_enable ^= True - return None - - -def toggle_ex_del(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.auto_del_zip - prefs.auto_del_zip ^= True - # if prefs.auto_del_zip is True: - # show_message("Caution! This function deletes things!", mode='info', "This could result in data loss if the process were to malfunction.") - return None - - -def toggle_dl_mon(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.monitor_downloads - prefs.monitor_downloads ^= True - return None - - -def toggle_music_ex(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.extract_to_music - prefs.extract_to_music ^= True - return None - - -def toggle_extract(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.auto_extract - prefs.auto_extract ^= True - if prefs.auto_extract is False: - prefs.auto_del_zip = False - return None - - -def toggle_top_tabs(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.tabs_on_top - prefs.tabs_on_top ^= True - return None - - -#def toggle_guitar_chords(mode: int = 0) -> bool | None: -# if mode == 1: -# return prefs.guitar_chords -# prefs.guitar_chords ^= True -# return None - - -# def toggle_auto_lyrics(mode: int = 0) -> bool | None: -# if mode == 1: -# return prefs.auto_lyrics -# prefs.auto_lyrics ^= True - - -def switch_single(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.transcode_mode == "single": - return True - return False - prefs.transcode_mode = "single" - return None - - -def switch_mp3(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.transcode_codec == "mp3": - return True - return False - prefs.transcode_codec = "mp3" - return None - - -def switch_ogg(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.transcode_codec == "ogg": - return True - return False - prefs.transcode_codec = "ogg" - return None - - -def switch_opus(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.transcode_codec == "opus": - return True - return False - prefs.transcode_codec = "opus" - return None - - -def switch_opus_ogg(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.transcode_opus_as: - return True - return False - prefs.transcode_opus_as ^= True - return None - - -def toggle_transcode_output(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.transcode_inplace: - return False - return True - prefs.transcode_inplace ^= True - if prefs.transcode_inplace: - transcode_icon.colour = [250, 20, 20, 255] - show_message( - _("DANGER! This will delete the original files. Keeping a backup is recommended in case of malfunction."), - _("For safety, this setting will default to off. Embedded thumbnails are not kept so you may want to extract them first."), - mode="warning") - else: - transcode_icon.colour = [239, 74, 157, 255] - return None - - -def toggle_transcode_inplace(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.transcode_inplace: - return True - return False - - if gui.sync_progress: - prefs.transcode_inplace = False - return None - - prefs.transcode_inplace ^= True - if prefs.transcode_inplace: - transcode_icon.colour = [250, 20, 20, 255] - show_message( - _("DANGER! This will delete the original files. Keeping a backup is recommended in case of malfunction."), - _("For safety, this setting will reset on restart. Embedded thumbnails are not kept so you may want to extract them first."), - mode="warning") - else: - transcode_icon.colour = [239, 74, 157, 255] - return None - - -def switch_flac(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.transcode_codec == "flac": - return True - return False - prefs.transcode_codec = "flac" - return None - - -def toggle_sbt(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.prefer_bottom_title - prefs.prefer_bottom_title ^= True - return None - - -def toggle_bba(mode: int = 0) -> bool | None: - if mode == 1: - return gui.bb_show_art - gui.bb_show_art ^= True - gui.update_layout() - return None - - -def toggle_use_title(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.use_title - prefs.use_title ^= True - return None - - -def switch_rg_off(mode: int = 0) -> bool | None: - if mode == 1: - return True if prefs.replay_gain == 0 else False - prefs.replay_gain = 0 - return None - - -def switch_rg_track(mode: int = 0) -> bool | None: - if mode == 1: - return True if prefs.replay_gain == 1 else False - prefs.replay_gain = 0 if prefs.replay_gain == 1 else 1 - # prefs.replay_gain = 1 - return None - - -def switch_rg_album(mode: int = 0) -> bool | None: - if mode == 1: - return True if prefs.replay_gain == 2 else False - prefs.replay_gain = 0 if prefs.replay_gain == 2 else 2 - return None - - -def switch_rg_auto(mode: int = 0) -> bool | None: - if mode == 1: - return True if prefs.replay_gain == 3 else False - prefs.replay_gain = 0 if prefs.replay_gain == 3 else 3 - return None - - -def toggle_jump_crossfade(mode: int = 0) -> bool | None: - if mode == 1: - return True if prefs.use_jump_crossfade else False - prefs.use_jump_crossfade ^= True - return None - - -def toggle_pause_fade(mode: int = 0) -> bool | None: - if mode == 1: - return True if prefs.use_pause_fade else False - prefs.use_pause_fade ^= True - return None - - -def toggle_transition_crossfade(mode: int = 0) -> bool | None: - if mode == 1: - return True if prefs.use_transition_crossfade else False - prefs.use_transition_crossfade ^= True - return None - - -def toggle_transition_gapless(mode: int = 0) -> bool | None: - if mode == 1: - return False if prefs.use_transition_crossfade else True - prefs.use_transition_crossfade ^= True - return None - - -def toggle_eq(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.use_eq - prefs.use_eq ^= True - pctl.playerCommand = "seteq" - pctl.playerCommandReady = True - return None - - -key_shiftr_down = False -key_ctrl_down = False -key_rctrl_down = False -key_meta = False -key_ralt = False -key_lalt = False - - -def reload_backend() -> None: - gui.backend_reloading = True - logging.info("Reload backend...") - wait = 0 - pre_state = pctl.stop(True) - - while pctl.playerCommandReady: - time.sleep(0.01) - wait += 1 - if wait > 20: - break - if tauon.thread_manager.player_lock.locked(): - try: - tauon.thread_manager.player_lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked player_lock") - else: - logging.exception("Unknown RuntimeError trying to release player_lock") - except Exception: - logging.exception("Unknown error trying to release player_lock") - - pctl.playerCommand = "unload" - pctl.playerCommandReady = True - - wait = 0 - while pctl.playerCommand != "done": - time.sleep(0.01) - wait += 1 - if wait > 200: - break - - tauon.thread_manager.ready_playback() - - if pre_state == 1: - pctl.revert() - gui.backend_reloading = False - - - -def gen_chart() -> None: - try: - - topchart = t_topchart.TopChart(tauon, album_art_gen) - - tracks = [] - - source_tracks = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - - if prefs.topchart_sorts_played: - source_tracks = gen_folder_top(0, custom_list=source_tracks) - dex = reload_albums(quiet=True, custom_list=source_tracks) - else: - dex = reload_albums(quiet=True, return_playlist=pctl.active_playlist_viewing) - - for item in dex: - tracks.append(pctl.get_track(source_tracks[item])) - - cascade = False - if prefs.chart_cascade: - cascade = ( - (prefs.chart_c1, prefs.chart_c2, prefs.chart_c3), - (prefs.chart_d1, prefs.chart_d2, prefs.chart_d3)) - - path = topchart.generate( - tracks, prefs.chart_bg, prefs.chart_rows, prefs.chart_columns, prefs.chart_text, - prefs.chart_font, prefs.chart_tile, cascade) - - except Exception: - logging.exception("There was an error generating the chart") - gui.generating_chart = False - show_message(_("There was an error generating the chart"), _("Sorry!"), mode="error") - return - - gui.generating_chart = False - - if path: - open_file(path) - else: - show_message(_("There was an error generating the chart"), _("Sorry!"), mode="error") - return - - show_message(_("Chart generated"), mode="done") - - -class Over: - def __init__(self): - - global window_size - - self.init2done = False - - self.about_image = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-a.png") - self.about_image2 = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-b.png") - self.about_image3 = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-c.png") - self.about_image4 = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-d.png") - self.about_image5 = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-e.png") - self.about_image6 = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-f.png") - self.title_image = asset_loader(scaled_asset_directory, loaded_asset_dc, "title.png", True) - - # self.tab_width = round(115 * gui.scale) - self.w = 100 - self.h = 100 - - self.box_x = 100 - self.box_y = 100 - self.item_x_offset = round(25 * gui.scale) - - self.current_path = os.path.expanduser("~") - self.view_offset = 0 - self.ext_ratio = {} - self.last_db_size = -1 - - self.enabled = False - self.click = False - self.right_click = False - self.scroll = 0 - self.lock = False - - self.drives = [] - - self.temp_lastfm_user = "" - self.temp_lastfm_pass = "" - self.lastfm_input_box = 0 - - self.func_page = 0 - self.tab_active = 0 - self.tabs = [ - [_("Function"), self.funcs], - [_("Audio"), self.audio], - [_("Tracklist"), self.config_v], - [_("Theme"), self.theme], - [_("Window"), self.config_b], - [_("View"), self.view2], - [_("Transcode"), self.codec_config], - [_("Lyrics"), self.lyrics], - [_("Accounts"), self.last_fm_box], - [_("Stats"), self.stats], - [_("About"), self.about], - ] - - self.stats_timer = Timer() - self.stats_timer.force_set(1000) - self.stats_pl_timer = Timer() - self.stats_pl_timer.force_set(1000) - self.total_albums = 0 - self.stats_pl = 0 - self.stats_pl_albums = 0 - self.stats_pl_length = 0 - - self.ani_cred = 0 - self.cred_page = 0 - self.ani_fade_on_timer = Timer(force=10) - self.ani_fade_off_timer = Timer(force=10) - - self.device_scroll_bar_position = 0 - - self.lyrics_panel = False - self.account_view = 0 - self.view_view = 0 - self.chart_view = 0 - self.eq_view = False - self.rg_view = False - self.sync_view = False - - self.account_text_field = -1 - - self.themes = [] - self.view_supporters = False - self.key_box = TextBox2() - self.key_box_focused = False - - def theme(self, x0, y0, w0, h0): - - global album_mode_art_size - global update_layout - - y = y0 + 13 * gui.scale - x = x0 + 25 * gui.scale - - ddt.text_background_colour = colours.box_background - ddt.text((x, y), _("Theme"), colours.box_text_label, 12) - - y += 25 * gui.scale - - self.toggle_square(x, y, toggle_auto_bg, _("Use album art as background")) - - y += 23 * gui.scale - - old = prefs.enable_fanart_bg - prefs.enable_fanart_bg = self.toggle_square(x + 10 * gui.scale, y, prefs.enable_fanart_bg, - _("Prefer artist backgrounds")) - if prefs.enable_fanart_bg and prefs.enable_fanart_bg != old: - if not prefs.auto_dl_artist_data: - prefs.auto_dl_artist_data = True - show_message(_("Also enabling 'auto-fech artist data' to scrape last.fm."), _("You can toggle this back off under Settings > Function")) - y += 23 * gui.scale - - self.toggle_square(x + 10 * gui.scale, y, toggle_auto_bg_strong, _("Stronger")) - # self.toggle_square(x + 10 * gui.scale, y, toggle_auto_bg_strong1, _("Lo")) - # self.toggle_square(x + 54 * gui.scale, y, toggle_auto_bg_strong2, _("Md")) - # self.toggle_square(x + 105 * gui.scale, y, toggle_auto_bg_strong3, _("Hi")) - - #y += 23 * gui.scale - self.toggle_square(x + 120 * gui.scale, y, toggle_auto_bg_blur, _("Blur")) - - y += 23 * gui.scale - self.toggle_square(x + 10 * gui.scale, y, toggle_auto_bg_showcase, _("Showcase only")) - - y += 23 * gui.scale - # prefs.center_bg = self.toggle_square(x + 10 * gui.scale, y, prefs.center_bg, _("Always center")) - prefs.showcase_overlay_texture = self.toggle_square( - x + 20 * gui.scale, y, prefs.showcase_overlay_texture, _("Pattern style")) - - y += 25 * gui.scale - - self.toggle_square(x, y, toggle_auto_theme, _("Auto-theme from album art")) - - y += 55 * gui.scale - - square = round(8 * gui.scale) - border = round(4 * gui.scale) - outer_border = round(2 * gui.scale) - - # theme_files = get_themes() - xx = x - yy = y - hover_name = None - for c, theme_name, theme_number in self.themes: - - if theme_name == gui.theme_name: - rect = [ - xx - outer_border, yy - outer_border, border * 2 + square * 2 + outer_border * 2, - border * 2 + square * 2 + outer_border * 2] - ddt.rect(rect, colours.box_text_label) - - rect = [xx, yy, border * 2 + square * 2, border * 2 + square * 2] - ddt.rect(rect, [5, 5, 5, 255]) - - rect = grow_rect(rect, 3) - fields.add(rect) - if coll(rect): - hover_name = theme_name - if self.click: - global theme - theme = theme_number - gui.reload_theme = True - - c1 = c.playlist_panel_background - c2 = c.artist_playing - c3 = c.title_playing - c4 = c.bottom_panel_colour - - if theme_name == "Carbon": - c1 = c.title_playing - c2 = c.playlist_panel_background - c3 = c.top_panel_background - - if theme_name == "Lavender Light": - c1 = c.tab_background_active - - if theme_name == "Neon Love": - c2 = c.artist_text - c4 = [118, 85, 194, 255] - c1 = c4 - - if theme_name == "Sky": - c2 = c.artist_text - - if theme_name == "Sunken": - c2 = c.title_text - c3 = c.artist_text - c4 = [59, 115, 109, 255] - c1 = c4 - - if c2 == c3 and colour_value(c1) < 200: - rect = [(xx + border + square) - (square // 2), (yy + border + square) - (square // 2), square, square] - ddt.rect(rect, c2) - else: - - # tl - rect = [xx + border, yy + border, square, square] - ddt.rect(rect, c1) - - # tr - rect = [xx + border + square, yy + border, square, square] - ddt.rect(rect, c2) - - # bl - rect = [xx + border, yy + border + square, square, square] - ddt.rect(rect, c3) - - # br - rect = [xx + border + square, yy + border + square, square, square] - ddt.rect(rect, c4) - - yy += round(27 * gui.scale) - if yy > y + 40 * gui.scale: - yy = y - xx += round(27 * gui.scale) - - name = gui.theme_name - if hover_name: - name = hover_name - ddt.text((x, y - 23 * gui.scale), name, colours.box_text_label, 214) - if gui.theme_name == "Neon Love" and not hover_name: - x += 95 * gui.scale - y -= 23 * gui.scale - # x += 165 * gui.scale - # y += -19 * gui.scale - - link_pa = draw_linked_text((x, y), - _("Based on") + " " + "https://love.holllo.cc/", colours.box_text_label, 312, replace="love.holllo.cc") - link_activate(x, y, link_pa, click=self.click) - - def rg(self, x0, y0, w0, h0): - y = y0 + 55 * gui.scale - x = x0 + 130 * gui.scale - - if self.button(x - 110 * gui.scale, y + 180 * gui.scale, _("Return"), width=75 * gui.scale): - self.rg_view = False - - y = y0 + round(15 * gui.scale) - x = x0 + round(50 * gui.scale) - - ddt.text((x, y), _("ReplayGain"), colours.box_text_label, 14) - y += round(25 * gui.scale) - - self.toggle_square(x, y, switch_rg_off, _("Off")) - self.toggle_square(x + round(80 * gui.scale), y, switch_rg_auto, _("Auto")) - y += round(22 * gui.scale) - self.toggle_square(x, y, switch_rg_album, _("Preserve album dynamics")) - y += round(22 * gui.scale) - self.toggle_square(x, y, switch_rg_track, _("Tracks equal loudness")) - - y += round(25 * gui.scale) - ddt.text((x, y), _("Will only have effect if ReplayGain metadata is present."), colours.box_text_label, 12) - y += round(26 * gui.scale) - - ddt.text((x, y), _("Pre-amp"), colours.box_text_label, 14) - y += round(26 * gui.scale) - - sw = round(170 * gui.scale) - sh = round(2 * gui.scale) - - slider = (x, y, sw, sh) - - gh = round(14 * gui.scale) - gw = round(8 * gui.scale) - grip = [0, y - (gh // 2), gw, gh] - - grip[0] = x - - bp = prefs.replay_preamp + 15 - - grip[0] += (bp / 30 * sw) - - m1 = (x, y, sh, sh * 2) - m2 = ((x + sw // 2), y, sh, sh * 2) - m3 = ((x + sw), y, sh, sh * 2) - - if coll(grow_rect(slider, 15)) and mouse_down: - bp = (mouse_position[0] - x) / sw * 30 - gui.update += 1 - - bp = round(bp) - bp = max(bp, 0) - bp = min(bp, 30) - prefs.replay_preamp = bp - 15 - - # grip[0] += (bp / 30 * sw) - - ddt.rect(slider, colours.box_text_border) - ddt.rect(m1, colours.box_text_border) - ddt.rect(m2, colours.box_text_border) - ddt.rect(m3, colours.box_text_border) - ddt.rect(grip, colours.box_text_label) - - text = f"{prefs.replay_preamp} dB" - if prefs.replay_preamp > 0: - text = "+" + text - - colour = colours.box_sub_text - if prefs.replay_preamp == 0: - colour = colours.box_text_label - ddt.text((x + sw + round(14 * gui.scale), y - round(8 * gui.scale)), text, colour, 11) - #logging.info(prefs.replay_preamp) - - y += round(18 * gui.scale) - ddt.text( - (x, y, 4, 310 * gui.scale, 300 * gui.scale), - _("Lower pre-amp values improve normalisation but will require a higher system volume."), - colours.box_text_label, 12) - - def eq(self, x0, y0, w0, h0): - - y = y0 + 55 * gui.scale - x = x0 + 130 * gui.scale - - if self.button(x - 110 * gui.scale, y + 180 * gui.scale, _("Return"), width=75 * gui.scale): - self.eq_view = False - - base_dis = 160 * gui.scale - center = base_dis // 2 - width = 25 * gui.scale - - range = 12 - - self.toggle_square(x - 90 * gui.scale, y - 35 * gui.scale, toggle_eq, _("Enable")) - - ddt.text((x - 17 * gui.scale, y + 2 * gui.scale), "+", colours.grey(130), 16) - ddt.text((x - 17 * gui.scale, y + base_dis - 15 * gui.scale), "-", colours.grey(130), 16) - - for i, q in enumerate(prefs.eq): - - bar = [x, y, width, base_dis] - - ddt.rect(bar, [255, 255, 255, 20]) - - bar[0] -= 2 * gui.scale - bar[1] -= 10 * gui.scale - bar[2] += 4 * gui.scale - bar[3] += 20 * gui.scale - - if coll(bar): - - if mouse_down: - target = mouse_position[1] - y - center - target = (target / center) * range - target = min(target, range) - target = max(target, range * -1) - if -0.1 < target < 0.1: - target = 0 - - prefs.eq[i] = target - - pctl.playerCommand = "seteq" - pctl.playerCommandReady = True - - if self.right_click: - prefs.eq[i] = 0 - pctl.playerCommand = "seteq" - pctl.playerCommandReady = True - - start = (q / range) * center - - bar = [x, y + center, width, start] - - ddt.rect(bar, [100, 200, 100, 255]) - - x += round(29 * gui.scale) - - def audio(self, x0, y0, w0, h0): - - global mouse_down - - ddt.text_background_colour = colours.box_background - y = y0 + 40 * gui.scale - x = x0 + 20 * gui.scale - - if self.eq_view: - self.eq(x0, y0, w0, h0) - return - - if self.rg_view: - self.rg(x0, y0, w0, h0) - return - - colour = colours.box_sub_text - - # if system == "Linux": - if not phazor_exists(tauon.pctl): - x += round(20 * gui.scale) - ddt.text((x, y - 25 * gui.scale), _("PHAzOR DLL not found!"), colour, 213) - - elif prefs.backend == 4: - - y = y0 + round(20 * gui.scale) - x = x0 + 20 * gui.scale - - x += round(2 * gui.scale) - - self.toggle_square(x, y, toggle_pause_fade, _("Use fade on pause/stop")) - y += round(23 * gui.scale) - self.toggle_square(x, y, toggle_jump_crossfade, _("Use fade on track jump")) - y += round(23 * gui.scale) - prefs.back_restarts = self.toggle_square(x, y, prefs.back_restarts, _("Back restarts to beginning")) - - y += round(40 * gui.scale) - if self.button(x, y, _("ReplayGain")): - mouse_down = False - self.rg_view = True - - y += round(45 * gui.scale) - prefs.precache = self.toggle_square(x, y, prefs.precache, _("Cache local files (for smb/nfs)")) - y += round(23 * gui.scale) - old = prefs.tmp_cache - prefs.tmp_cache = self.toggle_square(x, y, prefs.tmp_cache ^ True, _("Use persistent network cache")) ^ True - if old != prefs.tmp_cache and tauon.cachement: - tauon.cachement.__init__() - - y += round(22 * gui.scale) - ddt.text((x + round(22 * gui.scale), y), _("Cache size"), colours.box_text, 312) - y += round(18 * gui.scale) - prefs.cache_limit = int( - self.slide_control( - x + round(22 * gui.scale), y, None, _(" GB"), prefs.cache_limit / 1000, 0.5, - 1000, 0.5) * 1000) - - y += round(30 * gui.scale) - # prefs.device_buffer = self.slide_control(x + round(270 * gui.scale), y, _("Output buffer"), 'ms', - # prefs.device_buffer, 10, - # 500, 10, self.reload_device) - - # if prefs.device_buffer > 100: - # prefs.pa_fast_seek = True - # else: - # prefs.pa_fast_seek = False - - y = y0 + 37 * gui.scale - x = x0 + 270 * gui.scale - ddt.text_background_colour = colours.box_background - ddt.text((x, y - 22 * gui.scale), _("Set audio output device"), colours.box_text_label, 212) - - if platform_system == "Linux": - old = prefs.pipewire - prefs.pipewire = self.toggle_square(x + round(gui.scale * 110), self.box_y + self.h - 50 * gui.scale, - prefs.pipewire, _("PipeWire (unstable)")) - prefs.pipewire = self.toggle_square(x, self.box_y + self.h - 50 * gui.scale, - prefs.pipewire ^ True, _("PulseAudio")) ^ True - if old != prefs.pipewire: - show_message(_("Please restart Tauon for this change to take effect")) - - old = prefs.avoid_resampling - prefs.avoid_resampling = self.toggle_square(x, self.box_y + self.h - 27 * gui.scale, prefs.avoid_resampling, _("Avoid resampling")) - if prefs.avoid_resampling != old: - pctl.playerCommand = "reload" - pctl.playerCommandReady = True - if not old: - show_message( - _("Tip: To get samplerate to DAC you may need to check some settings, see:"), - "https://github.com/Taiko2k/Tauon/wiki/Audio-Specs", mode="link") - - self.device_scroll_bar_position -= pref_box.scroll - self.device_scroll_bar_position = max(self.device_scroll_bar_position, 0) - if self.device_scroll_bar_position > len(prefs.phazor_devices) - 11 > 11: - self.device_scroll_bar_position = len(prefs.phazor_devices) - 11 - - if len(prefs.phazor_devices) > 13: - self.device_scroll_bar_position = device_scroll.draw( - x + 250 * gui.scale, y, 11, 180, - self.device_scroll_bar_position, - len(prefs.phazor_devices) - 11, click=self.click) - - i = 0 - reload = False - for name in prefs.phazor_devices: - - if i < self.device_scroll_bar_position: - continue - if y > self.box_y + self.h - 40 * gui.scale: - break - - rect = (x, y + 4 * gui.scale, 245 * gui.scale, 13) - - if self.click and coll(rect): - prefs.phazor_device_selected = name - reload = True - - line = trunc_line(name, 10, 245 * gui.scale) - - fields.add(rect) - - if prefs.phazor_device_selected == name: - ddt.text((x, y), line, colours.box_sub_text, 10) - ddt.text((x - 12 * gui.scale, y + 1 * gui.scale), ">", colours.box_sub_text, 213) - elif coll(rect): - ddt.text((x, y), line, colours.box_sub_text, 10) - else: - ddt.text((x, y), line, colours.box_text_label, 10) - y += 14 * gui.scale - i += 1 - - if reload: - pctl.playerCommand = "set-device" - pctl.playerCommandReady = True - - def reload_device(self, _): - - pctl.playerCommand = "reload" - pctl.playerCommandReady = True - - def toggle_lyrics_view(self): - self.lyrics_panel ^= True - - def lyrics(self, x0, y0, w0, h0): - - x = x0 + 25 * gui.scale - y = y0 - 10 * gui.scale - y += 30 * gui.scale - - ddt.text_background_colour = colours.box_background - - # self.toggle_square(x, y, toggle_auto_lyrics, _("Auto search lyrics")) - if prefs.auto_lyrics: - if prefs.auto_lyrics_checked: - if self.button(x, y, _("Reset failed list")): - prefs.auto_lyrics_checked.clear() - y += 30 * gui.scale - -# self.toggle_square(x, y, toggle_guitar_chords, _("Enable chord lyrics")) - - y += 40 * gui.scale - ddt.text((x, y), _("Sources:"), colours.box_text_label, 11) - y += 23 * gui.scale - - for name in lyric_sources.keys(): - enabled = name in prefs.lyrics_enables - title = _(name) - if name in uses_scraping: - title += "*" - new = self.toggle_square(x, y, enabled, title) - y += round(23 * gui.scale) - if new != enabled: - if enabled: - prefs.lyrics_enables.clear() - else: - prefs.lyrics_enables.append(name) - - y += round(6 * gui.scale) - ddt.text((x + 12 * gui.scale, y), _("*Uses scraping. Enable at your own discretion."), colours.box_text_label, 11) - y += 20 * gui.scale - ddt.text((x + 12 * gui.scale, y), _("Tip: The order enabled will be the order searched."), colours.box_text_label, 11) - y += 20 * gui.scale - - def view2(self, x0, y0, w0, h0): - - x = x0 + 25 * gui.scale - y = y0 + 20 * gui.scale - - ddt.text_background_colour = colours.box_background - - ddt.text((x, y), _("Metadata side panel"), colours.box_text_label, 12) - - y += 25 * gui.scale - self.toggle_square(x, y, toggle_side_panel_layout, _("Use centered style")) - y += 25 * gui.scale - old = prefs.zoom_art - prefs.zoom_art = self.toggle_square(x, y, prefs.zoom_art, _("Zoom album art to fit")) - if prefs.zoom_art != old: - album_art_gen.clear_cache() - - global album_mode_art_size - global update_layout - y += 35 * gui.scale - ddt.text((x, y), _("Gallery"), colours.box_text_label, 12) - - y += 25 * gui.scale - # self.toggle_square(x, y, toggle_dim_albums, "Dim gallery when playing") - self.toggle_square(x, y, toggle_gallery_click, _("Single click to play")) - y += 25 * gui.scale - self.toggle_square(x, y, toggle_gallery_combine, _("Combine multi-discs")) - y += 25 * gui.scale - self.toggle_square(x, y, toggle_galler_text, _("Show titles")) - y += 25 * gui.scale - # self.toggle_square(x, y, toggle_gallery_row_space, _("Increase row spacing")) - # y += 25 * gui.scale - prefs.center_gallery_text = self.toggle_square( - x + round(10 * gui.scale), y, prefs.center_gallery_text, _("Center alignment")) - - y += 30 * gui.scale - - # y += 25 * gui.scale - - x -= 80 * gui.scale - x += ddt.get_text_w(_("Thumbnail size"), 312) - # x += 20 * gui.scale - - if album_mode_art_size < 160: - self.toggle_square(x + 235 * gui.scale, y + 2 * gui.scale, toggle_gallery_thin, _("Prefer thinner padding")) - - # ddt.text((x, y), _("Gallery art size"), colours.grey(220), 11) - - album_mode_art_size = self.slide_control( - x + 25 * gui.scale, y, _("Thumbnail size"), "px", album_mode_art_size, 70, 400, 10, img_slide_update_gall) - - def funcs(self, x0, y0, w0, h0): - - x = x0 + 25 * gui.scale - y = y0 - 10 * gui.scale - - ddt.text_background_colour = colours.box_background - - if self.func_page == 0: - - y += 23 * gui.scale - - self.toggle_square( - x, y, toggle_enable_web, _("Enable Listen Along"), subtitle=_("Start server for remote web playback")) - - if toggle_enable_web(1): - - link_pa2 = draw_linked_text( - (x + 300 * gui.scale, y - 1 * gui.scale), - f"http://localhost:{prefs.metadata_page_port!s}/listenalong", - colours.grey_blend_bg(190), 13) - link_rect2 = [x + 300 * gui.scale, y - 1 * gui.scale, link_pa2[1], 20 * gui.scale] - fields.add(link_rect2) - - if coll(link_rect2): - if not self.click: - gui.cursor_want = 3 - - if self.click: - webbrowser.open(link_pa2[2], new=2, autoraise=True) - - y += 38 * gui.scale - - old = gui.artist_info_panel - new = self.toggle_square( - x, y, gui.artist_info_panel, - _("Show artist info panel"), - subtitle=_("You can also toggle this with ctrl+o")) - if new != old: - view_box.artist_info(True) - - y += 38 * gui.scale - - self.toggle_square( - x, y, toggle_auto_artist_dl, - _("Auto fetch artist data"), - subtitle=_("Downloads data in background when artist panel is open")) - - y += 38 * gui.scale - prefs.always_auto_update_playlists = self.toggle_square( - x, y, prefs.always_auto_update_playlists, - _("Auto regenerate playlists"), - subtitle=_("Generated playlists reload when re-entering")) - - y += 38 * gui.scale - self.toggle_square( - x, y, toggle_top_tabs, _("Tabs in top panel"), - subtitle=_("Uncheck to disable the tab pin function")) - - y += 45 * gui.scale - # y += 30 * gui.scale - - wa = ddt.get_text_w(_("Open config file"), 211) + 10 * gui.scale - # wb = ddt.get_text_w(_("Open keymap file"), 211) + 10 * gui.scale - wc = ddt.get_text_w(_("Open data folder"), 211) + 10 * gui.scale - - ww = max(wa, wc) - - self.button(x, y, _("Open config file"), open_config_file, width=ww) - bg = None - if gui.opened_config_file: - bg = [90, 50, 130, 255] - self.button(x + ww + wc + 25 * gui.scale, y, _("Reload"), reload_config_file, bg=bg) - - self.button(x + wa + round(20 * gui.scale), y, _("Open data folder"), open_data_directory, ww) - - elif self.func_page == 1: - y += 23 * gui.scale - ddt.text((x, y), _("Enable/Disable track context menu functions:"), colours.box_text_label, 11) - y += 25 * gui.scale - - self.toggle_square(x, y, toggle_wiki, _("Wikipedia artist search")) - y += 23 * gui.scale - self.toggle_square(x, y, toggle_rym, _("Sonemic artist search")) - y += 23 * gui.scale - self.toggle_square(x, y, toggle_band, _("Bandcamp artist page search")) - # y += 23 * gui.scale - # self.toggle_square(x, y, toggle_gimage, _("Google image search")) - y += 23 * gui.scale - self.toggle_square(x, y, toggle_gen, _("Genius track search")) - y += 23 * gui.scale - self.toggle_square(x, y, toggle_transcode, _("Transcode folder")) - - y += 28 * gui.scale - - x = x0 + self.item_x_offset - - ddt.text((x, y), _("End of playlist action"), colours.box_text_label, 12) - - y += 25 * gui.scale - wa = ddt.get_text_w(_("Stop playback"), 13) + 10 * gui.scale - wb = ddt.get_text_w(_("Repeat playlist"), 13) + 10 * gui.scale - wc = max(wa, wb) + 20 * gui.scale - - self.toggle_square(x, y, self.set_playlist_stop, _("Stop playback")) - y += 25 * gui.scale - self.toggle_square(x, y, self.set_playlist_repeat, _("Repeat playlist")) - # y += 25 - y -= 25 * gui.scale - x += wc - self.toggle_square(x, y, self.set_playlist_advance, _("Play next playlist")) - y += 25 * gui.scale - self.toggle_square(x, y, self.set_playlist_cycle, _("Cycle all playlists")) - - elif self.func_page == 2: - y += 23 * gui.scale - # ddt.text((x, y), _("Auto download monitor and archive extractor"), colours.box_text_label, 11) - # y += 25 * gui.scale - self.toggle_square( - x, y, toggle_extract, _("Extract archives"), - subtitle=_("Extracts zip archives on drag and drop")) - y += 38 * gui.scale - self.toggle_square( - x + 10 * gui.scale, y, toggle_dl_mon, _("Enable download monitor"), - subtitle=_("One click import new archives and folders from downloads folder")) - y += 38 * gui.scale - self.toggle_square(x + 10 * gui.scale, y, toggle_ex_del, _("Trash archive after extraction")) - y += 23 * gui.scale - self.toggle_square(x + 10 * gui.scale, y, toggle_music_ex, _("Always extract to Music folder")) - - y += 38 * gui.scale - if not msys: - self.toggle_square(x, y, toggle_use_tray, _("Show icon in system tray")) - - y += 25 * gui.scale - self.toggle_square(x + round(10 * gui.scale), y, toggle_min_tray, _("Close to tray")) - - y += 25 * gui.scale - self.toggle_square(x + round(10 * gui.scale), y, toggle_text_tray, _("Show title text")) - - old = prefs.tray_theme - if not self.toggle_square(x + round(190 * gui.scale), y, prefs.tray_theme == "gray", _("Monochrome")): - prefs.tray_theme = "pink" - else: - prefs.tray_theme = "gray" - if prefs.tray_theme != old: - tauon.set_tray_icons(force=True) - show_message(_("Restart Tauon for change to take effect")) - - else: - self.toggle_square(x, y, toggle_min_tray, _("Close to tray")) - - - - elif self.func_page == 4: - y += 23 * gui.scale - prefs.use_gamepad = self.toggle_square( - x, y, prefs.use_gamepad, _("Enable use of gamepad as input"), - subtitle=_("Change requires restart")) - y += 37 * gui.scale - - elif self.func_page == 3: - y += 23 * gui.scale - old = prefs.enable_remote - prefs.enable_remote = self.toggle_square( - x, y, prefs.enable_remote, _("Enable remote control"), - subtitle=_("Change requires restart")) - y += 37 * gui.scale - - if prefs.enable_remote and prefs.enable_remote != old: - show_message( - _("Notice: This API is not security hardened."), - _("Only enable in a trusted LAN and do not expose port (7814) to the internet"), - mode="warning") - - old = prefs.block_suspend - prefs.block_suspend = self.toggle_square( - x, y, prefs.block_suspend, _("Block suspend"), - subtitle=_("Prevent system suspend during playback")) - y += 37 * gui.scale - old = prefs.block_suspend - prefs.resume_play_wake = self.toggle_square( - x, y, prefs.resume_play_wake, _("Resume from suspend"), - subtitle=_("Continue playback when waking from sleep")) - - y += 37 * gui.scale - old = prefs.auto_rec - prefs.auto_rec = self.toggle_square( - x, y, prefs.auto_rec, _("Record Radio"), - subtitle=_("Record and split songs when playing internet radio")) - if prefs.auto_rec != old and prefs.auto_rec: - show_message( - _("Tracks will now be recorded. Restart any playback for change to take effect."), - _("Tracks will be saved to \"Saved Radio Tracks\" playlist."), - mode="info") - - if tauon.update_play_lock is None: - prefs.block_suspend = False - # if flatpak_mode: - # show_message("Sandbox support not implemented") - elif old != prefs.block_suspend: - tauon.update_play_lock() - - y += 37 * gui.scale - ddt.text((x, y), "Discord", colours.box_text_label, 11) - y += 25 * gui.scale - old = prefs.discord_enable - prefs.discord_enable = self.toggle_square(x, y, prefs.discord_enable, _("Enable Discord Rich Presence")) - - if flatpak_mode: - if self.button(x + 215 * gui.scale, y, _("?")): - show_message( - _("For troubleshooting Discord RP"), - "https://github.com/Taiko2k/TauonMusicBox/wiki/Discord-RP", mode="link") - - if prefs.discord_enable and not old: - if snap_mode: - show_message(_("Sorry, this feature is unavailable with snap"), mode="error") - prefs.discord_enable = False - elif not discord_allow: - show_message(_("Missing dependency python-pypresence")) - prefs.discord_enable = False - else: - hit_discord() - - if old and not prefs.discord_enable: - if prefs.discord_active: - prefs.disconnect_discord = True - - y += 22 * gui.scale - text = _("Disabled") - if prefs.discord_enable: - text = gui.discord_status - ddt.text((x, y), _("Status: {state}").format(state=text), colours.box_text, 11) - - # Switcher - pages = 5 - x = x0 + round(18 * gui.scale) - y = (y0 + h0) - round(29 * gui.scale) - ww = round(40 * gui.scale) - - for p in range(pages): - if self.button2(x, y, str(p + 1), width=ww, center_text=True, force_on=self.func_page == p): - self.func_page = p - x += ww - - # self.button(x, y, _("Open keymap file"), open_keymap_file, width=wc) - - def button(self, x, y, text, plug=None, width=0, bg=None): - - w = width - if w == 0: - w = ddt.get_text_w(text, 211) + round(10 * gui.scale) - - h = round(20 * gui.scale) - border_size = round(2 * gui.scale) - - rect = (round(x), round(y), round(w), round(h)) - rect2 = (rect[0] - border_size, rect[1] - border_size, rect[2] + border_size * 2, rect[3] + border_size * 2) - - if bg is None: - bg = colours.box_background - - real_bg = bg - hit = False - - ddt.rect(rect2, colours.box_check_border) - ddt.rect(rect, bg) - - fields.add(rect) - if coll(rect): - ddt.rect(rect, [255, 255, 255, 15]) - real_bg = alpha_blend([255, 255, 255, 15], bg) - ddt.text((x + int(w / 2), rect[1] + 1 * gui.scale, 2), text, colours.box_title_text, 211, bg=real_bg) - if self.click: - hit = True - if plug is not None: - plug() - else: - ddt.text((x + int(w / 2), rect[1] + 1 * gui.scale, 2), text, colours.box_sub_text, 211, bg=real_bg) - - return hit - - def button2(self, x, y, text, width=0, center_text=False, force_on=False): - w = width - if w == 0: - w = ddt.get_text_w(text, 211) + 10 * gui.scale - rect = (x, y, w, 20 * gui.scale) - - bg_colour = colours.box_button_background - real_bg = bg_colour - - ddt.rect(rect, bg_colour) - fields.add(rect) - hit = False - - text_position = (x + int(7 * gui.scale), rect[1] + 1 * gui.scale) - if center_text: - text_position = (x + rect[2] // 2, rect[1] + 1 * gui.scale, 2) - - if coll(rect) or force_on: - ddt.rect(rect, colours.box_button_background_highlight) - bg_colour = colours.box_button_background - real_bg = alpha_blend(colours.box_button_background_highlight, bg_colour) - ddt.text(text_position, text, colours.box_button_text_highlight, 211, bg=real_bg) - if self.click and not force_on: - hit = True - else: - ddt.text(text_position, text, colours.box_button_text, 211, bg=real_bg) - return hit - - def toggle_square(self, x, y, function, text: str , click: bool = False, subtitle: str = "") -> bool: - - x = round(x) - y = round(y) - - border = round(2 * gui.scale) - gap = round(2 * gui.scale) - inner_square = round(6 * gui.scale) - - full_w = border * 2 + gap * 2 + inner_square - - if subtitle: - le = ddt.text((x + 20 * gui.scale, y - 1 * gui.scale), text, colours.box_text, 13) - se = ddt.text((x + 20 * gui.scale, y + 14 * gui.scale), subtitle, colours.box_text_label, 13) - hit_rect = (x - 10 * gui.scale, y - 3 * gui.scale, max(le, se) + 30 * gui.scale, 34 * gui.scale) - y += round(8 * gui.scale) - - else: - le = ddt.text((x + 20 * gui.scale, y - 1 * gui.scale), text, colours.box_text, 13) - hit_rect = (x - 10 * gui.scale, y - 3 * gui.scale, le + 30 * gui.scale, 22 * gui.scale) - - # Border outline - ddt.rect_a((x, y), (full_w, full_w), colours.box_check_border) - # Inner background - ddt.rect_a( - (x + border, y + border), (gap * 2 + inner_square, gap * 2 + inner_square), - alpha_blend([255, 255, 255, 14], colours.box_background)) - - # Check if box clicked - clicked = False - if (self.click or click) and coll(hit_rect): - clicked = True - - # There are two mode, function type, and passthrough bool type - active = False - if type(function) is bool: - active = function - else: - active = function(1) - - if clicked: - if type(function) is bool: - active ^= True - else: - function() - active = function(1) - - # Draw inner check mark if enabled - if active: - ddt.rect_a((x + border + gap, y + border + gap), (inner_square, inner_square), colours.toggle_box_on) - - return active - - def last_fm_box(self, x0, y0, w0, h0): - - x = x0 + round(20 * gui.scale) - y = y0 + round(15 * gui.scale) - - ddt.text_background_colour = colours.box_background - - text = "Last.fm" - if prefs.use_libre_fm: - text = "Libre.fm" - if self.button2(x, y, text, width=84 * gui.scale): - self.account_view = 1 - self.toggle_square(x + 105 * gui.scale, y + 2 * gui.scale, toggle_lfm_auto, _("Enable")) - - y += 28 * gui.scale - - if self.button2(x, y, "ListenBrainz", width=84 * gui.scale): - self.account_view = 2 - self.toggle_square(x + 105 * gui.scale, y + 2 * gui.scale, toggle_lb, _("Enable")) - - y += 28 * gui.scale - - if self.button2(x, y, "Maloja", width=84 * gui.scale): - self.account_view = 9 - self.toggle_square(x + 105 * gui.scale, y + 2 * gui.scale, toggle_maloja, _("Enable")) - - # if self.button2(x, y, "Discogs", width=84*gui.scale): - # self.account_view = 3 - - y += 28 * gui.scale - - if self.button2(x, y, "fanart.tv", width=84 * gui.scale): - self.account_view = 4 - - y += 28 * gui.scale - y += 28 * gui.scale - - y += 15 * gui.scale - - if key_shift_down and self.button2(x + round(95 * gui.scale), y, "koel", width=84 * gui.scale): - self.account_view = 6 - - if self.button2(x, y, "Jellyfin", width=84 * gui.scale): - self.account_view = 10 - - if self.button2(x + round(95 * gui.scale), y, "TIDAL", width=84 * gui.scale): - self.account_view = 12 - - y += 28 * gui.scale - - if self.button2(x, y, "Airsonic", width=84 * gui.scale): - self.account_view = 7 - - if self.button2(x + round(95 * gui.scale), y, "PLEX", width=84 * gui.scale): - self.account_view = 5 - - y += 28 * gui.scale - - if self.button2(x, y, "Spotify", width=84 * gui.scale): - self.account_view = 8 - - if self.button2(x + round(95 * gui.scale), y, "Satellite", width=84 * gui.scale): - self.account_view = 11 - - if self.account_view in (9, 2): - self.toggle_square( - x0 + 230 * gui.scale, y + 2 * gui.scale, toggle_scrobble_mark, - _("Show threshold marker")) - - x = x0 + 230 * gui.scale - y = y0 + round(20 * gui.scale) - - if self.account_view == 12: - ddt.text((x, y), "TIDAL", colours.box_sub_text, 213) - - y += round(30 * gui.scale) - - if os.path.isfile(tauon.tidal.save_path): - if self.button2(x, y, _("Logout"), width=84 * gui.scale): - tauon.tidal.logout() - elif tauon.tidal.login_stage == 0: - if self.button2(x, y, _("Login"), width=84 * gui.scale): - # webThread = threading.Thread(target=authserve, args=[tauon]) - # webThread.daemon = True - # webThread.start() - # time.sleep(0.1) - tauon.tidal.login1() - else: - ddt.text( - (x + 0 * gui.scale, y), _("Copy the full URL of the resulting 'oops' page"), colours.box_text_label, 11) - y += round(25 * gui.scale) - if self.button2(x, y, _("Paste Redirect URL"), width=84 * gui.scale): - text = copy_from_clipboard() - if text: - tauon.tidal.login2(text) - - if os.path.isfile(tauon.tidal.save_path): - y += round(30 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Paste TIDAL URL's into Tauon using ctrl+v"), colours.box_text_label, 11) - y += round(30 * gui.scale) - if self.button(x, y, _("Import Albums")): - show_message(_("Fetching playlist...")) - shooter(tauon.tidal.fav_albums) - - y += round(30 * gui.scale) - if self.button(x, y, _("Import Tracks")): - show_message(_("Fetching playlist...")) - shooter(tauon.tidal.fav_tracks) - - if self.account_view == 11: - ddt.text((x, y), "Tauon Satellite", colours.box_sub_text, 213) - - y += round(30 * gui.scale) - - field_width = round(245 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("IP"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 0 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_sat_url.text = prefs.sat_url - text_sat_url.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.sat_url = text_sat_url.text.strip() - - y += round(25 * gui.scale) - - y += round(30 * gui.scale) - - field_width = round(245 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Playlist name"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 1 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_sat_playlist.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, - width=rect1[2] - 8 * gui.scale, click=self.click) - - y += round(25 * gui.scale) - - if self.button(x, y, _("Get playlist")): - if tau.processing: - show_message(_("An operation is already running")) - else: - shooter(tau.get_playlist()) - - elif self.account_view == 9: - - ddt.text((x, y), _("Maloja Server"), colours.box_sub_text, 213) - if self.button(x + 260 * gui.scale, y, _("?")): - show_message( - _("Maloja is a self-hosted scrobble server."), - _("See here to learn more: {link}").format(link="https://github.com/krateng/maloja"), mode="link") - - if inp.key_tab_press: - self.account_text_field += 1 - if self.account_text_field > 2: - self.account_text_field = 0 - - field_width = round(245 * gui.scale) - - y += round(25 * gui.scale) - ddt.text( - (x + 0 * gui.scale, y), _("Server URL"), - colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 0 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_maloja_url.text = prefs.maloja_url - text_maloja_url.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.maloja_url = text_maloja_url.text.strip() - - y += round(23 * gui.scale) - ddt.text( - (x + 0 * gui.scale, y), _("API Key"), - colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 1 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_maloja_key.text = prefs.maloja_key - text_maloja_key.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.maloja_key = text_maloja_key.text.strip() - - y += round(35 * gui.scale) - - if self.button(x, y, _("Test connectivity")): - - if not prefs.maloja_url or not prefs.maloja_key: - show_message(_("One or more fields is missing.")) - else: - url = prefs.maloja_url - if not url.endswith("/mlj_1"): - if not url.endswith("/"): - url += "/" - url += "apis/mlj_1" - url += "/test" - - try: - r = requests.get(url, params={"key": prefs.maloja_key}, timeout=10) - if r.status_code == 403: - show_message(_("Connection appeared successful but the API key was invalid"), mode="warning") - elif r.status_code == 200: - show_message(_("Connection to Maloja server was successful."), mode="done") - else: - show_message(_("The Maloja server returned an error"), r.text, mode="warning") - except Exception: - logging.exception("Could not communicate with the Maloja server") - show_message(_("Could not communicate with the Maloja server"), mode="warning") - - y += round(30 * gui.scale) - - ws = ddt.get_text_w(_("Get scrobble counts"), 211) + 10 * gui.scale - wcc = ddt.get_text_w(_("Clear"), 211) + 15 * gui.scale - if self.button(x, y, _("Get scrobble counts")): - shooter(maloja_get_scrobble_counts) - self.button(x + ws + round(12 * gui.scale), y, _("Clear"), self.clear_scrobble_counts, width=wcc) - - if self.account_view == 8: - - ddt.text((x, y), "Spotify", colours.box_sub_text, 213) - - prefs.spot_mode = self.toggle_square(x + 80 * gui.scale, y + 2 * gui.scale, prefs.spot_mode, _("Enable")) - y += round(30 * gui.scale) - - if self.button(x, y, _("View setup instructions")): - webbrowser.open("https://github.com/Taiko2k/Tauon/wiki/Spotify", new=2, autoraise=True) - - field_width = round(245 * gui.scale) - - y += round(26 * gui.scale) - - ddt.text( - (x + 0 * gui.scale, y), _("Client ID"), - colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 0 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_spot_client.text = prefs.spot_client - text_spot_client.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.spot_client = text_spot_client.text.strip() - - y += round(19 * gui.scale) - ddt.text( - (x + 0 * gui.scale, y), _("Client Secret"), - colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 1 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_spot_secret.text = prefs.spot_secret - text_spot_secret.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.spot_secret = text_spot_secret.text.strip() - - y += round(27 * gui.scale) - - if prefs.spotify_token: - if self.button(x, y, _("Forget Account")): - tauon.spot_ctl.delete_token() - tauon.spot_ctl.cache_saved_albums.clear() - prefs.spot_username = "" - if not prefs.launch_spotify_local: - prefs.spot_password = "" - elif self.button(x, y, _("Authorise")): - webThread = threading.Thread(target=authserve, args=[tauon]) - webThread.daemon = True - webThread.start() - time.sleep(0.1) - - tauon.spot_ctl.auth() - - y += round(31 * gui.scale) - prefs.launch_spotify_web = self.toggle_square( - x, y, prefs.launch_spotify_web, - _("Prefer launching web player")) - - y += round(24 * gui.scale) - - old = prefs.launch_spotify_local - prefs.launch_spotify_local = self.toggle_square( - x, y, prefs.launch_spotify_local, - _("Enable local audio playback")) - - if prefs.launch_spotify_local and not tauon.enable_librespot: - show_message(_("Librespot not installed?")) - prefs.launch_spotify_local = False - - - if self.account_view == 7: - - ddt.text((x, y), _("Airsonic/Subsonic network streaming"), colours.box_sub_text, 213) - - if inp.key_tab_press: - self.account_text_field += 1 - if self.account_text_field > 2: - self.account_text_field = 0 - - field_width = round(245 * gui.scale) - - y += round(25 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Username / Email"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 0 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_air_usr.text = prefs.subsonic_user - text_air_usr.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.subsonic_user = text_air_usr.text - - y += round(23 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Password"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 1 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_air_pas.text = prefs.subsonic_password - text_air_pas.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, - width=rect1[2] - 8 * gui.scale, click=self.click, secret=True) - prefs.subsonic_password = text_air_pas.text - - y += round(23 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Server URL"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 2 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_air_ser.text = prefs.subsonic_server - text_air_ser.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 2, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.subsonic_server = text_air_ser.text - - y += round(40 * gui.scale) - self.button(x, y, _("Import music to playlist"), sub_get_album_thread) - - y += round(35 * gui.scale) - prefs.subsonic_password_plain = self.toggle_square( - x, y, prefs.subsonic_password_plain, - _("Use plain text authentication"), - subtitle=_("Needed for Nextcloud Music")) - - if self.account_view == 10: - - ddt.text((x, y), _("Jellyfin network streaming"), colours.box_sub_text, 213) - - if inp.key_tab_press: - self.account_text_field += 1 - if self.account_text_field > 2: - self.account_text_field = 0 - - field_width = round(245 * gui.scale) - - y += round(25 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Username"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 0 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_jelly_usr.text = prefs.jelly_username - text_jelly_usr.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.jelly_username = text_jelly_usr.text - - y += round(23 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Password"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 1 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_jelly_pas.text = prefs.jelly_password - text_jelly_pas.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, - width=rect1[2] - 8 * gui.scale, click=self.click, secret=True) - prefs.jelly_password = text_jelly_pas.text - - y += round(23 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Server URL"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 2 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_jelly_ser.text = prefs.jelly_server_url - text_jelly_ser.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 2, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.jelly_server_url = text_jelly_ser.text - - y += round(30 * gui.scale) - - self.button(x, y, _("Import music to playlist"), jellyfin_get_library_thread) - - y += round(30 * gui.scale) - if self.button(x, y, _("Import playlists")): - found = False - for item in pctl.gen_codes.values(): - if item.startswith("jelly"): - found = True - break - if not found: - gui.show_message(_("Run music import first")) - else: - jellyfin_get_playlists_thread() - - y += round(35 * gui.scale) - if self.button(x, y, _("Test connectivity")): - jellyfin.test() - - if self.account_view == 6: - - ddt.text((x, y), _("koel network streaming"), colours.box_sub_text, 213) - - if inp.key_tab_press: - self.account_text_field += 1 - if self.account_text_field > 2: - self.account_text_field = 0 - - field_width = round(245 * gui.scale) - - y += round(25 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Username / Email"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 0 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_koel_usr.text = prefs.koel_username - text_koel_usr.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.koel_username = text_koel_usr.text - - y += round(23 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Password"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 1 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_koel_pas.text = prefs.koel_password - text_koel_pas.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, - width=rect1[2] - 8 * gui.scale, click=self.click, secret=True) - prefs.koel_password = text_koel_pas.text - - y += round(23 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Server URL"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 2 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_koel_ser.text = prefs.koel_server_url - text_koel_ser.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 2, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.koel_server_url = text_koel_ser.text - - y += round(40 * gui.scale) - - self.button(x, y, _("Import music to playlist"), koel_get_album_thread) - - if self.account_view == 5: - - ddt.text((x, y), _("PLEX network streaming"), colours.box_sub_text, 213) - - if inp.key_tab_press: - self.account_text_field += 1 - if self.account_text_field > 2: - self.account_text_field = 0 - - field_width = round(245 * gui.scale) - - y += round(25 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Username / Email"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 0 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_plex_usr.text = prefs.plex_username - text_plex_usr.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.plex_username = text_plex_usr.text - - y += round(23 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Password"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 1 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_plex_pas.text = prefs.plex_password - text_plex_pas.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, - width=rect1[2] - 8 * gui.scale, click=self.click, secret=True) - prefs.plex_password = text_plex_pas.text - - y += round(23 * gui.scale) - ddt.text((x + 0 * gui.scale, y), _("Server name"), colours.box_text_label, 11) - y += round(19 * gui.scale) - rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) - fields.add(rect1) - if coll(rect1) and (self.click or level_2_right_click): - self.account_text_field = 2 - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - text_plex_ser.text = prefs.plex_servername - text_plex_ser.draw( - x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 2, - width=rect1[2] - 8 * gui.scale, click=self.click) - prefs.plex_servername = text_plex_ser.text - - y += round(40 * gui.scale) - self.button(x, y, _("Import music to playlist"), plex_get_album_thread) - - if self.account_view == 4: - - ddt.text((x, y), "fanart.tv", colours.box_sub_text, 213) - - y += 25 * gui.scale - ddt.text( - (x + 0 * gui.scale, y, 4, 270 * gui.scale, 600), - _("Fanart.tv can be used for sourcing of artist images and cover art."), - colours.box_text_label, 11) - y += 17 * gui.scale - - y += 22 * gui.scale - # . Limited space available. Limit 55 chars - link_pa2 = draw_linked_text( - (x + 0 * gui.scale, y), - _("They encourage you to contribute at {link}").format(link="https://fanart.tv"), - colours.box_text_label, 11) - link_activate(x, y, link_pa2) - - y += 35 * gui.scale - prefs.enable_fanart_cover = self.toggle_square( - x, y, prefs.enable_fanart_cover, - _("Cover art (Manual only)")) - y += 25 * gui.scale - prefs.enable_fanart_artist = self.toggle_square( - x, y, prefs.enable_fanart_artist, - _("Artist images (Automatic)")) - #y += 25 * gui.scale - # prefs.enable_fanart_bg = self.toggle_square(x, y, prefs.enable_fanart_bg, - # _("Artist backgrounds (Automatic)")) - y += 25 * gui.scale - x += 23 * gui.scale - if self.button(x, y, _("Flip current")): - if key_shift_down: - prefs.bg_flips.clear() - show_message(_("Reset flips"), mode="done") - else: - tr = pctl.playing_object() - artist = get_artist_safe(tr) - if artist: - if artist not in prefs.bg_flips: - prefs.bg_flips.add(artist) - else: - prefs.bg_flips.remove(artist) - style_overlay.flush() - show_message(_("OK"), mode="done") - - # if self.account_view == 3: - # - # ddt.text((x, y), 'Discogs', colours.box_sub_text, 213) - # - # y += 25 * gui.scale - # hh = ddt.text((x + 0 * gui.scale, y, 4, 260 * gui.scale, 300 * gui.scale), _("Discogs can be used for sourcing artist images. For this you will need a \"Personal Access Token\".\n\nYou can generate one with a Discogs account here:"), - # colours.box_text_label, 11) - # - # - # y += hh - # #y += 15 * gui.scale - # link_pa2 = draw_linked_text((x + 0 * gui.scale, y), "https://www.discogs.com/settings/developers",colours.box_text_label, 12) - # link_rect2 = [x + 0 * gui.scale, y, link_pa2[1], 20 * gui.scale] - # fields.add(link_rect2) - # if coll(link_rect2): - # if not self.click: - # gui.cursor_want = 3 - # if self.click: - # webbrowser.open(link_pa2[2], new=2, autoraise=True) - # - # y += 40 * gui.scale - # if self.button(x, y, _("Paste Token")): - # - # text = copy_from_clipboard() - # if text == "": - # show_message(_("There is no text in the clipboard", mode='error') - # elif len(text) == 40: - # prefs.discogs_pat = text - # - # # Reset caches ------------------- - # prefs.failed_artists.clear() - # artist_list_box.to_fetch = "" - # for key, value in artist_list_box.thumb_cache.items(): - # if value: - # SDL_DestroyTexture(value[0]) - # artist_list_box.thumb_cache.clear() - # artist_list_box.to_fetch = "" - # - # direc = os.path.join(a_cache_dir) - # if os.path.isdir(direc): - # for item in os.listdir(direc): - # if "-lfm.txt" in item: - # os.remove(os.path.join(direc, item)) - # # ----------------------------------- - # - # else: - # show_message(_("That is not a valid token", mode='error') - # y += 30 * gui.scale - # if self.button(x, y, _("Clear")): - # if not prefs.discogs_pat: - # show_message(_("There wasn't any token saved.") - # prefs.discogs_pat = "" - # save_prefs() - # - # y += 30 * gui.scale - # if prefs.discogs_pat: - # ddt.text((x + 0 * gui.scale, y - 0 * gui.scale), prefs.discogs_pat, colours.box_input_text, 211) - # - - if self.account_view == 1: - - text = "Last.fm" - if prefs.use_libre_fm: - text = "Libre.fm" - - ddt.text((x, y), text, colours.box_sub_text, 213) - - ww = ddt.get_text_w(_("Username:"), 212) - ddt.text((x + 65 * gui.scale, y - 0 * gui.scale), _("Username:"), colours.box_text_label, 212) - ddt.text( - (x + ww + 65 * gui.scale + 7 * gui.scale, y - 0 * gui.scale), prefs.last_fm_username, - colours.box_sub_text, 213) - - y += 25 * gui.scale - - if prefs.last_fm_token is None: - ww = ddt.get_text_w(_("Login"), 211) + 10 * gui.scale - ww2 = ddt.get_text_w(_("Done"), 211) + 40 * gui.scale - self.button(x, y, _("Login"), lastfm.auth1) - self.button(x + ww + 10 * gui.scale, y, _("Done"), lastfm.auth2) - - if prefs.last_fm_token is None and lastfm.url is None: - prefs.use_libre_fm = self.toggle_square( - x + ww + ww2, y + round(1 * gui.scale), prefs.use_libre_fm, _("Use LibreFM")) - - y += 25 * gui.scale - ddt.text( - (x + 2 * gui.scale, y, 4, 270 * gui.scale, 300 * gui.scale), - _("Click login to open the last.fm web authorisation page and follow prompt. Then return here and click \"Done\"."), - colours.box_text_label, 11, max_w=270 * gui.scale) - - else: - self.button(x, y, _("Forget account"), lastfm.auth3) - - x = x0 + 230 * gui.scale - y = y0 + round(130 * gui.scale) - - # self.toggle_square(x, y, toggle_scrobble_mark, "Show scrobble marker") - - wa = ddt.get_text_w(_("Get user loves"), 211) + 10 * gui.scale - wb = ddt.get_text_w(_("Clear local loves"), 211) + 10 * gui.scale - wc = ddt.get_text_w(_("Get friend loves"), 211) + 10 * gui.scale - ws = ddt.get_text_w(_("Get scrobble counts"), 211) + 10 * gui.scale - wcc = ddt.get_text_w(_("Clear"), 211) + 15 * gui.scale - # wd = ddt.get_text_w(_("Clear friend loves"),211) + 10 * gui.scale - ww = max(wa, wb, wc, ws) - - self.button(x, y, _("Get user loves"), self.get_user_love, width=ww) - self.button(x + ww + round(12 * gui.scale), y, _("Clear"), self.clear_local_loves, width=wcc) - - # y += 26 * gui.scale - # self.button(x, y, _("Clear local loves"), self.clear_local_loves, width=ww) - - y += 26 * gui.scale - - self.button(x, y, _("Get friend loves"), self.get_friend_love, width=ww) - self.button(x + ww + round(12 * gui.scale), y, _("Clear"), lastfm.clear_friends_love, width=wcc) - - y += 26 * gui.scale - self.button(x, y, _("Get scrobble counts"), self.get_scrobble_counts, width=ww) - self.button(x + ww + round(12 * gui.scale), y, _("Clear"), self.clear_scrobble_counts, width=wcc) - - - y += 33 * gui.scale - - old = prefs.lastfm_pull_love - prefs.lastfm_pull_love = self.toggle_square( - x, y, prefs.lastfm_pull_love, - _("Pull love on scrobble/rescan")) - if old != prefs.lastfm_pull_love and prefs.lastfm_pull_love: - show_message(_("Note that this will overwrite the local loved status if different to last.fm status")) - - y += 25 * gui.scale - - self.toggle_square( - x, y, toggle_scrobble_mark, - _("Show threshold marker")) - - if self.account_view == 2: - - ddt.text((x, y), "ListenBrainz", colours.box_sub_text, 213) - - y += 30 * gui.scale - self.button(x, y, _("Paste Token"), lb.paste_key) - - self.button(x + ddt.get_text_w(_("Paste Token"), 211) + 21 * gui.scale, y, _("Clear"), lb.clear_key) - - y += 35 * gui.scale - - if prefs.lb_token: - line = prefs.lb_token - ddt.text((x + 0 * gui.scale, y - 0 * gui.scale), line, colours.box_input_text, 212) - - y += 25 * gui.scale - link_pa2 = draw_linked_text((x + 0 * gui.scale, y), "https://listenbrainz.org/profile/", - colours.box_sub_text, 12) - link_rect2 = [x + 0 * gui.scale, y, link_pa2[1], 20 * gui.scale] - fields.add(link_rect2) - - if coll(link_rect2): - if not self.click: - gui.cursor_want = 3 - - if self.click: - webbrowser.open(link_pa2[2], new=2, autoraise=True) - - def clear_local_loves(self): - - if not key_shift_down: - show_message( - _("This will mark all tracks in local database as unloved!"), - _("Press button again while holding shift key if you're sure you want to do that."), - mode="warning") - return - - for key, star in star_store.db.items(): - star[1] = star[1].replace("L", "") - star_store.db[key] = star - - gui.pl_update += 1 - show_message(_("Cleared all loves"), mode="done") - - def get_scrobble_counts(self): - - if not key_shift_down: - t = lastfm.get_all_scrobbles_estimate_time() - if not t: - show_message(_("Error, not connected to last.fm")) - return - show_message( - _("Warning: This process will take approximately {T} minutes to complete.").format(T=(t // 60)), - _("Press again while holding Shift if you understand"), mode="warning") - return - - if not lastfm.scanning_friends and not lastfm.scanning_scrobbles and not lastfm.scanning_loves: - shoot_dl = threading.Thread(target=lastfm.get_all_scrobbles) - shoot_dl.daemon = True - shoot_dl.start() - else: - show_message(_("A process is already running. Wait for it to finish.")) - - def clear_scrobble_counts(self): - - for track in pctl.master_library.values(): - track.lfm_scrobbles = 0 - - show_message(_("Cleared all scrobble counts"), mode="done") - - def get_friend_love(self): - - if not key_shift_down: - show_message( - _("Warning: This process can take a long time to complete! (up to an hour or more)"), - _("This feature is not recommended for accounts that have many friends."), - _("Press again while holding Shift if you understand"), mode="warning") - return - - if not lastfm.scanning_friends and not lastfm.scanning_scrobbles and not lastfm.scanning_loves: - logging.info("Launch friend love thread") - shoot_dl = threading.Thread(target=lastfm.get_friends_love) - shoot_dl.daemon = True - shoot_dl.start() - else: - show_message(_("A process is already running. Wait for it to finish.")) - - def get_user_love(self): - - if not lastfm.scanning_friends and not lastfm.scanning_scrobbles and not lastfm.scanning_loves: - shoot_dl = threading.Thread(target=lastfm.dl_love) - shoot_dl.daemon = True - shoot_dl.start() - else: - show_message(_("A process is already running. Wait for it to finish.")) - - def codec_config(self, x0, y0, w0, h0): - - x = x0 + round(25 * gui.scale) - y = y0 - - y += 20 * gui.scale - ddt.text_background_colour = colours.box_background - - if self.sync_view: - - pl = None - if prefs.sync_playlist: - pl = id_to_pl(prefs.sync_playlist) - if pl is None: - prefs.sync_playlist = None - - y += 5 * gui.scale - if prefs.sync_playlist: - ww = ddt.text((x, y), _("Selected playlist:") + " ", colours.box_text_label, 11) - ddt.text((x + ww, y), pctl.multi_playlist[pl].title, colours.box_sub_text, 12, 400 * gui.scale) - else: - ddt.text((x, y), _("No sync playlist selected!"), colours.box_text_label, 11) - - y += 25 * gui.scale - ww = ddt.text((x, y), _("Path to device music folder: "), colours.box_text_label, 11) - y += 20 * gui.scale - - rect1 = (x + 0 * gui.scale, y, round(450 * gui.scale), round(17 * gui.scale)) - fields.add(rect1) - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - sync_target.draw( - x + round(4 * gui.scale), y, colours.box_input_text, not gui.sync_progress, - width=rect1[2] - 8 * gui.scale, click=self.click) - - rect = [x + rect1[2] + 11 * gui.scale, y - 2 * gui.scale, 15 * gui.scale, 19 * gui.scale] - fields.add(rect) - colour = colours.box_text_label - if coll(rect): - colour = [225, 160, 0, 255] - if self.click: - paths = auto_get_sync_targets() - if paths: - sync_target.text = paths[0] - show_message(_("A mounted music folder was found!"), mode="done") - else: - show_message( - _("Could not auto-detect mounted device path."), - _("Make sure the device is mounted and path is accessible.")) - - power_bar_icon.render(rect[0], rect[1], colour) - y += 30 * gui.scale - - prefs.sync_deletes = self.toggle_square(x, y, prefs.sync_deletes, _("Delete all other folders in target")) - y += 25 * gui.scale - prefs.bypass_transcode = self.toggle_square( - x, y, prefs.bypass_transcode ^ True, - _("Transcode files")) ^ True - y += 25 * gui.scale - prefs.smart_bypass = self.toggle_square( - x + round(10 * gui.scale), y, prefs.smart_bypass ^ True, - _("Bypass low bitrate")) ^ True - y += 30 * gui.scale - - text = _("Start Transcode and Sync") - ww = ddt.get_text_w(text, 211) + 25 * gui.scale - if prefs.bypass_transcode: - text = _("Start Sync") - - xx = (rect1[0] + (rect1[2] // 2)) - (ww // 2) - if gui.stop_sync: - self.button(xx, y, _("Stopping..."), width=ww) - elif not gui.sync_progress: - if self.button(xx, y, text, width=ww): - if pl is not None: - auto_sync(pl) - else: - show_message( - _("Select a source playlist"), - _("Right click tab > Misc... > Set as sync playlist")) - elif self.button(xx, y, _("Stop"), width=ww): - gui.stop_sync = True - gui.sync_progress = _("Aborting Sync") - - y += 60 * gui.scale - - if self.button(x, y, _("Return"), width=round(75 * gui.scale)): - self.sync_view = False - - if self.button(x + 485 * gui.scale, y, _("?")): - show_message( - _("See here for detailed instructions"), - "https://github.com/Taiko2k/Tauon/wiki/Transcode-and-Sync", mode="link") - - return - - # ---------- - - ddt.text((x, y + 13 * gui.scale), _("Output codec setting:"), colours.box_text_label, 11) - - ww = ddt.get_text_w(_("Open output folder"), 211) + 25 * gui.scale - self.button(x0 + w0 - ww, y - 4 * gui.scale, _("Open output folder"), open_encode_out) - - ww = ddt.get_text_w(_("Sync..."), 211) + 25 * gui.scale - if self.button(x0 + w0 - ww, y + 25 * gui.scale, _("Sync...")): - self.sync_view = True - - y += 40 * gui.scale - self.toggle_square(x, y, switch_flac, "FLAC") - y += 25 * gui.scale - self.toggle_square(x, y, switch_opus, "OPUS") - if prefs.transcode_codec == "opus": - self.toggle_square(x + 120 * gui.scale, y, switch_opus_ogg, _("Save opus as .ogg extension")) - y += 25 * gui.scale - self.toggle_square(x, y, switch_ogg, "OGG Vorbis") - y += 25 * gui.scale - - # if not flatpak_mode: - self.toggle_square(x, y, switch_mp3, "MP3") - # if prefs.transcode_codec == 'mp3' and not shutil.which("lame"): - # ddt.draw_text((x + 90 * gui.scale, y - 3 * gui.scale), "LAME not detected!", [220, 110, 110, 255], 12) - - if prefs.transcode_codec != "flac": - y += 35 * gui.scale - - prefs.transcode_bitrate = self.slide_control(x, y, _("Bitrate"), "kbs", prefs.transcode_bitrate, 32, 320, 8) - - y -= 1 * gui.scale - x += 280 * gui.scale - - x = x0 + round(20 * gui.scale) - y = y0 + 215 * gui.scale - - self.toggle_square(x, y, toggle_transcode_output, _("Save to output folder")) - y += 25 * gui.scale - self.toggle_square(x, y, toggle_transcode_inplace, _("Save and overwrite files inplace")) - - def devance_theme(self): - global theme - - theme -= 1 - gui.reload_theme = True - if theme < 0: - theme = len(get_themes()) - - def config_b(self, x0, y0, w0, h0): - - global album_mode_art_size - global update_layout - - ddt.text_background_colour = colours.box_background - x = x0 + round(25 * gui.scale) - y = y0 + round(20 * gui.scale) - - # ddt.text((x, y), _("Window"),colours.box_text_label, 12) - - if system == "Linux": - self.toggle_square(x, y, toggle_notifications, _("Emit track change notifications")) - - y += 25 * gui.scale - self.toggle_square(x, y, toggle_borderless, _("Draw own window decorations")) - - # y += 25 * gui.scale - # prefs.save_window_position = self.toggle_square(x, y, prefs.save_window_position, - # _("Restore window position on restart")) - - y += 25 * gui.scale - if not draw_border: - self.toggle_square(x, y, toggle_titlebar_line, _("Show playing in titlebar")) - - #y += 25 * gui.scale - # if system != 'windows' and (flatpak_mode or snap_mode): - # self.toggle_square(x, y, toggle_force_subpixel, _("Enable RGB text antialiasing")) - - y += 25 * gui.scale - old = prefs.mini_mode_on_top - prefs.mini_mode_on_top = self.toggle_square(x, y, prefs.mini_mode_on_top, _("Mini-mode always on top")) - if wayland and prefs.mini_mode_on_top and prefs.mini_mode_on_top != old: - show_message(_("Always-on-top feature not yet implemented for Wayland mode"), _("You can enable the x11 setting below as a workaround")) - - y += 25 * gui.scale - self.toggle_square(x, y, toggle_level_meter, _("Top-panel visualiser")) - - y += 25 * gui.scale - if prefs.backend == 4: - self.toggle_square(x, y, toggle_showcase_vis, _("Showcase visualisation")) - - y += round(30 * gui.scale) - # if not msys: - # y += round(15 * gui.scale) - - ddt.text((x, y), _("UI scale for HiDPI displays"), colours.box_text_label, 12) - - y += round(25 * gui.scale) - - sw = round(200 * gui.scale) - sh = round(2 * gui.scale) - - slider = (x, y, sw, sh) - - gh = round(14 * gui.scale) - gw = round(8 * gui.scale) - grip = [0, y - (gh // 2), gw, gh] - - grip[0] = x - grip[0] += ((prefs.scale_want - 0.5) / 3 * sw) - - m1 = (x + ((1.0 - 0.5) / 3 * sw), y, sh, sh * 2) - m2 = (x + ((2.0 - 0.5) / 3 * sw), y, sh, sh * 2) - m3 = (x + ((3.0 - 0.5) / 3 * sw), y, sh, sh * 2) - - if coll(grow_rect(slider, round(16 * gui.scale))) and mouse_down: - prefs.scale_want = ((mouse_position[0] - x) / sw * 3) + 0.5 - prefs.x_scale = False - gui.update_on_drag = True - prefs.scale_want = max(prefs.scale_want, 0.5) - prefs.scale_want = min(prefs.scale_want, 3.5) - prefs.scale_want = round(round(prefs.scale_want / 0.05) * 0.05, 2) - if prefs.scale_want == 0.95 or prefs.scale_want == 1.05: - prefs.scale_want = 1.0 - if prefs.scale_want == 1.95 or prefs.scale_want == 2.05: - prefs.scale_want = 2.0 - if prefs.scale_want == 2.95 or prefs.scale_want == 3.05: - prefs.scale_want = 3.0 - - text = str(prefs.scale_want) - if len(text) == 3: - text += "0" - text += "x" - - if prefs.x_scale: - text = "auto" - - font = 13 - if not prefs.x_scale and (prefs.scale_want == 1.0 or prefs.scale_want == 2.0 or prefs.scale_want == 3.0): - font = 313 - - ddt.text((x + sw + round(14 * gui.scale), y - round(8 * gui.scale)), text, colours.box_sub_text, font) - # ddt.text((x + sw + round(14 * gui.scale), y + round(10 * gui.scale)), _("Restart app to apply any changes"), colours.box_text_label, 11) - - ddt.rect(slider, colours.box_text_border) - ddt.rect(m1, colours.box_text_border) - ddt.rect(m2, colours.box_text_border) - ddt.rect(m3, colours.box_text_border) - ddt.rect(grip, colours.box_text_label) - - y += round(23 * gui.scale) - self.toggle_square(x, y, self.toggle_x_scale, _("Auto scale")) - - if prefs.scale_want != gui.scale: - gui.update += 1 - if not mouse_down: - gui.update_layout() - - y += round(25 * gui.scale) - if not msys and not macos: - x11_path = str(user_directory / "x11") - x11 = os.path.exists(x11_path) - old = x11 - x11 = self.toggle_square(x, y, x11, _("Prefer x11 when running in Wayland")) - if old is False and x11 is True: - with open(x11_path, "a"): - pass - elif old is True and x11 is False: - os.remove(x11_path) - - def toggle_x_scale(self, mode=0): - if mode == 1: - return prefs.x_scale - prefs.x_scale ^= True - auto_scale() - gui.update_layout() - - def about(self, x0, y0, w0, h0): - - x = x0 + int(w0 * 0.3) - 10 * gui.scale - y = y0 + 85 * gui.scale - - ddt.text_background_colour = colours.box_background - - icon_rect = (x - 110 * gui.scale, y - 15 * gui.scale, self.about_image.w, self.about_image.h) - - genre = "" - if pctl.playing_object() is not None: - genre = pctl.playing_object().genre.lower() - - if any(s in genre for s in ["ock", "lt"]): - self.about_image2.render(icon_rect[0], icon_rect[1]) - elif any(s in genre for s in ["kpop", "k-pop", "anime"]): - self.about_image6.render(icon_rect[0], icon_rect[1]) - elif any(s in genre for s in ["syn", "pop"]): - self.about_image3.render(icon_rect[0], icon_rect[1]) - elif any(s in genre for s in ["tro", "cid"]): - self.about_image4.render(icon_rect[0], icon_rect[1]) - elif any(s in genre for s in ["uture"]): - self.about_image5.render(icon_rect[0], icon_rect[1]) - else: - genre = "" - - if not genre: - self.about_image.render(icon_rect[0], icon_rect[1]) - - x += 20 * gui.scale - y -= 10 * gui.scale - - self.title_image.render(x - 1, y, alpha_mod(colours.box_sub_text, 240)) - - credit_pages = 5 - - if self.click and coll(icon_rect) and self.ani_cred == 0: - self.ani_cred = 1 - self.ani_fade_on_timer.set() - - fade = 0 - - if self.ani_cred == 1: - t = self.ani_fade_on_timer.get() - fade = round(t / 0.7 * 255) - fade = min(fade, 255) - - if t > 0.7: - self.ani_cred = 2 - self.cred_page += 1 - if self.cred_page > credit_pages: - self.cred_page = 0 - self.ani_fade_on_timer.set() - - gui.update = 2 - - if self.ani_cred == 2: - - t = self.ani_fade_on_timer.get() - fade = 255 - round(t / 0.7 * 255) - fade = max(fade, 0) - if t > 0.7: - self.ani_cred = 0 - - gui.update = 2 - - y += 32 * gui.scale - - block_y = y - 10 * gui.scale - - if self.cred_page == 0: - - ddt.text((x, y - 6 * gui.scale), t_version, colours.box_text_label, 313) - y += 19 * gui.scale - ddt.text((x, y), "Copyright © 2015-2024 Taiko2k captain.gxj@gmail.com", colours.box_sub_text, 13) - - y += 19 * gui.scale - link_pa = draw_linked_text( - (x, y), "https://tauonmusicbox.rocks", colours.box_sub_text, 12, - replace="tauonmusicbox.rocks") - link_rect = [x, y, link_pa[1], 18 * gui.scale] - if coll(link_rect): - if not self.click: - gui.cursor_want = 3 - if self.click: - webbrowser.open(link_pa[2], new=2, autoraise=True) - - fields.add(link_rect) - - y += 27 * gui.scale - ddt.text((x, y), _("This program comes with absolutely no warranty."), colours.box_text_label, 12) - y += 16 * gui.scale - link_gpl = "https://www.gnu.org/licenses/gpl-3.0.html" - link_pa = draw_linked_text( - (x, y), _("See the {link} license for details.").format(link=link_gpl), - colours.box_text_label, 12, replace="GNU GPLv3+") - link_rect = [x + link_pa[0], y, link_pa[1], 18 * gui.scale] - if coll(link_rect): - if not self.click: - gui.cursor_want = 3 - if self.click: - webbrowser.open(link_pa[2], new=2, autoraise=True) - fields.add(link_rect) - - elif self.cred_page == 1: - - y += 15 * gui.scale - - ddt.text((x, y + 1 * gui.scale), _("Created by"), colours.box_text_label, 13) - ddt.text((x + 120 * gui.scale, y + 1 * gui.scale), "Taiko2k", colours.box_sub_text, 13) - - y += 40 * gui.scale - link_pa = draw_linked_text( - (x, y), "https://github.com/Taiko2k/Tauon/graphs/contributors", - colours.box_sub_text, 12, replace=_("Contributors")) - link_rect = [x, y, link_pa[1], 18 * gui.scale] - if coll(link_rect): - if not self.click: - gui.cursor_want = 3 - if self.click: - webbrowser.open(link_pa[2], new=2, autoraise=True) - fields.add(link_rect) - - - elif self.cred_page == 2: - xx = x + round(160 * gui.scale) - xxx = x + round(240 * gui.scale) - ddt.text((x, y), _("Open source software used"), colours.box_text_label, 13) - font = 12 - spacing = round(18 * gui.scale) - y += spacing - ddt.text((x, y), "Simple DirectMedia Layer", colours.box_sub_text, font) - ddt.text((xx, y), "zlib", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://www.libsdl.org/", colours.box_sub_text, font, click=self.click, replace="libsdl.org") - - y += spacing - ddt.text((x, y), "Cairo Graphics", colours.box_sub_text, font) - ddt.text((xx, y), "MPL", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://www.cairographics.org/", colours.box_sub_text, font, click=self.click, replace="cairographics.org") - - y += spacing - ddt.text((x, y), "Pango", colours.box_sub_text, font) - ddt.text((xx, y), "LGPL", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://pango.gnome.org/", colours.box_sub_text, font, click=self.click, replace="pango.gnome.org") - - y += spacing - ddt.text((x, y), "FFmpeg", colours.box_sub_text, font) - ddt.text((xx, y), "GPL", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://ffmpeg.org/", colours.box_sub_text, font, click=self.click, replace="ffmpeg.org") - - y += spacing - ddt.text((x, y), "Pillow", colours.box_sub_text, font) - ddt.text((xx, y), "PIL License", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://python-pillow.org/", colours.box_sub_text, font, click=self.click, replace="python-pillow.org") - - - elif self.cred_page == 4: - xx = x + round(140 * gui.scale) - xxx = x + round(240 * gui.scale) - ddt.text((x, y), _("Open source software used (cont'd)"), colours.box_text_label, 13) - font = 12 - spacing = round(18 * gui.scale) - y += spacing - ddt.text((x, y), "PySDL2", colours.box_sub_text, font) - ddt.text((xx, y), _("Public Domain"), colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://github.com/marcusva/py-sdl2", colours.box_sub_text, font, click=self.click, replace="github") - - y += spacing - ddt.text((x, y), "Tekore", colours.box_sub_text, font) - ddt.text((xx, y), "MIT", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://github.com/felix-hilden/tekore", colours.box_sub_text, font, click=self.click, replace="github") - - y += spacing - ddt.text((x, y), "pyLast", colours.box_sub_text, font) - ddt.text((xx, y), "Apache 2.0", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://github.com/pylast/pylast", colours.box_sub_text, font, click=self.click, replace="github") - - y += spacing - ddt.text((x, y), "Noto Sans font", colours.box_sub_text, font) - ddt.text((xx, y), "Apache 2.0", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://fonts.google.com/specimen/Noto+Sans", colours.box_sub_text, font, click=self.click, replace="fonts.google.com") - - # y += spacing - # ddt.text((x, y), "Stagger", colours.box_sub_text, font) - # ddt.text((xx, y), "BSD 2-Clause", colours.box_text_label, font) - # d"raw_linked_text2(xxx, y, "https://github.com/staggerpkg/stagger", colours.box_sub_text, font, click=self.click, replace="github") - - y += spacing - ddt.text((x, y), "KISS FFT", colours.box_sub_text, font) - ddt.text((xx, y), "New BSD License", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://github.com/mborgerding/kissfft", colours.box_sub_text, font, click=self.click, replace="github") - - elif self.cred_page == 3: - xx = x + round(130 * gui.scale) - xxx = x + round(240 * gui.scale) - ddt.text((x, y), _("Open source software used (cont'd)"), colours.box_text_label, 13) - font = 12 - spacing = round(18 * gui.scale) - y += spacing - ddt.text((x, y), "libFLAC", colours.box_sub_text, font) - ddt.text((xx, y), "New BSD License", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://xiph.org/flac/", colours.box_sub_text, font, click=self.click, replace="xiph.org") - - y += spacing - ddt.text((x, y), "libvorbis", colours.box_sub_text, font) - ddt.text((xx, y), "BSD License", colours.box_text_label, font) - draw_linked_text2(xxx, y, "https://xiph.org/vorbis/", colours.box_sub_text, font, click=self.click, replace="xiph.org") - - y += spacing - ddt.text((x, y), "opusfile", colours.box_sub_text, font) - ddt.text((xx, y), "New BSD license", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://opus-codec.org/", colours.box_sub_text, font, click=self.click, replace="opus-codec.org") - - y += spacing - ddt.text((x, y), "mpg123", colours.box_sub_text, font) - ddt.text((xx, y), "LGPL 2.1", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://www.mpg123.de/", colours.box_sub_text, font, click=self.click, replace="mpg123.de") - - y += spacing - ddt.text((x, y), "Secret Rabbit Code", colours.box_sub_text, font) - ddt.text((xx, y), "BSD 2-Clause", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "http://www.mega-nerd.com/SRC/index.html", colours.box_sub_text, font, click=self.click, replace="mega-nerd.com") - - y += spacing - ddt.text((x, y), "libopenmpt", colours.box_sub_text, font) - ddt.text((xx, y), "New BSD License", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://lib.openmpt.org/libopenmpt", colours.box_sub_text, font, click=self.click, replace="lib.openmpt.org") - - elif self.cred_page == 5: - xx = x + round(130 * gui.scale) - xxx = x + round(240 * gui.scale) - ddt.text((x, y), _("Open source software used (cont'd)"), colours.box_text_label, 13) - font = 12 - spacing = round(18 * gui.scale) - y += spacing - ddt.text((x, y), "Mutagen", colours.box_sub_text, font) - ddt.text((xx, y), "GPLv2+", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://github.com/quodlibet/mutagen", colours.box_sub_text, font, click=self.click, replace="github") - - y += spacing - ddt.text((x, y), "unidecode", colours.box_sub_text, font) - ddt.text((xx, y), "GPL-2.0+", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://github.com/avian2/unidecode", colours.box_sub_text, font, click=self.click, replace="github") - - y += spacing - ddt.text((x, y), "pypresence", colours.box_sub_text, font) - ddt.text((xx, y), "MIT", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://github.com/qwertyquerty/pypresence", colours.box_sub_text, font, click=self.click, replace="github") - - y += spacing - ddt.text((x, y), "musicbrainzngs", colours.box_sub_text, font) - ddt.text((xx, y), "Simplified BSD", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://github.com/alastair/python-musicbrainzngs", colours.box_sub_text, font, click=self.click, replace="github") - - y += spacing - ddt.text((x, y), "Send2Trash", colours.box_sub_text, font) - ddt.text((xx, y), "New BSD License", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://github.com/arsenetar/send2trash", colours.box_sub_text, font, click=self.click, replace="github") - - y += spacing - ddt.text((x, y), "GTK/PyGObject", colours.box_sub_text, font) - ddt.text((xx, y), "LGPLv2.1+", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://gitlab.gnome.org/GNOME/pygobject", colours.box_sub_text, font, click=self.click, replace="gitlab.gnome.org") - - ddt.rect((x, block_y, 369 * gui.scale, 140 * gui.scale), alpha_mod(colours.box_background, fade)) - - y = y0 + h0 - round(33 * gui.scale) - x = x0 + w0 - 0 * gui.scale - - w = max(ddt.get_text_w(_("Credits"), 211), ddt.get_text_w(_("Next"), 211)) - x -= w + round(40 * gui.scale) - - text = _("Credits") - if self.cred_page != 0: - text = _("Next") - if self.button(x, y, text, width=w + round(25 * gui.scale)): - self.ani_cred = 1 - self.ani_fade_on_timer.set() - - def topchart(self, x0, y0, w0, h0): - - x = x0 + round(25 * gui.scale) - y = y0 + 20 * gui.scale - - ddt.text_background_colour = colours.box_background - - ddt.text((x, y), _("Chart Grid Generator"), colours.box_text, 214) - - y += 25 * gui.scale - ww = ddt.text((x, y), _("Target playlist: "), colours.box_sub_text, 312) - ddt.text( - (x + ww, y), pctl.multi_playlist[pctl.active_playlist_viewing].title, colours.box_text_label, 12, - 400 * gui.scale) - # x -= 210 * gui.scale - - y += 30 * gui.scale - - if prefs.chart_cascade: - if prefs.chart_d1: - prefs.chart_c1 = self.slide_control(x, y, _("Level 1"), "", prefs.chart_c1, 2, 20, 1, width=35) - y += 22 * gui.scale - if prefs.chart_d2: - prefs.chart_c2 = self.slide_control(x, y, _("Level 2"), "", prefs.chart_c2, 2, 20, 1, width=35) - y += 22 * gui.scale - if prefs.chart_d3: - prefs.chart_c3 = self.slide_control(x, y, _("Level 3"), "", prefs.chart_c3, 2, 20, 1, width=35) - - y -= 44 * gui.scale - x += 133 * gui.scale - prefs.chart_d1 = self.slide_control(x, y, _("by"), "", prefs.chart_d1, 0, 10, 1, width=35) - y += 22 * gui.scale - prefs.chart_d2 = self.slide_control(x, y, _("by"), "", prefs.chart_d2, 0, 10, 1, width=35) - y += 22 * gui.scale - prefs.chart_d3 = self.slide_control(x, y, _("by"), "", prefs.chart_d3, 0, 10, 1, width=35) - x -= 133 * gui.scale - - else: - - prefs.chart_rows = self.slide_control(x, y, _("Rows"), "", prefs.chart_rows, 1, 100, 1, width=35) - y += 22 * gui.scale - prefs.chart_columns = self.slide_control(x, y, _("Columns"), "", prefs.chart_columns, 1, 100, 1, width=35) - y += 22 * gui.scale - - y += 35 * gui.scale - x += 5 * gui.scale - - prefs.chart_cascade = self.toggle_square(x, y, prefs.chart_cascade, _("Cascade style")) - y += 25 * gui.scale - prefs.chart_tile = self.toggle_square(x, y, prefs.chart_tile ^ True, _("Use padding")) ^ True - - y -= 25 * gui.scale - x += 170 * gui.scale - - prefs.chart_text = self.toggle_square(x, y, prefs.chart_text, _("Include album titles")) - y += 25 * gui.scale - prefs.topchart_sorts_played = self.toggle_square(x, y, prefs.topchart_sorts_played, _("Sort by top played")) - - x = x0 + 15 * gui.scale + 320 * gui.scale - y = y0 + 100 * gui.scale - - # . Limited width. Max 13 chars - if self.button(x, y, _("Randomise BG")): - - r = round(random.random() * 40) - g = round(random.random() * 40) - b = round(random.random() * 40) - - prefs.chart_bg = [r, g, b] - - d = random.randrange(0, 4) - - if d == 1: - c = 5 + round(random.random() * 20) - prefs.chart_bg = [c, c, c] - - x += 100 * gui.scale - y -= 20 * gui.scale - - display_colour = (prefs.chart_bg[0], prefs.chart_bg[1], prefs.chart_bg[2], 255) - - rect = (x, y, 70 * gui.scale, 70 * gui.scale) - ddt.rect(rect, display_colour) - - ddt.rect_s(rect, (50, 50, 50, 255), round(1 * gui.scale)) - - # x = self.box_x + self.item_x_offset + 200 * gui.scale - # y = self.box_y + 180 * gui.scale - - x = x0 + 260 * gui.scale - y = y0 + 180 * gui.scale - - dex = reload_albums(quiet=True, return_playlist=pctl.active_playlist_viewing) - - x = x0 + round(110 * gui.scale) - y = y0 + 240 * gui.scale - - # . Limited width. Max 9 chars - if self.button(x, y, _("Generate"), width=80 * gui.scale): - if gui.generating_chart: - show_message(_("Be patient!")) - elif not prefs.chart_font: - show_message(_("No font set in config"), mode="error") - else: - shoot = threading.Thread(target=gen_chart) - shoot.daemon = True - shoot.start() - gui.generating_chart = True - - x += round(95 * gui.scale) - if gui.generating_chart: - ddt.text((x, y + round(1 * gui.scale)), _("Generating..."), colours.box_text_label, 12) - else: - - count = prefs.chart_rows * prefs.chart_columns - if prefs.chart_cascade: - count = prefs.chart_c1 * prefs.chart_d1 + prefs.chart_c2 * prefs.chart_d2 + prefs.chart_c3 * prefs.chart_d3 - - line = _("{N} Album chart").format(N=str(count)) - - ww = ddt.text((x, y + round(1 * gui.scale)), line, colours.box_text_label, 12) - - if len(dex) < count: - ddt.text( - (x + ww + round(10 * gui.scale), y + 1 * gui.scale), _("Not enough albums in the playlist!"), - [255, 120, 125, 255], 12) - - x = x0 + round(20 * gui.scale) - y = y0 + 240 * gui.scale - - # . Limited width. Max 8 chars - if self.button(x, y, _("Return"), width=75 * gui.scale): - self.chart_view = 0 - - def stats(self, x0, y0, w0, h0): - - x = x0 + 10 * gui.scale - y = y0 - - if self.chart_view == 1: - self.topchart(x0, y0, w0, h0) - return - - ww = ddt.get_text_w(_("Chart generator..."), 211) + 30 * gui.scale - if system == "Linux" and self.button(x0 + w0 - ww, y + 15 * gui.scale, _("Chart generator...")): - self.chart_view = 1 - - ddt.text_background_colour = colours.box_background - lt_font = 312 - lt_colour = colours.box_text_label - - w1 = ddt.get_text_w(_("Tracks in playlist"), 12) - w2 = ddt.get_text_w(_("Albums in playlist"), 12) - w3 = ddt.get_text_w(_("Playlist duration"), 12) - w4 = ddt.get_text_w(_("Tracks in database"), 12) - w5 = ddt.get_text_w(_("Total albums"), 12) - w6 = ddt.get_text_w(_("Total playtime"), 12) - - x1 = x + (8 + 10 + 10) * gui.scale - x2 = x1 + max(w1, w2, w3, w4, w5, w6) + 20 * gui.scale - y1 = y + 50 * gui.scale - - if self.stats_pl != pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int or self.stats_pl_timer.get() > 5: - self.stats_pl = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - self.stats_pl_timer.set() - - album_names = set() - folder_names = set() - count = 0 - - for track_id in default_playlist: - tr = pctl.get_track(track_id) - - if not tr.album: - if tr.parent_folder_path not in folder_names: - count += 1 - folder_names.add(tr.parent_folder_path) - else: - if tr.parent_folder_path not in folder_names and tr.album not in album_names: - count += 1 - folder_names.add(tr.parent_folder_path) - album_names.add(tr.album) - - self.stats_pl_albums = count - - self.stats_pl_length = 0 - for item in default_playlist: - self.stats_pl_length += pctl.master_library[item].length - - line = seconds_to_day_hms(self.stats_pl_length, strings.day, strings.days) - - ddt.text((x1, y1), _("Tracks in playlist"), lt_colour, lt_font) - ddt.text((x2, y1), py_locale.format_string("%d", len(default_playlist), True), colours.box_sub_text, 12) - y1 += 20 * gui.scale - ddt.text((x1, y1), _("Albums in playlist"), lt_colour, lt_font) - ddt.text((x2, y1), str(self.stats_pl_albums), colours.box_sub_text, 12) - y1 += 20 * gui.scale - ddt.text((x1, y1), _("Playlist duration"), lt_colour, lt_font) - - ddt.text((x2, y1), line, colours.box_sub_text, 12) - - if self.stats_timer.get() > 5: - album_names = set() - folder_names = set() - count = 0 - - for pl in pctl.multi_playlist: - for track_id in pl.playlist_ids: - tr = pctl.get_track(track_id) - - if not tr.album: - if tr.parent_folder_path not in folder_names: - count += 1 - folder_names.add(tr.parent_folder_path) - else: - if tr.parent_folder_path not in folder_names and tr.album not in album_names: - count += 1 - folder_names.add(tr.parent_folder_path) - album_names.add(tr.album) - - self.total_albums = count - - self.stats_timer.set() - - y1 += 40 * gui.scale - ddt.text((x1, y1), _("Tracks in database"), lt_colour, lt_font) - ddt.text((x2, y1), py_locale.format_string("%d", len(pctl.master_library), True), colours.box_sub_text, 12) - y1 += 20 * gui.scale - ddt.text((x1, y1), _("Total albums"), lt_colour, lt_font) - ddt.text((x2, y1), str(self.total_albums), colours.box_sub_text, 12) - - y1 += 20 * gui.scale - ddt.text((x1, y1), _("Total playtime"), lt_colour, lt_font) - ddt.text((x2, y1), seconds_to_day_hms(pctl.total_playtime, strings.day, strings.days), colours.box_sub_text, 15) - - # Ratio bar - if len(pctl.master_library) > 115 * gui.scale: - x = x0 - y = y0 + h0 - 7 * gui.scale - - full_rect = [x, y, w0, 7 * gui.scale] - d = 0 - - # Stats - try: - if self.last_db_size != len(pctl.master_library): - self.last_db_size = len(pctl.master_library) - self.ext_ratio = {} - for key, value in pctl.master_library.items(): - if value.file_ext in self.ext_ratio: - self.ext_ratio[value.file_ext] += 1 - else: - self.ext_ratio[value.file_ext] = 1 - - for key, value in self.ext_ratio.items(): - - colour = [200, 200, 200, 255] - if key in format_colours: - colour = format_colours[key] - - colour = colorsys.rgb_to_hls(colour[0] / 255, colour[1] / 255, colour[2] / 255) - colour = colorsys.hls_to_rgb(1 - colour[0], colour[1] * 0.8, colour[2] * 0.8) - colour = [int(colour[0] * 255), int(colour[1] * 255), int(colour[2] * 255), 255] - - h = int(round(value / len(pctl.master_library) * full_rect[2])) - block_rect = [full_rect[0] + d, full_rect[1], h, full_rect[3]] - - ddt.rect(block_rect, colour) - d += h - - block_rect = (block_rect[0], block_rect[1], block_rect[2] - 1, block_rect[3]) - fields.add(block_rect) - if coll(block_rect): - xx = block_rect[0] + int(block_rect[2] / 2) - xx = max(xx, x + 30 * gui.scale) - xx = min(xx, x0 + w0 - 30 * gui.scale) - ddt.text((xx, y0 + h0 - 35 * gui.scale, 2), key, colours.grey_blend_bg(220), 13) - - if self.click: - gen_codec_pl(key) - except Exception: - logging.exception("Error draw ext bar") - - def config_v(self, x0, y0, w0, h0): - - ddt.text_background_colour = colours.box_background - - x = x0 + self.item_x_offset - y = y0 + 17 * gui.scale - - self.toggle_square(x, y, rating_toggle, _("Track ratings")) - y += round(25 * gui.scale) - self.toggle_square(x, y, album_rating_toggle, _("Album ratings")) - y += round(35 * gui.scale) - - self.toggle_square(x, y, heart_toggle, " ") - heart_row_icon.render(x + round(23 * gui.scale), y + round(2 * gui.scale), colours.box_text) - rect = (x, y + round(2 * gui.scale), 40 * gui.scale, 15 * gui.scale) - fields.add(rect) - if coll(rect): - ex_tool_tip(x + round(45 * gui.scale), y - 20 * gui.scale, 0, _("Show track loves"), 12) - - x += (55 * gui.scale) - self.toggle_square(x, y, star_toggle, " ") - star_row_icon.render(x + round(22 * gui.scale), y + round(0 * gui.scale), colours.box_text) - rect = (x, y + round(2 * gui.scale), 40 * gui.scale, 15 * gui.scale) - fields.add(rect) - if coll(rect): - ex_tool_tip(x + round(35 * gui.scale), y - 20 * gui.scale, 0, _("Represent playtime as stars"), 12) - - x += (55 * gui.scale) - self.toggle_square(x, y, star_line_toggle, " ") - ddt.rect( - (x + round(21 * gui.scale), y + round(6 * gui.scale), round(15 * gui.scale), round(1 * gui.scale)), - colours.box_text) - rect = (x, y + round(2 * gui.scale), 40 * gui.scale, 15 * gui.scale) - fields.add(rect) - if coll(rect): - ex_tool_tip(x + round(35 * gui.scale), y - 20 * gui.scale, 0, _("Represent playcount as lines"), 12) - - x = x0 + self.item_x_offset - - # y += round(25 * gui.scale) - - # self.toggle_square(x, y, star_line_toggle, _('Show playtime lines')) - y += round(15 * gui.scale) - - # if gui.show_ratings: - # x += round(10 * gui.scale) - # #self.toggle_square(x, y, star_toggle, _('Show playtime stars')) - # if gui.show_ratings: - # x -= round(10 * gui.scale) - - - y += round(25 * gui.scale) - - if self.toggle_square(x, y, prefs.row_title_format == 2, _("Left align title style")): - prefs.row_title_format = 2 - else: - prefs.row_title_format = 1 - - y += round(25 * gui.scale) - - prefs.row_title_genre = self.toggle_square(x + round(10 * gui.scale), y, prefs.row_title_genre, _("Show album genre")) - y += round(25 * gui.scale) - - self.toggle_square(x, y, toggle_append_date, _("Show album release year")) - y += round(25 * gui.scale) - - self.toggle_square(x, y, toggle_append_total_time, _("Show album duration")) - y += round(35 * gui.scale) - - if self.toggle_square(x, y, prefs.row_title_separator_type == 0, " - "): - prefs.row_title_separator_type = 0 - if self.toggle_square(x + round(55 * gui.scale), y, prefs.row_title_separator_type == 1, " ‒ "): - prefs.row_title_separator_type = 1 - if self.toggle_square(x + round(110 * gui.scale), y, prefs.row_title_separator_type == 2, " ⦁ "): - prefs.row_title_separator_type = 2 - x = x0 + 330 * gui.scale - y = y0 + 25 * gui.scale - - prefs.playlist_font_size = self.slide_control(x, y, _("Font Size"), "", prefs.playlist_font_size, 12, 17) - y += 25 * gui.scale - prefs.playlist_row_height = self.slide_control(x, y, _("Row Size"), "px", prefs.playlist_row_height, 15, 45) - y += 25 * gui.scale - prefs.tracklist_y_text_offset = self.slide_control( - x, y, _("Baseline offset"), "px", prefs.tracklist_y_text_offset, -10, 10) - y += 25 * gui.scale - - x += 65 * gui.scale - self.button(x, y, _("Thin default"), self.small_preset, 124 * gui.scale) - y += 27 * gui.scale - self.button(x, y, _("Thick default"), self.large_preset, 124 * gui.scale) - - - def set_playlist_cycle(self, mode=0): - if mode == 1: - return True if prefs.end_setting == "cycle" else False - prefs.end_setting = "cycle" - # global pl_follow - # pl_follow = False - - def set_playlist_advance(self, mode=0): - if mode == 1: - return True if prefs.end_setting == "advance" else False - prefs.end_setting = "advance" - # global pl_follow - # pl_follow = False - - def set_playlist_stop(self, mode=0): - if mode == 1: - return True if prefs.end_setting == "stop" else False - prefs.end_setting = "stop" - - def set_playlist_repeat(self, mode=0): - if mode == 1: - return True if prefs.end_setting == "repeat" else False - prefs.end_setting = "repeat" - - def small_preset(self): - - prefs.playlist_row_height = round(22 * prefs.ui_scale) - prefs.playlist_font_size = 15 - prefs.tracklist_y_text_offset = 0 - gui.update_layout() - - def large_preset(self): - - prefs.playlist_row_height = round(27 * prefs.ui_scale) - prefs.playlist_font_size = 15 - gui.update_layout() - - def slide_control(self, x, y, label, units, value, lower_limit, upper_limit, step=1, callback=None, width=58): - - width = round(width * gui.scale) - - if label is not None: - ddt.text((x + 55 * gui.scale, y, 1), label, colours.box_text, 312) - x += 65 * gui.scale - y += 1 * gui.scale - rect = (x, y, 33 * gui.scale, 15 * gui.scale) - fields.add(rect) - ddt.rect(rect, colours.box_button_background) - abg = [255, 255, 255, 40] - if coll(rect): - - if self.click: - if value > lower_limit: - value -= step - gui.update_layout() - if callback is not None: - callback(value) - - if mouse_down: - abg = [230, 120, 20, 255] - else: - abg = [220, 150, 20, 255] - - if colour_value(colours.box_background) > 300: - abg = colours.box_sub_text - - dec_arrow.render(x + 1 * gui.scale, y, abg) - - x += 33 * gui.scale - - ddt.rect((x, y, width, 15 * gui.scale), alpha_mod(colours.box_button_background, 120)) - ddt.text((x + width / 2, y, 2), str(value) + units, colours.box_sub_text, 312) - - x += width - - rect = (x, y, 33 * gui.scale, 15 * gui.scale) - fields.add(rect) - ddt.rect(rect, colours.box_button_background) - abg = [255, 255, 255, 40] - if coll(rect): - - if self.click: - if value < upper_limit: - value += step - gui.update_layout() - if callback is not None: - callback(value) - if mouse_down: - abg = [230, 120, 20, 255] - else: - abg = [220, 150, 20, 255] - - if colour_value(colours.box_background) > 300: - abg = colours.box_sub_text - - inc_arrow.render(x + 1 * gui.scale, y, abg) - - return value - - # def style_up(self): - # prefs.line_style += 1 - # if prefs.line_style > 5: - # prefs.line_style = 1 - - def inside(self): - - return coll((self.box_x, self.box_y, self.w, self.h)) - - def init2(self): - - self.init2done = True - - def close(self): - self.enabled = False - fader.fall() - if gui.opened_config_file: - reload_config_file() - - def render(self): - - if self.init2done is False: - self.init2() - - if key_esc_press: - self.close() - - tab_width = 115 * gui.scale - - side_width = 115 * gui.scale - header_width = 0 - - top_mode = False - if window_size[0] < 700 * gui.scale: - top_mode = True - side_width = 0 * gui.scale - header_width = round(48 * gui.scale) # 48 - - content_width = round(545 * gui.scale) - content_height = round(275 * gui.scale) # 275 - full_width = content_width - full_height = content_height - - full_width += side_width - full_height += header_width - - x = int(window_size[0] / 2) - int(full_width / 2) - y = int(window_size[1] / 2) - int(full_height / 2) - - self.box_x = x - self.box_y = y - self.w = full_width - self.h = full_height - - border_colour = colours.box_border - - ddt.rect( - (x - 5 * gui.scale, y - 5 * gui.scale, full_width + 10 * gui.scale, full_height + 10 * gui.scale), border_colour) - ddt.rect_a((x, y), (full_width, full_height), colours.box_background) - - current_tab = 0 - tab_height = round(24 * gui.scale) # 30 - - tab_bg = colours.sys_tab_bg - tab_hl = colours.sys_tab_hl - tab_text = rgb_add_hls(tab_bg, 0, 0.3, -0.15) - if is_light(tab_bg): - h, l, s = rgb_to_hls(tab_bg[0], tab_bg[1], tab_bg[2]) - l = 0.1 - tab_text = hls_to_rgb(h, l, s) - tab_over = alpha_mod(rgb_add_hls(tab_bg, 0, 0.5, 0), 13) - - if top_mode: - - xx = x - yy = y - tab_width = 90 * gui.scale - - ddt.rect_a((x, y), (full_width, header_width), tab_bg) - - for item in self.tabs: - - if self.click and gui.message_box: - gui.message_box = False - - box = [xx, yy, tab_width, tab_height] - box2 = [xx, yy, tab_width, tab_height - 1] - fields.add(box2) - - if self.click and coll(box2): - self.tab_active = current_tab - self.lyrics_panel = False - - if current_tab == self.tab_active: - colour = copy.deepcopy(colours.sys_tab_hl) - ddt.text_background_colour = colour - ddt.rect(box, colour) - else: - ddt.text_background_colour = tab_bg - ddt.rect(box, tab_bg) - - if coll(box2): - ddt.rect(box, tab_over) - - alpha = 100 - if current_tab == self.tab_active: - alpha = 240 - - ddt.text((xx + (tab_width // 2), yy + 4 * gui.scale, 2), item[0], tab_text, 212) - - current_tab += 1 - xx += tab_width - if current_tab == 6: - yy += round(24 * gui.scale) # 30 - xx = x - - else: - - ddt.rect_a((x, y), (tab_width, full_height), tab_bg) - - for item in self.tabs: - - if self.click and gui.message_box: - if not coll(message_box.get_rect()): - gui.message_box = False - else: - inp.mouse_click = True - self.click = False - - box = [x, y + (current_tab * tab_height), tab_width, tab_height] - box2 = [x, y + (current_tab * tab_height), tab_width, tab_height - 1] - fields.add(box2) - - if self.click and coll(box2): - self.tab_active = current_tab - self.lyrics_panel = False - - if current_tab == self.tab_active: - bg_colour = copy.deepcopy(colours.sys_tab_hl) - ddt.text_background_colour = bg_colour - ddt.rect(box, bg_colour) - else: - ddt.text_background_colour = tab_bg - ddt.rect(box, tab_bg) - - if coll(box2): - ddt.rect(box, tab_over) - - yy = box[1] + 4 * gui.scale - - if current_tab == self.tab_active: - ddt.text( - (box[0] + (tab_width // 2), yy, 2), item[0], alpha_blend(colours.tab_text_active, ddt.text_background_colour), 213) - else: - ddt.text( - (box[0] + (tab_width // 2), yy, 2), item[0], tab_text, 213) - - current_tab += 1 - - # ddt.line(x + 110, self.box_y + 1, self.box_x + 110, self.box_y + self.h, colours.grey(50)) - - self.tabs[self.tab_active][1](x + side_width, y + header_width, content_width, content_height) - - self.click = False - self.right_click = False - - ddt.text_background_colour = colours.box_background - - -class Fields: - def __init__(self): - - self.id = [] - self.last_id = [] - - self.field_array = [] - self.force = False - - def add(self, rect, callback=None): - - self.field_array.append((rect, callback)) - - def test(self): - - if self.force: - self.force = False - return True - - self.last_id = self.id - #logging.info(len(self.id)) - self.id = [] - - for f in self.field_array: - if coll(f[0]): - self.id.append(1) # += "1" - if f[1] is not None: # Call callback if present - f[1]() - else: - self.id.append(0) # += "0" - - if self.last_id == self.id: - return False - - return True - - def clear(self): - - self.field_array = [] - - -fields = Fields() - - -def update_playlist_call(): - gui.update + 2 - gui.pl_update = 2 - - -pref_box = Over() - -inc_arrow = asset_loader(scaled_asset_directory, loaded_asset_dc, "inc.png", True) -dec_arrow = asset_loader(scaled_asset_directory, loaded_asset_dc, "dec.png", True) -corner_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "corner.png", True) - - -# ---------------------------------------------------------------------------------------- -# ---------------------------------------------------------------------------------------- -def pl_is_mut(pl: int) -> bool: - id = pl_to_id(pl) - if id is None: - return False - return not (pctl.gen_codes.get(id) and "self" not in pctl.gen_codes[id]) - -def clear_gen(id: int) -> None: - del pctl.gen_codes[id] - show_message(_("Okay, it's a normal playlist now."), mode="done") - -def clear_gen_ask(id: int) -> None: - if "jelly\"" in pctl.gen_codes.get(id, ""): - return - if "spl\"" in pctl.gen_codes.get(id, ""): - return - if "tpl\"" in pctl.gen_codes.get(id, ""): - return - if "tar\"" in pctl.gen_codes.get(id, ""): - return - if "tmix\"" in pctl.gen_codes.get(id, ""): - return - gui.message_box_confirm_callback = clear_gen - gui.message_box_confirm_reference = (id,) - show_message(_("You added tracks to a generator playlist. Do you want to clear the generator?"), mode="confirm") - - -class TopPanel: - def __init__(self): - - self.height = gui.panelY - self.ty = 0 - - self.start_space_left = round(46 * gui.scale) - self.start_space_compact_left = 46 * gui.scale - - self.tab_text_font = fonts.tabs - self.tab_extra_width = round(17 * gui.scale) - self.tab_text_start_space = 8 * gui.scale - self.tab_text_y_offset = 7 * gui.scale - self.tab_spacing = 0 - - self.ini_menu_space = 17 * gui.scale # 17 - self.menu_space = 17 * gui.scale - self.click_buffer = 4 * gui.scale - - self.tabs_right_x = 0 # computed for drag and drop code elsewhere (hacky) - self.tabs_left_x = 1 - - self.prime_tab = gui.saved_prime_tab - self.prime_side = gui.saved_prime_direction # 0=left, 1=right - self.shown_tabs = [] - - # --- - self.space_left = 0 - self.tab_text_spaces = [] - self.index_playing = -1 - self.drag_zone_start_x = 300 * gui.scale - - self.exit_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "ex.png", True) - self.maximize_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "max.png", True) - self.restore_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "restore.png", True) - self.restore_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "restore.png", True) - self.playlist_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "playlist.png", True) - self.return_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "return.png", True) - self.artist_list_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "artist-list.png", True) - self.folder_list_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "folder-list.png", True) - self.dl_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "dl.png", True) - self.overflow_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "overflow.png", True) - - self.drag_slide_timer = Timer(100) - self.tab_d_click_timer = Timer(10) - self.tab_d_click_ref = None - - self.adds = [] - - def left_overflow_switch_playlist(self, pl): - self.prime_side = 0 - self.prime_tab = pl - switch_playlist(pl) - - def right_overflow_switch_playlist(self, pl): - self.prime_side = 1 - self.prime_tab = pl - switch_playlist(pl) - - def render(self): - - # C-TD - global quick_drag - global update_layout - - hh = gui.panelY2 - yy = gui.panelY - hh - self.height = hh - - if quick_drag is True: - # gui.pl_update = 1 - gui.update_on_drag = True - - # Draw the background - ddt.rect((0, 0, window_size[0], gui.panelY), colours.top_panel_background) - - if prefs.shuffle_lock and not gui.compact_bar: - colour = [250, 250, 250, 255] - if colours.lm: - colour = [10, 10, 10, 255] - text = _("Tauon Music Box SHUFFLE!") - if prefs.album_shuffle_lock_mode: - text = _("Tauon Music Box ALBUM SHUFFLE!") - ddt.text((window_size[0] // 2, 8 * gui.scale, 2), text, colour, 212, bg=colours.top_panel_background) - if gui.top_bar_mode2: - tr = pctl.playing_object() - if tr: - album_art_gen.display(tr, (window_size[0] - gui.panelY - 1, 0), (gui.panelY, gui.panelY)) - if loading_in_progress or \ - to_scan or \ - cm_clean_db or \ - lastfm.scanning_friends or \ - after_scan or \ - move_in_progress or \ - plex.scanning or \ - transcode_list or tauon.spot_ctl.launching_spotify or tauon.spot_ctl.spotify_com or subsonic.scanning or \ - koel.scanning or gui.sync_progress or lastfm.scanning_scrobbles: - ddt.rect( - (window_size[0] - (gui.panelY + 20), gui.panelY - gui.panelY2, gui.panelY + 25, gui.panelY2), - colours.top_panel_background) - - maxx = window_size[0] - (gui.panelY + 30 * gui.scale) - title_colour = colours.grey(249) - if colours.lm: - title_colour = colours.grey(30) - title = tr.title - if not title: - title = tr.filename - artist = tr.artist - - if pctl.playing_state == 3 and not radiobox.dummy_track.title: - title = pctl.tag_meta - artist = radiobox.loaded_url # pctl.url - - ddt.text_background_colour = colours.top_panel_background - - ddt.text((round(14 * gui.scale), round(15 * gui.scale)), title, title_colour, 215, max_w=maxx) - ddt.text((round(14 * gui.scale), round(40 * gui.scale)), artist, colours.grey(120), 315, max_w=maxx) - - wwx = 0 - if prefs.left_window_control and not gui.compact_bar: - if gui.macstyle: - wwx = 24 - # wwx = round(64 * gui.scale) - if draw_min_button: - wwx += 20 - if draw_max_button: - wwx += 20 - wwx = round(wwx * gui.scale) - else: - wwx = 26 - # wwx = round(90 * gui.scale) - if draw_min_button: - wwx += 35 - if draw_max_button: - wwx += 33 - wwx = round(wwx * gui.scale) - - rect = (wwx + 9 * gui.scale, yy + 4 * gui.scale, 34 * gui.scale, 25 * gui.scale) - fields.add(rect) - - if coll(rect) and not prefs.shuffle_lock: - if inp.mouse_click: - - if gui.combo_mode: - gui.switch_showcase_off = True - else: - gui.lsp ^= True - - update_layout = True - gui.update += 1 - if mouse_down and quick_drag: - gui.lsp = True - update_layout = True - gui.update += 1 - - if middle_click: - toggle_left_last() - update_layout = True - gui.update += 1 - - if right_click: - # prefs.artist_list ^= True - lsp_menu.activate(position=(5 * gui.scale, gui.panelY)) - update_layout_do() - - colour = colours.corner_button # [230, 230, 230, 255] - - if gui.lsp: - colour = colours.corner_button_active - if gui.combo_mode: - colour = colours.corner_button - if coll(rect): - colour = colours.corner_button_active - - if not prefs.shuffle_lock: - if gui.combo_mode: - self.return_icon.render(wwx + 14 * gui.scale, yy + 8 * gui.scale, colour) - elif prefs.left_panel_mode == "artist list": - self.artist_list_icon.render(wwx + 13 * gui.scale, yy + 8 * gui.scale, colour) - elif prefs.left_panel_mode == "folder view": - self.folder_list_icon.render(wwx + 14 * gui.scale, yy + 8 * gui.scale, colour) - else: - self.playlist_icon.render(wwx + 13 * gui.scale, yy + 8 * gui.scale, colour) - - # if prefs.artist_list: - # self.artist_list_icon.render(13 * gui.scale, yy + 8 * gui.scale, colour) - # else: - # self.playlist_icon.render(13 * gui.scale, yy + 8 * gui.scale, colour) - - if playlist_box.drag: - drag_mode = False - - # Need to test length - self.tab_text_spaces = [] - - if gui.radio_view: - for item in pctl.radio_playlists: - le = ddt.get_text_w(item["name"], self.tab_text_font) - self.tab_text_spaces.append(le) - else: - for i, item in enumerate(pctl.multi_playlist): - le = ddt.get_text_w(pctl.multi_playlist[i].title, self.tab_text_font) - self.tab_text_spaces.append(le) - - x = self.start_space_left + wwx - y = yy # self.ty - - # Calculate position for playing text and text - offset = 15 * gui.scale - if draw_border and not prefs.left_window_control: - offset += 61 * gui.scale - if draw_max_button: - offset += 61 * gui.scale - if gui.turbo: - offset += 90 * gui.scale - if gui.vis == 3: - offset += 57 * gui.scale - if gui.top_bar_mode2: - offset = 0 - - p_text_len = 180 * gui.scale - right_space_es = p_text_len + offset - - x_start = x - - if playlist_box.drag and not gui.radio_view: - if mouse_up: - if mouse_up_position[0] > (gui.lspw if gui.lsp else 0) and mouse_up_position[1] > gui.panelY: - playlist_box.drag = False - if prefs.drag_to_unpin: - if playlist_box.drag_source == 0: - pctl.multi_playlist[playlist_box.drag_on].hidden = True - else: - pctl.multi_playlist[playlist_box.drag_on].hidden = False - gui.update += 1 - gui.update_on_drag = True - - # List all tabs eligible to be shown - #logging.info("-------------") - ready_tabs = [] - show_tabs = [] - - if prefs.tabs_on_top or gui.radio_view: - if gui.radio_view: - for i, tab in enumerate(pctl.radio_playlists): - ready_tabs.append(i) - self.prime_tab = min(self.prime_tab, len(pctl.radio_playlists) - 1) - else: - for i, tab in enumerate(pctl.multi_playlist): - # Skip if hide flag is set - if tab.hidden: - continue - ready_tabs.append(i) - self.prime_tab = min(self.prime_tab, len(pctl.multi_playlist) - 1) - max_w = window_size[0] - (x + right_space_es + round(34 * gui.scale)) - - left_tabs = [] - right_tabs = [] - if prefs.shuffle_lock: - for p in ready_tabs: - left_tabs.append(p) - - else: - for p in ready_tabs: - if p < self.prime_tab: - left_tabs.append(p) - - for p in ready_tabs: - if p > self.prime_tab: - right_tabs.append(p) - left_tabs.reverse() - - run = max_w - - if self.prime_tab in ready_tabs: - size = self.tab_text_spaces[self.prime_tab] + self.tab_extra_width - if size < run: - show_tabs.append(self.prime_tab) - run -= size - - if self.prime_side == 0: - for tab in right_tabs: - size = self.tab_text_spaces[tab] + self.tab_extra_width - if size < run: - show_tabs.append(tab) - run -= size - else: - break - for tab in left_tabs: - size = self.tab_text_spaces[tab] + self.tab_extra_width - if size < run: - show_tabs.insert(0, tab) - run -= size - else: - break - else: - for tab in left_tabs: - size = self.tab_text_spaces[tab] + self.tab_extra_width - if size < run: - show_tabs.insert(0, tab) - run -= size - else: - break - for tab in right_tabs: - size = self.tab_text_spaces[tab] + self.tab_extra_width - if size < run: - show_tabs.append(tab) - run -= size - else: - break - - # for tab in show_tabs: - # logging.info(pctl.multi_playlist[tab].title) - #logging.info("---") - left_overflow = [x for x in left_tabs if x not in show_tabs] - right_overflow = [x for x in right_tabs if x not in show_tabs] - self.shown_tabs = show_tabs - - if left_overflow: - hh = round(20 * gui.scale) - rect = [x, y + (self.height - hh), 17 * gui.scale, hh] - ddt.rect(rect, colours.tab_background) - self.overflow_icon.render(rect[0] + round(3 * gui.scale), rect[1] + round(4 * gui.scale), colours.tab_text) - - x += 17 * gui.scale - x_start = x - - if inp.mouse_click and coll(rect): - overflow_menu.items.clear() - for tab in reversed(left_overflow): - if gui.radio_view: - overflow_menu.add( - MenuItem(pctl.radio_playlists[tab]["name"], self.left_overflow_switch_playlist, - pass_ref=True, set_ref=tab)) - else: - overflow_menu.add( - MenuItem(pctl.multi_playlist[tab].title, self.left_overflow_switch_playlist, - pass_ref=True, set_ref=tab)) - overflow_menu.activate(0, (rect[0], rect[1] + rect[3])) - - xx = x + (max_w - run) # + round(6 * gui.scale) - self.tabs_left_x = x_start - - if right_overflow: - hh = round(20 * gui.scale) - rect = [xx, y + (self.height - hh), 17 * gui.scale, hh] - ddt.rect(rect, colours.tab_background) - self.overflow_icon.render( - rect[0] + round(3 * gui.scale), rect[1] + round(4 * gui.scale), - colours.tab_text) - if inp.mouse_click and coll(rect): - overflow_menu.items.clear() - for tab in right_overflow: - if gui.radio_view: - overflow_menu.add( - MenuItem( - pctl.radio_playlists[tab]["name"], self.left_overflow_switch_playlist, pass_ref=True, set_ref=tab)) - else: - overflow_menu.add( - MenuItem( - pctl.multi_playlist[tab].title, self.left_overflow_switch_playlist, pass_ref=True, set_ref=tab)) - overflow_menu.activate(0, (rect[0], rect[1] + rect[3])) - - if gui.radio_view: - if not mouse_down and pctl.radio_playlist_viewing not in show_tabs and pctl.radio_playlist_viewing in ready_tabs: - if pctl.radio_playlist_viewing < self.prime_tab: - self.prime_side = 0 - elif pctl.radio_playlist_viewing > self.prime_tab: - self.prime_side = 1 - self.prime_tab = pctl.radio_playlist_viewing - gui.update += 1 - elif not mouse_down and pctl.active_playlist_viewing not in show_tabs and pctl.active_playlist_viewing in ready_tabs: - if pctl.active_playlist_viewing < self.prime_tab: - self.prime_side = 0 - elif pctl.active_playlist_viewing > self.prime_tab: - self.prime_side = 1 - self.prime_tab = pctl.active_playlist_viewing - gui.update += 1 - - if playlist_box.drag and mouse_position[0] > xx and mouse_position[1] < gui.panelY: - gui.update += 1 - if 0.5 < self.drag_slide_timer.get() < 1 and show_tabs and right_overflow: - self.drag_slide_timer.set() - self.prime_side = 1 - self.prime_tab = right_overflow[0] - if self.drag_slide_timer.get() > 1: - self.drag_slide_timer.set() - if playlist_box.drag and mouse_position[0] < x and mouse_position[1] < gui.panelY: - gui.update += 1 - if 0.5 < self.drag_slide_timer.get() < 1 and show_tabs and left_overflow: - self.drag_slide_timer.set() - self.prime_side = 0 - self.prime_tab = left_overflow[0] - if self.drag_slide_timer.get() > 1: - self.drag_slide_timer.set() - - # TAB INPUT PROCESSING - target = pctl.multi_playlist - if gui.radio_view: - target = pctl.radio_playlists - for i, tab in enumerate(target): - - if not gui.radio_view: - if not prefs.tabs_on_top or prefs.shuffle_lock: - break - - if len(pctl.multi_playlist) != len(self.tab_text_spaces): - break - - if i not in show_tabs: - continue - - # Determine the tab width - tab_width = self.tab_text_spaces[i] + self.tab_extra_width - - # Save the far right boundary of the tabs (hacky) - self.tabs_right_x = x + tab_width - - # Detect mouse over and add tab to mouse over detection - f_rect = [x, y + 1, tab_width - 1, self.height - 1] - tab_hit = coll(f_rect) - - # Tab functions - if tab_hit: - if not gui.radio_view: - # Double click to play - if mouse_up and pl_to_id(i) == self.tab_d_click_ref == pl_to_id(pctl.active_playlist_viewing) and \ - self.tab_d_click_timer.get() < 0.25 and point_distance( - last_click_location, mouse_up_position) < 5 * gui.scale: - - if pctl.playing_state == 2 and pctl.active_playlist_playing == i: - pctl.play() - elif pctl.selected_ready() and (pctl.playing_state != 1 or pctl.active_playlist_playing != i): - pctl.jump(default_playlist[pctl.selected_in_playlist], pl_position=pctl.selected_in_playlist) - if mouse_up: - self.tab_d_click_timer.set() - self.tab_d_click_ref = pl_to_id(i) - - # Click to change playlist - if inp.mouse_click: - gui.pl_update = 1 - playlist_box.drag = True - playlist_box.drag_source = 0 - playlist_box.drag_on = i - if gui.radio_view: - pctl.radio_playlist_viewing = i - else: - switch_playlist(i) - set_drag_source() - - # Drag to move playlist - if mouse_up and playlist_box.drag and coll_point(mouse_up_position, f_rect): - - if gui.radio_view: - move_radio_playlist(playlist_box.drag_on, i) - else: - if playlist_box.drag_source == 1: - pctl.multi_playlist[playlist_box.drag_on].hidden = False - - if i != playlist_box.drag_on: - - # # Reveal the tab in case it has been hidden - # pctl.multi_playlist[playlist_box.drag_on].hidden = False - - if key_shift_down: - pctl.multi_playlist[i].playlist_ids += pctl.multi_playlist[playlist_box.drag_on].playlist_ids - delete_playlist(playlist_box.drag_on, check_lock=True, force=True) - else: - move_playlist(playlist_box.drag_on, i) - - playlist_box.drag = False - gui.update += 1 - - # Delete playlist on wheel click - elif tab_menu.active is False and middle_click: - # delete_playlist(i) - delete_playlist_ask(i) - break - - # Activate menu on right click - elif right_click: - if gui.radio_view: - radio_tab_menu.activate(copy.deepcopy(i)) - else: - tab_menu.activate(copy.deepcopy(i)) - gui.tab_menu_pl = i - - # Quick drop tracks - elif quick_drag is True and mouse_up: - self.tab_d_click_ref = -1 - self.tab_d_click_timer.force_set(100) - if (pctl.gen_codes.get(pl_to_id(i)) and "self" not in pctl.gen_codes[pl_to_id(i)]): - clear_gen_ask(pl_to_id(i)) - quick_drag = False - modified = False - gui.pl_update += 1 - - for item in shift_selection: - pctl.multi_playlist[i].playlist_ids.append(default_playlist[item]) - modified = True - if len(shift_selection) > 0: - modified = True - self.adds.append( - [pctl.multi_playlist[i].uuid_int, len(shift_selection), Timer()]) # ID, num, timer - - if modified: - pctl.after_import_flag = True - pctl.notify_change() - pctl.update_shuffle_pool(pctl.multi_playlist[i].uuid_int) - tree_view_box.clear_target_pl(i) - tauon.thread_manager.ready("worker") - - if mouse_up and radio_view.drag: - pctl.radio_playlists[i]["items"].append(radio_view.drag) - toast(_("Added station to: ") + pctl.radio_playlists[i]["name"]) - - radio_view.drag = None - - x += tab_width + self.tab_spacing - - # Test dupelicate tab function - if playlist_box.drag: - rect = (0, x, self.height, window_size[0]) - fields.add(rect) - - if mouse_up and playlist_box.drag and mouse_position[0] > x and mouse_position[1] < self.height: - if gui.radio_view: - pass - elif key_ctrl_down: - gen_dupe(playlist_box.drag_on) - - else: - if playlist_box.drag_source == 1: - pctl.multi_playlist[playlist_box.drag_on].hidden = False - - move_playlist(playlist_box.drag_on, i) - playlist_box.drag = False - - # Need to test length again - # Need to test length - self.tab_text_spaces = [] - - if gui.radio_view: - for item in pctl.radio_playlists: - le = ddt.get_text_w(item["name"], self.tab_text_font) - self.tab_text_spaces.append(le) - else: - for i, item in enumerate(pctl.multi_playlist): - le = ddt.get_text_w(pctl.multi_playlist[i].title, self.tab_text_font) - self.tab_text_spaces.append(le) - - # Reset X draw position - x = x_start - bar_highlight_size = round(2 * gui.scale) - - # TAB DRAWING - shown = [] - for i, tab in enumerate(target): - - if not gui.radio_view: - if not prefs.tabs_on_top or prefs.shuffle_lock: - break - - if len(pctl.multi_playlist) != len(self.tab_text_spaces): - break - - # if tab.hidden is True: - # continue - - if i not in show_tabs: - continue - - # if window_size[0] - x - (self.tab_text_spaces[i] + self.tab_extra_width) < right_space_es: - # break - - shown.append(i) - - tab_width = self.tab_text_spaces[i] + self.tab_extra_width - rect = [x, y, tab_width, self.height] - - # Detect mouse over and add tab to mouse over detection - f_rect = [x, y + 1, tab_width - 1, self.height - 1] - fields.add(f_rect) - tab_hit = coll(f_rect) - playing_hint = False - active = False - - # Determine tab background colour - if not gui.radio_view: - if i == pctl.active_playlist_viewing: - bg = colours.tab_background_active - active = True - elif ( - tab_menu.active is True and tab_menu.reference == i) or (tab_menu.active is False and tab_hit and not playlist_box.drag): - bg = colours.tab_highlight - elif i == pctl.active_playlist_playing: - bg = colours.tab_background - playing_hint = True - else: - bg = colours.tab_background - elif pctl.radio_playlist_viewing == i: - bg = colours.tab_background_active - active = True - else: - bg = colours.tab_background - - # Draw tab background - ddt.rect(rect, bg) - if playing_hint: - ddt.rect(rect, [255, 255, 255, 7]) - - # Determine text colour - if active: - fg = colours.tab_text_active - else: - fg = colours.tab_text - - # Draw tab text - if gui.radio_view: - text = tab["name"] - else: - text = tab.title - ddt.text((x + self.tab_text_start_space, y + self.tab_text_y_offset), text, fg, self.tab_text_font, bg=bg) - - # Drop pulse - if gui.pl_pulse and gui.drop_playlist_target == i: - if tab_pulse.render(x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size, r=200, - g=130) is False: - gui.pl_pulse = False - - # Drag to move playlist - if tab_hit: - if mouse_down and i != playlist_box.drag_on and playlist_box.drag is True: - - if key_shift_down: - ddt.rect((x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size), [80, 160, 200, 255]) - elif playlist_box.drag_on < i: - ddt.rect((x + tab_width - bar_highlight_size, y, bar_highlight_size, gui.panelY2), [80, 160, 200, 255]) - else: - ddt.rect((x, y, bar_highlight_size, gui.panelY2), [80, 160, 200, 255]) - - elif quick_drag is True and pl_is_mut(i): - ddt.rect((x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size), [80, 200, 180, 255]) - # Drag yellow line highlight if single track already in playlist - elif quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15 * gui.scale): - for item in shift_selection: - if item < len(default_playlist) and default_playlist[item] in tab.playlist_ids: - ddt.rect((x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size), [190, 160, 20, 255]) - break - # Drag red line highlight if playlist is generator playlist - if quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15 * gui.scale): - if not pl_is_mut(i): - ddt.rect((x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size), [200, 70, 50, 255]) - - if not gui.radio_view: - if len(self.adds) > 0: - for k in reversed(range(len(self.adds))): - if pctl.multi_playlist[i].uuid_int == self.adds[k][0]: - if self.adds[k][2].get() > 0.3: - del self.adds[k] - else: - ay = y + 4 - ay -= 6 * self.adds[k][2].get() / 0.3 - - ddt.text( - (x + tab_width - 3, int(round(ay)), 1), "+" + str(self.adds[k][1]), colours.pluse_colour, 212, bg=bg) - gui.update += 1 - - x += tab_width + self.tab_spacing - - # Quick drag single track onto bar to create new playlist function and indicator - if prefs.tabs_on_top: - if quick_drag and mouse_position[0] > x and mouse_position[1] < gui.panelY and quick_d_timer.get() > 1: - ddt.rect((x, y, 2 * gui.scale, gui.panelY2), [80, 200, 180, 255]) - - if mouse_up: - drop_tracks_to_new_playlist(shift_selection) - - # Draw end drag tab indicator - if playlist_box.drag and mouse_position[0] > x and mouse_position[1] < gui.panelY: - if key_ctrl_down: - ddt.rect((x, y, 2 * gui.scale, gui.panelY2), [255, 190, 0, 255]) - else: - ddt.rect((x, y, 2 * gui.scale, gui.panelY2), [80, 160, 200, 255]) - - if prefs.tabs_on_top and right_overflow: - x += 24 * gui.scale - self.tabs_right_x += 24 * gui.scale - - # ------------- - # Other input - if mouse_up: - quick_drag = False - playlist_box.drag = False - radio_view.drag = None - - # Scroll anywhere on panel to cycle playlist - # (This is a bit complicated because we need to skip over hidden playlists) - if mouse_wheel != 0 and 1 < mouse_position[1] < gui.panelY + 1 and len(pctl.multi_playlist) > 1 and mouse_position[0] > 5: - - cycle_playlist_pinned(mouse_wheel) - - gui.pl_update = 1 - if not prefs.tabs_on_top: - if pctl.active_playlist_viewing not in shown: # and not gui.lsp: - gui.mode_toast_text = _(pctl.multi_playlist[pctl.active_playlist_viewing].title) - toast_mode_timer.set() - gui.frame_callback_list.append(TestTimer(1)) - else: - toast_mode_timer.force_set(10) - gui.mode_toast_text = "" - # --------- - # Menu Bar - - x += self.ini_menu_space - y += 7 * gui.scale - ddt.text_background_colour = colours.top_panel_background - - # MENU ----------------------------- - - word = _("MENU") - word_length = ddt.get_text_w(word, 212) - rect = [x - self.click_buffer, yy + self.ty + 1, word_length + self.click_buffer * 2, self.height - 1] - hit = coll(rect) - fields.add(rect) - - if (x_menu.active or hit) and not tab_menu.active: - bg = colours.status_text_over - else: - bg = colours.status_text_normal - ddt.text((x, y), word, bg, 212) - - if hit and inp.mouse_click: - if x_menu.active: - x_menu.active = False - else: - xx = x - if x > window_size[0] - (210 * gui.scale): - xx = window_size[0] - round(210 * gui.scale) - x_menu.activate(position=(xx + round(12 * gui.scale), gui.panelY)) - view_box.activate(xx) - - # if True: - # border = round(3 * gui.scale) - # border_colour = colours.grey(30) - # rect = (5 * gui.scale, gui.panelY, round(90 * gui.scale), round(25 * gui.scale)) - # - - dl = len(dl_mon.ready) - watching = len(dl_mon.watching) - - if (dl > 0 or watching > 0) and core_timer.get() > 2 and prefs.auto_extract and prefs.monitor_downloads: - x += 52 * gui.scale - rect = (x - 5 * gui.scale, y - 2 * gui.scale, 30 * gui.scale, 23 * gui.scale) - fields.add(rect) - - if coll(rect): - colour = colours.corner_button_active - # if colours.lm: - # colour = [40, 40, 40, 255] - if dl > 0 or watching > 0: - if right_click: - dl_menu.activate(position=(mouse_position[0], gui.panelY)) - if dl > 0: - if inp.mouse_click: - pln = 0 - for item in dl_mon.ready: - load_order = LoadClass() - load_order.target = item - pln = pctl.active_playlist_viewing - load_order.playlist = pctl.multi_playlist[pln].uuid_int - - for i, pl in enumerate(pctl.multi_playlist): - if prefs.download_playlist is not None: - if pl.uuid_int == prefs.download_playlist: - load_order.playlist = pl.uuid_int - pln = i - break - else: - for i, pl in enumerate(pctl.multi_playlist): - if pl.title.lower() == "downloads": - load_order.playlist = pl.uuid_int - pln = i - break - - load_orders.append(copy.deepcopy(load_order)) - - if len(dl_mon.ready) > 0: - dl_mon.ready.clear() - switch_playlist(pln) - - pctl.playlist_view_position = len(default_playlist) - logging.debug("Position changed by track import") - gui.update += 1 - else: - colour = colours.corner_button # [60, 60, 60, 255] - # if colours.lm: - # colour = [180, 180, 180, 255] - if inp.mouse_click: - inp.mouse_click = False - show_message( - _("It looks like something is being downloaded..."), _("Let's check back later..."), mode="info") - - - else: - colour = colours.corner_button # [60, 60, 60, 255] - if colours.lm: - # colour = [180, 180, 180, 255] - if dl_mon.ready: - colour = colours.corner_button_active # [60, 60, 60, 255] - - self.dl_button.render(x, y + 1 * gui.scale, colour) - if dl > 0: - ddt.text((x + 18 * gui.scale, y - 4 * gui.scale), str(dl), colours.pluse_colour, 209) # [244, 223, 66, 255] - # [166, 244, 179, 255] - - # LAYOUT -------------------------------- - x += self.menu_space + word_length - - self.drag_zone_start_x = x - 5 * gui.scale - status = True - - if loading_in_progress: - - bg = colours.status_info_text - if to_got == "xspf": - text = _("Importing XSPF playlist") - elif to_got == "xspfl": - text = _("Importing XSPF playlist...") - elif to_got == "ex": - text = _("Extracting Archive...") - else: - text = _("Importing... ") + str(to_got) # + "/" + str(to_get) - if right_click and coll([x, y, 180 * gui.scale, 18 * gui.scale]): - cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) - elif after_scan: - # bg = colours.status_info_text - bg = [100, 200, 100, 255] - text = _("Scanning Tags... {N} remaining").format(N=str(len(after_scan))) - elif move_in_progress: - text = _("File copy in progress...") - bg = colours.status_info_text - elif cm_clean_db and to_get > 0: - per = str(int(to_got / to_get * 100)) - text = _("Cleaning db... ") + per + "%" - bg = [100, 200, 100, 255] - elif to_scan: - text = _("Rescanning Tags... {N} remaining").format(N=str(len(to_scan))) - bg = [100, 200, 100, 255] - elif plex.scanning: - text = _("Accessing PLEX library...") - if gui.to_got: - text += f" {gui.to_got}" - bg = [229, 160, 13, 255] - elif tauon.spot_ctl.launching_spotify: - text = _("Launching Spotify...") - bg = [30, 215, 96, 255] - elif tauon.spot_ctl.preparing_spotify: - text = _("Preparing Spotify Playback...") - bg = [30, 215, 96, 255] - elif tauon.spot_ctl.spotify_com: - text = _("Accessing Spotify library...") - bg = [30, 215, 96, 255] - elif subsonic.scanning: - text = _("Accessing AIRSONIC library...") - if gui.to_got: - text += f" {gui.to_got}" - bg = [58, 194, 224, 255] - elif koel.scanning: - text = _("Accessing KOEL library...") - bg = [111, 98, 190, 255] - elif jellyfin.scanning: - text = _("Accessing JELLYFIN library...") - bg = [90, 170, 240, 255] - elif tauon.chrome_mode: - text = _("Chromecast Mode") - bg = [207, 94, 219, 255] - elif gui.sync_progress and not transcode_list: - text = gui.sync_progress - bg = [100, 200, 100, 255] - if right_click and coll([x, y, 280 * gui.scale, 18 * gui.scale]): - cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) - elif transcode_list and gui.tc_cancel: - bg = [150, 150, 150, 255] - text = _("Stopping transcode...") - elif lastfm.scanning_friends or lastfm.scanning_loves: - text = _("Scanning: ") + lastfm.scanning_username - bg = [200, 150, 240, 255] - elif lastfm.scanning_scrobbles: - text = _("Scanning Scrobbles...") - bg = [219, 88, 18, 255] - elif gui.buffering: - text = _("Buffering... ") - text += gui.buffering_text - bg = [18, 180, 180, 255] - - elif lfm_scrobbler.queue and scrobble_warning_timer.get() < 260: - text = _("Network error. Will try again later.") - bg = [250, 250, 250, 255] - last_fm_icon.render(x - 4 * gui.scale, y + 4 * gui.scale, [250, 40, 40, 255]) - x += 21 * gui.scale - elif tauon.listen_alongers: - new = {} - for ip, timer in tauon.listen_alongers.items(): - if timer.get() < 6: - new[ip] = timer - tauon.listen_alongers = new - - text = _("{N} listening along").format(N=len(tauon.listen_alongers)) - bg = [40, 190, 235, 255] - else: - status = False - - if status: - x += ddt.text((x, y), text, bg, 311) - # x += ddt.get_text_w(text, 11) - # TODO list listenieng clients - elif transcode_list: - bg = colours.status_info_text - # if key_ctrl_down and key_c_press: - # del transcode_list[1:] - # gui.tc_cancel = True - if right_click and coll([x, y, 280 * gui.scale, 18 * gui.scale]): - cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) - - w = 100 * gui.scale - x += ddt.text((x, y), _("Transcoding"), bg, 311) + 8 * gui.scale - - if gui.transcoding_batch_total: - - # c1 = [40, 40, 40, 255] - # c2 = [60, 60, 60, 255] - # c3 = [130, 130, 130, 255] - # - # if colours.lm: - # c1 = [100, 100, 100, 255] - # c2 = [130, 130, 130, 255] - # c3 = [180, 180, 180, 255] - - c1 = [40, 40, 40, 255] - c2 = [100, 59, 200, 200] - c3 = [150, 70, 200, 255] - - if colours.lm: - c1 = [100, 100, 100, 255] - c2 = [170, 140, 255, 255] - c3 = [230, 170, 255, 255] - - yy = y + 4 * gui.scale - h = 9 * gui.scale - box = [x, yy, w, h] - # ddt.rect_r(box, [100, 100, 100, 255]) - ddt.rect(box, c1) - - done = round(gui.transcoding_bach_done / gui.transcoding_batch_total * 100) - doing = round(core_use / gui.transcoding_batch_total * 100) - - ddt.rect([x, yy, done, h], c3) - ddt.rect([x + done, yy, doing, h], c2) - - x += w + 8 * gui.scale - - if gui.sync_progress: - text = gui.sync_progress - else: - text = _("{N} Folder Remaining {T}").format(N=str(len(transcode_list)), T=transcode_state) - if len(transcode_list) > 1: - text = _("{N} Folders Remaining {T}").format(N=str(len(transcode_list)), T=transcode_state) - - x += ddt.text((x, y), text, bg, 311) + 8 * gui.scale - - - if colours.lm: - colours.tb_line = colours.grey(200) - ddt.rect((0, int(gui.panelY - 1 * gui.scale), window_size[0], int(1 * gui.scale)), colours.tb_line) - - -top_panel = TopPanel() - - -class BottomBarType1: - def __init__(self): - - self.mode = 0 - - self.seek_time = 0 - - self.seek_down = False - self.seek_hit = False - self.volume_hit = False - self.volume_bar_being_dragged = False - self.control_line_bottom = 35 * gui.scale - self.repeat_click_off = False - self.random_click_off = False - - self.seek_bar_position = [300 * gui.scale, window_size[1] - gui.panelBY] - self.seek_bar_size = [window_size[0] - (300 * gui.scale), 15 * gui.scale] - self.volume_bar_size = [135 * gui.scale, 14 * gui.scale] - self.volume_bar_position = [0, 45 * gui.scale] - - self.play_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "play.png", True) - self.forward_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "ff.png", True) - self.back_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "bb.png", True) - self.repeat_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_repeat.png", True) - self.repeat_button_off = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_repeat_off.png", True) - self.shuffle_button_off = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_shuffle_off.png", True) - self.shuffle_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_shuffle.png", True) - self.repeat_button_a = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_repeat_a.png", True) - self.shuffle_button_a = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_shuffle_a.png", True) - - self.buffer_shard = asset_loader(scaled_asset_directory, loaded_asset_dc, "shard.png", True) - - self.scrob_stick = 0 - - def update(self): - - if self.mode == 0: - self.volume_bar_position[0] = window_size[0] - (210 * gui.scale) - self.volume_bar_position[1] = window_size[1] - (27 * gui.scale) - self.seek_bar_position[1] = window_size[1] - gui.panelBY - - seek_bar_x = 300 * gui.scale - if window_size[0] < 600 * gui.scale: - seek_bar_x = 250 * gui.scale - - self.seek_bar_size[0] = window_size[0] - seek_bar_x - self.seek_bar_position[0] = seek_bar_x - - # if gui.bb_show_art: - # self.seek_bar_position[0] = 300 + gui.panelBY - # self.seek_bar_size[0] = window_size[0] - 300 - gui.panelBY - - # self.seek_bar_position[0] = 0 - # self.seek_bar_size[0] = window_size[0] - - def render(self): - - global volume_store - global clicked - global right_click - - ddt.rect_a((0, window_size[1] - gui.panelBY), (window_size[0], gui.panelBY), colours.bottom_panel_colour) - - ddt.rect_a(self.seek_bar_position, self.seek_bar_size, colours.seek_bar_background) - - right_offset = 0 - if gui.display_time_mode >= 2: - right_offset = 22 * gui.scale - - if window_size[0] < 670 * gui.scale: - right_offset -= 90 * gui.scale - # Scrobble marker - - if prefs.scrobble_mark and ( - prefs.auto_lfm or lb.enable or prefs.maloja_enable) and not prefs.scrobble_hold and pctl.playing_length > 0 and 3 > pctl.playing_state > 0: - if pctl.master_library[pctl.track_queue[pctl.queue_step]].length > 240 * 2: - l_target = 240 - else: - l_target = int(pctl.master_library[pctl.track_queue[pctl.queue_step]].length * 0.50) - l_lead = l_target - pctl.a_time - - if l_lead > 0 and pctl.master_library[pctl.track_queue[pctl.queue_step]].length > 30: - l_x = self.seek_bar_position[0] + int(math.ceil( - pctl.playing_time * self.seek_bar_size[0] / int(pctl.playing_length))) - l_x += int(math.ceil(self.seek_bar_size[0] / int(pctl.playing_length) * l_lead)) - - if abs(self.scrob_stick - l_x) < 2: - l_x = self.scrob_stick - else: - self.scrob_stick = l_x - ddt.rect((self.scrob_stick, self.seek_bar_position[1], 2 * gui.scale, self.seek_bar_size[1]), [240, 10, 10, 80]) - - # # MINI ALBUM ART - # if gui.bb_show_art: - # rect = [self.seek_bar_position[0] - gui.panelBY, self.seek_bar_position[1], gui.panelBY, gui.panelBY] - # ddt.rect_r(rect, [255, 255, 255, 8], True) - # if 3 > pctl.playing_state > 0: - # album_art_gen.display(pctl.track_queue[pctl.queue_step], (rect[0], rect[1]), (rect[2], rect[3])) - - # ddt.rect_r(rect, [255, 255, 255, 20]) - - # SEEK BAR------------------ - if pctl.playing_time < 1: - self.seek_time = 0 - - if inp.mouse_click and coll_point( - mouse_position, - self.seek_bar_position + [self.seek_bar_size[0]] + [ - self.seek_bar_size[1] + 2]): - self.seek_down = True - self.volume_hit = True - if right_click and coll_point( - mouse_position, self.seek_bar_position + [self.seek_bar_size[0]] + [self.seek_bar_size[1] + 2]): - pctl.pause() - if pctl.playing_state == 0: - pctl.play() - - fields.add(self.seek_bar_position + self.seek_bar_size) - if coll(self.seek_bar_position + self.seek_bar_size): - - if middle_click and pctl.playing_state > 0: - gui.seek_cur_show = True - - clicked = True - if mouse_wheel != 0: - pctl.seek_time(pctl.playing_time + (mouse_wheel * 3)) - - if gui.seek_cur_show: - gui.update += 1 - - # fields.add([mouse_position[0] - 1, mouse_position[1] - 1, 1, 1]) - # ddt.rect_r([mouse_position[0] - 1, mouse_position[1] - 1, 1, 1], [255,0,0,180], True) - - bargetX = mouse_position[0] - bargetX = min(bargetX, self.seek_bar_position[0] + self.seek_bar_size[0]) - bargetX = max(bargetX, self.seek_bar_position[0]) - bargetX -= self.seek_bar_position[0] - seek = bargetX / self.seek_bar_size[0] - gui.cur_time = get_display_time(pctl.playing_object().length * seek) - - if self.seek_down is True: - if mouse_position[0] == 0: - self.seek_down = False - self.seek_hit = True - - if (mouse_up and coll(self.seek_bar_position + self.seek_bar_size) and coll_point( - last_click_location, self.seek_bar_position + self.seek_bar_size) - and coll_point( - click_location, self.seek_bar_position + self.seek_bar_size)) or (mouse_up and self.volume_hit) or self.seek_hit: - - self.volume_hit = False - self.seek_down = False - self.seek_hit = False - - bargetX = mouse_position[0] - bargetX = min(bargetX, self.seek_bar_position[0] + self.seek_bar_size[0]) - bargetX = max(bargetX, self.seek_bar_position[0]) - bargetX -= self.seek_bar_position[0] - seek = bargetX / self.seek_bar_size[0] - - pctl.seek_decimal(seek) - #logging.info(seek) - - self.seek_time = pctl.playing_time - - if radiobox.load_connecting or gui.buffering: - x = self.seek_bar_position[0] - round(26 - gui.scale) - y = self.seek_bar_position[1] - while x < self.seek_bar_position[0] + self.seek_bar_size[0]: - offset = (math.floor(((core_timer.get() * 1) % 1) * 13) / 13) * self.buffer_shard.w - gui.delay_frame(0.01) - - # colour = colours.seek_bar_fill - h, l, s = rgb_to_hls( - colours.seek_bar_background[0], colours.seek_bar_background[1], colours.seek_bar_background[2]) - l = min(1, l + 0.05) - colour = hls_to_rgb(h, l, s) - colour[3] = colours.seek_bar_background[3] - - self.buffer_shard.render(x + offset, y, colour) - x += self.buffer_shard.w - - ddt.rect( - (self.seek_bar_position[0] - self.buffer_shard.w, y, self.buffer_shard.w, self.buffer_shard.h), - colours.bottom_panel_colour) - - if pctl.playing_length > 0: - - if pctl.download_time != 0: - - if pctl.download_time == -1: - pctl.download_time = pctl.playing_length - - colour = (255, 255, 255, 10) - if gui.theme_name == "Lavender Light" or gui.theme_name == "Carbon": - colour = (255, 255, 255, 40) - - gui.seek_bar_rect = ( - self.seek_bar_position[0], self.seek_bar_position[1], - int(pctl.download_time * self.seek_bar_size[0] / pctl.playing_length), - self.seek_bar_size[1]) - ddt.rect(gui.seek_bar_rect, colour) - - gui.seek_bar_rect = ( - self.seek_bar_position[0], self.seek_bar_position[1], - int(self.seek_time * self.seek_bar_size[0] / pctl.playing_length), - self.seek_bar_size[1]) - ddt.rect(gui.seek_bar_rect, colours.seek_bar_fill) - - if gui.seek_cur_show: - - if coll( - [self.seek_bar_position[0] - 50, self.seek_bar_position[1] - 50, self.seek_bar_size[0] + 50, self.seek_bar_size[1] + 100]): - if mouse_position[0] > self.seek_bar_position[0] - 1: - cur = [mouse_position[0] - 40, self.seek_bar_position[1] - 25, 42, 19] - ddt.rect(cur, colours.grey(15)) - # ddt.rect_r(cur, colours.grey(80)) - ddt.text( - (mouse_position[0] - 40 + 3, self.seek_bar_position[1] - 24), gui.cur_time, - colours.grey(180), 213, - bg=colours.grey(15)) - - ddt.rect( - [mouse_position[0], self.seek_bar_position[1], 2, self.seek_bar_size[1]], - [100, 100, 20, 255]) - - else: - gui.seek_cur_show = False - - if gui.buffering and pctl.buffering_percent: - ddt.rect_a((self.seek_bar_position[0], self.seek_bar_position[1] + self.seek_bar_size[1] - round(3 * gui.scale)), (self.seek_bar_size[0] * pctl.buffering_percent / 100, round(3 * gui.scale)), [255, 255, 255, 50]) - # Volume mouse wheel control ----------------------------------------- - if mouse_wheel != 0 and mouse_position[1] > self.seek_bar_position[1] + 4 and not coll_point( - mouse_position, self.seek_bar_position + self.seek_bar_size): - - pctl.player_volume += mouse_wheel * prefs.volume_wheel_increment - if pctl.player_volume < 1: - pctl.player_volume = 0 - elif pctl.player_volume > 100: - pctl.player_volume = 100 - - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() - - # Volume Bar 2 ------------------------------------------------ - if window_size[0] < 670 * gui.scale: - x = window_size[0] - right_offset - 207 * gui.scale - y = window_size[1] - round(14 * gui.scale) - - rect = (x - 8 * gui.scale, y - 17 * gui.scale, 55 * gui.scale, 23 * gui.scale) - # ddt.rect(rect, [255,255,255,25]) - if coll(rect) and mouse_down: - gui.update_on_drag = True - - h_rect = (x - 6 * gui.scale, y - 17 * gui.scale, 4 * gui.scale, 23 * gui.scale) - if coll(h_rect) and mouse_down: - pctl.player_volume = 0 - - step = round(1 * gui.scale) - min_h = round(4 * gui.scale) - spacing = round(5 * gui.scale) - - if right_click and coll((h_rect[0], h_rect[1], h_rect[2] + 50 * gui.scale, h_rect[3])): - if right_click: - pctl.toggle_mute() - - for bar in range(8): - - h = min_h + bar * step - rect = (x, y - h, 3 * gui.scale, h) - h_rect = (x - 1 * gui.scale, y - 17 * gui.scale, 4 * gui.scale, 23 * gui.scale) - - if coll(h_rect): - if mouse_down or mouse_up: - gui.update_on_drag = True - - if bar == 0: - pctl.player_volume = 5 - if bar == 1: - pctl.player_volume = 10 - if bar == 2: - pctl.player_volume = 20 - if bar == 3: - pctl.player_volume = 30 - if bar == 4: - pctl.player_volume = 45 - if bar == 5: - pctl.player_volume = 55 - if bar == 6: - pctl.player_volume = 70 - if bar == 7: - pctl.player_volume = 100 - - pctl.set_volume() - - colour = colours.mode_button_off - - if bar == 0 and pctl.player_volume > 0: - colour = colours.mode_button_active - elif bar == 1 and pctl.player_volume >= 10: - colour = colours.mode_button_active - elif bar == 2 and pctl.player_volume >= 20: - colour = colours.mode_button_active - elif bar == 3 and pctl.player_volume >= 30: - colour = colours.mode_button_active - elif bar == 4 and pctl.player_volume >= 45: - colour = colours.mode_button_active - elif bar == 5 and pctl.player_volume >= 55: - colour = colours.mode_button_active - elif bar == 6 and pctl.player_volume >= 70: - colour = colours.mode_button_active - elif bar == 7 and pctl.player_volume >= 95: - colour = colours.mode_button_active - - ddt.rect(rect, colour) - x += spacing - - # Volume Bar -------------------------------------------------------- - else: - if (inp.mouse_click and coll(( - self.volume_bar_position[0] - right_offset, self.volume_bar_position[1], self.volume_bar_size[0], - self.volume_bar_size[1] + 4))) or \ - self.volume_bar_being_dragged is True: - clicked = True - - if inp.mouse_click is True or self.volume_bar_being_dragged is True: - gui.update = 2 - - self.volume_bar_being_dragged = True - volgetX = mouse_position[0] - volgetX = min(volgetX, self.volume_bar_position[0] + self.volume_bar_size[0] - right_offset) - volgetX = max(volgetX, self.volume_bar_position[0] - right_offset) - volgetX -= self.volume_bar_position[0] - right_offset - pctl.player_volume = volgetX / self.volume_bar_size[0] * 100 - - time.sleep(0.02) - - if mouse_down is False: - self.volume_bar_being_dragged = False - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume(True) - - if mouse_down: - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume(False) - - if right_click and coll(( - self.volume_bar_position[0] - 15 * gui.scale, self.volume_bar_position[1] - 10 * gui.scale, - self.volume_bar_size[0] + 30 * gui.scale, - self.volume_bar_size[1] + 20 * gui.scale)): - - if pctl.player_volume > 0: - volume_store = pctl.player_volume - pctl.player_volume = 0 - else: - pctl.player_volume = volume_store - - pctl.set_volume() - - ddt.rect_a( - (self.volume_bar_position[0] - right_offset, self.volume_bar_position[1]), - self.volume_bar_size, colours.volume_bar_background) # 22 - - gui.volume_bar_rect = ( - self.volume_bar_position[0] - right_offset, self.volume_bar_position[1], - int(pctl.player_volume * self.volume_bar_size[0] / 100), self.volume_bar_size[1]) - - ddt.rect(gui.volume_bar_rect, colours.volume_bar_fill) - - fields.add(self.volume_bar_position + self.volume_bar_size) - if pctl.active_replaygain != 0 and (coll(( - self.volume_bar_position[0], self.volume_bar_position[1], self.volume_bar_size[0], - self.volume_bar_size[1])) or self.volume_bar_being_dragged): - - if pctl.player_volume > 50: - ddt.text( - (self.volume_bar_position[0] - right_offset + 8 * gui.scale, - self.volume_bar_position[1] - 1 * gui.scale), str(pctl.active_replaygain) + " dB", - colours.volume_bar_background, - 11, bg=colours.volume_bar_fill) - else: - ddt.text( - (self.volume_bar_position[0] - right_offset + 85 * gui.scale, - self.volume_bar_position[1] - 1 * gui.scale), str(pctl.active_replaygain) + " dB", - colours.volume_bar_fill, - 11, bg=colours.volume_bar_background) - - gui.show_bottom_title = gui.showed_title ^ True - if not prefs.hide_bottom_title: - gui.show_bottom_title = True - - if gui.show_bottom_title and pctl.playing_state > 0 and window_size[0] > 820 * gui.scale: - line = pctl.title_text() - - x = self.seek_bar_position[0] + 1 - mx = window_size[0] - 710 * gui.scale - # if gui.bb_show_art: - # x += 10 * gui.scale - # mx -= gui.panelBY - 10 - - # line = trunc_line(line, 213, mx) - ddt.text( - (x, self.seek_bar_position[1] + 24 * gui.scale), line, colours.bar_title_text, - fonts.panel_title, max_w=mx) - - if (inp.mouse_click or right_click) and coll(( - self.seek_bar_position[0] - 10 * gui.scale, self.seek_bar_position[1] + 20 * gui.scale, - window_size[0] - 710 * gui.scale, 30 * gui.scale)): - # if pctl.playing_state == 3: - # copy_to_clipboard(pctl.tag_meta) - # show_message("Copied text to clipboard") - # if input.mouse_click or right_click: - # input.mouse_click = False - # right_click = False - # else: - if inp.mouse_click and pctl.playing_state != 3: - pctl.show_current() - - if pctl.playing_ready() and not gui.fullscreen: - - if right_click: - mode_menu.activate() - - if d_click_timer.get() < 0.3 and inp.mouse_click: - set_mini_mode() - gui.update += 1 - return - d_click_timer.set() - - # TIME---------------------- - - x = window_size[0] - 57 * gui.scale - y = window_size[1] - 29 * gui.scale - - r_start = x - 10 * gui.scale - if gui.display_time_mode in (2, 3): - r_start -= 20 * gui.scale - rect = (r_start, y - 3 * gui.scale, 80 * gui.scale, 27 * gui.scale) - # ddt.rect_r(rect, [255, 0, 0, 40], True) - if inp.mouse_click and coll(rect): - gui.display_time_mode += 1 - if gui.display_time_mode > 3: - gui.display_time_mode = 0 - - if gui.display_time_mode == 0: - text_time = get_display_time(pctl.playing_time) - ddt.text( - (x + 1 * gui.scale, y), text_time, colours.time_playing, - fonts.bottom_panel_time) - elif gui.display_time_mode == 1: - if pctl.playing_state == 0: - text_time = get_display_time(0) - else: - text_time = get_display_time(pctl.playing_length - pctl.playing_time) - ddt.text( - (x + 1 * gui.scale, y), text_time, colours.time_playing, - fonts.bottom_panel_time) - ddt.text( - (x - 5 * gui.scale, y), "-", colours.time_playing, - fonts.bottom_panel_time) - elif gui.display_time_mode == 2: - - # colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) - - x -= 4 - text_time = get_display_time(pctl.playing_time) - ddt.text( - (x - 25 * gui.scale, y), text_time, colours.time_playing, - fonts.bottom_panel_time) - - offset1 = 10 * gui.scale - - if system == "Windows": - offset1 += 2 * gui.scale - - offset2 = offset1 + 7 * gui.scale - - ddt.text( - (x + offset1, y), "/", colours.time_sub, - fonts.bottom_panel_time) - text_time = get_display_time(pctl.playing_length) - if pctl.playing_state == 0: - text_time = get_display_time(0) - elif pctl.playing_state == 3: - text_time = "-- : --" - ddt.text( - (x + offset2, y), text_time, colours.time_sub, - fonts.bottom_panel_time) - - elif gui.display_time_mode == 3: - - # colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) - - track = pctl.playing_object() - if track and track.index != gui.dtm3_index: - - gui.dtm3_cum = 0 - gui.dtm3_total = 0 - run = True - collected = [] - for item in default_playlist: - if pctl.master_library[item].parent_folder_path == track.parent_folder_path: - if item not in collected: - collected.append(item) - gui.dtm3_total += pctl.master_library[item].length - if item == track.index: - run = False - if run: - gui.dtm3_cum += pctl.master_library[item].length - gui.dtm3_index = track.index - - x -= 4 - text_time = get_display_time(gui.dtm3_cum + pctl.playing_time) - - ddt.text( - (x - 25 * gui.scale, y), text_time, colours.time_playing, - fonts.bottom_panel_time) - - offset1 = 10 * gui.scale - if system == "Windows": - offset1 += 2 * gui.scale - offset2 = offset1 + 7 * gui.scale - - ddt.text( - (x + offset1, y), "/", colours.time_sub, - fonts.bottom_panel_time) - text_time = get_display_time(gui.dtm3_total) - if pctl.playing_state == 0: - text_time = get_display_time(0) - elif pctl.playing_state == 3: - text_time = "-- : --" - ddt.text( - (x + offset2, y), text_time, colours.time_sub, - fonts.bottom_panel_time) - - # BUTTONS - # bottom buttons - - if gui.mode == 1: - - # PLAY--- - buttons_x_offset = 0 - compact = False - if window_size[0] < 650 * gui.scale: - compact = True - - play_colour = colours.media_buttons_off - pause_colour = colours.media_buttons_off - stop_colour = colours.media_buttons_off - forward_colour = colours.media_buttons_off - back_colour = colours.media_buttons_off - - if pctl.playing_state == 1: - play_colour = colours.media_buttons_active - - if pctl.auto_stop: - stop_colour = colours.media_buttons_active - - if pctl.playing_state == 2 or (tauon.spot_ctl.coasting and tauon.spot_ctl.paused): - pause_colour = colours.media_buttons_active - play_colour = colours.media_buttons_active - elif pctl.playing_state == 3: - play_colour = colours.media_buttons_active - if tauon.stream_proxy.encode_running: - play_colour = [220, 50, 50, 255] - - if not compact or (compact and pctl.playing_state != 1): - rect = ( - buttons_x_offset + (10 * gui.scale), window_size[1] - self.control_line_bottom - (13 * gui.scale), - 50 * gui.scale, 40 * gui.scale) - fields.add(rect) - if coll(rect): - play_colour = colours.media_buttons_over - if inp.mouse_click: - if compact and pctl.playing_state == 1: - pctl.pause() - elif pctl.playing_state == 1 or tauon.spot_ctl.coasting: - pctl.show_current(highlight=True) - else: - pctl.play() - inp.mouse_click = False - tool_tip2.test(33 * gui.scale, y - 35 * gui.scale, _("Play, RC: Go to playing")) - - if right_click: - pctl.show_current(highlight=True) - - self.play_button.render(29 * gui.scale, window_size[1] - self.control_line_bottom, play_colour) - # ddt.rect_r(rect,[255,0,0,255], True) - - # PAUSE--- - if compact: - buttons_x_offset = -46 * gui.scale - - x = (75 * gui.scale) + buttons_x_offset - y = window_size[1] - self.control_line_bottom - - if not compact or (compact and pctl.playing_state == 1): - - rect = (x - 15 * gui.scale, y - 13 * gui.scale, 50 * gui.scale, 40 * gui.scale) - fields.add(rect) - if coll(rect) and not (pctl.playing_state == 3 and not tauon.spot_ctl.coasting): - pause_colour = colours.media_buttons_over - if inp.mouse_click: - pctl.pause() - if right_click: - pctl.show_current(highlight=True) - tool_tip2.test(x, y - 35 * gui.scale, _("Pause")) - - # ddt.rect_r(rect,[255,0,0,255], True) - ddt.rect_a((x, y + 0), (4 * gui.scale, 13 * gui.scale), pause_colour) - ddt.rect_a((x + 10 * gui.scale, y + 0), (4 * gui.scale, 13 * gui.scale), pause_colour) - - # STOP--- - x = 125 * gui.scale + buttons_x_offset - rect = (x - 14 * gui.scale, y - 13 * gui.scale, 50 * gui.scale, 40 * gui.scale) - fields.add(rect) - if coll(rect): - stop_colour = colours.media_buttons_over - if inp.mouse_click: - pctl.stop() - if right_click: - pctl.auto_stop ^= True - tool_tip2.test(x, y - 35 * gui.scale, _("Stop, RC: Toggle auto-stop")) - - ddt.rect_a((x, y + 0), (13 * gui.scale, 13 * gui.scale), stop_colour) - # ddt.rect_r(rect,[255,0,0,255], True) - - if compact: - buttons_x_offset -= 5 * gui.scale - - # FORWARD--- - rect = (buttons_x_offset + 230 * gui.scale, window_size[1] - self.control_line_bottom - 10 * gui.scale, - 50 * gui.scale, 35 * gui.scale) - fields.add(rect) - if coll(rect) and not (pctl.playing_state == 3 and not tauon.spot_ctl.coasting): - forward_colour = colours.media_buttons_over - if inp.mouse_click: - pctl.advance() - gui.tool_tip_lock_off_f = True - if right_click: - # pctl.random_mode ^= True - toggle_random() - gui.tool_tip_lock_off_f = True - # if window_size[0] < 600 * gui.scale: - # . Shuffle set to on - gui.mode_toast_text = _("Shuffle On") - if not pctl.random_mode: - # . Shuffle set to off - gui.mode_toast_text = _("Shuffle Off") - toast_mode_timer.set() - gui.delay_frame(1) - if middle_click: - pctl.advance(rr=True) - gui.tool_tip_lock_off_f = True - # tool_tip.test(buttons_x_offset + 230 * gui.scale + 50 * gui.scale, window_size[1] - self.control_line_bottom - 20 * gui.scale, "Advance") - # if not gui.tool_tip_lock_off_f: - # tool_tip2.test(x + 45 * gui.scale, y - 35 * gui.scale, _("Forward, RC: Toggle shuffle, MC: Radio random")) - else: - gui.tool_tip_lock_off_f = False - - self.forward_button.render( - buttons_x_offset + 240 * gui.scale, 1 + window_size[1] - self.control_line_bottom, forward_colour) - - # ddt.rect_r(rect,[255,0,0,255], True) - - # BACK--- - rect = (buttons_x_offset + 170 * gui.scale, window_size[1] - self.control_line_bottom - 10 * gui.scale, - 50 * gui.scale, 35 * gui.scale) - fields.add(rect) - if coll(rect) and not (pctl.playing_state == 3 and not tauon.spot_ctl.coasting): - back_colour = colours.media_buttons_over - if inp.mouse_click: - pctl.back() - gui.tool_tip_lock_off_b = True - if right_click: - toggle_repeat() - gui.tool_tip_lock_off_b = True - # if window_size[0] < 600 * gui.scale: - # . Repeat set to on - gui.mode_toast_text = _("Repeat On") - if not pctl.repeat_mode: - # . Repeat set to off - gui.mode_toast_text = _("Repeat Off") - toast_mode_timer.set() - gui.delay_frame(1) - if middle_click: - pctl.revert() - gui.tool_tip_lock_off_b = True - if not gui.tool_tip_lock_off_b: - tool_tip2.test(x, y - 35 * gui.scale, _("Back, RC: Toggle repeat, MC: Revert")) - else: - gui.tool_tip_lock_off_b = False - - self.back_button.render(buttons_x_offset + 180 * gui.scale, 1 + window_size[1] - self.control_line_bottom, - back_colour) - # ddt.rect_r(rect,[255,0,0,255], True) - - # menu button - - x = window_size[0] - 252 * gui.scale - right_offset - y = window_size[1] - round(26 * gui.scale) - rpbc = colours.mode_button_off - rect = (x - 9 * gui.scale, y - 5 * gui.scale, 40 * gui.scale, 25 * gui.scale) - fields.add(rect) - if coll(rect): - if not extra_menu.active: - tool_tip.test(x, y - 28 * gui.scale, _("Playback menu")) - rpbc = colours.mode_button_over - if inp.mouse_click: - extra_menu.activate(position=(x - 115 * gui.scale, y - 6 * gui.scale)) - elif right_click: - mode_menu.activate(position=(x - 115 * gui.scale, y - 6 * gui.scale)) - if extra_menu.active: - rpbc = colours.mode_button_active - - spacing = round(5 * gui.scale) - ddt.rect_a((x, y), (24 * gui.scale, 2 * gui.scale), rpbc) - y += spacing - ddt.rect_a((x, y), (24 * gui.scale, 2 * gui.scale), rpbc) - y += spacing - ddt.rect_a((x, y), (24 * gui.scale, 2 * gui.scale), rpbc) - - if self.mode == 0 and window_size[0] > 530 * gui.scale: - - # shuffle button - x = window_size[0] - 318 * gui.scale - right_offset - y = window_size[1] - 27 * gui.scale - - rect = (x - 5 * gui.scale, y - 5 * gui.scale, 60 * gui.scale, 25 * gui.scale) - fields.add(rect) - - rpbc = colours.mode_button_off - off = True - if (inp.mouse_click or right_click) and coll(rect): - - if inp.mouse_click: - # pctl.random_mode ^= True - toggle_random() - if pctl.random_mode is False: - self.random_click_off = True - else: - shuffle_menu.activate(position=(x + 30 * gui.scale, y - 7 * gui.scale)) - - if pctl.random_mode: - rpbc = colours.mode_button_active - off = False - if coll(rect): - tool_tip.test(x, y - 28 * gui.scale, _("Shuffle")) - elif coll(rect): - tool_tip.test(x, y - 28 * gui.scale, _("Shuffle")) - if self.random_click_off is True: - rpbc = colours.mode_button_off - elif pctl.random_mode is True: - rpbc = colours.mode_button_active - else: - rpbc = colours.mode_button_over - else: - self.random_click_off = False - - # Keep hover highlight on if menu is open - if shuffle_menu.active and not pctl.random_mode: - rpbc = colours.mode_button_over - - #self.shuffle_button.render(x + round(1 * gui.scale), y + round(1 * gui.scale), rpbc) - - #y += round(3 * gui.scale) - #ddt.rect_a((x, y), (25 * gui.scale, 3 * gui.scale), rpbc) - - if pctl.album_shuffle_mode: - self.shuffle_button_a.render(x + round(1 * gui.scale), y + round(1 * gui.scale), rpbc) - elif off: - self.shuffle_button_off.render(x + round(1 * gui.scale), y + round(1 * gui.scale), rpbc) - else: - self.shuffle_button.render(x + round(1 * gui.scale), y + round(1 * gui.scale), rpbc) - - #ddt.rect_a((x + 25 * gui.scale, y), (23 * gui.scale, 3 * gui.scale), rpbc) - - #y += round(5 * gui.scale) - #ddt.rect_a((x, y), (48 * gui.scale, 3 * gui.scale), rpbc) - - # REPEAT - x = window_size[0] - round(380 * gui.scale) - right_offset - y = window_size[1] - round(27 * gui.scale) - - rpbc = colours.mode_button_off - off = True - - rect = (x - 6 * gui.scale, y - 5 * gui.scale, 61 * gui.scale, 25 * gui.scale) - fields.add(rect) - if (inp.mouse_click or right_click) and coll(rect): - - if inp.mouse_click: - toggle_repeat() - if pctl.repeat_mode is False: - self.repeat_click_off = True - else: # right click - repeat_menu.activate(position=(x + 30 * gui.scale, y - 7 * gui.scale)) - # pctl.album_repeat_mode ^= True - # if not pctl.repeat_mode: - # self.repeat_click_off = True - - if pctl.repeat_mode: - rpbc = colours.mode_button_active - off = False - if coll(rect): - if pctl.album_repeat_mode: - tool_tip.test(x, y - 28 * gui.scale, _("Repeat album")) - else: - tool_tip.test(x, y - 28 * gui.scale, _("Repeat track")) - elif coll(rect): - - # Tooltips. But don't show tooltips if menus open - if not repeat_menu.active and not shuffle_menu.active: - if pctl.album_repeat_mode: - tool_tip.test(x, y - 28 * gui.scale, _("Repeat album")) - else: - tool_tip.test(x, y - 28 * gui.scale, _("Repeat track")) - - if self.repeat_click_off is True: - rpbc = colours.mode_button_off - elif pctl.repeat_mode is True: - rpbc = colours.mode_button_active - else: - rpbc = colours.mode_button_over - else: - self.repeat_click_off = False - - # Keep hover highlight on if menu is open - if repeat_menu.active and not pctl.repeat_mode: - rpbc = colours.mode_button_over - - rpbc = alpha_blend(rpbc, colours.bottom_panel_colour) # bake in alpha in case of overlap - - y += round(3 * gui.scale) - w = round(3 * gui.scale) - y = round(y) - x = round(x) - - ar = x + round(50 * gui.scale) - h = round(5 * gui.scale) - - if pctl.album_repeat_mode: - self.repeat_button_a.render(ar - round(45 * gui.scale), y - round(2 * gui.scale), rpbc) - #ddt.rect_a((x + round(4 * gui.scale), y), (round(25 * gui.scale), w), rpbc) - elif off: - self.repeat_button_off.render(ar - round(45 * gui.scale), y - round(2 * gui.scale), rpbc) - else: - self.repeat_button.render(ar - round(45 * gui.scale), y - round(2 * gui.scale), rpbc) - #ddt.rect_a((ar - round(25 * gui.scale), y), (round(25 * gui.scale), w), rpbc) - #ddt.rect_a((ar - w, y), (w, h), rpbc) - #ddt.rect_a((ar - round(50 * gui.scale), y + h), (round(50 * gui.scale), w), rpbc) - - # ddt.rect_a((x + round(25 * gui.scale), y), (round(25 * gui.scale), w), rpbc, True) - # ddt.rect_a((x + round(4 * gui.scale), y + round(5 * gui.scale)), (math.floor(46 * gui.scale), w), rpbc, True) - # ddt.rect_a((x + 50 * gui.scale - w, y), (w, 8 * gui.scale), rpbc, True) - # ddt.rect_a((x + round(50 * gui.scale) - w, y + w), (w, round(4 * gui.scale)), rpbc, True) - - -bottom_bar1 = BottomBarType1() - - -class BottomBarType_ao1: - def __init__(self): - - self.mode = 0 - - self.seek_time = 0 - - self.seek_down = False - self.seek_hit = False - self.volume_hit = False - self.volume_bar_being_dragged = False - self.control_line_bottom = 35 * gui.scale - self.repeat_click_off = False - self.random_click_off = False - - self.seek_bar_position = [300 * gui.scale, window_size[1] - gui.panelBY] - self.seek_bar_size = [window_size[0] - (300 * gui.scale), 15 * gui.scale] - self.volume_bar_size = [135 * gui.scale, 14 * gui.scale] - self.volume_bar_position = [0, 45 * gui.scale] - - self.play_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "play.png", True) - self.forward_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "ff.png", True) - self.back_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "bb.png", True) - - self.scrob_stick = 0 - - def update(self): - - if self.mode == 0: - self.volume_bar_position[0] = window_size[0] - (210 * gui.scale) - self.volume_bar_position[1] = window_size[1] - (27 * gui.scale) - self.seek_bar_position[1] = window_size[1] - gui.panelBY - - seek_bar_x = 300 * gui.scale - if window_size[0] < 600 * gui.scale: - seek_bar_x = 250 * gui.scale - - self.seek_bar_size[0] = window_size[0] - seek_bar_x - self.seek_bar_position[0] = seek_bar_x - - # if gui.bb_show_art: - # self.seek_bar_position[0] = 300 + gui.panelBY - # self.seek_bar_size[0] = window_size[0] - 300 - gui.panelBY - - # self.seek_bar_position[0] = 0 - # self.seek_bar_size[0] = window_size[0] - - def render(self): - - global volume_store - global clicked - global right_click - - ddt.rect_a((0, window_size[1] - gui.panelBY), (window_size[0], gui.panelBY), colours.bottom_panel_colour) - - right_offset = 0 - if gui.display_time_mode >= 2: - right_offset = 22 * gui.scale - - if window_size[0] < 670 * gui.scale: - right_offset -= 90 * gui.scale - - # # MINI ALBUM ART - # if gui.bb_show_art: - # rect = [self.seek_bar_position[0] - gui.panelBY, self.seek_bar_position[1], gui.panelBY, gui.panelBY] - # ddt.rect_r(rect, [255, 255, 255, 8], True) - # if 3 > pctl.playing_state > 0: - # album_art_gen.display(pctl.track_queue[pctl.queue_step], (rect[0], rect[1]), (rect[2], rect[3])) - - # ddt.rect_r(rect, [255, 255, 255, 20]) - - # Volume mouse wheel control ----------------------------------------- - if mouse_wheel != 0 and mouse_position[1] > self.seek_bar_position[1] + 4 and not coll_point( - mouse_position, self.seek_bar_position + self.seek_bar_size): - - pctl.player_volume += mouse_wheel * prefs.volume_wheel_increment - if pctl.player_volume < 1: - pctl.player_volume = 0 - elif pctl.player_volume > 100: - pctl.player_volume = 100 - - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() - - # mode menu - if right_click: - if mouse_position[0] > 190 * gui.scale and \ - mouse_position[1] > window_size[1] - gui.panelBY and \ - mouse_position[0] < window_size[0] - 190 * gui.scale: - mode_menu.activate() - - # Volume Bar 2 ------------------------------------------------ - if True: - x = window_size[0] - right_offset - 120 * gui.scale - y = window_size[1] - round(21 * gui.scale) - - if gui.compact_bar: - x -= 90 * gui.scale - - rect = (x - 8 * gui.scale, y - 17 * gui.scale, 55 * gui.scale, 23 * gui.scale) - # ddt.rect(rect, [255,255,255,25]) - if coll(rect) and mouse_down: - gui.update_on_drag = True - - h_rect = (x - 6 * gui.scale, y - 17 * gui.scale, 4 * gui.scale, 23 * gui.scale) - if coll(h_rect) and mouse_down: - pctl.player_volume = 0 - - step = round(1 * gui.scale) - min_h = round(4 * gui.scale) - spacing = round(5 * gui.scale) - - if right_click and coll((h_rect[0], h_rect[1], h_rect[2] + 50 * gui.scale, h_rect[3])): - if right_click: - if pctl.player_volume > 0: - volume_store = pctl.player_volume - pctl.player_volume = 0 - else: - pctl.player_volume = volume_store - - pctl.set_volume() - - for bar in range(8): - - h = min_h + bar * step - rect = (x, y - h, 3 * gui.scale, h) - h_rect = (x - 1 * gui.scale, y - 17 * gui.scale, 4 * gui.scale, 23 * gui.scale) - - if coll(h_rect): - if mouse_down: - gui.update_on_drag = True - - if bar == 0: - pctl.player_volume = 5 - if bar == 1: - pctl.player_volume = 10 - if bar == 2: - pctl.player_volume = 20 - if bar == 3: - pctl.player_volume = 30 - if bar == 4: - pctl.player_volume = 45 - if bar == 5: - pctl.player_volume = 55 - if bar == 6: - pctl.player_volume = 70 - if bar == 7: - pctl.player_volume = 100 - - pctl.set_volume() - - colour = colours.mode_button_off - - if bar == 0 and pctl.player_volume > 0: - colour = colours.mode_button_active - elif bar == 1 and pctl.player_volume >= 10: - colour = colours.mode_button_active - elif bar == 2 and pctl.player_volume >= 20: - colour = colours.mode_button_active - elif bar == 3 and pctl.player_volume >= 30: - colour = colours.mode_button_active - elif bar == 4 and pctl.player_volume >= 45: - colour = colours.mode_button_active - elif bar == 5 and pctl.player_volume >= 55: - colour = colours.mode_button_active - elif bar == 6 and pctl.player_volume >= 70: - colour = colours.mode_button_active - elif bar == 7 and pctl.player_volume >= 95: - colour = colours.mode_button_active - - ddt.rect(rect, colour) - x += spacing - - # TIME---------------------- - - x = window_size[0] - 57 * gui.scale - y = window_size[1] - 35 * gui.scale - - r_start = x - 10 * gui.scale - if gui.display_time_mode in (2, 3): - r_start -= 20 * gui.scale - rect = (r_start, y - 3 * gui.scale, 80 * gui.scale, 27 * gui.scale) - # ddt.rect_r(rect, [255, 0, 0, 40], True) - if inp.mouse_click and coll(rect): - gui.display_time_mode += 1 - if gui.display_time_mode > 3: - gui.display_time_mode = 0 - - if gui.display_time_mode == 0: - text_time = get_display_time(pctl.playing_time) - ddt.text((x + 1 * gui.scale, y), text_time, colours.time_playing, fonts.bottom_panel_time) - elif gui.display_time_mode == 1: - if pctl.playing_state == 0: - text_time = get_display_time(0) - else: - text_time = get_display_time(pctl.playing_length - pctl.playing_time) - ddt.text((x + 1 * gui.scale, y), text_time, colours.time_playing, fonts.bottom_panel_time) - ddt.text((x - 5 * gui.scale, y), "-", colours.time_playing, fonts.bottom_panel_time) - elif gui.display_time_mode == 2: - - colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) - - x -= 4 - text_time = get_display_time(pctl.playing_time) - ddt.text((x - 25 * gui.scale, y), text_time, colours.time_playing, fonts.bottom_panel_time) - - offset1 = 10 * gui.scale - - if system == "Windows": - offset1 += 2 * gui.scale - - offset2 = offset1 + 7 * gui.scale - - ddt.text((x + offset1, y), "/", colours.time_sub, fonts.bottom_panel_time) - text_time = get_display_time(pctl.playing_length) - if pctl.playing_state == 0: - text_time = get_display_time(0) - elif pctl.playing_state == 3: - text_time = "-- : --" - ddt.text((x + offset2, y), text_time, colours.time_sub, fonts.bottom_panel_time) - - elif gui.display_time_mode == 3: - - colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) - - track = pctl.playing_object() - if track and track.index != gui.dtm3_index: - - gui.dtm3_cum = 0 - gui.dtm3_total = 0 - run = True - collected = [] - for item in default_playlist: - if pctl.master_library[item].parent_folder_path == track.parent_folder_path: - if item not in collected: - collected.append(item) - gui.dtm3_total += pctl.master_library[item].length - if item == track.index: - run = False - if run: - gui.dtm3_cum += pctl.master_library[item].length - gui.dtm3_index = track.index - - x -= 4 - text_time = get_display_time(gui.dtm3_cum + pctl.playing_time) - - ddt.text((x - 25 * gui.scale, y), text_time, colours.time_playing, fonts.bottom_panel_time) - - offset1 = 10 * gui.scale - if system == "Windows": - offset1 += 2 * gui.scale - offset2 = offset1 + 7 * gui.scale - - ddt.text((x + offset1, y), "/", colours.time_sub, fonts.bottom_panel_time) - text_time = get_display_time(gui.dtm3_total) - if pctl.playing_state == 0: - text_time = get_display_time(0) - elif pctl.playing_state == 3: - text_time = "-- : --" - ddt.text((x + offset2, y), text_time, colours.time_sub, fonts.bottom_panel_time) - - # BUTTONS - # bottom buttons - - if gui.mode == 1: - - # PLAY--- - buttons_x_offset = 0 - compact = False - if window_size[0] < 650 * gui.scale: - compact = True - - play_colour = colours.media_buttons_off - pause_colour = colours.media_buttons_off - stop_colour = colours.media_buttons_off - forward_colour = colours.media_buttons_off - back_colour = colours.media_buttons_off - - if pctl.playing_state == 1: - play_colour = colours.media_buttons_active - - if pctl.auto_stop: - stop_colour = colours.media_buttons_active - - if pctl.playing_state == 2: - pause_colour = colours.media_buttons_active - play_colour = colours.media_buttons_active - elif pctl.playing_state == 3: - play_colour = colours.media_buttons_active - if pctl.record_stream: - play_colour = [220, 50, 50, 255] - - if not compact or (compact and pctl.playing_state != 2): - rect = ( - buttons_x_offset + (10 * gui.scale), window_size[1] - self.control_line_bottom - (13 * gui.scale), - 50 * gui.scale, 40 * gui.scale) - fields.add(rect) - if coll(rect): - play_colour = colours.media_buttons_over - if inp.mouse_click: - if compact and pctl.playing_state == 1: - pctl.pause() - elif pctl.playing_state == 1: - pctl.show_current(highlight=True) - else: - pctl.play() - inp.mouse_click = False - tool_tip2.test(33 * gui.scale, y - 35 * gui.scale, _("Play, RC: Go to playing")) - - if right_click: - pctl.show_current(highlight=True) - - self.play_button.render(29 * gui.scale, window_size[1] - self.control_line_bottom, play_colour) - # ddt.rect_r(rect,[255,0,0,255], True) - - # PAUSE--- - if compact: - buttons_x_offset = -46 * gui.scale - - x = (75 * gui.scale) + buttons_x_offset - y = window_size[1] - self.control_line_bottom - - if not compact or (compact and pctl.playing_state == 2): - - rect = (x - 15 * gui.scale, y - 13 * gui.scale, 50 * gui.scale, 40 * gui.scale) - fields.add(rect) - if coll(rect) and pctl.playing_state != 3: - pause_colour = colours.media_buttons_over - if inp.mouse_click: - pctl.pause() - if right_click: - pctl.show_current(highlight=True) - tool_tip2.test(x, y - 35 * gui.scale, _("Pause")) - - # ddt.rect_r(rect,[255,0,0,255], True) - ddt.rect_a((x, y + 0), (4 * gui.scale, 13 * gui.scale), pause_colour) - ddt.rect_a((x + 10 * gui.scale, y + 0), (4 * gui.scale, 13 * gui.scale), pause_colour) - - # FORWARD--- - rect = (buttons_x_offset + 125 * gui.scale, window_size[1] - self.control_line_bottom - 10 * gui.scale, - 50 * gui.scale, 35 * gui.scale) - fields.add(rect) - if coll(rect) and pctl.playing_state != 3: - forward_colour = colours.media_buttons_over - if inp.mouse_click: - pctl.advance() - gui.tool_tip_lock_off_f = True - if right_click: - # pctl.random_mode ^= True - toggle_random() - gui.tool_tip_lock_off_f = True - # if window_size[0] < 600 * gui.scale: - # . Shuffle set to on - gui.mode_toast_text = _("Shuffle On") - if not pctl.random_mode: - # . Shuffle set to off - gui.mode_toast_text = _("Shuffle Off") - toast_mode_timer.set() - gui.delay_frame(1) - if middle_click: - pctl.advance(rr=True) - gui.tool_tip_lock_off_f = True - # tool_tip.test(buttons_x_offset + 230 * gui.scale + 50 * gui.scale, window_size[1] - self.control_line_bottom - 20 * gui.scale, "Advance") - # if not gui.tool_tip_lock_off_f: - # tool_tip2.test(x + 45 * gui.scale, y - 35 * gui.scale, _("Forward, RC: Toggle shuffle, MC: Radio random")) - else: - gui.tool_tip_lock_off_f = False - - self.forward_button.render( - buttons_x_offset + 125 * gui.scale, - 1 + window_size[1] - self.control_line_bottom, forward_colour) - - -bottom_bar_ao1 = BottomBarType_ao1() - - -class MiniMode: - def __init__(self): - self.save_position = None - self.was_borderless = True - self.volume_timer = Timer() - self.volume_timer.force_set(100) - - self.left_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "left-slide.png", True) - self.right_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "right-slide.png", True) - self.repeat = asset_loader(scaled_asset_directory, loaded_asset_dc, "repeat-mini-mode.png", True) - self.shuffle = asset_loader(scaled_asset_directory, loaded_asset_dc, "shuffle-mini-mode.png", True) - - self.shuffle_fade_timer = Timer(100) - self.repeat_fade_timer = Timer(100) - - def render(self): - # We only set seek_r and seek_w if track is currently on, but use it anyway later, so make sure it exists - if 'seek_r' not in locals(): - seek_r = [0, 0, 0, 0] - seek_w = 0 - - w = window_size[0] - h = window_size[1] - - y1 = w - if w == h: - y1 -= 79 * gui.scale - - h1 = h - y1 - - # Draw background - bg = colours.mini_mode_background - # bg = [250, 250, 250, 255] - - ddt.rect((0, 0, w, h), bg) - ddt.text_background_colour = bg - - detect_mouse_rect = (3, 3, w - 6, h - 6) - fields.add(detect_mouse_rect) - mouse_in = coll(detect_mouse_rect) - - # Play / Pause when right clicking below art - if right_click: # and mouse_position[1] > y1: - pctl.play_pause() - - # Volume change on scroll - if mouse_wheel != 0: - self.volume_timer.set() - - pctl.player_volume += mouse_wheel * prefs.volume_wheel_increment * 3 - if pctl.player_volume < 1: - pctl.player_volume = 0 - elif pctl.player_volume > 100: - pctl.player_volume = 100 - - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() - - track = pctl.playing_object() - - control_hit_area = (3, y1 - 15 * gui.scale, w - 6, h1 - 3 + 15 * gui.scale) - mouse_in_area = coll(control_hit_area) - fields.add(control_hit_area) - - ddt.rect((0, 0, w, w), (0, 0, 0, 45)) - if track is not None: - - # Render album art - album_art_gen.display(track, (0, 0), (w, w)) - - line1c = colours.mini_mode_text_1 - line2c = colours.mini_mode_text_2 - - if h == w and mouse_in_area: - # ddt.pretty_rect = (0, 260 * gui.scale, w, 100 * gui.scale) - ddt.rect((0, y1, w, h1), [0, 0, 0, 220]) - line1c = [255, 255, 255, 240] - line2c = [255, 255, 255, 77] - - # Double click bottom text to return to full window - text_hit_area = (60 * gui.scale, y1 + 4, 230 * gui.scale, 50 * gui.scale) - - if coll(text_hit_area): - if inp.mouse_click: - if d_click_timer.get() < 0.3: - restore_full_mode() - gui.update += 1 - return - d_click_timer.set() - - # Draw title texts - line1 = track.artist - line2 = track.title - - # Calculate seek bar position - seek_w = int(w * 0.70) - - seek_r = [(w - seek_w) // 2, y1 + 58 * gui.scale, seek_w, 6 * gui.scale] - seek_r_hit = [seek_r[0], seek_r[1] - 4 * gui.scale, seek_r[2], seek_r[3] + 8 * gui.scale] - - if w != h or mouse_in_area: - - if not line1 and not line2: - ddt.text((w // 2, y1 + 18 * gui.scale, 2), track.filename, line1c, 214, window_size[0] - 30 * gui.scale) - else: - - ddt.text((w // 2, y1 + 10 * gui.scale, 2), line1, line2c, 514, window_size[0] - 30 * gui.scale) - - ddt.text((w // 2, y1 + 31 * gui.scale, 2), line2, line1c, 414, window_size[0] - 30 * gui.scale) - - # Test click to seek - if mouse_up and coll(seek_r_hit): - - click_x = mouse_position[0] - click_x = min(click_x, seek_r[0] + seek_r[2]) - click_x = max(click_x, seek_r[0]) - click_x -= seek_r[0] - - if click_x < 6 * gui.scale: - click_x = 0 - seek = click_x / seek_r[2] - - pctl.seek_decimal(seek) - - # Draw progress bar background - ddt.rect(seek_r, [255, 255, 255, 32]) - - # Calculate and draw bar foreground - progress_w = 0 - if pctl.playing_length > 1: - progress_w = pctl.playing_time * seek_w / pctl.playing_length - seek_colour = [210, 210, 210, 255] - if gui.theme_name == "Carbon": - seek_colour = colours.bottom_panel_colour - - if pctl.playing_state != 1: - seek_colour = [210, 40, 100, 255] - - seek_r[2] = progress_w - - if self.volume_timer.get() < 0.9: - progress_w = pctl.player_volume * (seek_w - (4 * gui.scale)) / 100 - gui.update += 1 - seek_colour = [210, 210, 210, 255] - seek_r[2] = progress_w - seek_r[0] += 2 * gui.scale - seek_r[1] += 2 * gui.scale - seek_r[3] -= 4 * gui.scale - - ddt.rect(seek_r, seek_colour) - - left_area = (1, y1, seek_r[0] - 1, 45 * gui.scale) - right_area = (seek_r[0] + seek_w, y1, seek_r[0] - 2, 45 * gui.scale) - - fields.add(left_area) - fields.add(right_area) - - hint = 0 - if coll(control_hit_area): - hint = 30 - if coll(left_area): - hint = 240 - if hint and not prefs.shuffle_lock: - self.left_slide.render(16 * gui.scale, y1 + 17 * gui.scale, [255, 255, 255, hint]) - - hint = 0 - if coll(control_hit_area): - hint = 30 - if coll(right_area): - hint = 240 - if hint: - self.right_slide.render(window_size[0] - self.right_slide.w - 16 * gui.scale, y1 + 17 * gui.scale, - [255, 255, 255, hint]) - - # Shuffle - - shuffle_area = (seek_r[0] + seek_w, seek_r[1] - 10 * gui.scale, 50 * gui.scale, 30 * gui.scale) - # fields.add(shuffle_area) - # ddt.rect_r(shuffle_area, [255, 0, 0, 100], True) - - if coll(control_hit_area) and not prefs.shuffle_lock: - colour = [255, 255, 255, 20] - if inp.mouse_click and coll(shuffle_area): - # pctl.random_mode ^= True - toggle_random() - if pctl.random_mode: - colour = [255, 255, 255, 190] - - sx = seek_r[0] + seek_w + 12 * gui.scale - sy = seek_r[1] - 2 * gui.scale - self.shuffle.render(sx, sy, colour) - - - # sx = seek_r[0] + seek_w + 8 * gui.scale - # sy = seek_r[1] - 1 * gui.scale - # ddt.rect_a((sx, sy), (14 * gui.scale, 2 * gui.scale), colour) - # sy += 4 * gui.scale - # ddt.rect_a((sx, sy), (28 * gui.scale, 2 * gui.scale), colour) - - shuffle_area = (seek_r[0] - 41 * gui.scale, seek_r[1] - 10 * gui.scale, 40 * gui.scale, 30 * gui.scale) - if coll(control_hit_area) and not prefs.shuffle_lock: - colour = [255, 255, 255, 20] - if inp.mouse_click and coll(shuffle_area): - toggle_repeat() - if pctl.repeat_mode: - colour = [255, 255, 255, 190] - - - sx = seek_r[0] - 36 * gui.scale - sy = seek_r[1] - 1 * gui.scale - self.repeat.render(sx, sy, colour) - - - # sx = seek_r[0] - 39 * gui.scale - # sy = seek_r[1] - 1 * gui.scale - - #tw = 2 * gui.scale - # ddt.rect_a((sx + 15 * gui.scale, sy), (13 * gui.scale, tw), colour) - # ddt.rect_a((sx + 4 * gui.scale, sy + 4 * gui.scale), (25 * gui.scale, tw), colour) - # ddt.rect_a((sx + 30 * gui.scale - tw, sy), (tw, 6 * gui.scale), colour) - - - # Forward and back clicking - if inp.mouse_click: - if coll(left_area) and not prefs.shuffle_lock: - pctl.back() - if coll(right_area): - pctl.advance() - - # Show exit/min buttons when mosue over - tool_rect = [window_size[0] - 110 * gui.scale, 2, 108 * gui.scale, 45 * gui.scale] - if prefs.left_window_control: - tool_rect[0] = 0 - fields.add(tool_rect) - if coll(tool_rect): - draw_window_tools() - - if w != h: - ddt.rect_s((1, 1, w - 2, h - 2), colours.mini_mode_border, 1 * gui.scale) - if gui.scale == 2: - ddt.rect_s((2, 2, w - 4, h - 4), colours.mini_mode_border, 1 * gui.scale) - - -mini_mode = MiniMode() - - -class MiniMode2: - - def __init__(self): - - self.save_position = None - self.was_borderless = True - self.volume_timer = Timer() - self.volume_timer.force_set(100) - - self.left_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "left-slide.png", True) - self.right_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "right-slide.png", True) - - def render(self): - - w = window_size[0] - h = window_size[1] - - x1 = h - - # Draw background - ddt.rect((0, 0, w, h), colours.mini_mode_background) - ddt.text_background_colour = colours.mini_mode_background - - detect_mouse_rect = (2, 2, w - 4, h - 4) - fields.add(detect_mouse_rect) - mouse_in = coll(detect_mouse_rect) - - # Play / Pause when right clicking below art - if right_click: # and mouse_position[1] > y1: - pctl.play_pause() - - # Volume change on scroll - if mouse_wheel != 0: - self.volume_timer.set() - - pctl.player_volume += mouse_wheel * prefs.volume_wheel_increment * 3 - if pctl.player_volume < 1: - pctl.player_volume = 0 - elif pctl.player_volume > 100: - pctl.player_volume = 100 - - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() - - track = pctl.playing_object() - - if track is not None: - - # Render album art - album_art_gen.display(track, (0, 0), (h, h)) - - text_hit_area = (x1, 0, w, h) - - if coll(text_hit_area): - if inp.mouse_click: - if d_click_timer.get() < 0.3: - restore_full_mode() - gui.update += 1 - return - d_click_timer.set() - - # Draw title texts - line1 = track.artist - line2 = track.title - - if not line1 and not line2: - - ddt.text( - (x1 + 15 * gui.scale, 44 * gui.scale), track.filename, colours.grey(150), 315, - window_size[0] - x1 - 30 * gui.scale) - else: - - # if ddt.get_text_w(line2, 215) > window_size[0] - x1 - 30 * gui.scale: - # ddt.text((x1 + 15 * gui.scale, 19 * gui.scale), line2, colours.grey(249), 413, - # window_size[0] - x1 - 35 * gui.scale) - # - # ddt.text((x1 + 15 * gui.scale, 43 * gui.scale), line1, colours.grey(110), 513, - # window_size[0] - x1 - 35 * gui.scale) - # else: - - ddt.text( - (x1 + 15 * gui.scale, 18 * gui.scale), line2, colours.grey(249), 514, - window_size[0] - x1 - 30 * gui.scale) - - ddt.text( - (x1 + 15 * gui.scale, 43 * gui.scale), line1, colours.grey(110), 514, - window_size[0] - x1 - 30 * gui.scale) - - # Show exit/min buttons when mosue over - tool_rect = [window_size[0] - 110 * gui.scale, 2, 108 * gui.scale, 45 * gui.scale] - if prefs.left_window_control: - tool_rect[0] = 0 - fields.add(tool_rect) - if coll(tool_rect): - draw_window_tools() - - # Seek bar - bg_rect = (h, h - round(5 * gui.scale), w - h, round(5 * gui.scale)) - ddt.rect(bg_rect, [255, 255, 255, 18]) - - if pctl.playing_state > 0: - - hit_rect = h - 5 * gui.scale, h - 12 * gui.scale, w - h + 5 * gui.scale, 13 * gui.scale - - if coll(hit_rect) and mouse_up: - p = (mouse_position[0] - h) / (w - h) - - if p < 0 or mouse_position[0] - h < 6 * gui.scale: - pctl.seek_time(0) - elif p > .96: - pctl.advance() - else: - pctl.seek_decimal(p) - - if pctl.playing_length: - seek_rect = ( - h, h - round(5 * gui.scale), round((w - h) * (pctl.playing_time / pctl.playing_length)), - round(5 * gui.scale)) - colour = colours.artist_text - if gui.theme_name == "Carbon": - colour = colours.bottom_panel_colour - if pctl.playing_state != 1: - colour = [210, 40, 100, 255] - ddt.rect(seek_rect, colour) - - -mini_mode2 = MiniMode2() - - - -class MiniMode3: - - def __init__(self): - - self.save_position = None - self.was_borderless = True - self.volume_timer = Timer() - self.volume_timer.force_set(100) - - self.left_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "left-slide.png", True) - self.right_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "right-slide.png", True) - - self.shuffle_fade_timer = Timer(100) - self.repeat_fade_timer = Timer(100) - - def render(self): - # We only set seek_r and seek_w if track is currently on, but use it anyway later, so make sure it exists - if 'seek_r' not in locals(): - seek_r = [0, 0, 0, 0] - seek_w = 0 - volume_r = [0, 0, 0, 0] - volume_w = 0 - - w = window_size[0] - h = window_size[1] - - y1 = w #+ 10 * gui.scale - # if w == h: - # y1 -= 79 * gui.scale - - h1 = h - y1 - - # Draw background - bg = colours.mini_mode_background - bg = [0, 0, 0, 0] - # bg = [250, 250, 250, 255] - - ddt.rect((0, 0, w, h), bg) - - style_overlay.display() - - transit = False - #ddt.text_background_colour = list(gui.center_blur_pixel) + [255,] #bg - if style_overlay.fade_on_timer.get() < 0.4 or style_overlay.stage != 2: - ddt.alpha_bg = True - transit = True - - detect_mouse_rect = (3, 3, w - 6, h - 6) - fields.add(detect_mouse_rect) - mouse_in = coll(detect_mouse_rect) - - # Play / Pause when right clicking below art - if right_click: # and mouse_position[1] > y1: - pctl.play_pause() - - # Volume change on scroll - if mouse_wheel != 0: - self.volume_timer.set() - - pctl.player_volume += mouse_wheel * prefs.volume_wheel_increment * 3 - if pctl.player_volume < 1: - pctl.player_volume = 0 - elif pctl.player_volume > 100: - pctl.player_volume = 100 - - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() - - track = pctl.playing_object() - - control_hit_area = (3, y1 - 15 * gui.scale, w - 6, h1 - 3 + 15 * gui.scale) - mouse_in_area = coll(control_hit_area) - fields.add(control_hit_area) - - #ddt.rect((0, 0, w, w), (0, 0, 0, 45)) - if track is not None: - - # Render album art - - wid = (w // 2) + round(60 * gui.scale) - ins = (window_size[0] - wid) / 2 - off = round(4 * gui.scale) - - drop_shadow.render(ins + off, ins + off, wid + off * 2, wid + off * 2) - ddt.rect((ins, ins, wid, wid), [20, 20, 20, 255]) - album_art_gen.display(track, (ins, ins), (wid, wid)) - - line1c = [255, 255, 255, 255] #colours.mini_mode_text_1 - line2c = [255, 255, 255, 255] #colours.mini_mode_text_2 - - # if h == w and mouse_in_area: - # # ddt.pretty_rect = (0, 260 * gui.scale, w, 100 * gui.scale) - # ddt.rect((0, y1, w, h1), [0, 0, 0, 220]) - # line1c = [255, 255, 255, 240] - # line2c = [255, 255, 255, 77] - - # Double click bottom text to return to full window - text_hit_area = (60 * gui.scale, y1 + 4, 230 * gui.scale, 50 * gui.scale) - - if coll(text_hit_area): - if inp.mouse_click: - if d_click_timer.get() < 0.3: - restore_full_mode() - gui.update += 1 - return - d_click_timer.set() - - # Draw title texts - line1 = track.artist - line2 = track.title - key = None - if not line1 and not line2: - if not ddt.alpha_bg: - key = (track.filename, 214, style_overlay.current_track_id) - ddt.text( - (w // 2, y1 + 18 * gui.scale, 2), track.filename, line1c, 214, - window_size[0] - 30 * gui.scale, real_bg=not transit, key=key) - else: - - if not ddt.alpha_bg: - key = (line1, 515, style_overlay.current_track_id) - ddt.text( - (w // 2, y1 + 5 * gui.scale, 2), line1, line2c, 515, - window_size[0] - 30 * gui.scale, real_bg=not transit, key=key) - if not ddt.alpha_bg: - key = (line2, 415, style_overlay.current_track_id) - ddt.text( - (w // 2, y1 + 31 * gui.scale, 2), line2, line1c, 415, - window_size[0] - 30 * gui.scale, real_bg=not transit, key=key) - - y1 += round(10 * gui.scale) - - # Calculate seek bar position - seek_w = int(w * 0.80) - - seek_r = [(w - seek_w) // 2, y1 + 58 * gui.scale, seek_w, 9 * gui.scale] - seek_r_hit = [seek_r[0], seek_r[1] - 5 * gui.scale, seek_r[2], seek_r[3] + 12 * gui.scale] - - if w != h or mouse_in_area: - - - # Test click to seek - if mouse_up and coll(seek_r_hit): - - click_x = mouse_position[0] - click_x = min(click_x, seek_r[0] + seek_r[2]) - click_x = max(click_x, seek_r[0]) - click_x -= seek_r[0] - - if click_x < 6 * gui.scale: - click_x = 0 - seek = click_x / seek_r[2] - - pctl.seek_decimal(seek) - - # Draw progress bar background - ddt.rect(seek_r, [255, 255, 255, 32]) - - # Calculate and draw bar foreground - progress_w = 0 - if pctl.playing_length > 1: - progress_w = pctl.playing_time * seek_w / pctl.playing_length - seek_colour = [210, 210, 210, 255] - if gui.theme_name == "Carbon": - seek_colour = colours.bottom_panel_colour - - if pctl.playing_state != 1: - seek_colour = [210, 40, 100, 255] - - seek_r[2] = progress_w - - ddt.rect(seek_r, seek_colour) - - - - volume_w = int(w * 0.50) - volume_r = [(w - volume_w) // 2, y1 + 80 * gui.scale, volume_w, 6 * gui.scale] - volume_r_hit = [volume_r[0], volume_r[1] - 5 * gui.scale, volume_r[2], volume_r[3] + 10 * gui.scale] - - # Test click to volume - if (mouse_up or mouse_down) and coll(volume_r_hit): - gui.update_on_drag = True - click_x = mouse_position[0] - click_x = min(click_x, volume_r[0] + volume_r[2]) - click_x = max(click_x, volume_r[0]) - click_x -= volume_r[0] - - if click_x < 6 * gui.scale: - click_x = 0 - volume = click_x / volume_r[2] - - pctl.player_volume = int(volume * 100) - pctl.set_volume() - - ddt.rect(volume_r, [255, 255, 255, 32]) - - #if self.volume_timer.get() < 0.9: - progress_w = pctl.player_volume * (volume_w - (4 * gui.scale)) / 100 - volume_colour = [210, 210, 210, 255] - volume_r[2] = progress_w - volume_r[0] += 2 * gui.scale - volume_r[1] += 2 * gui.scale - volume_r[3] -= 4 * gui.scale - - ddt.rect(volume_r, volume_colour) - - - left_area = (1, y1, volume_r[0] - 1, 45 * gui.scale) - right_area = (volume_r[0] + volume_w, y1, volume_r[0] - 2, 45 * gui.scale) - - fields.add(left_area) - fields.add(right_area) - - hint = 0 - if True: #coll(control_hit_area): - hint = 30 - if coll(left_area): - hint = 240 - if hint and not prefs.shuffle_lock: - self.left_slide.render(16 * gui.scale, y1 + 10 * gui.scale, [255, 255, 255, hint]) - - hint = 0 - if True: #coll(control_hit_area): - hint = 30 - if coll(right_area): - hint = 240 - if hint: - self.right_slide.render( - window_size[0] - self.right_slide.w - 16 * gui.scale, y1 + 10 * gui.scale, [255, 255, 255, hint]) - - # Shuffle - shuffle_area = (volume_r[0] + volume_w, volume_r[1] - 10 * gui.scale, 50 * gui.scale, 30 * gui.scale) - # fields.add(shuffle_area) - # ddt.rect_r(shuffle_area, [255, 0, 0, 100], True) - - if True: #coll(control_hit_area) and not prefs.shuffle_lock: - colour = [255, 255, 255, 20] - if inp.mouse_click and coll(shuffle_area): - # pctl.random_mode ^= True - toggle_random() - if pctl.random_mode: - colour = [255, 255, 255, 190] - - sx = volume_r[0] + volume_w + 12 * gui.scale - sy = volume_r[1] - 3 * gui.scale - mini_mode.shuffle.render(sx, sy, colour) - - # - # sx = volume_r[0] + volume_w + 8 * gui.scale - # sy = volume_r[1] - 1 * gui.scale - # ddt.rect_a((sx, sy), (14 * gui.scale, 2 * gui.scale), colour) - # sy += 4 * gui.scale - # ddt.rect_a((sx, sy), (28 * gui.scale, 2 * gui.scale), colour) - - shuffle_area = (volume_r[0] - 41 * gui.scale, volume_r[1] - 10 * gui.scale, 40 * gui.scale, 30 * gui.scale) - if True: #coll(control_hit_area) and not prefs.shuffle_lock: - colour = [255, 255, 255, 20] - if inp.mouse_click and coll(shuffle_area): - toggle_repeat() - if pctl.repeat_mode: - colour = [255, 255, 255, 190] - - sx = volume_r[0] - 39 * gui.scale - sy = volume_r[1] - 1 * gui.scale - mini_mode.repeat.render(sx, sy, colour) - - # sx = volume_r[0] - 39 * gui.scale - # sy = volume_r[1] - 1 * gui.scale - # - # tw = 2 * gui.scale - # ddt.rect_a((sx + 15 * gui.scale, sy), (13 * gui.scale, tw), colour) - # ddt.rect_a((sx + 4 * gui.scale, sy + 4 * gui.scale), (25 * gui.scale, tw), colour) - # ddt.rect_a((sx + 30 * gui.scale - tw, sy), (tw, 6 * gui.scale), colour) - - # Forward and back clicking - if inp.mouse_click: - if coll(left_area) and not prefs.shuffle_lock: - pctl.back() - if coll(right_area): - pctl.advance() - - search_over.render() - - - # Show exit/min buttons when mosue over - tool_rect = [window_size[0] - 110 * gui.scale, 2, 108 * gui.scale, 45 * gui.scale] - if prefs.left_window_control: - tool_rect[0] = 0 - fields.add(tool_rect) - if coll(tool_rect): - draw_window_tools() - - - # if w != h: - # ddt.rect_s((1, 1, w - 2, h - 2), colours.mini_mode_border, 1 * gui.scale) - # if gui.scale == 2: - # ddt.rect_s((2, 2, w - 4, h - 4), colours.mini_mode_border, 1 * gui.scale) - ddt.alpha_bg = False - -mini_mode3 = MiniMode3() - -def set_mini_mode(): - if gui.fullscreen: - return - - global mouse_down - global mouse_up - global old_window_position - mouse_down = False - mouse_up = False - inp.mouse_click = False - - if gui.maximized: - SDL_RestoreWindow(t_window) - update_layout_do() - - if gui.mode < 3: - old_window_position = get_window_position() - - if prefs.mini_mode_on_top: - SDL_SetWindowAlwaysOnTop(t_window, True) - - gui.mode = 3 - gui.vis = 0 - gui.turbo = False - gui.draw_vis4_top = False - gui.level_update = False - - i_y = pointer(c_int(0)) - i_x = pointer(c_int(0)) - SDL_GetWindowPosition(t_window, i_x, i_y) - gui.save_position = (i_x.contents.value, i_y.contents.value) - - mini_mode.was_borderless = draw_border - SDL_SetWindowBordered(t_window, False) - - size = (350, 429) - if prefs.mini_mode_mode == 1: - size = (330, 330) - if prefs.mini_mode_mode == 2: - size = (420, 499) - if prefs.mini_mode_mode == 3: - size = (430, 430) - if prefs.mini_mode_mode == 4: - size = (330, 80) - if prefs.mini_mode_mode == 5: - size = (350, 545) - style_overlay.flush() - tauon.thread_manager.ready("style") - - if logical_size == window_size: - size = (int(size[0] * gui.scale), int(size[1] * gui.scale)) - - logical_size[0] = size[0] - logical_size[1] = size[1] - - SDL_SetWindowMinimumSize(t_window, 100, 100) - - SDL_SetWindowResizable(t_window, False) - SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - - if mini_mode.save_position: - SDL_SetWindowPosition(t_window, mini_mode.save_position[0], mini_mode.save_position[1]) - - i_x = pointer(c_int(0)) - i_y = pointer(c_int(0)) - SDL_GL_GetDrawableSize(t_window, i_x, i_y) - window_size[0] = i_x.contents.value - window_size[1] = i_y.contents.value - - gui.update += 3 - - -restore_ignore_timer = Timer() -restore_ignore_timer.force_set(100) - - -def restore_full_mode(): - logging.info("RESTORE FULL") - i_y = pointer(c_int(0)) - i_x = pointer(c_int(0)) - SDL_GetWindowPosition(t_window, i_x, i_y) - mini_mode.save_position = [i_x.contents.value, i_y.contents.value] - - if not mini_mode.was_borderless: - SDL_SetWindowBordered(t_window, True) - - logical_size[0] = gui.save_size[0] - logical_size[1] = gui.save_size[1] - - SDL_SetWindowPosition(t_window, gui.save_position[0], gui.save_position[1]) - - - SDL_SetWindowResizable(t_window, True) - SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - SDL_SetWindowAlwaysOnTop(t_window, False) - - # if macos: - # SDL_SetWindowMinimumSize(t_window, 560, 330) - # else: - SDL_SetWindowMinimumSize(t_window, 560, 330) - - restore_ignore_timer.set() # Hacky - - gui.mode = 1 - - global mouse_down - global mouse_up - mouse_down = False - mouse_up = False - inp.mouse_click = False - - if gui.maximized: - SDL_MaximizeWindow(t_window) - time.sleep(0.05) - SDL_PumpEvents() - SDL_GetWindowSize(t_window, i_x, i_y) - logical_size[0] = i_x.contents.value - logical_size[1] = i_y.contents.value - - #logging.info(window_size) - - SDL_PumpEvents() - SDL_GL_GetDrawableSize(t_window, i_x, i_y) - window_size[0] = i_x.contents.value - window_size[1] = i_y.contents.value - - gui.update_layout() - if prefs.art_bg: - tauon.thread_manager.ready("style") - - -def line_render(n_track: TrackClass, p_track: TrackClass, y, this_line_playing, album_fade, start_x, width, style=1, ry=None): - timec = colours.bar_time - titlec = colours.title_text - indexc = colours.index_text - artistc = colours.artist_text - albumc = colours.album_text - - if this_line_playing is True: - timec = colours.time_text - titlec = colours.title_playing - indexc = colours.index_playing - artistc = colours.artist_playing - albumc = colours.album_playing - - if n_track.found is False: - timec = colours.playlist_text_missing - titlec = colours.playlist_text_missing - indexc = colours.playlist_text_missing - artistc = colours.playlist_text_missing - albumc = colours.playlist_text_missing - - artistoffset = 0 - indexLine = "" - - offset_font_extra = 0 - if gui.row_font_size > 14: - offset_font_extra = 8 - - # In windows (arial?) draws numbers too high (hack fix) - num_y_offset = 0 - # if system == 'Windows': - # num_y_offset = 1 - - if True or style == 1: - - # if not gui.rsp and not gui.combo_mode: - # width -= 10 * gui.scale - - dash = False - if n_track.artist and colours.artist_text == colours.title_text: - dash = True - - if n_track.title: - - line = track_number_process(n_track.track_number) - - indexLine = line - - if prefs.use_absolute_track_index and pctl.multi_playlist[pctl.active_playlist_viewing].hide_title: - indexLine = str(p_track) - if len(indexLine) > 3: - indexLine += " " - - line = "" - - if n_track.artist != "" and not dash: - line0 = n_track.artist - - artistoffset = ddt.text( - (start_x + 27 * gui.scale, y), - line0, - alpha_mod(artistc, album_fade), - gui.row_font_size, - int(width / 2)) - - line = n_track.title - else: - line += n_track.title - else: - line = \ - os.path.splitext(n_track.filename)[ - 0] - - if p_track >= len(default_playlist): - gui.pl_update += 1 - return - - index = default_playlist[p_track] - star_x = 0 - total = star_store.get(index) - - if gui.star_mode == "line" and total > 0 and pctl.master_library[index].length > 0: - - ratio = total / pctl.master_library[index].length - if ratio > 0.55: - star_x = int(ratio * 4 * gui.scale) - star_x = min(star_x, 60 * gui.scale) - sp = y - 0 - gui.playlist_text_offset + int(gui.playlist_row_height / 2) - if gui.playlist_row_height > 17 * gui.scale: - sp -= 1 - - lh = 1 - if gui.scale != 1: - lh = 2 - - colour = colours.star_line - if this_line_playing and colours.star_line_playing is not None: - colour = colours.star_line_playing - - ddt.rect( - [ - width + start_x - star_x - 45 * gui.scale - offset_font_extra, - sp, - star_x + 3 * gui.scale, - lh], - alpha_mod(colour, album_fade)) - - star_x += 6 * gui.scale - - if gui.show_ratings: - sx = round(width + start_x - round(40 * gui.scale) - offset_font_extra) - sy = round(ry + (gui.playlist_row_height // 2) - round(7 * gui.scale)) - sx -= round(68 * gui.scale) - - draw_rating_widget(sx, sy, n_track) - - star_x += round(70 * gui.scale) - - if gui.star_mode == "star" and total > 0 and pctl.master_library[ - index].length != 0: - - sx = width + start_x - 40 * gui.scale - offset_font_extra - sy = ry + (gui.playlist_row_height // 2) - (6 * gui.scale) - # if gui.scale == 1.25: - # sy += 1 - playtime_stars = star_count(total, pctl.master_library[index].length) - 1 - - sx2 = sx - selected_star = -2 - rated_star = -1 - - # if key_ctrl_down: - - c = 60 - d = 6 - - colour = [70, 70, 70, 255] - if colours.lm: - colour = [90, 90, 90, 255] - # colour = alpha_mod(indexc, album_fade) - - for count in range(8): - - if selected_star < count and playtime_stars < count and rated_star < count: - break - - if count == 0: - sx -= round(13 * gui.scale) - star_x += round(13 * gui.scale) - elif playtime_stars > 3: - dd = round((13 - (playtime_stars - 3)) * gui.scale) - sx -= dd - star_x += dd - else: - sx -= round(13 * gui.scale) - star_x += round(13 * gui.scale) - - # if playtime_stars > 4: - # colour = [c + d * count, c + d * count, c + d * count, 255] - # if playtime_stars > 6: # and count < 1: - # colour = [230, 220, 60, 255] - if gui.tracklist_bg_is_light: - colour = alpha_blend([0, 0, 0, 200], ddt.text_background_colour) - else: - colour = alpha_blend([255, 255, 255, 50], ddt.text_background_colour) - - # if selected_star > -2: - # if selected_star >= count: - # colour = (220, 200, 60, 255) - # else: - # if rated_star >= count: - # colour = (220, 200, 60, 255) - - star_pc_icon.render(sx, sy, colour) - - if gui.show_hearts: - - xxx = star_x - - count = 0 - spacing = 6 * gui.scale - - yy = ry + (gui.playlist_row_height // 2) - (5 * gui.scale) - if gui.scale == 1.25: - yy += 1 - if xxx > 0: - xxx += 3 * gui.scale - - if love(False, index): - count = 1 - - x = width + start_x - 52 * gui.scale - offset_font_extra - xxx - - f_store.store(display_you_heart, (x, yy)) - - star_x += 18 * gui.scale - - if "spotify-liked" in pctl.master_library[index].misc: - - x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx - - f_store.store(display_spot_heart, (x, yy)) - - star_x += heart_row_icon.w + spacing + 2 - - for name in pctl.master_library[index].lfm_friend_likes: - - # Limit to number of hears to display - if gui.star_mode == "none": - if count > 6: - break - elif count > 4: - break - - x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx - - f_store.store(display_friend_heart, (x, yy, name)) - - count += 1 - - star_x += heart_row_icon.w + spacing + 2 - - # Draw track number/index - display_queue = False - - if pctl.force_queue: - - marks = [] - album_type = False - for i, item in enumerate(pctl.force_queue): - if item.track_id == n_track.index and item.position == p_track and item.playlist_id == pl_to_id( - pctl.active_playlist_viewing): - if item.type == 0: # Only show mark if track type - marks.append(i) - # else: - # album_type = True - # marks.append(i) - - if marks: - display_queue = True - - if display_queue: - - li = str(marks[0] + 1) - if li == "1": - li = "N" - # if item.track_id == n_track.index and item.position == p_track and item.playlist_id == pctl.active_playlist_viewing - if pctl.playing_ready() and n_track.index == pctl.track_queue[ - pctl.queue_step] and p_track == pctl.playlist_playing_position: - li = "R" - # if album_type: - # li = "A" - - # rect = (start_x + 3 * gui.scale, y - 1 * gui.scale, 5 * gui.scale, 5 * gui.scale) - # ddt.rect_r(rect, [100, 200, 100, 255], True) - if len(marks) > 1: - li += " " + ("." * (len(marks) - 1)) - li = li[:5] - - # if album_type: - # li += "🠗" - - colour = [244, 200, 66, 255] - if colours.lm: - colour = [220, 40, 40, 255] - - ddt.text( - (start_x + 5 * gui.scale, y, 2), - li, colour, gui.row_font_size + 200 - 1) - - elif len(indexLine) > 2: - - ddt.text( - (start_x + 5 * gui.scale, y, 2), indexLine, - alpha_mod(indexc, album_fade), gui.row_font_size) - else: - - ddt.text( - (start_x, y), indexLine, - alpha_mod(indexc, album_fade), gui.row_font_size) - - if dash and n_track.artist and n_track.title: - line = n_track.artist + " - " + n_track.title - - ddt.text( - (start_x + 33 * gui.scale + artistoffset, y), - line, - alpha_mod(titlec, album_fade), - gui.row_font_size, - width - 71 * gui.scale - artistoffset - star_x - 20 * gui.scale) - - line = get_display_time(n_track.length) - - ddt.text( - (width + start_x - (round(36 * gui.scale) + offset_font_extra), - y + num_y_offset, 0), line, - alpha_mod(timec, album_fade), gui.row_font_size) - - f_store.recall_all() - - -pl_bg = None -if (user_directory / "bg.png").exists(): - pl_bg = LoadImageAsset( - scaled_asset_directory=scaled_asset_directory, path=str(user_directory / "bg.png"), is_full_path=True) - - -class StandardPlaylist: - def __init__(self): - pass - - def full_render(self): - - global highlight_left - global highlight_right - - global playlist_hold - global playlist_hold_position - global shift_selection - - global click_time - global quick_drag - global mouse_down - global mouse_up - global selection_stage - - global r_menu_index - global r_menu_position - - left = gui.playlist_left - width = gui.plw - - highlight_width = gui.tracklist_highlight_width - highlight_left = gui.tracklist_highlight_left - inset_width = gui.tracklist_inset_width - inset_left = gui.tracklist_inset_left - center_mode = gui.tracklist_center_mode - - w = 0 - gui.row_extra = 0 - cv = 0 # update gui.playlist_current_visible_tracks - - # Draw the background - SDL_SetRenderTarget(renderer, gui.tracklist_texture) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_RenderClear(renderer) - - rect = (left, gui.panelY, width, window_size[1]) - ddt.rect(rect, colours.playlist_panel_background) - - # This draws an optional background image - if pl_bg: - x = (left + highlight_width) - (pl_bg.w + round(60 * gui.scale)) - pl_bg.render(x, window_size[1] - gui.panelBY - pl_bg.h) - ddt.pretty_rect = (x, window_size[1] - gui.panelBY - pl_bg.h, pl_bg.w, pl_bg.h) - ddt.alpha_bg = True - else: - xx = left + inset_left + inset_width - if center_mode: - xx -= round(15 * gui.scale) - deco.draw(ddt, xx, window_size[1] - gui.panelBY, pretty_text=True) - - # Mouse wheel scrolling - if mouse_wheel != 0 and window_size[1] - gui.panelBY - 1 > mouse_position[ - 1] > gui.panelY - 2 and gui.playlist_left < mouse_position[0] < gui.playlist_left + gui.plw \ - and not (coll(pl_rect)) and not search_over.active and not radiobox.active: - - # Set scroll speed - mx = 4 - - if gui.playlist_view_length < 25: - mx = 3 - if gui.playlist_view_length < 10: - mx = 2 - pctl.playlist_view_position -= mouse_wheel * mx - - if gui.playlist_view_length > 40: - pctl.playlist_view_position -= mouse_wheel - - #if mouse_wheel: - #logging.debug("Position changed by mouse wheel scroll: " + str(mouse_wheel)) - - pctl.playlist_view_position = min(pctl.playlist_view_position, len(default_playlist)) - #logging.debug("Position changed by range bound") - if pctl.playlist_view_position < 1: - pctl.playlist_view_position = 0 - if default_playlist: - # edge_playlist.pulse() - edge_playlist2.pulse() - - scroll_hide_timer.set() - gui.frame_callback_list.append(TestTimer(0.9)) - - # Show notice if playlist empty - if len(default_playlist) == 0: - colour = alpha_mod(colours.index_text, 200) # colours.playlist_text_missing - - top_a = gui.panelY - if gui.artist_info_panel: - top_a += gui.artist_panel_height - - b = window_size[1] - top_a - gui.panelBY - half = int(top_a + (b * 0.60)) - - if pl_bg: - rect = (left + int(width / 2) - 80 * gui.scale, half - 10 * gui.scale, - 190 * gui.scale, 60 * gui.scale) - ddt.pretty_rect = rect - ddt.alpha_bg = True - - ddt.text( - (left + int(width / 2) + 10 * gui.scale, half, 2), - _("Playlist is empty"), colour, 213, bg=colours.playlist_panel_background) - ddt.text( - (left + int(width / 2) + 10 * gui.scale, half + 30 * gui.scale, 2), - _("Drag and drop files to import"), colour, 13, bg=colours.playlist_panel_background) - - ddt.pretty_rect = None - ddt.alpha_bg = False - - # Show notice if at end of playlist - elif pctl.playlist_view_position > len(default_playlist) - 1: - colour = alpha_mod(colours.index_text, 200) - - top_a = gui.panelY - if gui.artist_info_panel: - top_a += gui.artist_panel_height - - b = window_size[1] - top_a - gui.panelBY - half = int(top_a + (b * 0.17)) - - if pl_bg: - rect = (left + int(width / 2) - 60 * gui.scale, half - 5 * gui.scale, - 140 * gui.scale, 30 * gui.scale) - ddt.pretty_rect = rect - ddt.alpha_bg = True - - ddt.text( - (left + int(width / 2) + 10 * gui.scale, half, 2), _("End of Playlist"), - colour, 213) - - ddt.pretty_rect = None - ddt.alpha_bg = False - - # line = "Contains " + str(len(default_playlist)) + ' track' - # if len(default_playlist) > 1: - # line += "s" - # - # ddt.draw_text((left + int(width / 2) + 10 * gui.scale, half + 24 * gui.scale, 2), line, - # colour, 12) - - # Process Input - - # type (0 is track, 1 is fold title), track_position, track_object, box, input_box, - list_items = [] - number = 0 - - for i in range(gui.playlist_view_length + 1): - - track_position = i + pctl.playlist_view_position - - # Make sure the view position is valid - pctl.playlist_view_position = max(pctl.playlist_view_position, 0) - - # Break if we are at end of playlist - if len(default_playlist) <= track_position or number > gui.playlist_view_length: - break - - track_object = pctl.get_track(default_playlist[track_position]) - track_id = track_object.index - move_on_title = False - - line_y = gui.playlist_top + gui.playlist_row_height * number - - track_box = ( - left + highlight_left, line_y, highlight_width, - gui.playlist_row_height - 1) - - input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) - - # Are folder titles enabled? - if not pctl.multi_playlist[pctl.active_playlist_viewing].hide_title and break_enable: - # Is this track from a different folder than the last? - if track_position == 0 or track_object.parent_folder_path != pctl.get_track( - default_playlist[track_position - 1]).parent_folder_path: - # Make folder title - - highlight = False - drag_highlight = False - - # Shift selection highlight - if (track_position in shift_selection and len(shift_selection) > 1): - highlight = True - - # Tracks have been dropped? - if playlist_hold is True and coll(input_box): - if mouse_up: - move_on_title = True - - # Ignore click in ratings box - click_title = (inp.mouse_click or right_click or middle_click) and coll(input_box) - if click_title and gui.show_album_ratings: - if mouse_position[0] > (input_box[0] + input_box[2]) - 80 * gui.scale: - click_title = False - - # Detect folder title click - if click_title and mouse_position[1] < window_size[1] - gui.panelBY: - - gui.pl_update += 1 - # Add folder to queue if middle click - if middle_click and is_level_zero(): - if key_ctrl_down: # Add as ungrouped tracks - i = track_position - parent = pctl.get_track(default_playlist[i]).parent_folder_path - while i < len(default_playlist) and parent == pctl.get_track( - default_playlist[i]).parent_folder_path: - pctl.force_queue.append(queue_item_gen(default_playlist[i], i, pl_to_id( - pctl.active_playlist_viewing))) - i += 1 - queue_timer_set(plural=True) - if prefs.stop_end_queue: - pctl.auto_stop = False - - else: # Add as grouped album - add_album_to_queue(track_id, track_position) - pctl.selected_in_playlist = track_position - shift_selection = [pctl.selected_in_playlist] - gui.pl_update += 1 - - # Play if double click: - if d_mouse_click and track_position in shift_selection and coll_point( - last_click_location, (input_box)): - click_time -= 1.5 - pctl.jump(track_id, track_position) - line_hit = False - inp.mouse_click = False - - if album_mode: - goto_album(pctl.playlist_playing_position) - - # Show selection menu if right clicked after select - if right_click: - folder_menu.activate(track_id) - r_menu_position = track_position - selection_stage = 2 - gui.pl_update = 1 - - if track_position not in shift_selection: - shift_selection = [] - pctl.selected_in_playlist = track_position - u = track_position - while u < len(default_playlist) and track_object.parent_folder_path == \ - pctl.master_library[ - default_playlist[u]].parent_folder_path: - shift_selection.append(u) - u += 1 - - # Add folder to selection if clicked - if inp.mouse_click and not ( - scroll_enable and mouse_position[0] < 30 * gui.scale) and not side_drag: - - quick_drag = True - set_drag_source() - - if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: - playlist_hold = True - - selection_stage = 1 - temp = get_folder_tracks_local(track_position) - pctl.selected_in_playlist = track_position - - if len(shift_selection) > 0 and key_shift_down: - if track_position < shift_selection[0]: - for item in reversed(temp): - if item not in shift_selection: - shift_selection.insert(0, item) - else: - for item in temp: - if item not in shift_selection: - shift_selection.append(item) - - else: - shift_selection = copy.copy(temp) - - # Should draw drag highlight? - - if mouse_down and playlist_hold and coll(input_box) and track_position not in shift_selection: - - if len(shift_selection) < 2 and not key_shift_down: - pass - else: - drag_highlight = True - - # Something to do with quick search, I forgot - if pctl.selected_in_playlist > track_position + 1: - gui.row_extra += 1 - - list_items.append( - (1, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, False)) - number += 1 - - if number > gui.playlist_view_length: - break - - # Standard track --------------------------------------------------------------------- - playing = False - - highlight = False - drag_highlight = False - line_y = gui.playlist_top + gui.playlist_row_height * number - - track_box = ( - left + highlight_left, line_y, highlight_width, - gui.playlist_row_height - 1) - - input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) - - # Test if line has mouse over or been clicked - line_over = False - line_hit = False - if coll(input_box) and mouse_position[1] < window_size[1] - gui.panelBY: - line_over = True - if (inp.mouse_click or right_click or (middle_click and is_level_zero())): - line_hit = True - gui.pl_update += 1 - - else: - line_hit = False - else: - line_hit = False - line_over = False - - # Prevent click if near scroll bar - if scroll_enable and mouse_position[0] < 30: - line_hit = False - - # Double click to play - if key_shift_down is False and d_mouse_click and line_hit and track_position == pctl.selected_in_playlist and coll_point( - last_click_location, input_box): - - pctl.jump(track_id, track_position) - - click_time -= 1.5 - quick_drag = False - mouse_down = False - mouse_up = False - line_hit = False - - if album_mode: - goto_album(pctl.playlist_playing_position) - - if len(pctl.track_queue) > 0 and pctl.track_queue[pctl.queue_step] == track_id: - if track_position == pctl.playlist_playing_position and pctl.active_playlist_viewing == pctl.active_playlist_playing: - this_line_playing = True - - # Add to queue on middle click - if middle_click and line_hit: - pctl.force_queue.append( - queue_item_gen(track_id, - track_position, pl_to_id(pctl.active_playlist_viewing))) - pctl.selected_in_playlist = track_position - shift_selection = [pctl.selected_in_playlist] - gui.pl_update += 1 - queue_timer_set() - if prefs.stop_end_queue: - pctl.auto_stop = False - - # Deselect multiple if one clicked on and not dragged (mouse up is probably a bit of a hacky way of doing it) - if len(shift_selection) > 1 and mouse_up and line_over and not key_shift_down and not key_ctrl_down and point_proximity_test( - gui.drag_source_position, mouse_position, 15): # and not playlist_hold: - shift_selection = [track_position] - pctl.selected_in_playlist = track_position - gui.pl_update = 1 - gui.update = 2 - - # # Begin drag block selection - # if mouse_down and line_over and track_position in shift_selection and len(shift_selection) > 1: - # if not pl_is_locked(pctl.active_playlist_viewing): - # playlist_hold = True - # elif key_shift_down: - # playlist_hold = True - - # Begin drag single track - if inp.mouse_click and line_hit and not side_drag: - quick_drag = True - set_drag_source() - - # Shift Move Selection - if move_on_title or (mouse_up and playlist_hold is True and coll(( - left + highlight_left, line_y, highlight_width, gui.playlist_row_height))): - - if len(shift_selection) > 1 or key_shift_down: - if track_position not in shift_selection: # p_track != playlist_hold_position and - - if len(shift_selection) == 0: - - ref = default_playlist[playlist_hold_position] - default_playlist[playlist_hold_position] = "old" - if move_on_title: - default_playlist.insert(track_position, "new") - else: - default_playlist.insert(track_position + 1, "new") - default_playlist.remove("old") - pctl.selected_in_playlist = default_playlist.index("new") - default_playlist[default_playlist.index("new")] = ref - - gui.pl_update = 1 - - - else: - ref = [] - selection_stage = 2 - for item in shift_selection: - ref.append(default_playlist[item]) - - for item in shift_selection: - default_playlist[item] = "old" - - for item in shift_selection: - if move_on_title: - default_playlist.insert(track_position, "new") - else: - default_playlist.insert(track_position + 1, "new") - - for b in reversed(range(len(default_playlist))): - if default_playlist[b] == "old": - del default_playlist[b] - shift_selection = [] - for b in range(len(default_playlist)): - if default_playlist[b] == "new": - shift_selection.append(b) - default_playlist[b] = ref.pop(0) - - pctl.selected_in_playlist = shift_selection[0] - gui.pl_update += 1 - - reload_albums(True) - pctl.notify_change() - - # Test show drag indicator - if mouse_down and playlist_hold and coll(input_box) and track_position not in shift_selection: - if len(shift_selection) > 1 or key_shift_down: - drag_highlight = True - - # Right click menu activation - if right_click and line_hit and mouse_position[0] > gui.playlist_left + 10: - - if len(shift_selection) > 1 and track_position in shift_selection: - selection_menu.activate(default_playlist[track_position]) - selection_stage = 2 - else: - r_menu_index = default_playlist[track_position] - r_menu_position = track_position - track_menu.activate(default_playlist[track_position]) - gui.pl_update += 1 - gui.update += 1 - - if track_position not in shift_selection: - pctl.selected_in_playlist = track_position - shift_selection = [pctl.selected_in_playlist] - - if line_over and inp.mouse_click: - - if track_position in shift_selection: - pass - else: - selection_stage = 2 - if key_shift_down: - start_s = track_position - end_s = pctl.selected_in_playlist - if end_s < start_s: - end_s, start_s = start_s, end_s - for y in range(start_s, end_s + 1): - if y not in shift_selection: - shift_selection.append(y) - shift_selection.sort() - pctl.selected_in_playlist = track_position - elif key_ctrl_down: - shift_selection.append(track_position) - else: - pctl.selected_in_playlist = track_position - shift_selection = [pctl.selected_in_playlist] - - if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: - playlist_hold = True - playlist_hold_position = track_position - - # Activate drag if shift key down - if quick_drag and pl_is_locked(pctl.active_playlist_viewing) and mouse_down: - if key_shift_down: - playlist_hold = True - else: - playlist_hold = False - - # Multi Select Highlight - if track_position in shift_selection or track_position == pctl.selected_in_playlist: - highlight = True - - if pctl.playing_state != 3 and len(pctl.track_queue) > 0 and pctl.track_queue[pctl.queue_step] == \ - default_playlist[track_position]: - if track_position == pctl.playlist_playing_position and pctl.active_playlist_viewing == pctl.active_playlist_playing: - playing = True - - list_items.append( - (0, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, playing)) - number += 1 - - if number > gui.playlist_view_length: - break - # --------------------------------------------------------------------------------------- - - # For every track in view - # for i in range(gui.playlist_view_length + 1): - gui.tracklist_bg_is_light = test_lumi(colours.playlist_panel_background) < 0.55 - - for type, track_position, tr, track_box, input_box, highlight, number, drag_highlight, playing in list_items: - - line_y = gui.playlist_top + gui.playlist_row_height * number - - ddt.text_background_colour = colours.playlist_panel_background - - if type == 1: - - # Is type ALBUM TITLE - separator = " - " - if prefs.row_title_separator_type == 1: - separator = " ‒ " - if prefs.row_title_separator_type == 2: - separator = " ⦁ " - - date = "" - duration = "" - - line = tr.parent_folder_name - - # Use folder name if mixed/singles? - if len(default_playlist) > track_position + 1 and pctl.get_track( - default_playlist[track_position + 1]).album != tr.album and \ - pctl.get_track(default_playlist[track_position + 1]).parent_folder_path == tr.parent_folder_path: - line = tr.parent_folder_name - else: - - if tr.album_artist != "" and tr.album != "": - line = tr.album_artist + separator + tr.album - - if prefs.left_align_album_artist_title and not True: - album_artist_mode = True - line = tr.album - - if len(line) < 6 and "CD" in line: - line = tr.album - - if prefs.append_date and year_search.search(tr.date): - year = d_date_display2(tr) - if not year: - year = d_date_display(tr) - date = "(" + year + ")" - - if line.endswith(")"): - b = line.split("(") - if len(b) > 1 and len(b[1]) <= 11: - - match = year_search.search(b[1]) - - if match: - line = b[0] - date = "(" + b[1] - - elif line.startswith("("): - - b = line.split(")") - if len(b) > 1 and len(b[0]) <= 11: - - match = year_search.search(b[0]) - - if match: - line = b[1] - date = b[0] + ")" - - if "(" in line and year_search.search(line): - date = "" - - line = line.replace(" - ", separator) - - qq = 0 - d_date = date - title_line = line - - # Calculate folder duration - - q = track_position - - total_time = 0 - while q < len(default_playlist): - - if pctl.get_track(default_playlist[q]).parent_folder_path != tr.parent_folder_path: - break - - total_time += pctl.get_track(default_playlist[q]).length - - q += 1 - qq += 1 - - if qq > 1: - duration = " [ " + get_display_time(total_time) + " ]" # Hair space inside brackets for better visual spacing - - if prefs.append_total_time: - date += duration - - ex = left + highlight_left + highlight_width - 7 * gui.scale - - height = line_y + gui.playlist_row_height - 19 * gui.scale # gui.pl_title_y_offset - - star_offset = 0 - if gui.show_album_ratings: - star_offset = round(72 * gui.scale) - ex -= star_offset - draw_rating_widget(ex + 6 * gui.scale, height, tr, album=True) - - light_offset = 0 - if colours.lm: - light_offset = 3 * gui.scale - ex -= light_offset - - if qq > 1: - ex += 1 * gui.scale - - ddt.text_background_colour = colours.playlist_panel_background - - if gui.scale == 2: - height += 1 - - if highlight: - ddt.text_background_colour = alpha_blend( - colours.row_select_highlight, - colours.playlist_panel_background) - ddt.rect_a( - (left + highlight_left, gui.playlist_top + gui.playlist_row_height * number), - (highlight_width, gui.playlist_row_height), colours.row_select_highlight) - - - #logging.info(d_date) # date of album release / release year - #logging.info(tr.parent_folder_name) # folder name - #logging.info(tr.album) - #logging.info(tr.artist) - #logging.info(tr.album_artist) - #logging.info(tr.genre) - - - - if prefs.row_title_format == 2: - - separator = " | " - - start_offset = round(15 * gui.scale) - xx = left + highlight_left + start_offset - ww = highlight_width - - was = False - run = 0 - duration = get_display_time(total_time) - colour = colours.folder_title - colour = [colour[0], colour[1], colour[2], max(colour[3] - 50, 0)] - - if prefs.append_total_time and duration: - was = True - run += ddt.text( - (ex - run, height, 1), duration, colour, - gui.row_font_size + gui.pl_title_font_offset) - if d_date: - if was: - run += ddt.text( - (ex - run, height, 1), separator, colour, - gui.row_font_size + gui.pl_title_font_offset) - was = True - run += ddt.text( - (ex - run, height, 1), d_date.rstrip(")").lstrip("("), colour, - gui.row_font_size + gui.pl_title_font_offset) - if tr.genre and prefs.row_title_genre: - if was: - run += ddt.text( - (ex - run, height, 1), separator, colour, - gui.row_font_size + gui.pl_title_font_offset) - was = True - run += ddt.text( - (ex - run, height, 1), tr.genre, colour, - gui.row_font_size + gui.pl_title_font_offset) - - - w2 = ddt.text((xx, height), title_line, colours.folder_title, gui.row_font_size + gui.pl_title_font_offset, max_w=ww - (start_offset + run + round(10 * gui.scale))) - - - - - else: - date_w = 0 - if date: - date_w = ddt.text( - (ex, height, 1), date, colours.folder_title, - gui.row_font_size + gui.pl_title_font_offset) - date_w += 4 * gui.scale - if qq > 1: - date_w -= 1 * gui.scale - - aa = 0 - - ft_width = ddt.get_text_w(line, gui.row_font_size + gui.pl_title_font_offset) - - left_align = highlight_width - date_w - 13 * gui.scale - light_offset - - left_align -= star_offset - - extra = aa - - left_align -= extra - - if ft_width > left_align: - date_w += 19 * gui.scale - ddt.text( - (left + highlight_left + 8 * gui.scale + extra, height), line, - colours.folder_title, - gui.row_font_size + gui.pl_title_font_offset, - highlight_width - date_w - extra - star_offset) - - else: - ddt.text( - (ex - date_w, height, 1), line, - colours.folder_title, - gui.row_font_size + gui.pl_title_font_offset) - - # ----- - - # Draw separation line below title - ddt.rect( - (left + highlight_left, line_y + gui.playlist_row_height - 1 * gui.scale, highlight_width, - 1 * gui.scale), colours.folder_line) - - # Draw blue highlight insert line - if drag_highlight: - ddt.rect( - [left + highlight_left, line_y + gui.playlist_row_height - 1 * gui.scale, - highlight_width, 3 * gui.scale], [135, 145, 190, 255]) - - continue - - # Draw playing highlight - if playing: - ddt.rect(track_box, colours.row_playing_highlight) - ddt.text_background_colour = alpha_blend(colours.row_playing_highlight, ddt.text_background_colour) - - if tr.file_ext == "SPTY": - # if not tauon.spot_ctl.started_once: - # ddt.rect((track_box[0], track_box[1], track_box[2], track_box[3] + 1), [40, 190, 40, 20]) - # ddt.text_background_colour = alpha_blend([40, 190, 40, 20], ddt.text_background_colour) - ddt.rect((track_box[0] + track_box[2] - round(2 * gui.scale), track_box[1] + round(2 * gui.scale), round(2 * gui.scale), track_box[3] - round(3 * gui.scale)), [40, 190, 40, 230]) - - - # Blue drop line - if drag_highlight: # playlist_hold_position != p_track: - - ddt.rect( - [left + highlight_left, line_y + gui.playlist_row_height - 1 * gui.scale, highlight_width, - 3 * gui.scale], [125, 105, 215, 255]) - - # Highlight - if highlight: - ddt.rect_a( - (left + highlight_left, line_y), (highlight_width, gui.playlist_row_height), - colours.row_select_highlight) - - ddt.text_background_colour = alpha_blend(colours.row_select_highlight, ddt.text_background_colour) - - if track_position > 0 and track_position < len(default_playlist) and tr.disc_number != "" and tr.disc_number != "0" and tr.album and tr.disc_number != pctl.get_track(default_playlist[track_position - 1]).disc_number \ - and tr.album == pctl.get_track(default_playlist[track_position - 1]).album and tr.parent_folder_path == pctl.get_track(default_playlist[track_position - 1]).parent_folder_path: - # Draw disc change line - ddt.rect( - (left + highlight_left, line_y + 0 * gui.scale, highlight_width, - 1 * gui.scale), colours.folder_line) - - if not gui.set_mode: - - line_render( - tr, track_position, gui.playlist_text_offset + line_y, - playing, 255, left + inset_left, inset_width, 1, line_y) - - else: - # NEE --------------------------------------------------------- - n_track = tr - p_track = track_position - this_line_playing = playing - - start = 18 * gui.scale - - if center_mode: - start = inset_left - - elif gui.lsp: - start += gui.lspw - - run = start - end = start + gui.plw - - if center_mode: - end = highlight_width + start - - # gui.tracklist_center_mode = center_mode - # gui.tracklist_inset_left = inset_left - round(20 * gui.scale) - # gui.tracklist_inset_width = inset_width + round(20 * gui.scale) - - for h, item in enumerate(gui.pl_st): - - wid = item[1] - 20 * gui.scale - y = gui.playlist_text_offset + gui.playlist_top + gui.playlist_row_height * number - ry = gui.playlist_top + gui.playlist_row_height * number - - if run > end - 50 * gui.scale: - break - - if len(gui.pl_st) == h + 1: - wid -= 6 * gui.scale - - if item[0] == "Rating": - if wid > 50 * gui.scale: - yy = ry + (gui.playlist_row_height // 2) - (6 * gui.scale) - draw_rating_widget(run + 4 * gui.scale, yy, n_track) - - if item[0] == "Starline": - - total = star_store.get_by_object(n_track) - - if total > 0 and n_track.length != 0 and wid > 0: - if gui.star_mode == "star": - - star = star_count(total, n_track.length) - 1 - rr = 0 - if star > -1: - if gui.tracklist_bg_is_light: - colour = alpha_blend([0, 0, 0, 200], ddt.text_background_colour) - else: - colour = alpha_blend([255, 255, 255, 50], ddt.text_background_colour) - - sx = run + 6 * gui.scale - sy = ry + (gui.playlist_row_height // 2) - (6 * gui.scale) - for count in range(8): - if star < count or rr > wid + round(6 * gui.scale): - break - star_pc_icon.render(sx, sy, colour) - sx += round(13) * gui.scale - rr += round(13) * gui.scale - - else: - - ratio = total / n_track.length - if ratio > 0.55: - star_x = int(ratio * (4 * gui.scale)) - star_x = min(star_x, wid) - - colour = colours.star_line - if playing and colours.star_line_playing is not None: - colour = colours.star_line_playing - - sy = (gui.playlist_top + gui.playlist_row_height * number) + int( - gui.playlist_row_height / 2) - ddt.rect((run + 4 * gui.scale, sy, star_x, 1 * gui.scale), colour) - - else: - text = "" - font = gui.row_font_size - colour = [200, 200, 200, 255] - norm_colour = colour - y_off = 0 - if item[0] == "Title": - colour = colours.title_text - if n_track.title != "": - text = n_track.title - else: - text = n_track.filename - # colour = colours.index_playing - if this_line_playing is True: - colour = colours.title_playing - - elif item[0] == "Artist": - text = n_track.artist - colour = colours.artist_text - norm_colour = colour - if this_line_playing is True: - colour = colours.artist_playing - elif item[0] == "Album": - text = n_track.album - colour = colours.album_text - norm_colour = colour - if this_line_playing is True: - colour = colours.album_playing - elif item[0] == "Album Artist": - text = n_track.album_artist - if not text and prefs.column_aa_fallback_artist: - text = n_track.artist - colour = colours.artist_text - norm_colour = colour - if this_line_playing is True: - colour = colours.artist_playing - elif item[0] == "Composer": - text = n_track.composer - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "Comment": - text = n_track.comment.replace("\n", " ").replace("\r", " ") - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "S": - if n_track.lfm_scrobbles > 0: - text = str(n_track.lfm_scrobbles) - - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "#": - - if prefs.use_absolute_track_index and pctl.multi_playlist[pctl.active_playlist_viewing].hide_title: - text = str(p_track) - else: - text = track_number_process(n_track.track_number) - - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "Date": - text = n_track.date - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "Filepath": - text = clean_string(n_track.fullpath) - colour = colours.index_text - norm_colour = colour - elif item[0] == "Filename": - text = clean_string(n_track.filename) - colour = colours.index_text - norm_colour = colour - elif item[0] == "Disc": - text = str(n_track.disc_number) - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "Codec": - text = n_track.file_ext - if text == "JELY" and "container" in tr.misc: - text = tr.misc["container"] - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "Lyrics": - text = "" - if n_track.lyrics != "": - text = "Y" - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "CUE": - text = "" - if n_track.is_cue: - text = "Y" - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "Genre": - text = n_track.genre - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "Bitrate": - text = str(n_track.bitrate) - if text == "0": - text = "" - - ex = n_track.file_ext - if n_track.misc.get("container") is not None: - ex = n_track.misc.get("container") - if ex == "FLAC" or ex == "WAV" or ex == "APE": - text = str(round(n_track.samplerate / 1000, 1)).rstrip("0").rstrip(".") + "|" + str( - n_track.bit_depth) - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "Time": - text = get_display_time(n_track.length) - colour = colours.bar_time - norm_colour = colour - # colour = colours.time_text - if this_line_playing is True: - colour = colours.time_text - elif item[0] == "❤": - # col love - u = 5 * gui.scale - yy = ry + (gui.playlist_row_height // 2) - (5 * gui.scale) - if gui.scale == 1.25: - yy += 1 - - if get_love(n_track): - - j = 0 # justify right - if run < start + 100 * gui.scale: - j = 1 # justify left - display_you_heart(run + 6 * gui.scale, yy, j) - u += 18 * gui.scale - - if "spotify-liked" in n_track.misc: - j = 0 # justify right - if run < start + 100 * gui.scale: - j = 1 # justify left - display_spot_heart(run + u, yy, j) - u += 18 * gui.scale - - count = 0 - for name in n_track.lfm_friend_likes: - spacing = 6 * gui.scale - if u + (heart_row_icon.w + spacing) * count > wid + 7 * gui.scale: - break - - x = run + u + (heart_row_icon.w + spacing) * count - - j = 0 # justify right - if run < start + 100 * gui.scale: - j = 1 # justify left - - display_friend_heart(x, yy, name, j) - count += 1 - - # if n_track.track_number == 1 or n_track.track_number == "1": - # ss = wid - (wid % 15) - # tauon.gall_ren.render(n_track, (run, y), ss) - - - elif item[0] == "P": - ratio = 0 - total = star_store.get_by_object(n_track) - if total > 0 and n_track.length > 2: - if n_track.length > 15: - total += 2 - ratio = total / (n_track.length - 1) - - text = str(str(int(ratio))) - if text == "0": - text = "" - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - - if prefs.dim_art and album_mode and \ - n_track.parent_folder_name \ - != pctl.master_library[pctl.track_queue[pctl.queue_step]].parent_folder_name: - colour = alpha_mod(colour, 150) - if n_track.found is False: - colour = colours.playlist_text_missing - - if text: - if item[0] in colours.column_colours: - colour = colours.column_colours[item[0]] - - if this_line_playing and item[0] in colours.column_colours_playing: - colour = colours.column_colours_playing[item[0]] - - if run + 6 * gui.scale + wid > end: - wid = end - run - 40 * gui.scale - if center_mode: - wid += 25 * gui.scale - - wid = max(0, wid) - - # # Hacky. Places a dark background behind light text for readability over mascot - # if pl_bg and gui.set_mode and colour_value(norm_colour) < 400 and not colours.lm: - # w, h = ddt.get_text_wh(text, font, wid) - # quick_box = [run + round(5 * gui.scale), y + y_off, w + round(2 * gui.scale), h] - # if coll_rect((left + width - pl_bg.w - 60 * gui.scale, window_size[1] - gui.panelBY - pl_bg.h, pl_bg.w, pl_bg.h), quick_box): - # quick_box = (run, ry, item[1], gui.playlist_row_height) - # ddt.rect(quick_box, [0, 0, 0, 40], True) - # ddt.rect(quick_box, alpha_mod(colours.playlist_panel_background, 150), True) - - ddt.text( - (run + 6 * gui.scale, y + y_off), - text, - colour, - font, - max_w=wid) - - if ddt.was_truncated: - #logging.info(text) - rect = (run, y, wid - 1, gui.playlist_row_height - 1) - gui.heart_fields.append(rect) - - if coll(rect): - columns_tool_tip.set(run - 7 * gui.scale, y, text, font, rect) - - run += item[1] - - # ----------------------------------------------------------------- - # Count the number if visable tracks (used by Show Current function) - if gui.playlist_top + gui.playlist_row_height * w > window_size[0] - gui.panelBY - gui.playlist_row_height: - pass - else: - cv += 1 - - # w += 1 - # if w > gui.playlist_view_length: - # break - - # This is a bit hacky since its only generated after drawing - # Used to keep track of how many tracks are actually in view - gui.playlist_current_visible_tracks = cv - gui.playlist_current_visible_tracks_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - - if (right_click and gui.playlist_top + 5 * gui.scale + gui.playlist_row_height * len(list_items) < - mouse_position[1] < window_size[ - 1] - 55 and width + left > mouse_position[0] > gui.playlist_left + 15): - playlist_menu.activate() - - SDL_SetRenderTarget(renderer, gui.main_texture) - SDL_RenderCopy(renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) - - if mouse_down is False: - playlist_hold = False - - ddt.pretty_rect = None - ddt.alpha_bg = False - - def cache_render(self): - - SDL_RenderCopy(renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) - - -playlist_render = StandardPlaylist() - - -class ArtBox: - - def __init__(self): - pass - - def draw(self, x, y, w, h, target_track=None, tight_border=False, default_border=None): - - # Draw a background for whole area - ddt.rect((x, y, w, h), colours.side_panel_background) - # ddt.rect_r((x, y, w ,h), [255, 0, 0, 200], True) - - # We need to find the size of the inner square for the artwork - # box = min(w, h) - - box_w = w - box_h = h - - box_w -= 17 * gui.scale # Inset the square a bit - box_h -= 17 * gui.scale # Inset the square a bit - - box_x = x + ((w - box_w) // 2) - box_y = y + ((h - box_h) // 2) - - # And position the square - rect = (box_x, box_y, box_w, box_h) - gui.main_art_box = rect - - # Draw the album art. If side bar is being dragged set quick draw flag - showc = None - result = 1 - - if target_track: # Only show if song playing or paused - result = album_art_gen.display(target_track, (rect[0], rect[1]), (box_w, box_h), side_drag) - showc = album_art_gen.get_info(target_track) - - # Draw faint border on album art - if tight_border: - if result == 0 and gui.art_drawn_rect: - border = gui.art_drawn_rect - ddt.rect_s(gui.art_drawn_rect, colours.art_box, 1 * gui.scale) - elif default_border: - border = default_border - ddt.rect_s(default_border, colours.art_box, 1 * gui.scale) - else: - border = rect - else: - ddt.rect_s(rect, colours.art_box, 1 * gui.scale) - border = rect - - fields.add(border) - - # Draw image downloading indicator - if gui.image_downloading: - ddt.text( - (x + int(box_w / 2), 38 * gui.scale + int(box_h / 2), 2), _("Fetching image..."), - colours.side_bar_line1, - 14, bg=colours.side_panel_background) - gui.update = 2 - - # Input for album art - if target_track: - - # Cycle images on click - - if coll(gui.main_art_box) and inp.mouse_click is True and key_focused == 0: - - album_art_gen.cycle_offset(target_track) - - if pctl.mpris: - pctl.mpris.update(force=True) - - # Activate picture context menu on right click - if tight_border and gui.art_drawn_rect: - if right_click and coll(gui.art_drawn_rect) and target_track: - picture_menu.activate(in_reference=target_track) - elif right_click and coll(rect) and target_track: - picture_menu.activate(in_reference=target_track) - - # Draw picture metadata - if showc is not None and coll(border) \ - and rename_track_box.active is False \ - and radiobox.active is False \ - and pref_box.enabled is False \ - and gui.rename_playlist_box is False \ - and gui.message_box is False \ - and track_box is False \ - and gui.layer_focus == 0: - - padding = 6 * gui.scale - - xw = box_x + box_w - yh = box_y + box_h - if tight_border and gui.art_drawn_rect and gui.art_drawn_rect[2] > 50 * gui.scale: - xw = gui.art_drawn_rect[0] + gui.art_drawn_rect[2] - yh = gui.art_drawn_rect[1] + gui.art_drawn_rect[3] - - art_metadata_overlay(xw, yh, showc) - - -art_box = ArtBox() - - -class ScrollBox: - - def __init__(self): - - self.held = False - self.slide_hold = False - self.source_click_y = 0 - self.source_bar_y = 0 - self.direction_lock = -1 - self.d_position = 0 - - def draw( - self, x, y, w, h, value, max_value, force_dark_theme=False, click=None, r_click=False, jump_distance=4, extend_field=0): - - if max_value < 2: - return 0 - - if click is None: - click = inp.mouse_click - - bar_height = round(90 * gui.scale) - - if h > 400 * gui.scale and max_value < 20: - bar_height = round(180 * gui.scale) - - bg = [255, 255, 255, 7] - fg = [255, 255, 255, 30] - fg_h = [255, 255, 255, 40] - fg_off = [255, 255, 255, 15] - - if colours.lm and not force_dark_theme: - bg = [0, 0, 0, 15] - fg_off = [0, 0, 0, 30] - fg = [0, 0, 0, 60] - fg_h = [0, 0, 0, 70] - - ddt.rect((x, y, w, h), bg) - - half = bar_height // 2 - - ratio = value / max_value - - mi = y + half - mo = y + h - half - distance = mo - mi - position = int(round(distance * ratio)) - - fw = w + extend_field - fx = x - extend_field - - if coll((fx, y, fw, h)): - - if mouse_down: - gui.update += 1 - - if r_click: - p = mouse_position[1] - half - y - p = max(0, p) - - range = h - bar_height - p = min(p, range) - - per = p / range - - value = int(round(max_value * per)) - - ratio = value / max_value - - mi = y + half - mo = y + h - half - distance = mo - mi - position = int(round(distance * ratio)) - - in_bar = False - if coll((x, mi + position - half, w, bar_height)): - in_bar = True - if click: - self.held = True - - # p_y = pointer(c_int(0)) - # SDL_GetGlobalMouseState(None, p_y) - get_sdl_input.mouse_capture_want = True - self.source_click_y = mouse_position[1] - self.source_bar_y = position - - if pctl.playlist_view_position < 0: - pctl.playlist_view_position = 0 - - - elif mouse_down and not self.held: - - if click and not in_bar: - self.slide_hold = True - self.direction_lock = 1 - if mouse_position[1] - y < position: - self.direction_lock = 0 - - self.d_position = value / max_value - - if self.slide_hold: - if (self.direction_lock == 1 and mouse_position[1] - y < position + half) or \ - (self.direction_lock == 0 and mouse_position[1] - y > position + half): - pass - else: - - tt = scroll_timer.hit() - if tt > 0.1: - tt = 0 - - flip = -1 - if self.direction_lock: - flip = 1 - - self.d_position = min(max(self.d_position + (((tt * jump_distance) / max_value) * flip), 0), 1) - - else: - self.slide_hold = False - - if (self.held and mouse_up) or not mouse_down: - self.held = False - - if self.held and not window_is_focused(): - self.held = False - - if self.held: - get_sdl_input.mouse_capture_want = True - new_y = mouse_position[1] - gui.update += 1 - - offset = new_y - self.source_click_y - - position = self.source_bar_y + offset - - position = max(position, 0) - position = min(position, distance) - - ratio = position / distance - value = int(round(max_value * ratio)) - - colour = fg_off - rect = (x, mi + position - half, w, bar_height) - fields.add(rect) - if coll(rect): - colour = fg - if self.held: - colour = fg_h - - ddt.rect(rect, colour) - - if self.slide_hold: - return round(max_value * self.d_position) - - return value - - -mini_lyrics_scroll = ScrollBox() -playlist_panel_scroll = ScrollBox() -artist_info_scroll = ScrollBox() -device_scroll = ScrollBox() -artist_list_scroll = ScrollBox() -gallery_scroll = ScrollBox() -tree_view_scroll = ScrollBox() -radio_view_scroll = ScrollBox() - - -class RadioBox: - - def __init__(self): - - self.active = False - self.station_editing = None - self.edit_mode = True - self.add_mode = False - self.radio_field_active = 1 - self.radio_field = TextBox2() - self.radio_field_title = TextBox2() - self.radio_field_search = TextBox2() - - self.x = 1 - self.y = 1 - self.w = 1 - self.h = 1 - self.center = False - - self.scroll_position = 0 - self.scroll = ScrollBox() - - self.dummy_track = TrackClass() - self.dummy_track.index = -2 - self.dummy_track.is_network = True - self.dummy_track.art_url_key = "" # radio" - self.dummy_track.file_ext = "RADIO" - self.playing_title = "" - - self.proxy_started = False - self.loaded_url = None - self.loaded_station = None - self.load_connecting = False - self.load_failed = False - self.searching = False - self.load_failed_timer = Timer() - self.right_clicked_station = None - self.right_clicked_station_p = None - self.click_point = (0, 0) - - self.song_key = "" - - self.drag = None - - self.tab = 0 - self.temp_list = [] - - self.hosts = None - self.host = None - - self.search_menu = Menu(170) - self.search_menu.add(MenuItem(_("Search Tag"), self.search_tag, pass_ref=True)) - self.search_menu.add(MenuItem(_("Search Country Code"), self.search_country, pass_ref=True)) - self.search_menu.add(MenuItem(_("Search Title"), self.search_title, pass_ref=True)) - - self.websocket = None - self.ws_interval = 4.5 - self.websocket_source_urls = ("https://listen.moe/kpop/stream", "https://listen.moe/stream") - self.run_proxy = True - - def parse_vorbis_okay(self): - return ( - self.loaded_url not in self.websocket_source_urls) and \ - "radio.plaza.one" not in self.loaded_url and \ - "gensokyoradio.net" not in self.loaded_url - - def search_country(self, text): - - if len(text) == 2 and text.isalpha(): - self.search_radio_browser( - "/json/stations/search?countrycode=" + text + "&order=votes&limit=250&reverse=true") - else: - self.search_radio_browser( - "/json/stations/search?country=" + text + "&order=votes&limit=250&reverse=true") - - def search_tag(self, text): - - text = text.lower() - self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&tag=" + text) - - def search_title(self, text): - - text = text.lower() - self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&name=" + text) - - def is_m3u(self, url): - return url.lower().endswith(".m3u") or url.lower().endswith(".m3u8") - - def extract_stream_m3u(self, url, recursion_limit=5): - if recursion_limit <= 0: - return None - logging.info("Fetching M3U...") - - try: - response = requests.get(url, timeout=10) - if response.status_code != 200: - logging.error(f"M3U Fetch error code: {response.status_code}") - return None - - content = response.text - lines = content.strip().split("\n") - - for line in lines: - line = line.strip() - if not line.startswith("#") and len(line) > 0: - if self.is_m3u(line): - next_url = urllib.parse.urljoin(url, line) - return self.extract_stream_m3u(next_url, recursion_limit - 1) - return urllib.parse.urljoin(url, line) - - return None - - except Exception: - logging.exception("Failed to extract M3U") - return None - - def start(self, item): - url = item["stream_url"] - logging.info("Start radio") - logging.info(url) - if self.is_m3u(url): - url = self.extract_stream_m3u(url) - logging.info(f"Extracted URL is: {url}") - if not url: - logging.info("Failed to extract stream from M3U") - return - - if self.load_connecting: - return - - if tauon.spot_ctl.playing or tauon.spot_ctl.coasting: - tauon.spot_ctl.control("stop") - - try: - self.websocket.close() - logging.info("Websocket closed") - except Exception: - logging.exception("No socket to close?") - - self.playing_title = "" - self.playing_title = item["title"] - self.dummy_track.art_url_key = "" - self.dummy_track.title = "" - self.dummy_track.artist = "" - self.dummy_track.album = "" - self.dummy_track.date = "" - pctl.radio_meta_on = "" - - album_art_gen.clear_cache() - - if not tauon.test_ffmpeg(): - prefs.auto_rec = False - return - - self.run_proxy = True - if url.endswith(".ts"): - self.run_proxy = False - - if self.run_proxy and not self.proxy_started and prefs.backend != 4: - shoot = threading.Thread(target=stream_proxy, args=[tauon]) - shoot.daemon = True - shoot.start() - self.proxy_started = True - - # pctl.url = url - pctl.url = f"http://127.0.0.1:{7812}" - if not self.run_proxy: - pctl.url = item["stream_url"] - self.loaded_url = None - pctl.tag_meta = "" - pctl.radio_meta_on = "" - pctl.found_tags = {} - self.song_key = "" - pctl.playing_time = 0 - pctl.decode_time = 0 - self.loaded_station = item - - if tauon.stream_proxy.download_running: - tauon.stream_proxy.abort = True - - self.load_connecting = True - self.load_failed = False - - shoot = threading.Thread(target=self.start2, args=[url]) - shoot.daemon = True - shoot.start() - - def start2(self, url): - - if self.run_proxy and not tauon.stream_proxy.start_download(url): - self.load_failed_timer.set() - self.load_failed = True - self.load_connecting = False - gui.update += 1 - logging.error("Starting radio failed") - # show_message(_("Failed to establish a connection"), mode="error") - return - - self.loaded_url = url - pctl.playing_state = 0 - pctl.record_stream = False - pctl.playerCommand = "url" - pctl.playerCommandReady = True - pctl.playing_state = 3 - pctl.playing_time = 0 - pctl.decode_time = 0 - pctl.playing_length = 0 - tauon.thread_manager.ready_playback() - hit_discord() - - if tauon.update_play_lock is not None: - tauon.update_play_lock() - - time.sleep(0.1) - self.load_connecting = False - self.load_failed = False - gui.update += 1 - - wss = "" - if url == "https://listen.moe/kpop/stream": - wss = "wss://listen.moe/kpop/gateway_v2" - if url == "https://listen.moe/stream": - wss = "wss://listen.moe/gateway_v2" - if wss: - logging.info("Connecting to Listen.moe") - import websocket - import _thread as th - - def send_heartbeat(ws): - #logging.info(self.ws_interval) - time.sleep(self.ws_interval) - ws.send("{\"op\":9}") - logging.info("Send heatbeat") - - def on_message(ws, message): - logging.info(message) - d = json.loads(message) - if d["op"] == 10: - shoot = threading.Thread(target=send_heartbeat, args=[ws]) - shoot.daemon = True - shoot.start() - - if d["op"] == 0: - self.ws_interval = d["d"]["heartbeat"] / 1000 - ws.send("{\"op\":9}") - - if d["op"] == 1: - try: - - found_tags = {} - found_tags["title"] = d["d"]["song"]["title"] - if d["d"]["song"]["artists"]: - found_tags["artist"] = d["d"]["song"]["artists"][0]["name"] - line = "" - if "title" in found_tags: - line += found_tags["title"] - if "artist" in found_tags: - line = found_tags["artist"] + " - " + line - - pctl.found_tags = found_tags - pctl.tag_meta = line - - filename = d["d"]["song"]["albums"][0]["image"] - fulllink = "https://cdn.listen.moe/covers/" + filename - - #logging.info(fulllink) - art_response = requests.get(fulllink, timeout=10) - #logging.info(art_response.status_code) - - if art_response.status_code == 200: - if pctl.radio_image_bin: - pctl.radio_image_bin.close() - pctl.radio_image_bin = None - pctl.radio_image_bin = io.BytesIO(art_response.content) - pctl.radio_image_bin.seek(0) - radiobox.dummy_track.art_url_key = "ok" - logging.info("Got new art") - - - except Exception: - logging.exception("No image") - if pctl.radio_image_bin: - pctl.radio_image_bin.close() - pctl.radio_image_bin = None - gui.clear_image_cache_next += 1 - gui.update += 1 - - def on_error(ws, error): -# pass - logging.error(error) - - def on_close(ws): -# pass - logging.info("### closed ###") - - def on_open(ws): - def run(*args): - pass - # for i in range(3): - # time.sleep(4.5) - # ws.send("{\"op\":9}") - # time.sleep(10) - # ws.close() - #logging.info("thread terminating...") - - th.start_new_thread(run, ()) - - # websocket.enableTrace(True) - #logging.info(wss) - ws = websocket.WebSocketApp(wss, - on_message=on_message, - on_error=on_error) - ws.on_open = on_open - self.websocket = ws - shoot = threading.Thread(target=ws.run_forever) - shoot.daemon = True - shoot.start() - - def delete_radio_entry(self, item): - for i, saved in enumerate(prefs.radio_urls): - if saved["stream_url"] == item["stream_url"] and saved["title"] == item["title"]: - del prefs.radio_urls[i] - - def delete_radio_entry_after(self, item): - p = radiobox.right_clicked_station_p - del prefs.radio_urls[p + 1:] - - def edit_entry(self, item): - radio = item - self.radio_field_title.text = radio["title"] - self.radio_field.text = radio["stream_url"] - - def browser_get_hosts(self): - - import socket - """ - Get all base urls of all currently available radiobrowser servers - - Returns: - list: a list of strings - - """ - hosts = [] - # get all hosts from DNS - ips = socket.getaddrinfo( - "all.api.radio-browser.info", 80, 0, 0, socket.IPPROTO_TCP) - for ip_tupple in ips: - try: - ip = ip_tupple[4][0] - - # do a reverse lookup on every one of the ips to have a nice name for it - host_addr = socket.gethostbyaddr(ip) - # add the name to a list if not already in there - if host_addr[0] not in hosts: - hosts.append(host_addr[0]) - except Exception: - logging.exception("IPv4 lookup fail") - - # sort list of names - hosts.sort() - # add "https://" in front to make it an url - return list(map(lambda x: "https://" + x, hosts)) - - def search_page(self): - - y = self.y - x = self.x - w = self.w - h = self.h - - yy = y + round(40 * gui.scale) - - width = round(330 * gui.scale) - rect = (x + 8 * gui.scale, yy - round(2 * gui.scale), width, 22 * gui.scale) - fields.add(rect) - # if (coll(rect) and gui.level_2_click) or (input.key_tab_press and self.radio_field_active == 2): - # self.radio_field_active = 1 - # input.key_tab_press = False - if not self.radio_field_search.text and not editline: - ddt.text((x + 14 * gui.scale, yy), _("Search text…"), colours.box_text_label, 312) - self.radio_field_search.draw( - x + 14 * gui.scale, yy, colours.box_input_text, - active=True, - width=width, click=gui.level_2_click) - - ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) - - if draw.button( - _("Search"), x + width + round(21 * gui.scale), yy - round(3 * gui.scale), - press=gui.level_2_click, w=round(80 * gui.scale)) or inp.level_2_enter: - - text = self.radio_field_search.text.replace("/", "").replace(":", "").replace("\\", "").replace(".", "").replace( - "-", "").upper() - text = urllib.parse.quote(text) - if len(text) > 1: - self.search_menu.activate(text, position=(x + width + round(21 * gui.scale), yy + round(20 * gui.scale))) - if draw.button(_("Get Top Voted"), x + round(8 * gui.scale), yy + round(30 * gui.scale), press=gui.level_2_click): - self.search_radio_browser("/json/stations?order=votes&limit=250&reverse=true") - - ww = ddt.get_text_w(_("Get Top Voted"), 212) - if key_shift_down: - if draw.button(_("Developer Picks"), x + ww + round(35 * gui.scale), yy + round(30 * gui.scale), press=gui.level_2_click): - self.temp_list.clear() - - radio = {} - radio["title"] = "Nightwave Plaza" - radio["stream_url_unresolved"] = "https://radio.plaza.one/ogg" - radio["stream_url"] = "https://radio.plaza.one/ogg" - radio["website_url"] = "https://plaza.one/" - radio["icon"] = "https://plaza.one/icons/apple-touch-icon.png" - radio["country"] = "Japan" - self.temp_list.append(radio) - - radio = {} - radio["title"] = "Gensokyo Radio" - radio["stream_url_unresolved"] = " https://stream.gensokyoradio.net/GensokyoRadio-enhanced.m3u" - radio["stream_url"] = "https://stream.gensokyoradio.net/1" - radio["website_url"] = "https://gensokyoradio.net/" - radio["icon"] = "https://gensokyoradio.net/favicon.ico" - radio["country"] = "Japan" - self.temp_list.append(radio) - - radio = {} - radio["title"] = "Listen.moe | Jpop" - radio["stream_url_unresolved"] = "https://listen.moe/stream" - radio["stream_url"] = "https://listen.moe/stream" - radio["website_url"] = "https://listen.moe/" - radio["icon"] = "https://avatars.githubusercontent.com/u/26034028?s=200&v=4" - radio["country"] = "Japan" - self.temp_list.append(radio) - - radio = {} - radio["title"] = "Listen.moe | Kpop" - radio["stream_url_unresolved"] = "https://listen.moe/kpop/stream" - radio["stream_url"] = "https://listen.moe/kpop/stream" - radio["website_url"] = "https://listen.moe/" - radio["icon"] = "https://avatars.githubusercontent.com/u/26034028?s=200&v=4" - radio["country"] = "Korea" - - self.temp_list.append(radio) - - radio = {} - radio["title"] = "HBR1 Dream Factory | Ambient" - radio["stream_url_unresolved"] = "http://radio.hbr1.com:19800/ambient.ogg" - radio["stream_url"] = "http://radio.hbr1.com:19800/ambient.ogg" - radio["website_url"] = "http://www.hbr1.com/" - self.temp_list.append(radio) - - radio = {} - radio["title"] = "Yggdrasil Radio | Anime & Jpop" - radio["stream_url_unresolved"] = "http://shirayuki.org:9200/" - radio["stream_url"] = "http://shirayuki.org:9200/" - radio["website_url"] = "https://yggdrasilradio.net/" - self.temp_list.append(radio) - - for station in primary_stations: - self.temp_list.append(station) - - def search_radio_browser(self, param): - if self.searching: - return - self.searching = True - shoot = threading.Thread(target=self.search_radio_browser2, args=[param]) - shoot.daemon = True - shoot.start() - - def search_radio_browser2(self, param): - - if not self.hosts: - self.hosts = self.browser_get_hosts() - if not self.host: - self.host = random.choice(self.hosts) - - uri = self.host + param - req = urllib.request.Request(uri) - req.add_header("User-Agent", t_agent) - req.add_header("Content-Type", "application/json") - response = urllib.request.urlopen(req, context=ssl_context) - data = response.read() - data = json.loads(data.decode()) - self.parse_data(data) - self.searching = False - - def parse_data(self, data): - - self.temp_list.clear() - - for station in data: - radio: dict[str, int | str] = {} - #logging.info(station) - radio["title"] = station["name"] - radio["stream_url_unresolved"] = station["url"] - radio["stream_url"] = station["url_resolved"] - radio["icon"] = station["favicon"] - radio["country"] = station["country"] - if radio["country"] == "The Russian Federation": - radio["country"] = "Russia" - elif radio["country"] == "The United States Of America": - radio["country"] = "USA" - elif radio["country"] == "The United Kingdom Of Great Britain And Northern Ireland": - radio["country"] = "United Kingdom" - elif radio["country"] == "Islamic Republic Of Iran": - radio["country"] = "Iran" - elif len(station["country"]) > 20: - radio["country"] = station["countrycode"] - radio["website_url"] = station["homepage"] - if "homepage" in station: - radio["website_url"] = station["homepage"] - self.temp_list.append(radio) - gui.update += 1 - - def render(self) -> None: - - if self.edit_mode: - w = round(510 * gui.scale) - h = round(120 * gui.scale) # + sh - - self.w = w - self.h = h - # self.x = x - # self.y = y - width = w - if self.center: - x = int(window_size[0] / 2) - int(w / 2) - y = int(window_size[1] / 2) - int(h / 2) - yy = y - self.y = y - self.x = x - else: - yy = self.y - y = self.y - x = self.x - ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) - ddt.rect_a((x, y), (w, h), colours.box_background) - ddt.text_background_colour = colours.box_background - if key_esc_press or (gui.level_2_click and not coll((x, y, w, h))): - self.active = False - - if self.add_mode: - ddt.text((x + 10 * gui.scale, yy + 8 * gui.scale), _("Add Station"), colours.box_title_text, 213) - else: - ddt.text((x + 10 * gui.scale, yy + 8 * gui.scale), _("Edit Station"), colours.box_title_text, 213) - - self.saved() - return - - w = round(510 * gui.scale) - h = round(356 * gui.scale) # + sh - x = int(window_size[0] / 2) - int(w / 2) - y = int(window_size[1] / 2) - int(h / 2) - - self.w = w - self.h = h - self.x = x - self.y = y - - yy = y - - ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) - ddt.rect_a((x, y), (w, h), colours.box_background) - - ddt.text_background_colour = colours.box_background - - if key_esc_press or (gui.level_2_click and not coll((x, y, w, h))): - self.active = False - - ddt.text((x + 10 * gui.scale, yy + 8 * gui.scale), _("Station Browser"), colours.box_title_text, 213) - - # --- - if self.load_connecting: - ddt.text((x + 495 * gui.scale, yy + 8 * gui.scale, 1), _("Connecting..."), colours.box_title_text, 311) - elif self.load_failed: - ddt.text((x + 495 * gui.scale, yy + 8 * gui.scale, 1), _("Failed to connect!"), colours.box_title_text, 311) - if self.load_failed_timer.get() > 3: - gui.delay_frame(0.2) - self.load_failed = False - - elif self.searching: - ddt.text((x + 495 * gui.scale, yy + 8 * gui.scale, 1), _("Searching..."), colours.box_title_text, 311) - elif pctl.playing_state == 3: - - text = "" - if tauon.stream_proxy.s_format: - text = str(tauon.stream_proxy.s_format) - if tauon.stream_proxy.s_bitrate and tauon.stream_proxy.s_bitrate.isnumeric(): - text += " " + tauon.stream_proxy.s_bitrate + _("kbps") - - ddt.text((x + 495 * gui.scale, yy + 8 * gui.scale, 1), text, colours.box_title_text, 311) - # if tauon.stream_proxy.s_format: - # ddt.text((x + 425 * gui.scale, yy + 8 * gui.scale,), tauon.stream_proxy.s_format, colours.box_title_text, 311) - # if tauon.stream_proxy.s_bitrate: - # ddt.text((x + 454 * gui.scale, yy + 8 * gui.scale,), tauon.stream_proxy.s_bitrate + "kbps", colours.box_title_text, 311) - - # --- ---------------------------------------------------------------------- - if self.tab == 1: - self.search_page() - elif self.tab == 0: - self.saved() - self.draw_list() - # self.footer() - return - - def saved(self): - y = self.y - x = self.x - w = self.w - h = self.h - - yy = y + round(40 * gui.scale) - - width = round(370 * gui.scale) - - rect = (x + 8 * gui.scale, yy - round(2 * gui.scale), width, 22 * gui.scale) - fields.add(rect) - if (coll(rect) and gui.level_2_click) or (inp.key_tab_press and self.radio_field_active == 2): - self.radio_field_active = 1 - inp.key_tab_press = False - if not self.radio_field_title.text and not (self.radio_field_active == 1 and editline): - ddt.text((x + 14 * gui.scale, yy), _("Name / Title"), colours.box_text_label, 312) - self.radio_field_title.draw(x + 14 * gui.scale, yy, colours.box_input_text, - active=self.radio_field_active == 1, - width=width, click=gui.level_2_click) - - ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) - - yy += round(30 * gui.scale) - - rect = (x + 8 * gui.scale, yy - round(2 * gui.scale), width, 22 * gui.scale) - ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) - fields.add(rect) - if (coll(rect) and gui.level_2_click) or (inp.key_tab_press and self.radio_field_active == 1): - self.radio_field_active = 2 - inp.key_tab_press = False - - if not self.radio_field.text and not (self.radio_field_active == 2 and editline): - ddt.text((x + 14 * gui.scale, yy), _("Raw Stream URL http://example.stream:1234"), colours.box_text_label, 312) - self.radio_field.draw( - x + 14 * gui.scale, yy, colours.box_input_text, active=self.radio_field_active == 2, - width=width, click=gui.level_2_click) - - if draw.button(_("Save"), x + width + round(21 * gui.scale), yy - round(20 * gui.scale), press=gui.level_2_click): - - if not self.radio_field.text: - show_message(_("Enter a stream URL")) - elif "http://" in self.radio_field.text or "https://" in self.radio_field.text: - radio = self.station_editing - if self.add_mode: - radio: dict[str, int | str] = {} - radio["title"] = self.radio_field_title.text - radio["stream_url"] = self.radio_field.text - radio["website_url"] = "" - - if self.add_mode: - pctl.radio_playlists[pctl.radio_playlist_viewing]["items"].append(radio) - self.active = False - - else: - show_message(_("Could not validate URL. Must start with https:// or http://")) - - def draw_list(self): - - x = self.x - y = self.y - w = self.w - h = self.h - - if self.drag: - gui.update_on_drag = True - - yy = y + round(100 * gui.scale) - x += round(10 * gui.scale) - - radio_list = prefs.radio_urls - if self.tab == 1: - radio_list = self.temp_list - - rect = (x, y, w, h) - if coll(rect): - self.scroll_position += mouse_wheel * -1 - self.scroll_position = max(self.scroll_position, 0) - self.scroll_position = min(self.scroll_position, len(radio_list) // 2 - 7) - - if len(radio_list) // 2 > 9: - self.scroll_position = self.scroll.draw( - (x + w) - round(35 * gui.scale), yy, round(15 * gui.scale), - round(210 * gui.scale), self.scroll_position, - len(radio_list) // 2 - 7, True, click=gui.level_2_click) - - self.scroll_position = max(self.scroll_position, 0) - - p = self.scroll_position * 2 - offset = 0 - to_delete = None - swap = None - - while True: - - if p > len(radio_list) - 1: - break - - xx = x + offset - item = radio_list[p] - - rect = (xx, yy, round(233 * gui.scale), round(40 * gui.scale)) - fields.add(rect) - - bg = colours.box_background - text_colour = colours.box_input_text - - playing = pctl.playing_state == 3 and self.loaded_url == item["stream_url"] - - if playing: - # bg = colours.box_sub_highlight - # ddt.rect(rect, bg, True) - - bg = colours.tab_background_active - text_colour = colours.tab_text_active - ddt.rect(rect, bg) - - if radio_view.drag: - if item == radio_view.drag: - text_colour = colours.box_sub_text - bg = [255, 255, 255, 10] - ddt.rect(rect, bg) - elif (radio_entry_menu.active and radio_entry_menu.reference == p) or \ - ((not radio_entry_menu.active and coll(rect)) and not playing): - text_colour = colours.box_sub_text - bg = [255, 255, 255, 10] - ddt.rect(rect, bg) - - if coll(rect): - - if gui.level_2_click: - # self.drag = p - # self.click_point = copy.copy(mouse_position) - radio_view.drag = item - radio_view.click_point = copy.copy(mouse_position) - if mouse_up: # gui.level_2_click: - gui.update += 1 - # if self.drag is not None and p != self.drag: - # swap = p - if point_proximity_test(radio_view.click_point, mouse_position, round(4 * gui.scale)): - self.start(item) - if middle_click: - to_delete = p - if level_2_right_click: - self.right_clicked_station = item - self.right_clicked_station_p = p - radio_entry_menu.activate(item) - - bg = alpha_blend(bg, colours.box_background) - - boxx = round(32 * gui.scale) - toff = boxx + round(10 * gui.scale) - if item["title"]: - ddt.text( - (xx + toff, yy + round(3 * gui.scale)), item["title"], text_colour, 212, bg=bg, - max_w=rect[2] - (15 * gui.scale + toff)) - else: - ddt.text( - (xx + toff, yy + round(3 * gui.scale)), item["stream_url"], text_colour, 212, bg=bg, - max_w=rect[2] - (15 * gui.scale + toff)) - - country = item.get("country") - if country: - ddt.text( - (xx + toff, yy + round(18 * gui.scale)), country, text_colour, 11, bg=bg, - max_w=rect[2] - (15 * gui.scale + toff)) - - b_rect = (xx + round(4 * gui.scale), yy + round(4 * gui.scale), boxx, boxx) - ddt.rect(b_rect, colours.box_thumb_background) - radio_thumb_gen.draw(item, b_rect[0], b_rect[1], b_rect[2]) - - if offset == 0: - offset = rect[2] + round(4 * gui.scale) - else: - offset = 0 - yy += round(43 * gui.scale) - - if yy > y + 300 * gui.scale: - break - - p += 1 - - # if to_delete is not None: - # del radio_list[to_delete] - # - # if mouse_up and self.drag and mouse_position[1] > yy + round(22 * gui.scale): - # swap = len(radio_list) - - # if self.drag and not point_proximity_test(self.click_point, mouse_position, round(4 * gui.scale)): - # ddt.rect(( - # mouse_position[0] + round(8 * gui.scale), mouse_position[1] - round(8 * gui.scale), 45 * gui.scale, - # 13 * gui.scale), colours.grey(70)) - - # if swap is not None: - # - # old = radio_list[self.drag] - # radio_list[self.drag] = None - # - # if swap > self.drag: - # swap += 1 - # - # radio_list.insert(swap, old) - # radio_list.remove(None) - # - # self.drag = None - # gui.update += 1 - - # if not mouse_down: - # self.drag = None - - def footer(self): - - y = self.y - x = self.x + round(15 * gui.scale) - w = self.w - h = self.h - - yy = y + round(328 * gui.scale) - if pctl.playing_state == 3 and not prefs.auto_rec: - old = prefs.auto_rec - if not old and pref_box.toggle_square( - x, yy, prefs.auto_rec, _("Record and auto split songs"), - click=gui.level_2_click): - show_message(_("Please stop playback first before toggling this setting")) - elif pctl.playing_state == 3: - old = prefs.auto_rec - if old and not pref_box.toggle_square( - x, yy, prefs.auto_rec, _("Record and auto split songs"), - click=gui.level_2_click): - show_message(_("Please stop playback first to end current recording")) - - else: - old = prefs.auto_rec - prefs.auto_rec = pref_box.toggle_square( - x, yy, prefs.auto_rec, _("Record and auto split songs"), - click=gui.level_2_click) - if prefs.auto_rec != old and prefs.auto_rec: - show_message( - _("Tracks will now be recorded."), - _("Tip: You can press F9 to view the output folder."), mode="info") - - if self.tab == 0: - if draw.button( - _("Browse"), (x + w) - round(130 * gui.scale), yy - round(3 * gui.scale), - press=gui.level_2_click, w=round(100 * gui.scale)): - self.tab = 1 - elif self.tab == 1: - if draw.button( - _("Saved"), (x + w) - round(130 * gui.scale), yy - round(3 * gui.scale), - press=gui.level_2_click, w=round(100 * gui.scale)): - self.tab = 0 - gui.level_2_click = False - - -radiobox = RadioBox() -tauon.radiobox = radiobox -tauon.dummy_track = radiobox.dummy_track - - -# def visit_radio_site_show_test(p): -# return "website_url" in prefs.radio_urls[p] and prefs.radio_urls[p]["website_url"] -# - -def visit_radio_site_deco(item): - if "website_url" in item and item["website_url"]: - return [colours.menu_text, colours.menu_background, None] - return [colours.menu_text_disabled, colours.menu_background, None] - - -def visit_radio_station_site_deco(item): - return visit_radio_site_deco(item[1]) - - -def visit_radio_site(item): - if "website_url" in item and item["website_url"]: - webbrowser.open(item["website_url"], new=2, autoraise=True) - - -def visit_radio_station(item): - visit_radio_site(item[1]) - - -def radio_saved_panel_test(_): - return radiobox.tab == 0 - - -def save_to_radios(item): - pctl.radio_playlists[pctl.radio_playlist_viewing]["items"].append(item) - toast(_("Added station to: ") + pctl.radio_playlists[pctl.radio_playlist_viewing]["name"]) - - -radio_entry_menu.add(MenuItem(_("Visit Website"), visit_radio_site, visit_radio_site_deco, pass_ref=True, pass_ref_deco=True)) -radio_entry_menu.add(MenuItem(_("Save"), save_to_radios, pass_ref=True)) - - -class RenamePlaylistBox: - - def __init__(self): - - self.x = 300 - self.y = 300 - self.playlist_index = 0 - - self.edit_generator = False - - def toggle_edit_gen(self): - - self.edit_generator ^= True - if self.edit_generator: - - if len(rename_text_area.text) > 0: - pctl.multi_playlist[self.playlist_index].title = rename_text_area.text - - pl = self.playlist_index - id = pl_to_id(pl) - - text = pctl.gen_codes.get(id) - if not text: - text = "" - - rename_text_area.set_text(text) - rename_text_area.highlight_none() - - gui.regen_single = rename_playlist_box.playlist_index - tauon.thread_manager.ready("worker") - - - else: - rename_text_area.set_text(pctl.multi_playlist[self.playlist_index].title) - rename_text_area.highlight_none() - # rename_text_area.highlight_all() - - def render(self): - - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False - - if inp.key_tab_press: - self.toggle_edit_gen() - - text_w = ddt.get_text_w(rename_text_area.text, 315) - min_w = max(250 * gui.scale, text_w + 50 * gui.scale) - - rect = [self.x, self.y, min_w, 37 * gui.scale] - bg = [40, 40, 40, 255] - if self.edit_generator: - bg = [70, 50, 100, 255] - ddt.text_background_colour = bg - - # Draw background - ddt.rect(rect, bg) - - # Draw text entry - rename_text_area.draw( - rect[0] + 10 * gui.scale, rect[1] + 8 * gui.scale, colours.alpha_grey(250), - width=350 * gui.scale, font=315) - - # Draw accent - rect2 = [self.x, self.y + rect[3] - 4 * gui.scale, min_w, 4 * gui.scale] - ddt.rect(rect2, [255, 255, 255, 60]) - - if self.edit_generator: - pl = self.playlist_index - id = pl_to_id(pl) - pctl.gen_codes[id] = rename_text_area.text - - if input_text or key_backspace_press: - gui.regen_single = rename_playlist_box.playlist_index - tauon.thread_manager.ready("worker") - - # regenerate_playlist(rename_playlist_box.playlist_index) - # if gui.gen_code_errors: - # del_icon.render(rect[0] + rect[2] - 21 * gui.scale, rect[1] + 10 * gui.scale, (255, 70, 70, 255)) - ddt.text_background_colour = [4, 4, 4, 255] - hint_rect = [rect[0], rect[1] + round(50 * gui.scale), round(560 * gui.scale), round(300 * gui.scale)] - - if hint_rect[0] + hint_rect[2] > window_size[0]: - hint_rect[0] = window_size[0] - hint_rect[2] - - ddt.rect(hint_rect, [0, 0, 0, 245]) - xx0 = hint_rect[0] + round(15 * gui.scale) - xx = hint_rect[0] + round(25 * gui.scale) - xx2 = hint_rect[0] + round(85 * gui.scale) - yy = hint_rect[1] + round(10 * gui.scale) - - text_colour = [150, 150, 150, 255] - title_colour = text_colour - code_colour = [250, 250, 250, 255] - hint_colour = [110, 110, 110, 255] - - title_font = 311 - code_font = 311 - hint_font = 310 - - # ddt.pretty_rect = hint_rect - - ddt.text( - (xx0, yy), _("Type codes separated by spaces. Codes will be executed left to right."), text_colour, title_font) - yy += round(18 * gui.scale) - ddt.text((xx0, yy), _("Select sources: (default: all playlists)"), title_colour, title_font) - yy += round(14 * gui.scale) - ddt.text((xx, yy), "s\"name\"", code_colour, code_font) - ddt.text((xx2, yy), _("Select source playlist by name"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "self", code_colour, code_font) - ddt.text((xx2, yy), _("Select playlist itself"), hint_colour, hint_font) - - yy += round(16 * gui.scale) - ddt.text((xx0, yy), _("Add tracks from sources: (at least 1 required)"), title_colour, title_font) - yy += round(14 * gui.scale) - - ddt.text((xx, yy), "a\"name\"", code_colour, code_font) - ddt.text((xx2, yy), _("Search artist name"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "g\"genre\"", code_colour, code_font) - ddt.text((xx2, yy), _("Search genre"), hint_colour, hint_font) - # yy += round(12 * gui.scale) - # ddt.text((xx, yy), "p\"text\"", code_colour, code_font) - # ddt.text((xx2, yy), "Search filepath segment", hint_colour, hint_font) - - yy += round(12 * gui.scale) - ddt.text((xx, yy), "f\"terms\"", code_colour, code_font) - ddt.text((xx2, yy), _("Find / Search / Path"), hint_colour, hint_font) - - # yy += round(12 * gui.scale) - # ddt.text((xx, yy), "ext\"flac\"", code_colour, code_font) - # ddt.text((xx2, yy), "Search by file type", hint_colour, hint_font) - - yy += round(12 * gui.scale) - ddt.text((xx, yy), "a", code_colour, code_font) - ddt.text((xx2, yy), _("Add all tracks"), hint_colour, hint_font) - - yy += round(16 * gui.scale) - ddt.text((xx0, yy), _("Filters"), title_colour, title_font) - yy += round(14 * gui.scale) - ddt.text((xx, yy), "n123", code_colour, code_font) - ddt.text((xx2, yy), _("Limit to number of tracks"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "y>1999", code_colour, code_font) - ddt.text((xx2, yy), _("Year: >, <, ="), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "pc>5", code_colour, code_font) - ddt.text((xx2, yy), _("Play count: >, <"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "d>120", code_colour, code_font) - ddt.text((xx2, yy), _("Duration (seconds): >, <"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "rat>3.5", code_colour, code_font) - ddt.text((xx2, yy), _("Track rating 0-5: >, <, ="), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "l", code_colour, code_font) - ddt.text((xx2, yy), _("Loved tracks"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "ly", code_colour, code_font) - ddt.text((xx2, yy), _("Has lyrics"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "ff\"terms\"", code_colour, code_font) - ddt.text((xx2, yy), _("Search and keep"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "fx\"terms\"", code_colour, code_font) - ddt.text((xx2, yy), _("Search and exclude"), hint_colour, hint_font) - - # yy += round(12 * gui.scale) - # ddt.text((xx, yy), "com\"text\"", code_colour, code_font) - # ddt.text((xx2, yy), "Search in comment", hint_colour, hint_font) - # yy += round(12 * gui.scale) - - xx += round(260 * gui.scale) - xx2 += round(260 * gui.scale) - xx0 += round(260 * gui.scale) - yy = hint_rect[1] + round(10 * gui.scale) - yy += round(18 * gui.scale) - - # yy += round(16 * gui.scale) - ddt.text((xx0, yy), _("Sorters"), title_colour, title_font) - yy += round(14 * gui.scale) - - ddt.text((xx, yy), "st", code_colour, code_font) - ddt.text((xx2, yy), _("Shuffle tracks"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "ra", code_colour, code_font) - ddt.text((xx2, yy), _("Shuffle albums"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "y>", code_colour, code_font) - ddt.text((xx2, yy), _("Year: >, <"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "d>", code_colour, code_font) - ddt.text((xx2, yy), _("Duration: >, <"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "pt>", code_colour, code_font) - ddt.text((xx2, yy), _("Track Playtime: >, <"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "pa>", code_colour, code_font) - ddt.text((xx2, yy), _("Album playtime: >, <"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "rv", code_colour, code_font) - ddt.text((xx2, yy), _("Invert tracks"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "rva", code_colour, code_font) - ddt.text((xx2, yy), _("Invert albums"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "rat>", code_colour, code_font) - ddt.text((xx2, yy), _("Track rating: >, <"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "rata>", code_colour, code_font) - ddt.text((xx2, yy), _("Album rating: >, <"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "m>", code_colour, code_font) - ddt.text((xx2, yy), _("Modification date: >, <"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "path", code_colour, code_font) - ddt.text((xx2, yy), _("Filepath"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "tn", code_colour, code_font) - ddt.text((xx2, yy), _("Track number per album"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "ypa", code_colour, code_font) - ddt.text((xx2, yy), _("Year per artist"), hint_colour, hint_font) - yy += round(12 * gui.scale) - ddt.text((xx, yy), "\"artist\">", code_colour, code_font) - ddt.text((xx2, yy), _("Sort by column name: >, <"), hint_colour, hint_font) - - yy += round(16 * gui.scale) - ddt.text((xx0, yy), _("Special"), title_colour, title_font) - yy += round(14 * gui.scale) - ddt.text((xx, yy), "auto", code_colour, code_font) - ddt.text((xx2, yy), _("Automatically reload on imports"), hint_colour, hint_font) - - yy += round(24 * gui.scale) - # xx += round(80 * gui.scale) - xx2 = xx - xx2 += ddt.text((xx2, yy), _("Status:"), [90, 90, 90, 255], 212) + round(6 * gui.scale) - if rename_text_area.text: - if gui.gen_code_errors: - if gui.gen_code_errors == "playlist": - ddt.text((xx2, yy), _("Playlist not found"), [255, 100, 100, 255], 212) - elif gui.gen_code_errors == "empty": - ddt.text((xx2, yy), _("Result is empty"), [250, 190, 100, 255], 212) - elif gui.gen_code_errors == "close": - ddt.text((xx2, yy), _("Close quotation..."), [110, 110, 110, 255], 212) - else: - ddt.text((xx2, yy), "...", [255, 100, 100, 255], 212) - else: - ddt.text((xx2, yy), _("OK"), [100, 255, 100, 255], 212) - else: - ddt.text((xx2, yy), _("Disabled"), [110, 110, 110, 255], 212) - - # ddt.pretty_rect = None - - # If enter or click outside of box: save and close - if inp.key_return_press or (key_esc_press and len(editline) == 0) \ - or ((inp.mouse_click or level_2_right_click) and not coll(rect)): - gui.rename_playlist_box = False - - if self.edit_generator: - pass - elif len(rename_text_area.text) > 0: - if gui.radio_view: - pctl.radio_playlists[self.playlist_index]["name"] = rename_text_area.text - else: - pctl.multi_playlist[self.playlist_index].title = rename_text_area.text - inp.key_return_press = False - - -rename_playlist_box = RenamePlaylistBox() - - -class PlaylistBox: - - def recalc(self): - self.tab_h = round(25 * gui.scale) - self.gap = round(2 * gui.scale) - - self.text_offset = 2 * gui.scale - if gui.scale == 1.25: - self.text_offset = 3 - - def __init__(self): - - self.scroll_on = prefs.old_playlist_box_position - self.drag = False - self.drag_source = 0 - self.drag_on = -1 - - self.adds = [] - - self.indicate_w = round(2 * gui.scale) - - self.lock_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "lock-corner.png", True) - self.pin_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "dia-pin.png", True) - self.gen_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "gen-gear.png", True) - self.spot_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "spot-playlist.png", True) - - - # if gui.scale == 1.25: - self.tab_h = 0 - self.gap = 0 - - self.text_offset = 2 * gui.scale - self.recalc() - - def draw(self, x, y, w, h): - - global quick_drag - - # ddt.rect_r((x, y, w, h), colours.side_panel_background, True) - ddt.rect((x, y, w, h), colours.playlist_box_background) - ddt.text_background_colour = colours.playlist_box_background - - max_tabs = (h - 10 * gui.scale) // (self.gap + self.tab_h) - - tab_title_colour = [230, 230, 230, 255] - - bg_lumi = test_lumi(colours.playlist_box_background) - light_mode = False - - if bg_lumi < 0.55: - light_mode = True - tab_title_colour = [20, 20, 20, 255] - - dark_mode = False - if bg_lumi > 0.8: - dark_mode = True - - if light_mode: - indicate_w = round(3 * gui.scale) - else: - indicate_w = round(2 * gui.scale) - - show_scroll = False - tab_start = x + 10 * gui.scale - - if window_size[0] < 700 * gui.scale: - tab_start = x + 4 * gui.scale - - if mouse_wheel != 0 and coll((x, y, w, h)): - self.scroll_on -= mouse_wheel - - self.scroll_on = min(self.scroll_on, len(pctl.multi_playlist) - max_tabs + 1) - - self.scroll_on = max(self.scroll_on, 0) - - if len(pctl.multi_playlist) > max_tabs: - show_scroll = True - else: - self.scroll_on = 0 - - if show_scroll: - tab_start += 15 * gui.scale - - if colours.lm: - w -= round(6 * gui.scale) - tab_width = w - tab_start # - 0 * gui.scale - - # Draw scroll bar - if show_scroll: - self.scroll_on = playlist_panel_scroll.draw(x + 2, y + 1, 15 * gui.scale, h, self.scroll_on, - len(pctl.multi_playlist) - max_tabs + 1) - - draw_pin_indicator = False # prefs.tabs_on_top - - # if not gui.album_tab_mode: - # if key_left_press or key_right_press: - # if pctl.active_playlist_viewing < self.scroll_on: - # self.scroll_on = pctl.active_playlist_viewing - # elif pctl.active_playlist_viewing + 1 > self.scroll_on + max_tabs: - # self.scroll_on = (pctl.active_playlist_viewing - max_tabs) + 1 - - # Process inputs - delete_pl = None - tab_on = 0 - yy = y + 5 * gui.scale - for i, pl in enumerate(pctl.multi_playlist): - - if tab_on >= max_tabs: - break - if i < self.scroll_on: - continue - - # if not pl.hidden and i in tabs_on_top: - # continue - - tab_on += 1 - - if coll((tab_start, yy - 1, tab_width, (self.tab_h + 1))): - if right_click: - if gui.radio_view: - radio_tab_menu.activate(i, mouse_position) - else: - tab_menu.activate(i, mouse_position) - gui.tab_menu_pl = i - - if tab_menu.active is False and middle_click: - delete_pl = i - # delete_playlist(i) - # break - - if mouse_up and self.drag and coll_point(mouse_up_position, (tab_start, yy - 1, tab_width, (self.tab_h + 1))): - - # If drag from top bar to side panel, make hidden - if self.drag_source == 0 and prefs.drag_to_unpin: - pctl.multi_playlist[self.drag_on].hidden = True - - # Move playlist tab - if i != self.drag_on and not point_proximity_test(gui.drag_source_position, mouse_position, 10 * gui.scale): - if key_shift_down: - pctl.multi_playlist[i].playlist_ids += pctl.multi_playlist[self.drag_on].playlist_ids - delete_playlist(self.drag_on, force=True) - else: - move_playlist(self.drag_on, i) - - gui.update += 1 - - # Double click to play - if mouse_up and pl_to_id(i) == top_panel.tab_d_click_ref == pl_to_id(pctl.active_playlist_viewing) and \ - top_panel.tab_d_click_timer.get() < 0.25 and \ - point_distance(last_click_location, mouse_up_position) < 5 * gui.scale: - - if pctl.playing_state == 2 and pctl.active_playlist_playing == i: - pctl.play() - elif pctl.selected_ready() and (pctl.playing_state != 1 or pctl.active_playlist_playing != i): - pctl.jump(default_playlist[pctl.selected_in_playlist], pl_position=pctl.selected_in_playlist) - if mouse_up: - top_panel.tab_d_click_timer.set() - top_panel.tab_d_click_ref = pl_to_id(i) - - if not draw_pin_indicator: - if inp.mouse_click: - switch_playlist(i) - self.drag_on = i - self.drag = True - self.drag_source = 1 - set_drag_source() - - # Process input of dragging tracks onto tab - if quick_drag is True and mouse_up: - top_panel.tab_d_click_ref = -1 - top_panel.tab_d_click_timer.force_set(100) - if (pctl.gen_codes.get(pl_to_id(i)) and "self" not in pctl.gen_codes[pl_to_id(i)]): - clear_gen_ask(pl_to_id(i)) - quick_drag = False - modified = False - gui.pl_update += 1 - - for item in shift_selection: - pctl.multi_playlist[i].playlist_ids.append(default_playlist[item]) - modified = True - if len(shift_selection) > 0: - self.adds.append( - [pctl.multi_playlist[i].uuid_int, len(shift_selection), Timer()]) # ID, num, timer - modified = True - if modified: - pctl.after_import_flag = True - tauon.thread_manager.ready("worker") - pctl.notify_change() - pctl.update_shuffle_pool(pctl.multi_playlist[i].uuid_int) - tree_view_box.clear_target_pl(i) - - # Toggle hidden flag on click - if draw_pin_indicator and inp.mouse_click and coll( - (tab_start + 5 * gui.scale, yy + 3 * gui.scale, 25 * gui.scale, 26 * gui.scale)): - pl.hidden ^= True - - yy += self.tab_h + self.gap - - # Draw tabs - # delete_pl = None - tab_on = 0 - yy = y + 5 * gui.scale - for i, pl in enumerate(pctl.multi_playlist): - - # if yy + self.tab_h > y + h: - # break - if tab_on >= max_tabs: - break - if i < self.scroll_on: - continue - - tab_on += 1 - - name = pl.title - hidden = pl.hidden - - # Background is insivible by default (for hightlighting if selected) - bg = [0, 0, 0, 0] - - # Highlight if playlist selected (viewing) - if i == pctl.active_playlist_viewing or (tab_menu.active and tab_menu.reference == i): - # bg = [255, 255, 255, 25] - - # Adjust highlight for different background brightnesses - bg = rgb_add_hls(colours.playlist_box_background, 0, 0.06, 0) - if light_mode: - bg = [0, 0, 0, 25] - - # Highlight target playlist when tragging tracks over - if coll( - (tab_start + 50 * gui.scale, yy - 1, tab_width - 50 * gui.scale, (self.tab_h + 1))) and quick_drag and not ( - pctl.gen_codes.get(pl_to_id(i)) and "self" not in pctl.gen_codes[pl_to_id(i)]): - # bg = [255, 255, 255, 15] - bg = rgb_add_hls(colours.playlist_box_background, 0, 0.04, 0) - if light_mode: - bg = [0, 0, 0, 16] - - # Get actual bg from blend for text bg - real_bg = alpha_blend(bg, colours.playlist_box_background) - - # Draw highlight - ddt.rect((tab_start, yy - round(1 * gui.scale), tab_width, self.tab_h), bg) - - # Draw title text - text_start = 10 * gui.scale - if draw_pin_indicator: - # text_start = 40 * gui.scale - text_start = 32 * gui.scale - - if pctl.gen_codes.get(pl_to_id(i), "")[:3] in ["sal", "slt", "spl"]: - text_start = 28 * gui.scale - self.spot_icon.render(tab_start + round(7 * gui.scale), yy + round(3 * gui.scale), alpha_mod(tab_title_colour, 170)) - - if not pl.hidden and prefs.tabs_on_top: - cl = [255, 255, 255, 25] - - if light_mode: - cl = [0, 0, 0, 40] - - xx = tab_start + tab_width - self.lock_icon.w - self.lock_icon.render(xx, yy, cl) - - text_max_w = tab_width - text_start - 15 * gui.scale - # if indicator_run_x: - # text_max_w = tab_width - (indicator_run_x + text_start + 17 * gui.scale + slide) - ddt.text( - (tab_start + text_start, yy + self.text_offset), name, tab_title_colour, 211, max_w=text_max_w, bg=real_bg) - - # Is mouse collided with tab? - hit = coll((tab_start + 50 * gui.scale, yy - 1, tab_width - 50 * gui.scale, (self.tab_h + 1))) - - # if not prefs.tabs_on_top: - if i == pctl.active_playlist_playing: - - indicator_colour = colours.title_playing - if colours.lm: - indicator_colour = colours.seek_bar_fill - - ddt.rect((tab_start + 0 - 2 * gui.scale, yy - round(1 * gui.scale), indicate_w, self.tab_h), indicator_colour) - - # # If mouse over - if hit: - # Draw indicator for dragging tracks - if quick_drag and pl_is_mut(i): - ddt.rect((tab_start + tab_width - self.indicate_w, yy, self.indicate_w, self.tab_h), [80, 200, 180, 255]) - - # Draw indicators for moving tab - if self.drag and i != self.drag_on and not point_proximity_test( - gui.drag_source_position, mouse_position, 10 * gui.scale): - if key_shift_down: - ddt.rect( - (tab_start + tab_width - 4 * gui.scale, yy, self.indicate_w, self.tab_h), - [80, 160, 200, 255]) - elif i < self.drag_on: - ddt.rect((tab_start, yy - self.indicate_w, tab_width, self.indicate_w), [80, 160, 200, 255]) - else: - ddt.rect((tab_start, yy + (self.tab_h - self.indicate_w), tab_width, self.indicate_w), [80, 160, 200, 255]) - - elif quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15 * gui.scale): - for item in shift_selection: - if len(default_playlist) > item and default_playlist[item] in pl.playlist_ids: - ddt.rect((tab_start + tab_width - self.indicate_w, yy, self.indicate_w, self.tab_h), [190, 170, 20, 255]) - break - # Drag red line highlight if playlist is generator playlist - if quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15 * gui.scale): - if not pl_is_mut(i): - ddt.rect((tab_start + tab_width - self.indicate_w, yy, self.indicate_w, self.tab_h), [200, 70, 50, 255]) - - # Draw effect of adding tracks to playlist - if len(self.adds) > 0: - for k in reversed(range(len(self.adds))): - if pctl.multi_playlist[i].uuid_int == self.adds[k][0]: - if self.adds[k][2].get() > 0.3: - del self.adds[k] - else: - ay = yy + 4 * gui.scale - ay -= 6 * gui.scale * self.adds[k][2].get() / 0.3 - - ddt.text( - (tab_start + tab_width - 10 * gui.scale, int(round(ay)), 1), - "+" + str(self.adds[k][1]), colours.pluse_colour, 212, bg=real_bg) - gui.update += 1 - - ddt.rect( - (tab_start + tab_width, yy, self.indicate_w, self.tab_h - self.indicate_w), - [244, 212, 66, int(255 * self.adds[k][2].get() / 0.3) * -1]) - - yy += self.tab_h + self.gap - - if delete_pl is not None: - # delete_playlist(delete_pl) - delete_playlist_ask(delete_pl) - gui.update += 1 - - # Create new playlist if drag in blank space after tabs - rect = (x, yy, w - 10 * gui.scale, h - (yy - y)) - fields.add(rect) - - if coll(rect): - if quick_drag: - ddt.rect((tab_start, yy, tab_width, self.indicate_w), [80, 160, 200, 255]) - if mouse_up: - drop_tracks_to_new_playlist(shift_selection) - - if right_click: - extra_tab_menu.activate(pctl.active_playlist_viewing) - - # Move tab to end playlist if dragged past end - if self.drag: - if mouse_up: - if key_ctrl_down: - # Duplicate playlist on ctrl - gen_dupe(playlist_box.drag_on) - gui.update += 2 - self.drag = False - else: - # If drag from top bar to side panel, make hidden - if self.drag_source == 0 and prefs.drag_to_unpin: - pctl.multi_playlist[self.drag_on].hidden = True - - move_playlist(self.drag_on, i) - gui.update += 2 - self.drag = False - elif key_ctrl_down: - ddt.rect((tab_start, yy, tab_width, self.indicate_w), [255, 190, 0, 255]) - else: - ddt.rect((tab_start, yy, tab_width, self.indicate_w), [80, 160, 200, 255]) - - -playlist_box = PlaylistBox() - - -def create_artist_pl(artist: str, replace: bool = False): - source_pl = pctl.active_playlist_viewing - this_pl = pctl.active_playlist_viewing - - if pctl.multi_playlist[source_pl].parent_playlist_id: - if pctl.multi_playlist[source_pl].title.startswith("Artist:"): - new = id_to_pl(pctl.multi_playlist[source_pl].parent_playlist_id) - if new is None: - # The original playlist is now gone - pctl.multi_playlist[source_pl].parent_playlist_id = "" - else: - source_pl = new - # replace = True - - playlist = [] - - for item in pctl.multi_playlist[source_pl].playlist_ids: - track = pctl.get_track(item) - if track.artist == artist or track.album_artist == artist: - playlist.append(item) - - if replace: - pctl.multi_playlist[this_pl].playlist_ids[:] = playlist[:] - pctl.multi_playlist[this_pl].title = _("Artist: ") + artist - if album_mode: - reload_albums() - - # Transfer playing track back to original playlist - if pctl.multi_playlist[this_pl].parent_playlist_id: - new = id_to_pl(pctl.multi_playlist[this_pl].parent_playlist_id) - tr = pctl.playing_object() - if new is not None and tr and pctl.active_playlist_playing == this_pl: - if tr.index not in pctl.multi_playlist[this_pl].playlist_ids and tr.index in pctl.multi_playlist[source_pl].playlist_ids: - logging.info("Transfer back playing") - pctl.active_playlist_playing = source_pl - pctl.playlist_playing_position = pctl.multi_playlist[source_pl].playlist_ids.index(tr.index) - - pctl.gen_codes[pl_to_id(this_pl)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" - - else: - - pctl.multi_playlist.append( - pl_gen( - title=_("Artist: ") + artist, - playlist_ids=playlist, - hide_title=False, - parent=pl_to_id(source_pl))) - - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" - - switch_playlist(len(pctl.multi_playlist) - 1) - - -artist_list_menu.add(MenuItem(_("Filter to New Playlist"), create_artist_pl, pass_ref=True, icon=filter_icon)) - -artist_list_menu.add_sub(_("View..."), 140) - - -def aa_sort_alpha(): - prefs.artist_list_sort_mode = "alpha" - artist_list_box.saves.clear() - - -def aa_sort_popular(): - prefs.artist_list_sort_mode = "popular" - artist_list_box.saves.clear() - - -def aa_sort_play(): - prefs.artist_list_sort_mode = "play" - artist_list_box.saves.clear() - - -def toggle_artist_list_style(): - if prefs.artist_list_style == 1: - prefs.artist_list_style = 2 - else: - prefs.artist_list_style = 1 - - -def toggle_artist_list_threshold(): - if prefs.artist_list_threshold > 0: - prefs.artist_list_threshold = 0 - else: - prefs.artist_list_threshold = 4 - artist_list_box.saves.clear() - -def toggle_artist_list_threshold_deco(): - if prefs.artist_list_threshold == 0: - return [colours.menu_text, colours.menu_background, _("Filter Small Artists")] - save = artist_list_box.saves.get(pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int) - if save and save[5] == 0: - return [colours.menu_text_disabled, colours.menu_background, _("Include All Artists")] - return [colours.menu_text, colours.menu_background, _("Include All Artists")] - -artist_list_menu.add_to_sub(0, MenuItem(_("Sort Alphabetically"), aa_sort_alpha)) -artist_list_menu.add_to_sub(0, MenuItem(_("Sort by Popularity"), aa_sort_popular)) -artist_list_menu.add_to_sub(0, MenuItem(_("Sort by Playtime"), aa_sort_play)) -artist_list_menu.add_to_sub(0, MenuItem(_("Toggle Thumbnails"), toggle_artist_list_style)) -artist_list_menu.add_to_sub(0, MenuItem(_("Toggle Filter"), toggle_artist_list_threshold, toggle_artist_list_threshold_deco)) - - -def verify_discogs(): - return len(prefs.discogs_pat) == 40 - - -def save_discogs_artist_thumb(artist, filepath): - logging.info("Searching discogs for artist image...") - - # Make artist name url safe - artist = artist.replace("/", "").replace("\\", "").replace(":", "") - - # Search for Discogs artist id - url = "https://api.discogs.com/database/search" - r = requests.get(url, params={"query": artist, "type": "artist", "token": prefs.discogs_pat}, headers={"User-Agent": t_agent}, timeout=10) - id = r.json()["results"][0]["id"] - - # Search artist info, get images - url = "https://api.discogs.com/artists/" + str(id) - r = requests.get(url, headers={"User-Agent": t_agent}, params={"token": prefs.discogs_pat}, timeout=10) - images = r.json()["images"] - - # Respect rate limit - rate_remaining = r.headers["X-Discogs-Ratelimit-Remaining"] - if int(rate_remaining) < 30: - time.sleep(5) - - # Find a square image in list of images - for image in images: - if image["height"] == image["width"]: - logging.info("Found square") - url = image["uri"] - break - else: - url = images[0]["uri"] - - response = urllib.request.urlopen(url, context=ssl_context) - im = Image.open(response) - - width, height = im.size - if width > height: - delta = width - height - left = int(delta / 2) - upper = 0 - right = height + left - lower = height - else: - delta = height - width - left = 0 - upper = int(delta / 2) - right = width - lower = width + upper - - im = im.crop((left, upper, right, lower)) - im.save(filepath, "JPEG", quality=90) - im.close() - logging.info("Found artist image from Discogs") - - -def save_fanart_artist_thumb(mbid, filepath, preview=False): - logging.info("Searching fanart.tv for image...") - #logging.info("mbid is " + mbid) - r = requests.get("https://webservice.fanart.tv/v3/music/" + mbid + "?api_key=" + prefs.fatvap, timeout=5) - #logging.info(r.json()) - thumblink = r.json()["artistthumb"][0]["url"] - if preview: - thumblink = thumblink.replace("/fanart/music", "/preview/music") - - response = urllib.request.urlopen(thumblink, timeout=10, context=ssl_context) - info = response.info() - - t = io.BytesIO() - t.seek(0) - t.write(response.read()) - l = 0 - t.seek(0, 2) - l = t.tell() - t.seek(0) - - if info.get_content_maintype() == "image" and l > 1000: - f = open(filepath, "wb") - f.write(t.read()) - f.close() - - if prefs.fanart_notify: - prefs.fanart_notify = False - show_message( - _("Notice: Artist image sourced from fanart.tv"), - _("They encourage you to contribute at {link}").format(link="https://fanart.tv"), mode="link") - logging.info("Found artist thumbnail from fanart.tv") - - -class ArtistList: - - def __init__(self): - - self.tab_h = round(60 * gui.scale) - self.thumb_size = round(55 * gui.scale) - - self.current_artists = [] - self.current_album_counts = {} - self.current_artist_track_counts = {} - - self.thumb_cache = {} - - self.to_fetch = "" - self.to_fetch_mbid_a = "" - - self.scroll_position = 0 - - self.id_to_load = "" - - self.d_click_timer = Timer() - self.d_click_ref = -1 - - self.click_ref = -1 - self.click_highlight_timer = Timer() - - self.saves = {} - - self.load = False - - self.shown_letters = [] - - self.hover_on = "NONE" - self.hover_timer = Timer(10) - - self.sample_tracks = {} - - def load_img(self, artist): - - filepath = artist_info_box.get_data(artist, get_img_path=True) - - if filepath and os.path.isfile(filepath): - - try: - g = io.BytesIO() - g.seek(0) - - im = Image.open(filepath) - - w, h = im.size - if w != h: - m = min(w, h) - im = im.crop(( - round((w - m) / 2), - round((h - m) / 2), - round((w + m) / 2), - round((h + m) / 2), - )) - - im.thumbnail((self.thumb_size, self.thumb_size), Image.Resampling.LANCZOS) - - im.save(g, "PNG") - g.seek(0) - - wop = rw_from_object(g) - s_image = IMG_Load_RW(wop, 0) - texture = SDL_CreateTextureFromSurface(renderer, s_image) - SDL_FreeSurface(s_image) - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) - SDL_QueryTexture(texture, None, None, tex_w, tex_h) - sdl_rect = SDL_Rect(0, 0) - sdl_rect.w = int(tex_w.contents.value) - sdl_rect.h = int(tex_h.contents.value) - - self.thumb_cache[artist] = [texture, sdl_rect] - except Exception: - logging.exception("Artist thumbnail processing error") - self.thumb_cache[artist] = None - - elif artist in prefs.failed_artists: - self.thumb_cache[artist] = None - elif not self.to_fetch: - - if prefs.auto_dl_artist_data: - self.to_fetch = artist - tauon.thread_manager.ready("worker") - - else: - self.thumb_cache[artist] = None - - def worker(self): - - if self.load: - - if after_scan: - return - - self.prep() - self.load = False - return - - if self.to_fetch: - - if get_lfm_wait_timer.get() < 2: - return - - artist = self.to_fetch - f_artist = filename_safe(artist) - filename = f_artist + "-lfm.png" - filename2 = f_artist + "-lfm.txt" - filename3 = f_artist + "-ftv.jpg" - filename4 = f_artist + "-dcg.jpg" - filepath = os.path.join(a_cache_dir, filename) - filepath2 = os.path.join(a_cache_dir, filename2) - filepath3 = os.path.join(a_cache_dir, filename3) - filepath4 = os.path.join(a_cache_dir, filename4) - got_image = False - try: - # Lookup artist info on last.fm - logging.info("lastfm lookup artist: " + artist) - mbid = lastfm.artist_mbid(artist) - get_lfm_wait_timer.set() - # if data[0] is not False: - # #cover_link = data[2] - # text = data[1] - # - # if not os.path.exists(filepath2): - # f = open(filepath2, 'w', encoding='utf-8') - # f.write(text) - # f.close() - - if mbid and prefs.enable_fanart_artist: - save_fanart_artist_thumb(mbid, filepath3, preview=True) - got_image = True - - except Exception: - logging.exception("Failed to find image from fanart.tv") - - if not got_image and verify_discogs(): - try: - save_discogs_artist_thumb(artist, filepath4) - except Exception: - logging.exception("Failed to find image from discogs") - - if os.path.exists(filepath3) or os.path.exists(filepath4): - gui.update += 1 - elif artist not in prefs.failed_artists: - logging.error("Failed fetching: " + artist) - prefs.failed_artists.append(artist) - - self.to_fetch = "" - - def prep(self): - self.scroll_position = 0 - - curren_pl_no = id_to_pl(self.id_to_load) - if curren_pl_no is None: - return - current_pl = pctl.multi_playlist[curren_pl_no] - - all = [] - artist_parents = {} - counts = {} - play_time = {} - filtered = 0 - b = 0 - - try: - - for item in current_pl.playlist_ids: - b += 1 - if b % 100 == 0: - time.sleep(0.001) - - track = pctl.get_track(item) - - if "artists" in track.misc: - artists = track.misc["artists"] - else: - if prefs.artist_list_prefer_album_artist and track.album_artist: - artists = track.album_artist - else: - artists = get_artist_strip_feat(track) - - artists = [x.strip() for x in artists.split(";")] - - pp = 0 - if prefs.artist_list_sort_mode == "play": - pp = star_store.get(item) - - for artist in artists: - - if artist: - - # Add play time - if prefs.artist_list_sort_mode == "play": - p = play_time.get(artist, 0) - play_time[artist] = p + pp - - # Get a sample track for fallback art - if artist not in self.sample_tracks: - self.sample_tracks[artist] = track - - # Confirm to final list if appeared at least 5 times - # if artist not in all: - if artist not in counts: - counts[artist] = 0 - counts[artist] += 1 - if artist not in all: - if counts[artist] > prefs.artist_list_threshold or len(current_pl.playlist_ids) < 1000: - all.append(artist) - else: - filtered += 1 - - if artist not in artist_parents: - artist_parents[artist] = [] - if track.parent_folder_path not in artist_parents[artist]: - artist_parents[artist].append(track.parent_folder_path) - - current_album_counts = artist_parents - - if prefs.artist_list_sort_mode == "popular": - all.sort(key=counts.get, reverse=True) - elif prefs.artist_list_sort_mode == "play": - all.sort(key=play_time.get, reverse=True) - else: - all.sort(key=lambda y: y.lower().removeprefix("the ")) - - except Exception: - logging.exception("Album scan failure") - time.sleep(4) - return - - # Artist-list, album-counts, scroll-position, playlist-length, number ignored - save = [all, current_album_counts, 0, len(current_pl.playlist_ids), counts, filtered] - - # Scroll to playing artist - scroll = 0 - if pctl.playing_ready(): - track = pctl.playing_object() - for i, item in enumerate(save[0]): - if item == track.artist or item == track.album_artist: - scroll = i - break - save[2] = scroll - - viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - if viewing_pl_id in self.saves: - self.saves[viewing_pl_id][2] = self.scroll_position # TODO(Martin): Is saves a list[TauonPlaylist] here? If so, [2] should be .playlist_ids - - self.saves[current_pl.uuid_int] = save - gui.update += 1 - - def locate_artist_letter(self, text): - - if not text or prefs.artist_list_sort_mode != "alpha": - return - - letter = text[0].lower() - letter_upper = letter.upper() - for i, item in enumerate(self.current_artists): - if item.startswith(("the ", "The ")): - if len(item) > 4 and (item[4] == letter or item[4] == letter_upper): - self.scroll_position = i - break - elif item and (item[0] == letter or item[0] == letter_upper): - self.scroll_position = i - break - - viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - if pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id: - viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id - if viewing_pl_id in self.saves: - self.saves[viewing_pl_id][2] = self.scroll_position - - def locate_artist(self, track: TrackClass): - - for i, item in enumerate(self.current_artists): - if item == track.artist or item == track.album_artist or ( - "artists" in track.misc and item in track.misc["artists"]): - self.scroll_position = i - break - - viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - if viewing_pl_id in self.saves: - self.saves[viewing_pl_id][2] = self.scroll_position - - def draw_card_text_only(self, artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg): - - album_mode = False - for albums in self.current_album_counts.values(): - if len(albums) > 1: - album_mode = True - break - - if not album_mode: - count = self.current_artist_track_counts[artist] - if count > 1: - text = _("{N} tracks").format(N=str(count)) - else: - text = _("{N} track").format(N=str(count)) - else: - album_count = len(self.current_album_counts[artist]) - if album_count > 1: - text = _("{N} tracks").format(N=str(album_count)) - else: - text = _("{N} track").format(N=str(album_count)) - - if gui.preview_artist_loading == artist: - # . Max 20 chars. Alt: Downloading image, Loading image - text = _("Downloading data...") - - x_text = round(10 * gui.scale) - artist_font = 313 - count_font = 312 - extra_text_space = 0 - ddt.text( - (x_text, y + round(2 * gui.scale)), artist, line1_colour, artist_font, - extra_text_space + w - x_text - 30 * gui.scale, bg=bg) - # ddt.text((x_text, y + self.tab_h // 2 - 2 * gui.scale), text, line2_colour, count_font, - # extra_text_space + w - x_text - 15 * gui.scale, bg=bg) - - def draw_card_with_thumbnail(self, artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg): - - if artist not in self.thumb_cache: - self.load_img(artist) - - thumb_x = round(x + 10 * gui.scale) - x_text = x + self.thumb_size + 19 * gui.scale - artist_font = 513 - count_font = 312 - extra_text_space = 0 - if thin_mode: - thumb_x = round(x + 10 * gui.scale) - x_text = x + self.thumb_size + 17 * gui.scale - artist_font = 211 - count_font = 311 - extra_text_space = 135 * gui.scale - thin_mode = True - area = (4 * gui.scale, y, w - 7 * gui.scale, self.tab_h - 2) - fields.add(area) - - back_colour = [30, 30, 30, 255] - back_colour_2 = [27, 27, 27, 255] - border_colour = [60, 60, 60, 255] - # if colours.lm: - # back_colour = [200, 200, 200, 255] - # back_colour_2 = [240, 240, 240, 255] - # border_colour = [160, 160, 160, 255] - rect = (thumb_x, round(y), self.thumb_size, self.thumb_size) - - if thin_mode and coll(area) and is_level_zero() and y + self.tab_h < window_size[1] - gui.panelBY: - tab_rect = (x, y - round(2 * gui.scale), round(190 * gui.scale), self.tab_h - round(1 * gui.scale)) - - for r in subtract_rect(tab_rect, rect): - r = SDL_Rect(r[0], r[1], r[2], r[3]) - style_overlay.hole_punches.append(r) - - ddt.rect(tab_rect, back_colour_2) - bg = back_colour_2 - - ddt.rect(rect, back_colour) - ddt.rect(rect, border_colour) - - fields.add(rect) - if coll(rect) and is_level_zero(True): - self.hover_any = True - - hover_delay = 0.5 - if gui.compact_artist_list: - hover_delay = 2 - - if gui.preview_artist != artist: - if self.hover_on != artist: - self.hover_on = artist - gui.preview_artist = "" - self.hover_timer.set() - gui.delay_frame(hover_delay) - elif self.hover_timer.get() > hover_delay and not gui.preview_artist_loading: - gui.preview_artist = "" - path = artist_info_box.get_data(artist, get_img_path=True) - if not path: - gui.preview_artist_loading = artist - shoot = threading.Thread( - target=get_artist_preview, - args=((artist, round(thumb_x + self.thumb_size), round(y)))) - shoot.daemon = True - shoot.start() - - if path: - set_artist_preview(path, artist, round(thumb_x + self.thumb_size), round(y)) - - if inp.mouse_click: - self.hover_timer.force_set(-2) - gui.delay_frame(2 + hover_delay) - - drawn = False - if artist in self.thumb_cache: - thumb = self.thumb_cache[artist] - if thumb is not None: - thumb[1].x = thumb_x - thumb[1].y = round(y) - SDL_RenderCopy(renderer, thumb[0], None, thumb[1]) - drawn = True - if prefs.art_bg: - rect = SDL_Rect(thumb_x, round(y), self.thumb_size, self.thumb_size) - if (rect.y + rect.h) > window_size[1] - gui.panelBY: - diff = (rect.y + rect.h) - (window_size[1] - gui.panelBY) - rect.h -= round(diff) - style_overlay.hole_punches.append(rect) - if not drawn: - track = self.sample_tracks.get(artist) - if track: - tauon.gall_ren.render(track, (round(thumb_x), round(y)), self.thumb_size) - - if thin_mode: - text = artist[:2].title() - if text not in self.shown_letters: - ww = ddt.get_text_w(text, 211) - ddt.rect( - (thumb_x + round(1 * gui.scale), y + self.tab_h - 20 * gui.scale, ww + 5 * gui.scale, 13 * gui.scale), - [20, 20, 20, 255]) - ddt.text( - (thumb_x + 3 * gui.scale, y + self.tab_h - 23 * gui.scale), text, [240, 240, 240, 255], 210, - bg=[20, 20, 20, 255]) - self.shown_letters.append(text) - - # Draw labels - if not thin_mode or (coll(area) and is_level_zero() and y + self.tab_h < window_size[1] - gui.panelBY): - - album_mode = False - for albums in self.current_album_counts.values(): - if len(albums) > 1: - album_mode = True - break - - if not album_mode: - count = self.current_artist_track_counts[artist] - if count > 1: - text = _("{N} tracks").format(N=str(count)) - else: - text = _("{N} track").format(N=str(count)) - else: - album_count = len(self.current_album_counts[artist]) - if album_count > 1: - text = _("{N} tracks").format(N=str(album_count)) - else: - text = _("{N} track").format(N=str(album_count)) - - if gui.preview_artist_loading == artist: - # . Max 20 chars. Alt: Downloading image, Loading image - text = _("Downloading data...") - - ddt.text( - (x_text, y + self.tab_h // 2 - 19 * gui.scale), artist, line1_colour, artist_font, - extra_text_space + w - x_text - 30 * gui.scale, bg=bg) - ddt.text( - (x_text, y + self.tab_h // 2 - 2 * gui.scale), text, line2_colour, count_font, - extra_text_space + w - x_text - 15 * gui.scale, bg=bg) - - def draw_card(self, artist, x, y, w): - - area = (4 * gui.scale, y, w - 26 * gui.scale, self.tab_h - 2) - if prefs.artist_list_style == 2: - area = (4 * gui.scale, y, w - 26 * gui.scale, self.tab_h - 1) - - fields.add(area) - - light_mode = False - line1_colour = [235, 235, 235, 255] - line2_colour = [255, 255, 255, 120] - fade_max = 50 - - thin_mode = False - if gui.compact_artist_list: - thin_mode = True - line2_colour = [115, 115, 115, 255] - - elif test_lumi(colours.side_panel_background) < 0.55 and not thin_mode: - light_mode = True - fade_max = 20 - line1_colour = [35, 35, 35, 255] - line2_colour = [100, 100, 100, 255] - - # Fade on click - bg = colours.side_panel_background - if not thin_mode: - - if coll(area) and is_level_zero( - True): # or pctl.get_track(default_playlist[pctl.playlist_view_position]).artist == artist: - ddt.rect(area, [50, 50, 50, 50]) - bg = alpha_blend([50, 50, 50, 50], colours.side_panel_background) - else: - - fade = 0 - t = self.click_highlight_timer.get() - if self.click_ref == artist and (t < 2.2 or artist_list_menu.active): - - if t < 1.9 or artist_list_menu.active: - fade = fade_max - else: - fade = fade_max - round((t - 1.9) / 0.3 * fade_max) - - gui.update += 1 - ddt.rect(area, [50, 50, 50, fade]) - - bg = alpha_blend([50, 50, 50, fade], colours.side_panel_background) - - if prefs.artist_list_style == 1: - self.draw_card_with_thumbnail(artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg) - else: - self.draw_card_text_only(artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg) - - if coll(area) and mouse_position[1] < window_size[1] - gui.panelBY: - if inp.mouse_click: - if self.click_ref != artist: - pctl.playlist_view_position = 0 - pctl.selected_in_playlist = 0 - self.click_ref = artist - - double_click = False - if self.d_click_timer.get() < 0.4 and self.d_click_ref == artist: - double_click = True - - self.click_highlight_timer.set() - - if pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id and \ - pctl.multi_playlist[pctl.active_playlist_viewing].title.startswith("Artist:"): - create_artist_pl(artist, replace=True) - - - blocks = [] - current_block = [] - - in_artist = False - this_artist = artist.casefold() - last_ref = None - on = 0 - - for i in range(len(default_playlist)): - track = pctl.get_track(default_playlist[i]) - if track.artist.casefold() == this_artist or track.album_artist.casefold() == this_artist or ( - "artists" in track.misc and artist in track.misc["artists"]): - # Matchin artist - if not in_artist: - in_artist = True - last_ref = track - current_block.append(i) - - elif (last_ref and track.album != last_ref.album) or track.parent_folder_path != last_ref.parent_folder_path: - current_block.append(i) - last_ref = track - # Not matching - elif in_artist: - blocks.append(current_block) - current_block = [] - in_artist = False - - if current_block: - blocks.append(current_block) - current_block = [] - - #logging.info(blocks) - # return - - # block_starts = [] - # current = False - # for i in range(len(default_playlist)): - # track = pctl.get_track(default_playlist[i]) - # if current is False: - # if track.artist == artist or track.album_artist == artist or ( - # 'artists' in track.misc and artist in track.misc['artists']): - # block_starts.append(i) - # current = True - # else: - # if track.artist != artist and track.album_artist != artist or ( - # 'artists' in track.misc and artist in track.misc['artists']): - # current = False - # - # if not block_starts: - # logging.info("No matching artists found in playlist") - # return - - if not blocks: - return - - #select = block_starts[0] - - # if len(block_starts) > 1: - # if -1 < pctl.selected_in_playlist < len(default_playlist): - # if pctl.selected_in_playlist in block_starts: - # scroll_hide_timer.set() - # gui.frame_callback_list.append(TestTimer(0.9)) - # if block_starts[-1] == pctl.selected_in_playlist: - # pass - # else: - # select = block_starts[block_starts.index(pctl.selected_in_playlist) + 1] - - gui.pl_update += 1 - - self.click_highlight_timer.set() - - select = blocks[0][0] - - if double_click: - # Stat first artist track in playlist - - pctl.jump(default_playlist[select], pl_position=select) - pctl.playlist_view_position = select - pctl.selected_in_playlist = select - shift_selection.clear() - self.d_click_timer.force_set(10) - else: - # Goto next artist section in playlist - c = pctl.selected_in_playlist - next = False - track = pctl.get_track_in_playlist(c, -1) - if track is None: - logging.error("Index out of range!") - pctl.selected_in_playlist = 0 - return - if track.artist.casefold != artist.casefold: - pctl.selected_in_playlist = 0 - pctl.playlist_view_position = 0 - if len(blocks) == 1: - block = blocks[0] - if len(block) > 1: - if c < block[0] or c >= block[-1]: - select = block[0] - toast(_("First of artist's albums ({N} albums)") - .format(N=len(block))) - else: - select = block[-1] - toast(_("Last of artist's albums ({N} albums)") - .format(N=len(block))) - else: - select = None - for bb, block in enumerate(blocks): - for i, al in enumerate(block): - if al <= c: - continue - next = True - if i == 0: - select = al - if len(block) > 1: - toast(_("Start of location {N} of {T} ({Nb} albums)") - .format(N=bb + 1, T=len(blocks), Nb=len(block))) - else: - toast(_("Location {N} of {T}") - .format(N=bb + 1, T=len(blocks))) - break - - if next and not select: - select = block[-1] - if len(block) > 1: - toast(_("End of location {N} of {T} ({Nb} albums)") - .format(N=bb + 1, T=len(blocks), Nb=len(block))) - else: - toast(_("Location {N} of {T}") - .format(N=bb, T=len(blocks))) - break - if select: - break - if not select: - select = blocks[0][0] - if len(blocks[0]) > 1: - if len(blocks) > 1: - toast(_("Start of location 1 of {N} ({Nb} albums)") - .format(N=len(blocks), Nb=len(blocks[0]))) - else: - toast(_("Location 1 of {N} ({Nb} albums)") - .format(N=len(blocks), Nb=len(blocks[0]))) - else: - toast(_("Location 1 of {N}") - .format(N=len(blocks))) - - pctl.playlist_view_position = select - pctl.selected_in_playlist = select - self.d_click_ref = artist - self.d_click_timer.set() - if album_mode: - goto_album(select) - - if middle_click: - self.click_ref = artist - self.click_highlight_timer.set() - create_artist_pl(artist) - - if right_click: - self.click_ref = artist - self.click_highlight_timer.set() - - artist_list_menu.activate(in_reference=artist) - - def render(self, x, y, w, h): - - if prefs.artist_list_style == 1: - self.tab_h = round(60 * gui.scale) - else: - self.tab_h = round(22 * gui.scale) - - viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - - # use parent playlst is set - if pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id: - - # test if parent still exists - new = id_to_pl(pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id) - if new is None or not pctl.multi_playlist[pctl.active_playlist_viewing].title.startswith("Artist:"): - pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id = "" - else: - viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id - - if viewing_pl_id in self.saves: - self.current_artists = self.saves[viewing_pl_id][0] - self.current_album_counts = self.saves[viewing_pl_id][1] - self.current_artist_track_counts = self.saves[viewing_pl_id][4] - self.scroll_position = self.saves[viewing_pl_id][2] - - if self.saves[viewing_pl_id][3] != len(pctl.multi_playlist[id_to_pl(viewing_pl_id)].playlist_ids): - del self.saves[viewing_pl_id] - return - - else: - - # if self.current_pl != viewing_pl_id: - self.id_to_load = viewing_pl_id - if not self.load: - # self.prep() - self.current_artists = [] - self.current_album_counts = [] - self.current_artist_track_counts = {} - self.load = True - tauon.thread_manager.ready("worker") - - area = (x, y, w, h) - area2 = (x + 1, y, w - 3, h) - - ddt.rect(area, colours.side_panel_background) - ddt.text_background_colour = colours.side_panel_background - - if coll(area) and mouse_wheel: - mx = 1 - if prefs.artist_list_style == 2: - mx = 3 - self.scroll_position -= mouse_wheel * mx - self.scroll_position = max(self.scroll_position, 0) - - range = (h // self.tab_h) - 1 - - whole_rage = math.floor(h // self.tab_h) - - if range > 4 and self.scroll_position > len(self.current_artists) - range: - self.scroll_position = len(self.current_artists) - range - - if len(self.current_artists) <= whole_rage: - self.scroll_position = 0 - - fields.add(area2) - scroll_x = x + w - 18 * gui.scale - if colours.lm: - scroll_x = x + w - 22 * gui.scale - if (coll(area2) or artist_list_scroll.held) and not pref_box.enabled: - scroll_width = 15 * gui.scale - inset = 0 - if gui.compact_artist_list: - pass - # scroll_width = round(6 * gui.scale) - # scroll_x += round(9 * gui.scale) - else: - self.scroll_position = artist_list_scroll.draw( - scroll_x, y + 1, scroll_width, h, self.scroll_position, - len(self.current_artists) - range, r_click=right_click, - jump_distance=35, extend_field=6 * gui.scale) - - if not self.current_artists: - text = _("No artists in playlist") - - if default_playlist: - text = _("Artist threshold not met") - if self.load: - text = _("Loading Artist List...") - if loading_in_progress or transcode_list or after_scan: - text = _("Busy...") - - ddt.text( - (x + w // 2, y + (h // 7), 2), text, alpha_mod(colours.side_bar_line2, 100), 212, - max_w=w - 17 * gui.scale) - - yy = y + 12 * gui.scale - - i = int(self.scroll_position) - - if viewing_pl_id in self.saves: - self.saves[viewing_pl_id][2] = self.scroll_position - - prefetch_mode = False - prefetch_distance = 22 - - self.shown_letters.clear() - - self.hover_any = False - - for i, artist in enumerate(self.current_artists[i:], start=i): - - if not prefetch_mode: - self.draw_card(artist, x, round(yy), w) - - yy += self.tab_h - - if yy - y > h - 24 * gui.scale: - prefetch_mode = True - continue - - if prefetch_mode: - if prefs.artist_list_style == 2: - break - prefetch_distance -= 1 - if prefetch_distance < 1: - break - if artist not in self.thumb_cache: - self.load_img(artist) - break - - if not self.hover_any: - gui.preview_artist = "" - self.hover_timer.force_set(10) - artist_preview_render.show = False - self.hover_on = False - - -artist_list_box = ArtistList() - - -class TreeView: - - def __init__(self): - - self.trees = {} # Per playlist tree - self.rows = [] # For display (parsed from tree) - self.rows_id = "" - - self.opens = {} # Folders clicks to show per playlist - - self.scroll_positions = {} - - # Recursive gen_rows vars - self.count = 0 - self.depth = 0 - - self.background_processing = False - self.d_click_timer = Timer(100) - self.d_click_id = "" - - self.menu_selected = "" - self.folder_colour_cache = {} - self.dragging_name = "" - - self.force_opens = [] - self.click_drag_source = None - - self.tooltip_on = "" - self.tooltip_timer = Timer(10) - - self.lock_pl = None - - # self.bold_colours = ColourGenCache(0.6, 0.7) - - def clear_all(self): - self.rows_id = "" - self.trees.clear() - - def collapse_all(self): - pl_id = pl_to_id(pctl.active_playlist_viewing) - - if self.lock_pl: - pl_id = self.lock_pl - - opens = self.opens.get(pl_id) - if opens is None: - opens = [] - self.opens[pl_id] = opens - - opens.clear() - self.rows_id = "" - - def clear_target_pl(self, pl_number, pl_id=None): - - if pl_id is None: - pl_id = pl_to_id(pl_number) - - if gui.lsp and prefs.left_panel_mode == "folder view": - - if pl_id in self.trees: - if not self.background_processing: - self.background_processing = True - shoot_dl = threading.Thread(target=self.gen_tree, args=[pl_id]) - shoot_dl.daemon = True - shoot_dl.start() - elif pl_id in self.trees: - del self.trees[pl_id] - - def show_track(self, track: TrackClass) -> None: - - if track is None: - return - - # Get tree and opened folder data for this playlist - pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - opens = self.opens.get(pl_id) - if opens is None: - opens = [] - self.opens[pl_id] = opens - - tree = self.trees.get(pl_id) - if not tree: - return - - scroll_position = self.scroll_positions.get(pl_id) - if scroll_position is None: - scroll_position = 0 - - # Clear all opened folders - opens.clear() - - # Set every folder in path as opened - path = "" - crumbs = track.parent_folder_path.split("/")[1:] - for c in crumbs: - path += "/" + c - opens.append(path) - - # Regenerate row display - self.gen_rows(tree, opens) - - # Locate and set scroll position to playing folder - for i, row in enumerate(self.rows): - if row[1] + "/" + row[0] == track.parent_folder_path: - - scroll_position = i - 5 - scroll_position = max(scroll_position, 0) - break - - max_scroll = len(self.rows) - ((window_size[0] - (gui.panelY + gui.panelBY)) // round(22 * gui.scale)) - scroll_position = min(scroll_position, max_scroll) - scroll_position = max(scroll_position, 0) - - self.scroll_positions[pl_id] = scroll_position - - gui.update_layout() - gui.update += 1 - - def get_pl_id(self): - if self.lock_pl: - return self.lock_pl - return pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - - def render(self, x, y, w, h): - - global quick_drag - - pl_id = self.get_pl_id() - - tree = self.trees.get(pl_id) - - # Generate tree data if not done yet - if tree is None: - if not self.background_processing: - self.background_processing = True - shoot_dl = threading.Thread(target=self.gen_tree, args=[pl_id]) - shoot_dl.daemon = True - shoot_dl.start() - - self.playlist_id_on = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - - opens = self.opens.get(pl_id) - if opens is None: - opens = [] - self.opens[pl_id] = opens - - scroll_position = self.scroll_positions.get(pl_id) - if scroll_position is None: - scroll_position = 0 - - area = (x, y, w, h) - fields.add(area) - ddt.rect(area, colours.side_panel_background) - ddt.text_background_colour = colours.side_panel_background - - if self.background_processing and self.rows_id != pl_id: - ddt.text( - (x + w // 2, y + (h // 7), 2), _("Loading Folder Tree..."), alpha_mod(colours.side_bar_line2, 100), - 212, max_w=w - 17 * gui.scale) - return - - # if not tree or not self.rows: - # ddt.text((x + w // 2, y + (h // 7), 2), _("Folder Tree"), alpha_mod(colours.side_bar_line2, 100), - # 212, max_w=w - 17 * gui.scale) - # return - if not tree: - ddt.text( - (x + w // 2, y + (h // 7), 2), _("Folder Tree"), alpha_mod(colours.side_bar_line2, 100), - 212, max_w=w - 17 * gui.scale) - return - - if self.rows_id != pl_id: - if not self.background_processing: - self.gen_rows(tree, opens) - self.rows_id = pl_id - max_scroll = len(self.rows) - (h // round(22 * gui.scale)) - scroll_position = min(scroll_position, max_scroll) - - else: - return - - if not self.rows: - ddt.text( - (x + w // 2, y + (h // 7), 2), _("Folder Tree"), alpha_mod(colours.side_bar_line2, 100), - 212, max_w=w - 17 * gui.scale) - return - - yy = y + round(11 * gui.scale) - xx = x + round(22 * gui.scale) - - spacing = round(21 * gui.scale) - max_scroll = len(self.rows) - (h // round(22 * gui.scale)) - - mouse_in = coll(area) - - # Mouse wheel scrolling - if mouse_in and mouse_wheel: - scroll_position += mouse_wheel * -2 - scroll_position = max(scroll_position, 0) - scroll_position = min(scroll_position, max_scroll) - - focused = is_level_zero() - - # Draw scroll bar - if mouse_in or tree_view_scroll.held: - scroll_position = tree_view_scroll.draw( - x + w - round(12 * gui.scale), y + 1, round(11 * gui.scale), h, - scroll_position, - max_scroll, r_click=right_click, jump_distance=40) - - self.scroll_positions[pl_id] = scroll_position - - # Draw folder rows - playing_track = pctl.playing_object() - max_w = w - round(45 * gui.scale) - - light_mode = test_lumi(colours.side_panel_background) < 0.5 - semilight_mode = test_lumi(colours.side_panel_background) < 0.8 - - for i, item in enumerate(self.rows): - - if i < scroll_position: - continue - - if yy > y + h - spacing: - break - - target = item[1] + "/" + item[0] - - inset = item[2] * round(10 * gui.scale) - rect = (xx + inset - round(15 * gui.scale), yy, max_w - inset + round(15 * gui.scale), spacing - 1) - fields.add(rect) - - # text_colour = [255, 255, 255, 100] - text_colour = rgb_add_hls(colours.side_panel_background, 0, 0.35, -0.15) - - box_colour = [200, 100, 50, 255] - - if semilight_mode: - text_colour = [255, 255, 255, 180] - - if light_mode: - text_colour = [0, 0, 0, 200] - - full_folder_path = item[1] + "/" + item[0] - - # Hold highlight while menu open - if (folder_tree_menu.active or folder_tree_stem_menu.active) and full_folder_path == self.menu_selected: - text_colour = [255, 255, 255, 170] - if semilight_mode: - text_colour = (255, 255, 255, 255) - if light_mode: - text_colour = [0, 0, 0, 255] - - # Hold highlight while dragging folder - if quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15): - if shift_selection: - if pctl.get_track(pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids[shift_selection[0]]).fullpath.startswith( - full_folder_path + "/") and self.dragging_name and item[0].endswith(self.dragging_name): - text_colour = (255, 255, 255, 230) - if semilight_mode: - text_colour = (255, 255, 255, 255) - if light_mode: - text_colour = [0, 0, 0, 255] - - # Set highlight colours if folder is playing - if 0 < pctl.playing_state < 3 and playing_track: - if playing_track.parent_folder_path == full_folder_path or full_folder_path + "/" in playing_track.fullpath: - text_colour = [255, 255, 255, 225] - box_colour = [140, 220, 20, 255] - if semilight_mode: - text_colour = (255, 255, 255, 255) - if light_mode: - text_colour = [0, 0, 0, 255] - - if right_click: - mouse_in = coll(rect) and is_level_zero(False) - else: - mouse_in = coll(rect) and focused and not ( - quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15)) - - if mouse_in and not tree_view_scroll.held: - - if middle_click: - stem_to_new_playlist(full_folder_path) - - elif right_click: - - if item[3]: - - for p, id in enumerate(pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids): - if msys: - if pctl.get_track(id).fullpath.startswith(target.lstrip("/")): - folder_tree_menu.activate(in_reference=id) - self.menu_selected = full_folder_path - break - elif pctl.get_track(id).fullpath.startswith(target): - folder_tree_menu.activate(in_reference=id) - self.menu_selected = full_folder_path - break - elif msys: - folder_tree_stem_menu.activate(in_reference=full_folder_path.lstrip("/")) - self.menu_selected = full_folder_path.lstrip("/") - else: - folder_tree_stem_menu.activate(in_reference=full_folder_path) - self.menu_selected = full_folder_path - - elif inp.mouse_click: - # quick_drag = True - - if not self.click_drag_source: - self.click_drag_source = item - set_drag_source() - - elif mouse_up and self.click_drag_source == item: - # Click tree level folder to open/close branch - - if target not in opens: - opens.append(target) - else: - for s in reversed(range(len(opens))): - if opens[s].startswith(target): - del opens[s] - - if item[3]: - - # Locate the first track of folder in playlist - track_id = None - for p, id in enumerate(default_playlist): - if msys: - if pctl.get_track(id).fullpath.startswith(target.lstrip("/")): - track_id = id - break - elif pctl.get_track(id).fullpath.startswith(target): - track_id = id - break - else: # Fallback to folder name if full-path not found (hack for networked items) - for p, id in enumerate(default_playlist): - if pctl.get_track(id).parent_folder_name == item[0]: - track_id = id - break - - if track_id is not None: - # Single click base folder to locate in playlist - if self.d_click_timer.get() > 0.5 or self.d_click_id != target: - pctl.show_current(select=True, index=track_id, no_switch=True, highlight=True, folder_list=False) - self.d_click_timer.set() - self.d_click_id = target - - # Double click base folder to play - else: - pctl.jump(track_id) - - # Regenerate display rows after clicking - self.gen_rows(tree, opens) - - # Highlight folder text on mouse over - if (mouse_in and not mouse_down) or item == self.click_drag_source: - text_colour = (255, 255, 255, 235) - if semilight_mode: - text_colour = (255, 255, 255, 255) - if light_mode: - text_colour = [0, 0, 0, 255] - - # Render folder name text - if item[4] > 50: - font = 514 - text_label_colour = text_colour # self.bold_colours.get(full_folder_path) - else: - font = 414 - text_label_colour = text_colour - - if mouse_in: - tw = ddt.get_text_w(item[0], font) - - if self.tooltip_on != item: - self.tooltip_on = item - self.tooltip_timer.set() - gui.frame_callback_list.append(TestTimer(0.6)) - - if tw > max_w - inset and self.tooltip_on == item and self.tooltip_timer.get() >= 0.6: - rect = (xx + inset, yy - 2 * gui.scale, tw + round(20 * gui.scale), 20 * gui.scale) - ddt.rect(rect, ddt.text_background_colour) - ddt.text((xx + inset, yy), item[0], text_label_colour, font) - else: - ddt.text((xx + inset, yy), item[0], text_label_colour, font, max_w=max_w - inset) - else: - ddt.text((xx + inset, yy), item[0], text_label_colour, font, max_w=max_w - inset) - - # # Draw inset bars - # for m in range(item[2] + 1): - # if m == 0: - # continue - # colour = (255, 255, 255, 20) - # if semilight_mode: - # colour = (255, 255, 255, 30) - # if light_mode: - # colour = (0, 0, 0, 60) - # - # if i > 0 and self.rows[i - 1][2] == m - 1: # the top one needs to be slightly lower lower - # ddt.rect((x + (12 * m) + 2, yy - round(1 * gui.scale), round(1 * gui.scale), round(17 * gui.scale)), colour, True) - # else: - # ddt.rect((x + (12 * m) + 2, yy - round(5 * gui.scale), round(1 * gui.scale), round(21 * gui.scale)), colour, True) - - if prefs.folder_tree_codec_colours: - box_colour = self.folder_colour_cache.get(full_folder_path) - if box_colour is None: - box_colour = (150, 150, 150, 255) - - # Draw indicator box and +/- icons next to folder name - if item[3]: - rect = (xx + inset - round(9 * gui.scale), yy + round(7 * gui.scale), round(4 * gui.scale), - round(4 * gui.scale)) - if light_mode or semilight_mode: - border = round(1 * gui.scale) - ddt.rect((rect[0] - border, rect[1] - border, rect[2] + border * 2, rect[3] + border * 2), [0, 0, 0, 150]) - ddt.rect(rect, box_colour) - - elif True: - if not mouse_in or tree_view_scroll.held: - # text_colour = [255, 255, 255, 50] - text_colour = rgb_add_hls(colours.side_panel_background, 0, 0.2, -0.10) - if semilight_mode: - text_colour = [255, 255, 255, 70] - if light_mode: - text_colour = [0, 0, 0, 70] - if target in opens: - ddt.text((xx + inset - round(7 * gui.scale), yy + round(1 * gui.scale), 2), "-", text_colour, 19) - else: - ddt.text((xx + inset - round(7 * gui.scale), yy + round(1 * gui.scale), 2), "+", text_colour, 19) - - yy += spacing - - if self.click_drag_source and not point_proximity_test(gui.drag_source_position, mouse_position, 15) and \ - default_playlist is pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids: - quick_drag = True - global playlist_hold - playlist_hold = True - - self.dragging_name = self.click_drag_source[0] - logging.info(self.dragging_name) - - if "/" in self.dragging_name: - self.dragging_name = os.path.basename(self.dragging_name) - - shift_selection.clear() - set_drag_source() - for p, id in enumerate(pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids): - if msys: - if pctl.get_track(id).fullpath.startswith( - self.click_drag_source[1].lstrip("/") + "/" + self.click_drag_source[0] + "/"): - shift_selection.append(p) - elif pctl.get_track(id).fullpath.startswith(f"{self.click_drag_source[1]}/{self.click_drag_source[0]}/"): - shift_selection.append(p) - self.click_drag_source = None - - if self.dragging_name and not quick_drag: - self.dragging_name = "" - if not mouse_down: - self.click_drag_source = None - - def gen_row(self, tree_point, path, opens): - - for item in tree_point: - p = path + "/" + item[1] - self.count += 1 - enter_level = False - if len(tree_point) > 1 or path in self.force_opens: # Ignore levels that are only a single folder wide - - if path in opens or self.depth == 0 or path in self.force_opens: # Only show if parent stem is open, but always show the root displayed folders - - # If there is a single base folder in subfolder, combine the path and show it in upper level - if len(item[0]) == 1 and len(item[0][0][0]) == 1 and len(item[0][0][0][0][0]) == 0: - self.rows.append( - [item[1] + "/" + item[0][0][1] + "/" + item[0][0][0][0][1], path, self.depth, True, len(item[0])]) - elif len(item[0]) == 1 and len(item[0][0][0]) == 0: - self.rows.append([item[1] + "/" + item[0][0][1], path, self.depth, True, len(item[0])]) - - # Add normal base folder type - else: - self.rows.append([item[1], path, self.depth, len(item[0]) == 0, len(item[0])]) # Folder name, folder path, depth, is bottom - - # If folder is open and has only one subfolder, mark that subfolder as open - if len(item[0]) == 1 and (p in opens or p in self.force_opens): - self.force_opens.append(p + "/" + item[0][0][1]) - - self.depth += 1 - enter_level = True - - self.gen_row(item[0], p, opens) - - if enter_level: - self.depth -= 1 - - def gen_rows(self, tree, opens): - self.count = 0 - self.depth = 0 - self.rows.clear() - self.force_opens.clear() - - self.gen_row(tree, "", opens) - - gui.update_layout() - gui.update += 1 - - def gen_tree(self, pl_id): - pl_no = id_to_pl(pl_id) - if pl_no is None: - return - - playlist = pctl.multi_playlist[pl_no].playlist_ids - # Generate list of all unique folder paths - paths = [] - z = 5000 - for p in playlist: - - z += 1 - if z > 1000: - time.sleep(0.01) # Throttle thread - z = 0 - track = pctl.get_track(p) - path = track.parent_folder_path - if path not in paths: - paths.append(path) - self.folder_colour_cache[path] = format_colours.get(track.file_ext) - - # Genterate tree from folder paths - tree = [] - news = [] - for path in paths: - z += 1 - if z > 5000: - time.sleep(0.01) # Throttle thread - z = 0 - split_path = path.split("/") - on = tree - for level in split_path: - if not level: - continue - # Find if level already exists - for sub_level in on: - if sub_level[1] == level: - on = sub_level[0] - break - else: # Create new level - new = [[], level] - news.append(new) - on.append(new) - on = new[0] - - self.trees[pl_id] = tree - self.rows_id = "" - self.background_processing = False - gui.update += 1 - tauon.wake() - - -tree_view_box = TreeView() - - -def queue_pause_deco(): - if pctl.pause_queue: - return [colours.menu_text, colours.menu_background, _("Resume Queue")] - return [colours.menu_text, colours.menu_background, _("Pause Queue")] - - -# def finish_current_deco(): -# -# colour = colours.menu_text -# line = "Finish Playing Album" -# -# if pctl.playing_object() is None: -# colour = colours.menu_text_disabled -# if pctl.force_queue and pctl.force_queue[0].album_stage == 1: -# colour = colours.menu_text_disabled -# -# return [colour, colours.menu_background, line] - -class QueueBox: - - def recalc(self): - self.tab_h = 34 * gui.scale - def __init__(self): - - self.dragging = None - self.fq = [] - self.drag_start_y = 0 - self.drag_start_top = 0 - self.tab_h = 0 - self.scroll_position = 0 - self.right_click_id = None - self.d_click_ref = None - self.recalc() - - queue_menu.add(MenuItem(_("Remove This"), self.right_remove_item, show_test=self.queue_remove_show)) - queue_menu.add(MenuItem(_("Play Now"), self.play_now, show_test=self.queue_remove_show)) - queue_menu.add(MenuItem("Auto-Stop Here", self.toggle_auto_stop, self.toggle_auto_stop_deco, show_test=self.queue_remove_show)) - - queue_menu.add(MenuItem("Pause Queue", self.toggle_pause, queue_pause_deco)) - queue_menu.add(MenuItem(_("Clear Queue"), clear_queue, queue_deco, hint="Alt+Shift+Q")) - - queue_menu.add(MenuItem(_("↳ Except for This"), self.clear_queue_crop, show_test=self.except_for_this_show_test)) - - queue_menu.add(MenuItem(_("Queue to New Playlist"), self.make_as_playlist, queue_deco)) - # queue_menu.add("Finish Playing Album", finish_current, finish_current_deco) - - def except_for_this_show_test(self, _): - return self.queue_remove_show(_) and test_shift(_) - - def make_as_playlist(self): - - if pctl.force_queue: - playlist = [] - for item in pctl.force_queue: - - if item.type == 0: - playlist.append(item.track_id) - else: - - pl = id_to_pl(item.playlist_id) - if pl is None: - logging.info("Lost the target playlist") - continue - - pp = pctl.multi_playlist[pl].playlist_ids - - i = item.position # = pctl.playlist_playing_position + 1 - - parts = [] - album_parent_path = pctl.get_track(item.track_id).parent_folder_path - - while i < len(pp): - if pctl.get_track(pp[i]).parent_folder_path != album_parent_path: - break - - parts.append((pp[i], i)) - i += 1 - - for part in parts: - playlist.append(part[0]) - - pctl.multi_playlist.append( - pl_gen( - title=_("Queued Tracks"), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) - - def drop_tracks_insert(self, insert_position): - - global quick_drag - - if not shift_selection: - return - - # remove incomplete album from queue - if insert_position == 0 and pctl.force_queue and pctl.force_queue[0].album_stage == 1: - split_queue_album(pctl.force_queue[0].uuid_int) - - playlist_index = pctl.active_playlist_viewing - playlist_id = pl_to_id(pctl.active_playlist_viewing) - - main_track_position = shift_selection[0] - main_track_id = default_playlist[main_track_position] - quick_drag = False - - if len(shift_selection) > 1: - - # if shift selection contains only same folder - for position in shift_selection: - if pctl.get_track(default_playlist[position]).parent_folder_path != pctl.get_track( - main_track_id).parent_folder_path or key_ctrl_down: - break - else: - # Add as album type - pctl.force_queue.insert( - insert_position, queue_item_gen(main_track_id, main_track_position, playlist_id, 1)) - return - - if len(shift_selection) == 1: - pctl.force_queue.insert(insert_position, queue_item_gen(main_track_id, main_track_position, playlist_id)) - else: - # Add each track - for position in reversed(shift_selection): - pctl.force_queue.insert( - insert_position, queue_item_gen(default_playlist[position], position, playlist_id)) - - def clear_queue_crop(self): - - save = False - for item in pctl.force_queue: - if item.uuid_int == self.right_click_id: - save = item - break - - clear_queue() - if save: - pctl.force_queue.append(save) - - def play_now(self): - - queue_item = None - queue_index = 0 - for i, item in enumerate(pctl.force_queue): - if item.uuid_int == self.right_click_id: - queue_item = item - queue_index = i - break - else: - return - - del pctl.force_queue[queue_index] - # [trackid, position, pl_id, type, album_stage, uid_gen(), auto_stop] - - if pctl.force_queue and pctl.force_queue[0].album_stage == 1: - split_queue_album(None) - - target_track_id = queue_item.track_id - - pl = id_to_pl(queue_item.playlist_id) - if pl is not None: - pctl.active_playlist_playing = pl - - if target_track_id not in pctl.playing_playlist(): - pctl.advance() - return - - pctl.jump(target_track_id, queue_item.position) - - if queue_item.type == 1: # is album type - queue_item.album_stage = 1 # set as partway playing - pctl.force_queue.insert(0, queue_item) - - def toggle_auto_stop(self) -> None: - - for item in pctl.force_queue: - if item.uuid_int == self.right_click_id: - item.auto_stop ^= True - break - - def toggle_auto_stop_deco(self): - - enabled = False - for item in pctl.force_queue: - if item.uuid_int == self.right_click_id: - if item.auto_stop: - enabled = True - break - - if enabled: - return [colours.menu_text, colours.menu_background, _("Cancel Auto-Stop")] - return [colours.menu_text, colours.menu_background, _("Auto-Stop")] - - def queue_remove_show(self, id: int) -> bool: - - if self.right_click_id is not None: - return True - return False - - def right_remove_item(self) -> None: - - if self.right_click_id is None: - show_message(_("Eh?")) - - for u in reversed(range(len(pctl.force_queue))): - if pctl.force_queue[u].uuid_int == self.right_click_id: - del pctl.force_queue[u] - gui.pl_update += 1 - break - else: - show_message(_("Looks like it's gone now anyway")) - - def toggle_pause(self) -> None: - pctl.pause_queue ^= True - - def draw_card( - self, - x: int, y: int, - w: int, h: int, - yy: int, - track: TrackClass, fqo: TauonQueueItem, - draw_back: bool = False, draw_album_indicator: bool = True, - ) -> None: - - # text_colour = [230, 230, 230, 255] - bg = colours.queue_background - - # if fq[i].type == 0: - - rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h) - - if draw_back: - ddt.rect(rect, colours.queue_card_background) - bg = colours.queue_card_background - - text_colour1 = rgb_add_hls(bg, 0, 0.28, -0.15) # [255, 255, 255, 70] - text_colour2 = [255, 255, 255, 230] - if test_lumi(bg) < 0.2: - text_colour1 = [0, 0, 0, 130] - text_colour2 = [0, 0, 0, 230] - - tauon.gall_ren.render(track, (rect[0] + 4 * gui.scale, rect[1] + 4 * gui.scale), round(28 * gui.scale)) - - ddt.rect((rect[0] + 4 * gui.scale, rect[1] + 4 * gui.scale, 26, 26), [0, 0, 0, 6]) - - line = track.album - if fqo.type == 0: - line = track.title - - if not line: - line = clean_string(track.filename) - - line2y = yy + 14 * gui.scale - - artist_line = track.artist - if fqo.type == 1 and track.album_artist: - artist_line = track.album_artist - - if fqo.type == 0 and not artist_line: - line2y -= 7 * gui.scale - - ddt.text( - (rect[0] + (40 * gui.scale), yy - 1 * gui.scale), artist_line, text_colour1, 210, - max_w=rect[2] - 60 * gui.scale, bg=bg) - - ddt.text( - (rect[0] + (40 * gui.scale), line2y), line, text_colour2, 211, - max_w=rect[2] - 60 * gui.scale, bg=bg) - - if draw_album_indicator: - if fqo.type == 1: - if fqo.album_stage == 0: - ddt.rect((rect[0] + rect[2] - 5 * gui.scale, rect[1], 5 * gui.scale, rect[3]), [220, 130, 20, 255]) - else: - ddt.rect((rect[0] + rect[2] - 5 * gui.scale, rect[1], 5 * gui.scale, rect[3]), [140, 220, 20, 255]) - - if fqo.auto_stop: - xx = rect[0] + rect[2] - 9 * gui.scale - if fqo.type == 1: - xx -= 11 * gui.scale - ddt.rect((xx, rect[1] + 5 * gui.scale, 7 * gui.scale, 7 * gui.scale), [230, 190, 0, 255]) - - def draw(self, x: int, y: int, w: int, h: int): - - yy = y - - yy += round(4 * gui.scale) - - sep_colour = alpha_blend([255, 255, 255, 11], colours.queue_background) - - if y > gui.panelY + 10 * gui.scale: # Draw fancy light mode border - gui.queue_frame_draw = y - # else: - # if not colours.lm: - # ddt.rect((x, y, w, 3 * gui.scale), colours.queue_background, True) - - yy += round(3 * gui.scale) - - box_rect = (x, yy - 6 * gui.scale, w, h) - ddt.rect(box_rect, colours.queue_background) - ddt.text_background_colour = colours.queue_background - - if coll(box_rect) and quick_drag and not pctl.force_queue: - ddt.rect(box_rect, [255, 255, 255, 2]) - ddt.text_background_colour = alpha_blend([255, 255, 255, 2], ddt.text_background_colour) - - # if y < gui.panelY * 2: - # ddt.rect((x, y - 3 * gui.scale, w, 30 * gui.scale), colours.queue_background, True) - - if h > 40 * gui.scale: - if not pctl.force_queue: - if quick_drag: - text = _("Add to Queue") - else: - text = _("Queue") - ddt.text((x + (w // 2), y + 15 * gui.scale, 2), text, alpha_mod(colours.index_text, 200), 212) - - qb_right_click = 0 - - if coll(box_rect): - # Update scroll position - self.scroll_position += mouse_wheel * -1 - self.scroll_position = max(self.scroll_position, 0) - - if right_click: - qb_right_click = 1 - - # text_colour = [255, 255, 255, 91] - text_colour = rgb_add_hls(colours.queue_background, 0, 0.3, -0.15) - if test_lumi(colours.queue_background) < 0.2: - text_colour = [0, 0, 0, 200] - - line = _("Up Next:") - if pctl.force_queue: - # line = "Queue" - ddt.text((x + (10 * gui.scale), yy + 2 * gui.scale), line, text_colour, 211) - - yy += 7 * gui.scale - - if len(pctl.force_queue) < 3: - self.scroll_position = 0 - - # Draw square dots to indicate view has been scrolled down - if self.scroll_position > 0: - ds = 3 * gui.scale - gp = 4 * gui.scale - - ddt.rect((x + int(w / 2), yy, ds, ds), [230, 190, 0, 255]) - ddt.rect((x + int(w / 2), yy + gp, ds, ds), [230, 190, 0, 255]) - ddt.rect((x + int(w / 2), yy + gp + gp, ds, ds), [230, 190, 0, 255]) - - # Draw pause icon - if pctl.pause_queue: - ddt.rect((x + w - 24 * gui.scale, yy + 2 * gui.scale, 3 * gui.scale, 9 * gui.scale), [230, 190, 0, 255]) - ddt.rect((x + w - 19 * gui.scale, yy + 2 * gui.scale, 3 * gui.scale, 9 * gui.scale), [230, 190, 0, 255]) - - yy += 6 * gui.scale - - yy += 10 * gui.scale - - i = 0 - - # Get new copy of queue if not dragging - if not self.dragging: - self.fq = copy.deepcopy(pctl.force_queue) - else: - # gui.update += 1 - gui.update_on_drag = True - - # End drag if mouse not in correct state for it - if not mouse_down and not mouse_up: - self.dragging = None - - if not queue_menu.active: - self.right_click_id = None - - fq = self.fq - - list_top = yy - - i = self.scroll_position - - # Limit scroll distance - if i > len(fq): - self.scroll_position = len(fq) - i = self.scroll_position - - showed_indicator = False - list_extends = False - x1 = x + 13 * gui.scale # highlight position - w1 = w - 28 * gui.scale - 10 * gui.scale - - while i < len(fq) + 1: - - # Stop drawing if past window - if yy > window_size[1] - gui.panelBY - gui.panelY - (50 * gui.scale): - list_extends = True - break - - # Calculate drag collision box. Special case for first and last which extend out in y direction - h_rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h + 3 * gui.scale) - if i == len(fq): - h_rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h + 3 * gui.scale + 1000 * gui.scale) - if i == 0: - h_rect = ( - 0, yy - 1000 * gui.scale, w - 28 * gui.scale + 10000, self.tab_h + 3 * gui.scale + 1000 * gui.scale) - - if self.dragging is not None and coll(h_rect) and mouse_up: - - ob = None - for u in reversed(range(len(pctl.force_queue))): - - if pctl.force_queue[u].uuid_int == self.dragging: - ob = pctl.force_queue[u] - pctl.force_queue[u] = None - break - - else: - self.dragging = None - - if self.dragging: - pctl.force_queue.insert(i, ob) - self.dragging = None - - for u in reversed(range(len(pctl.force_queue))): - if pctl.force_queue[u] is None: - del pctl.force_queue[u] - gui.pl_update += 1 - continue - - # Reset album in flag if not first item - if pctl.force_queue[u].album_stage == 1: - if u != 0: - pctl.force_queue[u].album_stage = 0 - - inp.mouse_click = False - self.draw(x, y, w, h) - return - - if i > len(fq) - 1: - break - - track = pctl.get_track(fq[i].track_id) - - rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h) - - if inp.mouse_click and coll(rect): - - self.dragging = fq[i].uuid_int - self.drag_start_y = mouse_position[1] - self.drag_start_top = yy - - if d_click_timer.get() < 1: - - if self.d_click_ref == fq[i].uuid_int: - - pl = id_to_pl(fq[i].uuid_int) - if pl is not None: - switch_playlist(pl) - - pctl.show_current(playing=False, highlight=True, index=fq[i].track_id) - self.d_click_ref = None - # else: - self.d_click_ref = fq[i].uuid_int - - d_click_timer.set() - - if self.dragging and coll(h_rect): - yy += self.tab_h - yy += 4 * gui.scale - - if qb_right_click and coll(rect): - self.right_click_id = fq[i].uuid_int - qb_right_click = 2 - - if middle_click and coll(rect): - pctl.force_queue.remove(fq[i]) - gui.pl_update += 1 - - if fq[i].uuid_int == self.dragging: - # ddt.rect_r(rect, [22, 22, 22, 255], True) - pass - else: - - db = False - if fq[i].uuid_int == self.right_click_id: - db = True - - self.draw_card(x, y, w, h, yy, track, fq[i], db) - - # Drag tracks from main playlist and insert ------------ - if quick_drag: - - if x < mouse_position[0] < x + w: - - y1 = yy - 4 * gui.scale - y2 = y1 - h1 = self.tab_h // 2 - if i == 0: - # Extend up if first element - y1 -= 5 * gui.scale - h1 += 10 * gui.scale - - insert_position = None - - if y1 < mouse_position[1] < y1 + h1: - ddt.rect((x1, yy - 2 * gui.scale, w1, 2 * gui.scale), colours.queue_drag_indicator_colour) - showed_indicator = True - - if mouse_up: - insert_position = i - - elif y2 < mouse_position[1] < y2 + self.tab_h + 5 * gui.scale: - ddt.rect( - (x1, yy + self.tab_h + 2 * gui.scale, w1, 2 * gui.scale), - colours.queue_drag_indicator_colour) - showed_indicator = True - - if mouse_up: - insert_position = i + 1 - - if insert_position is not None: - self.drop_tracks_insert(insert_position) - - # ----------------------------------------- - yy += self.tab_h - yy += 4 * gui.scale - - i += 1 - - # Show drag marker if mouse holding below list - if quick_drag and not list_extends and not showed_indicator and fq and mouse_position[ - 1] > yy - 4 * gui.scale and coll(box_rect): - yy -= self.tab_h - yy -= 4 * gui.scale - ddt.rect((x1, yy + self.tab_h + 2 * gui.scale, w1, 2 * gui.scale), colours.queue_drag_indicator_colour) - yy += self.tab_h - yy += 4 * gui.scale - - yy += 15 * gui.scale - if fq: - ddt.rect((x, yy, w, 3 * gui.scale), sep_colour) - yy += 11 * gui.scale - - # Calculate total queue duration - duration = 0 - tracks = 0 - - for item in fq: - if item.type == 0: - duration += pctl.get_track(item.track_id).length - tracks += 1 - else: - pl = id_to_pl(item.playlist_id) - if pl is not None: - playlist = pctl.multi_playlist[pl].playlist_ids - i = item.position - - album_parent_path = pctl.get_track(item.track_id).parent_folder_path - - playing_track = pctl.playing_object() - - if pl == pctl.active_playlist_playing \ - and item.album_stage \ - and playing_track and playing_track.parent_folder_path == album_parent_path: - i = pctl.playlist_playing_position + 1 - - if item.track_id not in playlist: - continue - if i > len(playlist) - 1: - continue - if playlist[i] != item.track_id: - i = playlist.index(item.track_id) - - while i < len(playlist): - if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: - break - - duration += pctl.get_track(playlist[i]).length - tracks += 1 - i += 1 - - # Show total duration text "n Tracks [0:00:00]" - if tracks and fq: - if tracks < 2: - line = _("{N} Track").format(N=str(tracks)) + " [" + get_hms_time(duration) + "]" - ddt.text((x + 12 * gui.scale, yy), line, text_colour, 11.5, bg=colours.queue_background) - else: - line = _("{N} Tracks").format(N=str(tracks)) + " [" + get_hms_time(duration) + "]" - ddt.text((x + 12 * gui.scale, yy), line, text_colour, 11.5, bg=colours.queue_background) - - - - if self.dragging: - - fqo = None - for item in fq: - if item.uuid_int == self.dragging: - fqo = item - break - else: - self.dragging = False - - if self.dragging: - yyy = self.drag_start_top + (mouse_position[1] - self.drag_start_y) - yyy = max(yyy, list_top) - track = pctl.get_track(fqo.track_id) - self.draw_card(x, y, w, h, yyy, track, fqo, draw_back=True) - - # Drag and drop tracks from main playlist into queue - if quick_drag and mouse_up and coll(box_rect) and shift_selection: - self.drop_tracks_insert(len(fq)) - - # Right click context menu in blank space - if qb_right_click: - if qb_right_click == 1: - self.right_click_id = None - queue_menu.activate(position=mouse_position) - - -queue_box = QueueBox() - - -def art_metadata_overlay(right, bottom, showc): - if not showc: - return - - padding = 6 * gui.scale - - if not key_shift_down: - - line = "" - if showc[0] == 1: - line += "E " - elif showc[0] == 2: - line += "N " - else: - line += "F " - - line += str(showc[2] + 1) + "/" + str(showc[1]) - - y = bottom - 40 * gui.scale - - tag_width = ddt.get_text_w(line, 12) + 12 * gui.scale - ddt.rect_a((right - (tag_width + padding), y), (tag_width, 18 * gui.scale), [8, 8, 8, 255]) - ddt.text(((right) - (6 * gui.scale + padding), y, 1), line, [200, 200, 200, 255], 12, bg=[30, 30, 30, 255]) - - else: # Extended metadata - - line = "" - if showc[0] == 1: - line += "Embedded" - elif showc[0] == 2: - line += "Network" - else: - line += "File" - - y = bottom - 76 * gui.scale - - tag_width = ddt.get_text_w(line, 12) + 12 * gui.scale - ddt.rect_a((right - (tag_width + padding), y), (tag_width, 18 * gui.scale), [8, 8, 8, 255]) - ddt.text(((right) - (6 * gui.scale + padding), y, 1), line, [200, 200, 200, 255], 12, bg=[30, 30, 30, 255]) - - y += 18 * gui.scale - - line = "" - line += showc[4] - line += " " + str(showc[3][0]) + "×" + str(showc[3][1]) - - tag_width = ddt.get_text_w(line, 12) + 12 * gui.scale - ddt.rect_a((right - (tag_width + padding), y), (tag_width, 18 * gui.scale), [8, 8, 8, 255]) - ddt.text(((right) - (6 * gui.scale + padding), y, 1), line, [200, 200, 200, 255], 12, bg=[30, 30, 30, 255]) - - y += 18 * gui.scale - - line = "" - line += str(showc[2] + 1) + "/" + str(showc[1]) - - tag_width = ddt.get_text_w(line, 12) + 12 * gui.scale - ddt.rect_a((right - (tag_width + padding), y), (tag_width, 18 * gui.scale), [8, 8, 8, 255]) - ddt.text(((right) - (6 * gui.scale + padding), y, 1), line, [200, 200, 200, 255], 12, bg=[30, 30, 30, 255]) - - -class MetaBox: - - def l_panel(self, x, y, w, h, track, top_border=True): - - if not track: - return - - border_colour = [255, 255, 255, 30] - line1_colour = [255, 255, 255, 235] - line2_colour = [255, 255, 255, 200] - if test_lumi(colours.gallery_background) < 0.55: - border_colour = [0, 0, 0, 30] - line1_colour = [0, 0, 0, 200] - line2_colour = [0, 0, 0, 230] - - rect = (x, y, w, h) - - ddt.rect(rect, colours.gallery_background) - if top_border: - ddt.rect((x, y, w, round(1 * gui.scale)), border_colour) - else: - ddt.rect((x, y + h - round(1 * gui.scale), w, round(1 * gui.scale)), border_colour) - - ddt.text_background_colour = colours.gallery_background - - insert = round(9 * gui.scale) - border = round(2 * gui.scale) - - compact_mode = False - if w < h * 1.9: - compact_mode = True - - art_rect = [x + insert - 2 * gui.scale, y + insert, h - insert * 2 + 1 * gui.scale, - h - insert * 2 + 1 * gui.scale] - - if compact_mode: - art_rect[0] = x + round(w / 2 - art_rect[2] / 2) - round(1 * gui.scale) # - border - - border_rect = ( - art_rect[0] - border, art_rect[1] - border, art_rect[2] + (border * 2), art_rect[3] + (border * 2)) - - if (inp.mouse_click or right_click) and is_level_zero(False): - if coll(border_rect): - if inp.mouse_click: - album_art_gen.cycle_offset(target_track) - if right_click: - picture_menu.activate(in_reference=target_track) - elif coll(rect): - if inp.mouse_click: - pctl.show_current() - if right_click: - showcase_menu.activate(track) - - ddt.rect(border_rect, border_colour) - ddt.rect(art_rect, colours.gallery_background) - album_art_gen.display(track, (art_rect[0], art_rect[1]), (art_rect[2], art_rect[3])) - - fields.add(border_rect) - if coll(border_rect) and is_level_zero(True): - showc = album_art_gen.get_info(target_track) - art_metadata_overlay( - art_rect[0] + art_rect[2] + 2 * gui.scale, art_rect[1] + art_rect[3] + 12 * gui.scale, showc) - - if not compact_mode: - text_x = border_rect[0] + border_rect[2] + round(10 * gui.scale) - max_w = w - (border_rect[2] + 28 * gui.scale) - yy = y + round(15 * gui.scale) - - ddt.text((text_x, yy), track.title, line1_colour, 316, max_w=max_w) - yy += round(20 * gui.scale) - ddt.text((text_x, yy), track.artist, line2_colour, 14, max_w=max_w) - yy += round(30 * gui.scale) - ddt.text((text_x, yy), track.album, line2_colour, 14, max_w=max_w) - yy += round(20 * gui.scale) - ddt.text((text_x, yy), track.date, line2_colour, 14, max_w=max_w) - - gui.showed_title = True - - def lyrics(self, x, y, w, h, track: TrackClass): - - ddt.rect((x, y, w, h), colours.side_panel_background) - ddt.text_background_colour = colours.side_panel_background - - if not track: - return - - # Test for show lyric menu on right ckick - if coll((x + 10, y, w - 10, h)): - if right_click: # and 3 > pctl.playing_state > 0: - gui.force_showcase_index = -1 - showcase_menu.activate(track) - - # Test for scroll wheel input - if mouse_wheel != 0 and coll((x + 10, y, w - 10, h)): - lyrics_ren_mini.lyrics_position += mouse_wheel * 30 * gui.scale - if lyrics_ren_mini.lyrics_position > 0: - lyrics_ren_mini.lyrics_position = 0 - lyric_side_top_pulse.pulse() - - gui.update += 1 - - tw, th = ddt.get_text_wh(track.lyrics + "\n", 15, w - 50 * gui.scale, True) - - oth = th - - th -= h - th += 25 * gui.scale # Empty space buffer at end - - if lyrics_ren_mini.lyrics_position * -1 > th: - lyrics_ren_mini.lyrics_position = th * -1 - if oth > h: - lyric_side_bottom_pulse.pulse() - - scroll_w = 15 * gui.scale - if gui.maximized: - scroll_w = 17 * gui.scale - - lyrics_ren_mini.lyrics_position = mini_lyrics_scroll.draw( - x + w - 17 * gui.scale, y, scroll_w, h, - lyrics_ren_mini.lyrics_position * -1, th, - jump_distance=160 * gui.scale) * -1 - - margin = 10 * gui.scale - if colours.lm: - margin += 1 * gui.scale - - lyrics_ren_mini.render( - pctl.track_queue[pctl.queue_step], x + margin, - y + lyrics_ren_mini.lyrics_position + 13 * gui.scale, - w - 50 * gui.scale, - None, 0) - - ddt.rect((x, y + h - 1, w, 1), colours.side_panel_background) - - lyric_side_top_pulse.render(x, y, w - round(17 * gui.scale), 16 * gui.scale) - lyric_side_bottom_pulse.render(x, y + h, w - round(17 * gui.scale), 15 * gui.scale, bottom=True) - - def draw(self, x, y, w, h, track=None): - - ddt.rect((x, y, w, h), colours.side_panel_background) - - if not track: - return - - # Test for show lyric menu on right ckick - if coll((x + 10, y, w - 10, h)): - if right_click: # and 3 > pctl.playing_state > 0: - gui.force_showcase_index = -1 - showcase_menu.activate(track) - - if pctl.playing_state == 0: - if not prefs.meta_persists_stop and not prefs.meta_shows_selected and not prefs.meta_shows_selected_always: - return - - if h < 15: - return - - # Check for lyrics if auto setting - test_auto_lyrics(track) - - # # Draw lyrics if avaliable - # if prefs.show_lyrics_side and pctl.track_queue \ - # and track.lyrics != "" and h > 45 * gui.scale and w > 200 * gui.scale: - # - # self.lyrics(x, y, w, h, track) - - # Draw standard metadata - if len(pctl.track_queue) > 0: - - if pctl.playing_state == 0: - if not prefs.meta_persists_stop and not prefs.meta_shows_selected and not prefs.meta_shows_selected_always: - return - - ddt.text_background_colour = colours.side_panel_background - - if coll((x + 10, y, w - 10, h)): - # Click area to jump to current track - if inp.mouse_click: - pctl.show_current() - gui.update += 1 - - title = "" - album = "" - artist = "" - ext = "" - date = "" - genre = "" - - margin = x + 10 * gui.scale - if colours.lm: - margin += 2 * gui.scale - - text_width = w - 25 * gui.scale - tr = None - - # if pctl.playing_state < 3: - - if pctl.playing_state == 0 and prefs.meta_persists_stop: - tr = pctl.master_library[pctl.track_queue[pctl.queue_step]] - if pctl.playing_state == 0 and prefs.meta_shows_selected: - - if -1 < pctl.selected_in_playlist < len(pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids): - tr = pctl.get_track(pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids[pctl.selected_in_playlist]) - - if prefs.meta_shows_selected_always and pctl.playing_state != 3: - if -1 < pctl.selected_in_playlist < len(pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids): - tr = pctl.get_track(pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids[pctl.selected_in_playlist]) - - if tr is None: - tr = pctl.playing_object() - if tr is None: - return - - title = tr.title - album = tr.album - artist = tr.artist - ext = tr.file_ext - if ext == "JELY": - ext = "Jellyfin" - if "container" in tr.misc: - ext = tr.misc.get("container", "") + " | Jellyfin" - if tr.lyrics: - ext += "," - date = tr.date - genre = tr.genre - - if not title and not artist: - title = pctl.tag_meta - - if h > 58 * gui.scale: - - block_y = y + 7 * gui.scale - - if not prefs.show_side_art: - block_y += 3 * gui.scale - - if title != "": - ddt.text( - (margin, block_y + 2 * gui.scale), title, colours.side_bar_line1, fonts.side_panel_line1, - max_w=text_width) - if artist != "": - ddt.text( - (margin, block_y + 23 * gui.scale), artist, colours.side_bar_line2, fonts.side_panel_line2, - max_w=text_width) - - gui.showed_title = True - - if h > 140 * gui.scale: - - block_y = y + 80 * gui.scale - if artist != "": - ddt.text( - (margin, block_y), album, colours.side_bar_line2, - fonts.side_panel_line2, max_w=text_width) - - if not genre == date == "": - line = date - if genre != "": - if line != "": - line += " | " - line += genre - - ddt.text( - (margin, block_y + 20 * gui.scale), line, colours.side_bar_line2, - fonts.side_panel_line2, max_w=text_width) - - if ext != "": - if ext == "SPTY": - ext = "Spotify" - if ext == "RADIO": - ext = radiobox.playing_title - sp = ddt.text( - (margin, block_y + 40 * gui.scale), ext, colours.side_bar_line2, - fonts.side_panel_line2, max_w=text_width) - - if tr and tr.lyrics: - if draw_internel_link( - margin + sp + 6 * gui.scale, block_y + 40 * gui.scale, "Lyrics", colours.side_bar_line2, fonts.side_panel_line2): - prefs.show_lyrics_showcase = True - enter_showcase_view(track_id=tr.index) - - -meta_box = MetaBox() - - -class PictureRender: - - def __init__(self): - self.show = False - self.path = "" - - self.image_data = None - self.texture = None - self.sdl_rect = None - self.size = (0, 0) - - def load(self, path, box_size=None): - - if not os.path.isfile(path): - logging.warning("NO PICTURE FILE TO LOAD") - return - - g = io.BytesIO() - g.seek(0) - - im = Image.open(path) - if box_size is not None: - im.thumbnail(box_size, Image.Resampling.LANCZOS) - - im.save(g, "BMP") - g.seek(0) - self.image_data = g - logging.info("Save BMP to memory") - self.size = im.size[0], im.size[1] - - def draw(self, x, y): - - if self.show is False: - return - - if self.image_data is not None: - if self.texture is not None: - SDL_DestroyTexture(self.texture) - - # Convert raw image to sdl texture - #logging.info("Create Texture") - wop = rw_from_object(self.image_data) - s_image = IMG_Load_RW(wop, 0) - self.texture = SDL_CreateTextureFromSurface(renderer, s_image) - SDL_FreeSurface(s_image) - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) - SDL_QueryTexture(self.texture, None, None, tex_w, tex_h) - self.sdl_rect = SDL_Rect(round(x), round(y)) - self.sdl_rect.w = int(tex_w.contents.value) - self.sdl_rect.h = int(tex_h.contents.value) - self.image_data = None - - if self.texture is not None: - self.sdl_rect.x = round(x) - self.sdl_rect.y = round(y) - SDL_RenderCopy(renderer, self.texture, None, self.sdl_rect) - style_overlay.hole_punches.append(self.sdl_rect) - - -artist_picture_render = PictureRender() -artist_preview_render = PictureRender() - - -class ArtistInfoBox: - - def __init__(self): - self.artist_on = None - self.min_rq_timer = Timer() - self.min_rq_timer.force_set(10) - - self.text = "" - - self.status = "" - - self.scroll_y = 0 - - self.process_text_artist = "" - self.processed_text = "" - self.th = 0 - self.w = 0 - self.lock = False - - self.mini_box = asset_loader(scaled_asset_directory, loaded_asset_dc, "mini-box.png", True) - - def manual_dl(self): - - track = pctl.playing_object() - if track is None or not track.artist: - show_message(_("No artist name found"), mode="warning") - return - - # Check if the artist has changed - self.artist_on = track.artist - - if not self.lock and self.artist_on: - self.lock = True - # self.min_rq_timer.set() - - self.scroll_y = 0 - self.status = _("Looking up...") - self.process_text_artist = "" - - shoot_dl = threading.Thread(target=self.get_data, args=([self.artist_on, False, True])) - shoot_dl.daemon = True - shoot_dl.start() - - def draw(self, x, y, w, h): - - if gui.artist_panel_height > 300 and w < 500 * gui.scale: - bio_set_small() - - if w < 300 * gui.scale: - gui.artist_info_panel = False - gui.update_layout() - return - - track = pctl.playing_object() - if track is None: - return - - # Check if the artist has changed - artist = track.artist - wait = False - - # Activate menu - if right_click and coll((x, y, w, h)): - artist_info_menu.activate(in_reference=artist) - - background = colours.artist_bio_background - text_colour = colours.artist_bio_text - ddt.rect((x + 10, y + 5, w - 15, h - 5), background) - - if artist != self.artist_on: - - if artist == "": - return - - if self.min_rq_timer.get() < 10: # Limit rate - if os.path.isfile(os.path.join(a_cache_dir, artist + "-lfm.txt")): - pass - else: - self.status = _("Cooldown...") - wait = True - - if pctl.playing_time < 2: - if os.path.isfile(os.path.join(a_cache_dir, artist + "-lfm.txt")): - pass - else: - self.status = "..." - wait = True - - if not wait and not self.lock: - self.lock = True - # self.min_rq_timer.set() - - self.scroll_y = 0 - self.status = _("Loading...") - - shoot_dl = threading.Thread(target=self.get_data, args=([artist])) - shoot_dl.daemon = True - shoot_dl.start() - - if self.process_text_artist != self.artist_on: - self.process_text_artist = self.artist_on - - text = self.text - lic = "" - link = "" - - if "<a" in text: - text, ex = text.split('<a href="', 1) - - link, ex = ex.split('">', 1) - - lic = ex.split("</a>. ", 1)[1] - - text += "\n" - - self.urls = [(link, [200, 60, 60, 255], "L")] - for word in text.replace("\n", " ").split(" "): - if word.strip()[:4] == "http" or word.strip()[:4] == "www.": - word = word.rstrip(".") - if word.strip()[:4] == "www.": - word = "http://" + word - if "bandcamp" in word: - self.urls.append((word.strip(), [200, 150, 70, 255], "B")) - elif "soundcloud" in word: - self.urls.append((word.strip(), [220, 220, 70, 255], "S")) - elif "twitter" in word: - self.urls.append((word.strip(), [80, 110, 230, 255], "T")) - elif "facebook" in word: - self.urls.append((word.strip(), [60, 60, 230, 255], "F")) - elif "youtube" in word: - self.urls.append((word.strip(), [210, 50, 50, 255], "Y")) - else: - self.urls.append((word.strip(), [120, 200, 60, 255], "W")) - - self.processed_text = text - self.w = -1 # trigger text recalc - - if self.status == "Ready": - - # if self.w != w: - # tw, th = ddt.get_text_wh(self.processed_text, 14.5, w - 250 * gui.scale, True) - # self.th = th - # self.w = w - p_off = round(5 * gui.scale) - if artist_picture_render.show and artist_picture_render.sdl_rect: - p_off += artist_picture_render.sdl_rect.w + round(12 * gui.scale) - - text_max_w = w - (round(55 * gui.scale) + p_off) - - if self.w != w: - tw, th = ddt.get_text_wh(self.processed_text, 14.5, text_max_w - (text_max_w % 20), True) - self.th = th - self.w = w - - scroll_max = self.th - (h - 26) - - if coll((x, y, w, h)): - self.scroll_y += mouse_wheel * -20 - self.scroll_y = max(self.scroll_y, 0) - self.scroll_y = min(self.scroll_y, scroll_max) - - right = x + w - 25 * gui.scale - - if self.th > h - 26: - self.scroll_y = artist_info_scroll.draw( - x + w - 20, y + 5, 15, h - 5, - self.scroll_y, scroll_max, True, jump_distance=250 * gui.scale) - right -= 15 - # text_max_w -= 15 - - artist_picture_render.draw(x + 20 * gui.scale, y + 10 * gui.scale) - width = text_max_w - (text_max_w % 20) - if width > 20 * gui.scale: - ddt.text( - (x + p_off + round(15 * gui.scale), y + 14 * gui.scale, 4, width, 14000), self.processed_text, - text_colour, 14.5, bg=background, range_height=h - 22 * gui.scale, range_top=self.scroll_y) - - yy = y + 12 - for item in self.urls: - - rect = (right - 2, yy - 2, 16, 16) - - fields.add(rect) - self.mini_box.render(right, yy, alpha_mod(item[1], 100)) - if coll(rect): - if not inp.mouse_click: - gui.cursor_want = 3 - if inp.mouse_click: - webbrowser.open(item[0], new=2, autoraise=True) - gui.pl_update += 1 - w = ddt.get_text_w(item[0], 13) - xx = (right - w) - 17 * gui.scale - ddt.rect( - (xx - 10 * gui.scale, yy - 4 * gui.scale, w + 20 * gui.scale, 24 * gui.scale), - [15, 15, 15, 255]) - ddt.rect( - (xx - 10 * gui.scale, yy - 4 * gui.scale, w + 20 * gui.scale, 24 * gui.scale), - [50, 50, 50, 255]) - - ddt.text((xx, yy), item[0], [250, 250, 250, 255], 13, bg=[15, 15, 15, 255]) - self.mini_box.render(right, yy, (item[1][0] + 20, item[1][1] + 20, item[1][2] + 20, 255)) - # ddt.rect_r(rect, [210, 80, 80, 255], True) - - yy += 19 * gui.scale - - else: - ddt.text((x + w // 2, y + h // 2 - 7 * gui.scale, 2), self.status, [255, 255, 255, 60], 313, bg=background) - - def get_data(self, artist: str, get_img_path: bool = False, force_dl: bool = False) -> str | None: - - if not get_img_path: - logging.info("Load Bio Data") - - if artist is None and not get_img_path: - self.artist_on = artist - self.lock = False - return "" - - f_artist = filename_safe(artist) - - img_filename = f_artist + "-ftv-full.jpg" - text_filename = f_artist + "-lfm.txt" - img_filepath_dcg = os.path.join(a_cache_dir, f_artist + "-dcg.jpg") - img_filepath = os.path.join(a_cache_dir, img_filename) - text_filepath = os.path.join(a_cache_dir, text_filename) - - standard_path = os.path.join(a_cache_dir, f_artist + "-lfm.webp") - image_paths = [ - str(user_directory / "artist-pictures" / (f_artist + ".png")), - str(user_directory / "artist-pictures" / (f_artist + ".jpg")), - str(user_directory / "artist-pictures" / (f_artist + ".webp")), - os.path.join(a_cache_dir, f_artist + "-ftv-full.jpg"), - os.path.join(a_cache_dir, f_artist + "-lfm.png"), - os.path.join(a_cache_dir, f_artist + "-lfm.jpg"), - os.path.join(a_cache_dir, f_artist + "-lfm.webp"), - os.path.join(a_cache_dir, f_artist + "-dcg.jpg"), - ] - - if get_img_path: - for path in image_paths: - if os.path.isfile(path): - return path - return "" - - # Check for cache - box_size = ( - round(gui.artist_panel_height - 20 * gui.scale) * 2, round(gui.artist_panel_height - 20 * gui.scale)) - try: - - if os.path.isfile(text_filepath): - logging.info("Load cached bio and image") - - artist_picture_render.show = False - - for path in image_paths: - if os.path.isfile(path): - filepath = path - artist_picture_render.load(filepath, box_size) - artist_picture_render.show = True - break - - with open(text_filepath, encoding="utf-8") as f: - self.text = f.read() - self.status = "Ready" - gui.update = 2 - self.artist_on = artist - self.lock = False - - return "" - - if not force_dl and not prefs.auto_dl_artist_data: - # . Alt: No artist data has been downloaded (try imply this needs to be manually triggered) - self.status = _("No artist data downloaded") - self.artist_on = artist - artist_picture_render.show = False - self.lock = False - return None - - # Get new from last.fm - # . Alt: Looking up artist data - self.status = _("Looking up...") - gui.update += 1 - data = lastfm.artist_info(artist) - self.text = "" - if data[0] is False: - artist_picture_render.show = False - self.status = _("No artist bio found") - self.artist_on = artist - self.lock = False - return None - if data[1]: - self.text = data[1] - # cover_link = data[2] - # Save text as file - f = open(text_filepath, "w", encoding="utf-8") - f.write(self.text) - f.close() - logging.info("Save bio text") - - artist_picture_render.show = False - if data[3] and prefs.enable_fanart_artist: - try: - save_fanart_artist_thumb(data[3], img_filepath) - artist_picture_render.load(img_filepath, box_size) - - artist_picture_render.show = True - except Exception: - logging.exception("Failed to find image from fanart.tv") - if not artist_picture_render.show: - if verify_discogs(): - try: - save_discogs_artist_thumb(artist, img_filepath_dcg) - artist_picture_render.load(img_filepath_dcg, box_size) - - artist_picture_render.show = True - except Exception: - logging.exception("Failed to find image from discogs") - if not artist_picture_render.show and data[4]: - try: - r = requests.get(data[4], timeout=10) - html = BeautifulSoup(r.text, "html.parser") - tag = html.find("meta", property="og:image") - url = tag["content"] - if url: - r = requests.get(url, timeout=10) - assert len(r.content) > 1000 - with open(standard_path, "wb") as f: - f.write(r.content) - artist_picture_render.load(standard_path, box_size) - artist_picture_render.show = True - except Exception: - logging.exception("Failed to scrape art") - - # Trigger reload of thumbnail in artist list box - for key, value in list(artist_list_box.thumb_cache.items()): - if key is None and key == artist: - del artist_list_box.thumb_cache[artist] - break - - self.status = "Ready" - gui.update = 2 - - # if cover_link and 'http' in cover_link: - # # Fetch cover_link - # try: - # #logging.info("Fetching artist image...") - # response = urllib.request.urlopen(cover_link) - # info = response.info() - # #logging.info("got response") - # if info.get_content_maintype() == 'image': - # - # f = open(filepath, 'wb') - # f.write(response.read()) - # f.close() - # - # #logging.info("written file, now loading...") - # - # artist_picture_render.load(filepath, round(gui.artist_panel_height - 20 * gui.scale)) - # artist_picture_render.show = True - # - # self.status = "Ready" - # gui.update = 2 - # # except HTTPError as e: - # # self.status = e - # # logging.exception("request failed") - # except Exception: - # logging.exception("request failed") - # self.status = "Request Failed" - - - except Exception: - logging.exception("Failed to load bio") - self.status = _("Load Failed") - - self.artist_on = artist - self.processed_text = "" - self.process_text_artist = "" - self.min_rq_timer.set() - self.lock = False - gui.update = 2 - return "" - - -# artist info box def -artist_info_box = ArtistInfoBox() - - -def artist_dl_deco(): - if artist_info_box.status == "Ready": - return [colours.menu_text_disabled, colours.menu_background, None] - return [colours.menu_text, colours.menu_background, None] - - -artist_info_menu.add(MenuItem(_("Download Artist Data"), artist_info_box.manual_dl, artist_dl_deco, show_test=test_artist_dl)) -artist_info_menu.add(MenuItem(_("Clear Bio"), flush_artist_bio, pass_ref=True, show_test=test_shift)) - -class RadioThumbGen: - def __init__(self): - self.cache = {} - self.requests = [] - self.size = 100 - - def loader(self): - - while self.requests: - item = self.requests[0] - del self.requests[0] - station = item[0] - size = item[1] - key = (station["title"], size) - src = None - filename = filename_safe(station["title"]) - - cache_path = os.path.join(r_cache_dir, filename + ".jpg") - if os.path.isfile(cache_path): - src = open(cache_path, "rb") - else: - cache_path = os.path.join(r_cache_dir, filename + ".png") - if os.path.isfile(cache_path): - src = open(cache_path, "rb") - else: - cache_path = os.path.join(r_cache_dir, filename) - if os.path.isfile(cache_path): - src = open(cache_path, "rb") - - if src: - pass - #logging.info("found cached") - elif station.get("icon") and station["icon"] not in prefs.radio_thumb_bans: - try: - r = requests.get(station.get("icon"), headers={"User-Agent": t_agent}, timeout=5, stream=True) - if r.status_code != 200 or int(r.headers.get("Content-Length", 0)) > 2000000: - raise Exception("Error get radio thumb") - except Exception: - logging.exception("error get radio thumb") - self.cache[key] = [0] - if station.get("icon") and station.get("icon") not in prefs.radio_thumb_bans: - prefs.radio_thumb_bans.append(station.get("icon")) - continue - src = io.BytesIO() - length = 0 - for chunk in r.iter_content(1024): - src.write(chunk) - length += len(chunk) - if length > 2000000: - scr = None - if src is None: - self.cache[key] = [0] - if station.get("icon") and station.get("icon") not in prefs.radio_thumb_bans: - prefs.radio_thumb_bans.append(station.get("icon")) - continue - src.seek(0) - with open(cache_path, "wb") as f: - f.write(src.read()) - src.seek(0) - else: - # logging.info("no icon") - self.cache[key] = [0] - continue - - try: - im = Image.open(src) - if im.mode != "RGBA": - im = im.convert("RGBA") - except Exception: - logging.exception("malform get radio thumb") - self.cache[key] = [0] - if station.get("icon") and station.get("icon") not in prefs.radio_thumb_bans: - prefs.radio_thumb_bans.append(station.get("icon")) - continue - if src is not None: - src.close() - - im = im.resize((size, size), Image.Resampling.LANCZOS) - g = io.BytesIO() - g.seek(0) - im.save(g, "PNG") - g.seek(0) - wop = rw_from_object(g) - s_image = IMG_Load_RW(wop, 0) - self.cache[key] = [2, None, None, s_image] - gui.update += 1 - - def draw(self, station, x, y, w): - if not station.get("title"): - return 0 - key = (station["title"], w) - - r = self.cache.get(key) - if r is None: - if len(self.requests) < 3: - self.requests.append((station, w)) - tauon.thread_manager.ready("radio-thumb") - return 0 - if r[0] == 2: - texture = SDL_CreateTextureFromSurface(renderer, r[3]) - SDL_FreeSurface(r[3]) - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) - SDL_QueryTexture(texture, None, None, tex_w, tex_h) - sdl_rect = SDL_Rect(0, 0) - sdl_rect.w = int(tex_w.contents.value) - sdl_rect.h = int(tex_h.contents.value) - r[2] = texture - r[1] = sdl_rect - r[0] = 1 - if r[0] == 1: - r[1].x = round(x) - r[1].y = round(y) - SDL_RenderCopy(renderer, r[2], None, r[1]) - return 1 - return 0 - - -radio_thumb_gen = RadioThumbGen() - - -def station_browse(): - radiobox.active = True - radiobox.edit_mode = False - radiobox.add_mode = False - radiobox.center = True - radiobox.tab = 1 - - -def add_station(): - radiobox.active = True - radiobox.edit_mode = True - radiobox.add_mode = True - radiobox.radio_field.text = "" - radiobox.radio_field_title.text = "" - radiobox.station_editing = None - radiobox.center = True - - -def rename_station(item): - station = item[1] - radiobox.active = True - radiobox.center = False - radiobox.edit_mode = True - radiobox.add_mode = False - radiobox.radio_field.text = station["stream_url"] - radiobox.radio_field_title.text = station.get("title", "") - radiobox.station_editing = station - - -radio_context_menu.add(MenuItem(_("Edit..."), rename_station, pass_ref=True)) -radio_context_menu.add( - MenuItem(_("Visit Website"), visit_radio_station, visit_radio_station_site_deco, pass_ref=True, pass_ref_deco=True)) - - -def remove_station(item): - index = item[0] - del pctl.radio_playlists[pctl.radio_playlist_viewing]["items"][index] - - -radio_context_menu.add(MenuItem(_("Remove"), remove_station, pass_ref=True)) - - -class RadioView: - def __init__(self): - self.add_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "add-station.png", True) - self.search_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "station-search.png", True) - self.save_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "save-station.png", True) - self.menu_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "radio-menu.png", True) - self.drag = None - self.click_point = (0, 0) - - def render(self): - # box = int(window_size[1] * 0.4 + 120 * gui.scale) - # box = min(window_size[0] // 2, box) - bg = colours.playlist_panel_background - ddt.rect((0, gui.panelY, window_size[0], window_size[1] - gui.panelY), bg) - #logging.info(prefs.radio_urls) - - # Add station button - x = window_size[0] - round(60 * gui.scale) - y = gui.panelY + round(30 * gui.scale) - rect = (x, y, round(25 * gui.scale), round(25 * gui.scale)) - fields.add(rect) - - # right buttions colours - a_colour = rgb_add_hls(bg, l=0.2, s=-0.3) #colours.box_button_text_highlight - b_colour = rgb_add_hls(bg, l=0.4, s=-0.3) #colours.box_button_text_highlight - if test_lumi(bg) < 0.38: - a_colour = [20, 20, 20, 200] - b_colour = [60, 60, 60, 200] - - if coll(rect): - colour = b_colour - if inp.mouse_click: - add_station() - else: - colour = a_colour - - self.add_icon.render(rect[0] + round(4 * gui.scale), rect[1] + round(4 * gui.scale), colour) - - y += round(33 * gui.scale) - rect = (x, y, round(25 * gui.scale), round(25 * gui.scale)) - fields.add(rect) - - if not coll(rect): - colour = a_colour - else: - colour = b_colour - if inp.mouse_click: - station_browse() - self.search_icon.render(rect[0] + round(4 * gui.scale), rect[1] + round(4 * gui.scale), colour) - - if pctl.radio_playlist_viewing > len(pctl.radio_playlists) - 1: - pctl.radio_playlist_viewing = 0 - if not pctl.radio_playlists: - return - radios = pctl.radio_playlists[pctl.radio_playlist_viewing]["items"] - - y += round(32 * gui.scale) - if pctl.playing_state == 3 and radiobox.loaded_station not in radios: - rect = (x, y, round(25 * gui.scale), round(25 * gui.scale)) - fields.add(rect) - - if not coll(rect): - colour = a_colour - else: - colour = b_colour - if inp.mouse_click: - radios.append(radiobox.loaded_station) - toast(_("Added station to: ") + pctl.radio_playlists[pctl.radio_playlist_viewing]["name"]) - - self.save_icon.render(rect[0] + round(3 * gui.scale), rect[1] + round(4 * gui.scale), colour) - - x = round(30 * gui.scale) - y = gui.panelY + round(30 * gui.scale) - yy = y - - rbg = rgb_add_hls(colours.playlist_panel_background, 0, 0.03, -0.03) - tbg = rgb_add_hls(colours.playlist_panel_background, 0, 0.07, -0.05) - if contrast_ratio(bg, rbg) < 1.05: - rbg = [30, 30, 30, 255] - tbg = [60, 60, 60, 255] - - w = round(400 * gui.scale) - h = round(55 * gui.scale) - gap = round(7 * gui.scale) - - mm = (window_size[1] - (gui.panelBY + yy + h + round(15 * gui.scale))) // (h + gap) + 1 - - count = 0 - scroll = pctl.radio_playlists[pctl.radio_playlist_viewing].get("scroll", 0) - if not radiobox.active or (radiobox.active and not coll((radiobox.x, radiobox.y, radiobox.w, radiobox.h))): - if gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY and mouse_position[0] < w + round( - 70 * gui.scale): - scroll += mouse_wheel * -1 - scroll = min(scroll, len(radios) - mm + 1) - scroll = max(scroll, 0) - if len(radios) > mm: - scroll = radio_view_scroll.draw(round(7 * gui.scale), yy, round(15 * gui.scale), (mm * (h + gap)) - gap, - scroll, len(radios) - mm + 1) - else: - scroll = 0 - - pctl.radio_playlists[pctl.radio_playlist_viewing]["scroll"] = scroll - insert = None - - for i, radio in enumerate(radios): - if count == mm: - break - if i < scroll: - continue - count += 1 - rect = (x, yy, w, h) - ddt.rect(rect, rbg) - yyy = yy - pic_rect = ( - x + round(5 * gui.scale), yy + round(5 * gui.scale), h - round(10 * gui.scale), h - round(10 * gui.scale)) - ddt.rect(pic_rect, tbg) - radio_thumb_gen.draw(radio, pic_rect[0], pic_rect[1], pic_rect[2]) - - l1_colour = [10, 10, 10, 210] - if test_lumi(rbg) > 0.45: - l1_colour = [255, 255, 255, 220] - l2_colour = [30, 30, 30, 200] - if test_lumi(rbg) > 0.45: - l2_colour = [245, 245, 245, 200] - - toff = h + round(2 * gui.scale) - yyy += round(9 * gui.scale) - ddt.text( - (x + toff, yyy), radio["title"], l1_colour, 212, - max_w=w - (toff + round(90 * gui.scale)), bg=rbg) - yyy += round(19 * gui.scale) - ddt.text( - (x + toff, yyy), radio.get("country", ""), l2_colour, 312, - max_w=w - (toff + round(90 * gui.scale)), bg=rbg) - - hit = False - start_rect = ( - x + (w - round(40 * gui.scale)), yy + round(8 * gui.scale), h - round(15 * gui.scale), - round(42 * gui.scale)) - # ddt.rect(hit_rect, [255, 255, 255, 3]) - fields.add(start_rect) - colour = rgb_add_hls(tbg, l=0.05) - if coll(start_rect): - if inp.mouse_click: - radiobox.start(radio) - hit = True - colour = rgb_add_hls(colour, l=0.3) - - bottom_bar1.play_button.render(x + (w - round(30 * gui.scale)), yy + round(23 * gui.scale), colour) - - extra_rect = ( - x + (w - round(82 * gui.scale)), yy + round(8 * gui.scale), h - round(15 * gui.scale), - round(35 * gui.scale)) - # ddt.rect(extra_rect, [255, 255, 255, 2]) - fields.add(extra_rect) - colour = rgb_add_hls(tbg, l=0.05) - if coll(extra_rect): - colour = rgb_add_hls(colour, l=0.3) #alpha_mod(colours.side_bar_line1, 47) - if inp.mouse_click: - hit = True - radiobox.x = extra_rect[0] + extra_rect[2] - radiobox.y = extra_rect[1] - radio_context_menu.activate((i, radio), position=(radiobox.x, yy + round(20 * gui.scale))) - - self.menu_icon.render(x + (w - round(75 * gui.scale)), yy + round(26 * gui.scale), colour) - - # bottom_bar1.play_button.render(x + (w - round(30 * gui.scale)), yy + round(23 * gui.scale), colour) - if mouse_up and self.drag and coll(rect): - if radiobox.active and coll((radiobox.x, radiobox.y, radiobox.w, radiobox.h)): - pass - else: - insert = i - if not radiobox.active and self.drag in radios and radios.index(self.drag) < i: - insert += 1 - elif coll(rect) and not hit and inp.mouse_click: - self.drag = radio - self.click_point = copy.copy(mouse_position) - - yy += round(h + gap) - - if mouse_up and self.drag and not insert and self.drag not in radios: - if not (radiobox.active and coll((radiobox.x, radiobox.y, radiobox.w, radiobox.h))): - if mouse_position[1] > gui.panelY: - insert = len(radios) - - count = ((window_size[0] - w) / 2) + w - boxx = round(200 * gui.scale) - art_rect = (count - boxx / 2, window_size[1] / 3 - boxx / 2, boxx, boxx) - - if window_size[0] > round(700 * gui.scale): - if pctl.playing_state == 3 and radiobox.loaded_station: - r = album_art_gen.display(radiobox.dummy_track, (art_rect[0], art_rect[1]), (art_rect[2], art_rect[3])) - if r: - r = radio_thumb_gen.draw(radiobox.loaded_station, art_rect[0], art_rect[1], art_rect[2]) - # if not r: - # ddt.rect(art_rect, colours.b) - # else: - # ddt.rect(art_rect, [40, 40, 40, 255]) - - yy = window_size[1] / 3 - boxx / 2 - yy += boxx + round(30 * gui.scale) - - if radiobox.loaded_station and pctl.playing_state == 3: - space = window_size[0] - round(500 * gui.scale) - ddt.text( - (count, yy, 2), radiobox.loaded_station.get("title", ""), [230, 230, 230, 255], 213, max_w=space) - yy += round(25 * gui.scale) - ddt.text((count, yy, 2), radiobox.song_key, [230, 230, 230, 255], 313, max_w=space) - if radiobox.dummy_track.album: - yy += round(21 * gui.scale) - ddt.text((count, yy, 2), radiobox.dummy_track.album, [230, 230, 230, 255], 313, max_w=space) - - if self.drag: - gui.update_on_drag = True - - if insert is not None: - radios.insert(insert, "New") - if self.drag in radios: - radios.remove(self.drag) - else: - toast(_("Added station to: ") + pctl.radio_playlists[pctl.radio_playlist_viewing]["name"]) - - radios[radios.index("New")] = self.drag - self.drag = None - gui.update += 1 - - -radio_view = RadioView() - -class Showcase: - - def __init__(self): - - self.lastfm_artist = None - self.artist_mode = False - - def render(self): - - global right_click - - box = int(window_size[1] * 0.4 + 120 * gui.scale) - box = min(window_size[0] // 2, box) - - hide_art = False - if window_size[0] < 900 * gui.scale: - hide_art = True - - x = int(window_size[0] * 0.15) - y = int((window_size[1] / 2) - (box / 2)) - 10 * gui.scale - - if hide_art: - box = 45 * gui.scale - elif window_size[1] / window_size[0] > 0.7: - x = int(window_size[0] * 0.07) - - bbg = rgb_add_hls(colours.playlist_panel_background, 0, 0.05, 0) # [255, 255, 255, 18] - bfg = rgb_add_hls(colours.playlist_panel_background, 0, 0.09, 0) # [255, 255, 255, 30] - bft = colours.grey(235) - bbt = colours.grey(200) - - t1 = colours.grey(250) - - gui.vis_4_colour = None - light_mode = False - if colours.lm: - bbg = colours.vis_colour - bfg = alpha_blend([255, 255, 255, 60], colours.vis_colour) - bft = colours.grey(250) - bbt = colours.grey(245) - elif prefs.art_bg and prefs.bg_showcase_only: - bbg = [255, 255, 255, 18] - bfg = [255, 255, 255, 30] - bft = [255, 255, 255, 250] - bbt = [255, 255, 255, 200] - - if test_lumi(colours.playlist_panel_background) < 0.7: - light_mode = True - t1 = colours.grey(30) - gui.vis_4_colour = [40, 40, 40, 255] - - ddt.rect((0, gui.panelY, window_size[0], window_size[1] - gui.panelY), colours.playlist_panel_background) - - if prefs.bg_showcase_only and prefs.art_bg: - style_overlay.display() - - # Draw textured background - if not light_mode and not colours.lm and prefs.showcase_overlay_texture: - rect = SDL_Rect() - rect.x = 0 - rect.y = 0 - rect.w = 300 - rect.h = 300 - - xx = 0 - yy = 0 - while yy < window_size[1]: - xx = 0 - while xx < window_size[0]: - rect.x = xx - rect.y = yy - SDL_RenderCopy(renderer, overlay_texture_texture, None, rect) - xx += 300 - yy += 300 - - if prefs.bg_showcase_only and prefs.art_bg: - ddt.alpha_bg = True - ddt.force_gray = True - - # if not prefs.shuffle_lock: - # if draw.button(_("Return"), 25 * gui.scale, window_size[1] - gui.panelBY - 40 * gui.scale, - # text_highlight_colour=bft, text_colour=bbt, backgound_colour=bbg, - # background_highlight_colour=bfg): - # gui.switch_showcase_off = True - # gui.update += 1 - # gui.update_layout() - - # ddt.force_gray = True - - if pctl.playing_state == 3 and not radiobox.dummy_track.title: - - if not pctl.tag_meta: - y = int(window_size[1] / 2) - 60 - gui.scale - ddt.text((window_size[0] // 2, y, 2), pctl.url, colours.side_bar_line2, 317) - else: - w = window_size[0] - (x + box) - 30 * gui.scale - x = int((window_size[0]) / 2) - - y = int(window_size[1] / 2) - 60 - gui.scale - ddt.text((x, y, 2), pctl.tag_meta, colours.side_bar_line1, 216, w) - - else: - - if len(pctl.track_queue) < 1: - ddt.alpha_bg = False - return - - # if draw.button("Return", 20, gui.panelY + 5, bg=colours.grey(30)): - # pass - - if prefs.bg_showcase_only and prefs.art_bg: - ddt.alpha_bg = True - ddt.force_gray = True - - if gui.force_showcase_index >= 0: - if draw.button( - _("Playing"), 25 * gui.scale, gui.panelY + 20 * gui.scale, text_highlight_colour=bft, - text_colour=bbt, background_colour=bbg, background_highlight_colour=bfg): - gui.force_showcase_index = -1 - ddt.force_gray = False - - if gui.force_showcase_index >= 0: - index = gui.force_showcase_index - track = pctl.master_library[index] - else: - - if pctl.playing_state == 3: - track = radiobox.dummy_track - else: - index = pctl.track_queue[pctl.queue_step] - track = pctl.master_library[index] - - if not hide_art: - - # Draw frame around art box - # drop_shadow.render(x + 5 * gui.scale, y + 5 * gui.scale, box + 10 * gui.scale, box + 10 * gui.scale) - ddt.rect( - (x - round(2 * gui.scale), y - round(2 * gui.scale), box + round(4 * gui.scale), - box + round(4 * gui.scale)), [60, 60, 60, 135]) - ddt.rect((x, y, box, box), colours.playlist_panel_background) - rect = SDL_Rect(round(x), round(y), round(box), round(box)) - style_overlay.hole_punches.append(rect) - - # Draw album art in box - album_art_gen.display(track, (x, y), (box, box)) - - # Click art to cycle - if coll((x, y, box, box)): - if inp.mouse_click is True: - album_art_gen.cycle_offset(track) - if right_click: - picture_menu.activate(in_reference=track) - right_click = False - - # Check for lyrics if auto setting - test_auto_lyrics(track) - - gui.draw_vis4_top = False - - if gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY: - if mouse_wheel != 0: - lyrics_ren.lyrics_position += mouse_wheel * 35 * gui.scale - if right_click: - # track = pctl.playing_object() - if track != None: - showcase_menu.activate(track) - - gcx = x + box + int(window_size[0] * 0.15) + 10 * gui.scale - gcx -= 100 * gui.scale - - timed_ready = False - if True and prefs.show_lyrics_showcase: - timed_ready = timed_lyrics_ren.generate(track) - - if timed_ready and track.lyrics: - - # if not prefs.guitar_chords or guitar_chords.test_ready_status(track) != 1: - # - # line = _("Prefer synced") - # if prefs.prefer_synced_lyrics: - # line = _("Prefer static") - # if draw.button(line, 25 * gui.scale, window_size[1] - gui.panelBY - 70 * gui.scale, - # text_highlight_colour=bft, text_colour=bbt, background_colour=bbg, - # background_highlight_colour=bfg): - # prefs.prefer_synced_lyrics ^= True - - timed_ready = prefs.prefer_synced_lyrics - -# if prefs.guitar_chords and track.title and prefs.show_lyrics_showcase and guitar_chords.render(track, gcx, y): -# if not guitar_chords.auto_scroll: -# if draw.button( -# _("Auto-Scroll"), 25 * gui.scale, window_size[1] - gui.panelBY - 70 * gui.scale, -# text_highlight_colour=bft, text_colour=bbt, background_colour=bbg, -# background_highlight_colour=bfg): -# guitar_chords.auto_scroll = True - - if True and prefs.show_lyrics_showcase and timed_ready: - w = window_size[0] - (x + box) - round(30 * gui.scale) - timed_lyrics_ren.render(track.index, gcx, y, w=w) - - elif track.lyrics == "" or not prefs.show_lyrics_showcase: - - w = window_size[0] - (x + box) - round(30 * gui.scale) - x = int(x + box + (window_size[0] - x - box) / 2) - - if hide_art: - x = window_size[0] // 2 - - # x = int((window_size[0]) / 2) - y = int(window_size[1] / 2) - round(60 * gui.scale) - - if prefs.showcase_vis and prefs.backend == 1: - y -= round(30 * gui.scale) - - if track.artist == "" and track.title == "": - - ddt.text((x, y, 2), clean_string(track.filename), t1, 216, w) - - else: - - ddt.text((x, y, 2), track.artist, t1, 20, w) - - y += round(48 * gui.scale) - - if window_size[0] < 700 * gui.scale: - if len(track.title) < 30: - ddt.text((x, y, 2), track.title, t1, 220, w) - elif len(track.title) < 40: - ddt.text((x, y, 2), track.title, t1, 217, w) - else: - ddt.text((x, y, 2), track.title, t1, 213, w) - - elif len(track.title) < 35: - ddt.text((x, y, 2), track.title, t1, 220, w) - elif len(track.title) < 50: - ddt.text((x, y, 2), track.title, t1, 219, w) - else: - ddt.text((x, y, 2), track.title, t1, 216, w) - - gui.spec4_rec.x = x - (gui.spec4_rec.w // 2) - gui.spec4_rec.y = y + round(50 * gui.scale) - - if prefs.showcase_vis and window_size[1] > 369 and not search_over.active and not ( - tauon.spot_ctl.coasting or tauon.spot_ctl.playing): - - if gui.message_box or not is_level_zero(include_menus=True): - self.render_vis() - else: - gui.draw_vis4_top = True - - else: - x += box + int(window_size[0] * 0.15) + 10 * gui.scale - x -= 100 * gui.scale - w = window_size[0] - x - 30 * gui.scale - - if key_up_press and not (key_ctrl_down or key_shift_down or key_shiftr_down): - lyrics_ren.lyrics_position += 35 * gui.scale - if key_down_press and not (key_ctrl_down or key_shift_down or key_shiftr_down): - lyrics_ren.lyrics_position -= 35 * gui.scale - - lyrics_ren.test_update(track) - tw, th = ddt.get_text_wh(lyrics_ren.text + "\n", 17, w, True) - - lyrics_ren.lyrics_position = max(lyrics_ren.lyrics_position, th * -1 + 100 * gui.scale) - lyrics_ren.lyrics_position = min(lyrics_ren.lyrics_position, 70 * gui.scale) - - lyrics_ren.render( - x, - y + lyrics_ren.lyrics_position, - w, - int(window_size[1] - 100 * gui.scale), - 0) - ddt.alpha_bg = False - ddt.force_gray = False - - def render_vis(self, top=False): - - SDL_SetRenderTarget(renderer, gui.spec4_tex) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_RenderClear(renderer) - - bx = 0 - by = 50 * gui.scale - - if gui.vis_4_colour is not None: - SDL_SetRenderDrawColor( - renderer, gui.vis_4_colour[0], gui.vis_4_colour[1], gui.vis_4_colour[2], gui.vis_4_colour[3]) - - if (pctl.playing_time < 0.5 and (pctl.playing_state == 1 or pctl.playing_state == 3)) or ( - pctl.playing_state == 0 and gui.spec4_array.count(0) != len(gui.spec4_array)): - gui.update = 2 - gui.level_update = True - - for i in range(len(gui.spec4_array)): - gui.spec4_array[i] -= 0.1 - gui.spec4_array[i] = max(gui.spec4_array[i], 0) - - if not top and (pctl.playing_state == 1 or pctl.playing_state == 3): - gui.update = 2 - - slide = 0.7 - for i, bar in enumerate(gui.spec4_array): - - # We wont draw higher bars that may not move - if i > 40: - break - - # Scale input amplitude to pixel distance (Applying a slight exponentional) - dis = (2 + math.pow(bar / (2 + slide), 1.5)) - slide -= 0.03 # Set a slight bias for higher bars - - # Define colour for bar - if gui.vis_4_colour is None: - set_colour( - hsl_to_rgb( - 0.7 + min(0.15, (bar / 150)) + pctl.total_playtime / 300, min(0.9, 0.7 + (dis / 300)), - min(0.9, 0.7 + (dis / 600)))) - - # Define bar size and draw - gui.bar4.x = int(bx) - gui.bar4.y = round(by - dis * gui.scale) - gui.bar4.w = round(2 * gui.scale) - gui.bar4.h = round(dis * 2 * gui.scale) - - SDL_RenderFillRect(renderer, gui.bar4) - - # Set distance between bars - bx += 8 * gui.scale - - if top: - SDL_SetRenderTarget(renderer, None) - else: - SDL_SetRenderTarget(renderer, gui.main_texture) - - # SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) - SDL_RenderCopy(renderer, gui.spec4_tex, None, gui.spec4_rec) - - -showcase = Showcase() - - -# Animates colour between two colours -class ColourPulse2: - - def __init__(self): - - self.timer = Timer() - self.in_timer = Timer() - self.out_timer = Timer() - self.out_timer.start = 0 - self.active = False - - def get(self, hit, on, off, low_hls, high_hls): - - if on: - return high_hls - # rgb = colorsys.hls_to_rgb(high_hls[0], high_hls[1], high_hls[2]) - # return [int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255), 255] - if off: - return low_hls - # rgb = colorsys.hls_to_rgb(low_hls[0], low_hls[1], low_hls[2]) - # return [int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255), 70] - - ani_time = 0.15 - - if hit is True and self.active is False: - self.active = True - self.in_timer.set() - - out_time = self.out_timer.get() - if out_time < ani_time: - self.in_timer.force_set(ani_time - out_time) - - elif hit is False and self.active is True: - self.active = False - self.out_timer.set() - - in_time = self.in_timer.get() - if in_time < ani_time: - self.out_timer.force_set(ani_time - in_time) - - pro = 0.5 - if self.active: - time = self.in_timer.get() - if time <= 0: - pro = 0 - elif time >= ani_time: - pro = 1 - else: - pro = time / ani_time - gui.update = 2 - else: - time = self.out_timer.get() - if time <= 0: - pro = 1 - elif time >= ani_time: - pro = 0 - else: - pro = 1 - (time / ani_time) - gui.update = 2 - - return colour_slide(low_hls, high_hls, pro, 1) - - -cctest = ColourPulse2() - - -class ViewBox: - - def __init__(self, reload=False): - self.x = 0 - self.y = gui.panelY - self.w = 52 * gui.scale - self.h = 260 * gui.scale # 257 - self.active = False - - self.border = 3 * gui.scale - - self.tracks_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "tracks.png", True) - self.side_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "tracks+side.png", True) - self.gallery1_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "gallery1.png", True) - self.gallery2_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "gallery2.png", True) - self.combo_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "combo.png", True) - self.lyrics_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "lyrics.png", True) - self.gallery2_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "gallery2.png", True) - self.radio_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "radio.png", True) - self.col_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "col.png", True) - # self.artist_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "artist.png", True) - - # _ .15 0 - self.tracks_colour = ColourPulse2() # (0.5) # .5 .6 .75 - self.side_colour = ColourPulse2() # (0.55) # .55 .6 .75 - self.gallery1_colour = ColourPulse2() # (0.6) # .6 .6 .75 - self.radio_colour = ColourPulse2() # (0.6) # .6 .6 .75 - # self.combo_colour = ColourPulse(0.75) - self.lyrics_colour = ColourPulse2() # (0.7) - # self.gallery2_colour = ColourPulse(0.65) - self.col_colour = ColourPulse2() # (0.14) - self.artist_colour = ColourPulse2() # (0.2) - - self.on_colour = [255, 190, 50, 255] - self.over_colour = [255, 190, 50, 255] - self.off_colour = colours.grey(40) - - if not reload: - gui.combo_was_album = False - - def activate(self, x): - self.x = x - self.active = True - self.clicked = False - - self.tracks_colour.out_timer.force_set(10) - self.side_colour.out_timer.force_set(10) - self.gallery1_colour.out_timer.force_set(10) - self.radio_colour.out_timer.force_set(10) - # self.combo_colour.out_timer.force_set(10) - self.lyrics_colour.out_timer.force_set(10) - # self.gallery2_colour.out_timer.force_set(10) - self.col_colour.out_timer.force_set(10) - self.artist_colour.out_timer.force_set(10) - - self.tracks_colour.active = False - self.side_colour.active = False - self.gallery1_colour.active = False - self.radio_colour.active = False - # self.combo_colour.active = False - self.lyrics_colour.active = False - # self.gallery2_colour.active = False - self.col_colour.active = False - self.artist_colour.active = False - - self.col_force_off = False - - # gui.level_2_click = False - gui.update = 2 - - def button(self, x, y, asset, test, colour_get=None, name="Unknown", animate=True, low=0, high=0): - - on = test() - rect = [x - 8 * gui.scale, - y - 8 * gui.scale, - asset.w + 16 * gui.scale, - asset.h + 16 * gui.scale] - fields.add(rect) - - if on: - colour = self.on_colour - - else: - colour = self.off_colour - - fun = None - col = False - if coll(rect): - - tool_tip.test(x + asset.w + 10 * gui.scale, y - 15 * gui.scale, name) - - col = True - if gui.level_2_click: - fun = test - if colour_get is None: - colour = self.over_colour - - colour = colour_get.get(col, on, not on and not animate, low, high) - - # if "+" in name: - # - # colour = cctest.get(col, on, [0, 0.2, 0.0], [0, 0.8, 0.8]) - - # if not on and not animate: - # colour = self.off_colour - - asset.render(x, y, colour) - - return fun - - def tracks(self, hit=False): - - if hit is False: - return album_mode is False and \ - gui.combo_mode is False and \ - gui.rsp is False - - if not (album_mode is False and \ - gui.combo_mode is False and \ - gui.rsp is False): - if x_menu.active: - x_menu.close_next_frame = True - - view_tracks() - - def side(self, hit=False): - - if hit is False: - return album_mode is False and \ - gui.combo_mode is False and \ - gui.rsp is True - if not (album_mode is False and \ - gui.combo_mode is False and \ - gui.rsp is True): - if x_menu.active: - x_menu.close_next_frame = True - - view_standard_meta() - - def gallery1(self, hit: bool = False) -> bool | None: - - if hit is False: - return album_mode is True # and gui.show_playlist is True - - if album_mode and not gui.combo_mode: - gui.hide_tracklist_in_gallery ^= True - gui.rspw = gui.pref_gallery_w - gui.update_layout() - # x_menu.active = False - x_menu.close_next_frame = True - # Menu.active = False - return None - - if x_menu.active: - x_menu.close_next_frame = True - - force_album_view() - - def radio(self, hit=False): - - if hit is False: - return gui.radio_view - - if not gui.radio_view: - enter_radio_view() - else: - exit_combo(restore=True) - - if x_menu.active: - x_menu.close_next_frame = True - - def lyrics(self, hit=False): - - if hit is False: - return gui.showcase_mode - - if not gui.showcase_mode: - if gui.radio_view: - gui.was_radio = True - enter_showcase_view() - - elif gui.was_radio: - enter_radio_view() - else: - exit_combo(restore=True) - if x_menu.active: - x_menu.close_next_frame = True - - def col(self, hit=False): - - if hit is False: - return gui.set_mode - - if not gui.set_mode: - if gui.combo_mode: - exit_combo() - - if album_mode and gui.plw < 550 * gui.scale: - toggle_album_mode() - - toggle_library_mode() - - def artist_info(self, hit=False): - - if hit is False: - return gui.artist_info_panel - - gui.artist_info_panel ^= True - gui.update_layout() - - def render(self): - - if prefs.shuffle_lock: - self.active = False - self.clicked = False - return - - if not self.active: - return - - # rect = [self.x, self.y, self.w, self.h] - # if x_menu.clicked or inp.mouse_click: - if self.clicked: - gui.level_2_click = True - self.clicked = False - - x = self.x - 40 * gui.scale - - vr = [x, gui.panelY, self.w, self.h] - # vr = [x, gui.panelY, 52 * gui.scale, 220 * gui.scale] - - border_colour = colours.menu_tab # colours.grey(30) - if colours.lm: - ddt.rect((vr[0], vr[1], vr[2] + round(4 * gui.scale), vr[3]), border_colour) - else: - ddt.rect( - (vr[0] - round(4 * gui.scale), vr[1], vr[2] + round(8 * gui.scale), - vr[3] + round(4 * gui.scale)), border_colour) - ddt.rect(vr, colours.menu_background) - - x += 7 * gui.scale - y = gui.panelY + 14 * gui.scale - - func = None - - # low = (0, .15, 0) - # low = (0, .40, 0) - # low = rgb_to_hls(*alpha_blend(colours.menu_icons, colours.menu_background)[:3]) # fix me - low = alpha_blend(colours.menu_icons, colours.menu_background) - - # if colours.lm: - # low = (0, 0.5, 0) - - # ---- - #logging.info(hls_to_rgb(.55, .6, .75)) - high = [76, 183, 229, 255] # (.55, .6, .75) - if colours.lm: - # high = (.55, .75, .75) - high = [63, 63, 63, 255] - - test = self.button(x, y, self.side_img, self.side, self.side_colour, _("Tracks + Art"), low=low, high=high) - if test is not None: - func = test - - # ---- - - y += 40 * gui.scale - - high = [76, 137, 229, 255] # (.6, .6, .75) - if colours.lm: - # high = (.6, .80, .85) - high = [63, 63, 63, 255] - - if gui.hide_tracklist_in_gallery: - test = self.button( - x - round(1 * gui.scale), y, self.gallery2_img, self.gallery1, self.gallery1_colour, - _("Gallery"), low=low, high=high) - else: - test = self.button( - x, y, self.gallery1_img, self.gallery1, self.gallery1_colour, _("Gallery"), low=low, high=high) - if test is not None: - func = test - - # --- - - y += 40 * gui.scale - - high = [76, 229, 229, 255] - if colours.lm: - # high = (.5, .7, .65) - high = [63, 63, 63, 255] - - test = self.button( - x + 3 * gui.scale, y, self.tracks_img, self.tracks, self.tracks_colour, _("Tracks only"), - low=low, high=high) - if test is not None: - func = test - - # --- - - y += 45 * gui.scale - - high = [107, 76, 229, 255] - if colours.lm: - # high = (.7, .75, .75) - high = [63, 63, 63, 255] - - test = self.button( - x + 4 * gui.scale, y, self.lyrics_img, self.lyrics, self.lyrics_colour, - _("Showcase + Lyrics"), low=low, high=high) - if test is not None: - func = test - - # -- - - y += 40 * gui.scale - - high = [92, 86, 255, 255] - if colours.lm: - # high = (.7, .75, .75) - high = [63, 63, 63, 255] - - test = self.button( - x + 3 * gui.scale, y, self.radio_img, self.radio, self.radio_colour, _("Radio"), low=low, high=high) - if test is not None: - func = test - - # -- - - y += 45 * gui.scale - - high = [229, 205, 76, 255] - if colours.lm: - # high = (.9, .75, .65) - high = [63, 63, 63, 255] - - test = self.button( - x + 5 * gui.scale, y, self.col_img, self.col, self.col_colour, _("Toggle columns"), False, low=low, high=high) - if test is not None: - func = test - - # -- - - # y += 41 * gui.scale - # - # high = [198, 229, 76, 255] - # if colours.lm: - # #high = (.2, .6, .75) - # high = [63, 63, 63, 255] - # - # if gui.scale == 1.25: - # x-= 1 - # - # test = self.button(x + 2 * gui.scale, y, self.artist_img, self.artist_info, self.artist_colour, _("Toggle artist info"), False, low=low, high=high) - # if test is not None: - # func = test - - if func is not None: - func(True) - - if gui.level_2_click and coll(vr): - x_menu.clicked = False - - gui.level_2_click = False - if not x_menu.active: - self.active = False - - -view_box = ViewBox() - - -class DLMon: - - def __init__(self): - - self.ticker = Timer() - self.ticker.force_set(8) - - self.watching = {} - self.ready = set() - self.done = set() - - def scan(self): - - if len(self.watching) == 0: - if self.ticker.get() < 10: - return - elif self.ticker.get() < 2: - return - - self.ticker.set() - - for downloads in download_directories: - - for item in os.listdir(downloads): - - path = os.path.join(downloads, item) - - if path in self.done: - continue - - if path in self.ready and not os.path.exists(path): - del self.ready[path] - continue - - if path in self.watching and not os.path.exists(path): - del self.watching[path] - continue - - # stamp = os.stat(path)[stat.ST_MTIME] - try: - stamp = os.path.getmtime(path) - except Exception: - logging.exception(f"Failed to scan item at {path}") - self.done.add(path) - continue - - min_age = (time.time() - stamp) / 60 - ext = os.path.splitext(path)[1][1:].lower() - - if msys and "TauonMusicBox" in path: - continue - - if min_age < 240 and os.path.isfile(path) and ext in Archive_Formats: - size = os.path.getsize(path) - #logging.info("Check: " + path) - if path in self.watching: - # Check if size is stable, then scan for audio files - #logging.info("watching...") - if size == self.watching[path] and size != 0: - #logging.info("scan") - del self.watching[path] - - # Check if folder to extract to exists - split = os.path.splitext(path) - target_dir = split[0] - if prefs.extract_to_music and music_directory is not None: - target_dir = os.path.join(str(music_directory), os.path.basename(target_dir)) - - if os.path.exists(target_dir): - pass - #logging.info("Target folder for archive already exists") - - elif archive_file_scan(path, DA_Formats, launch_prefix) >= 0.4: - self.ready.add(path) - gui.update += 1 - #logging.info("Archive detected as music") - else: - pass - #logging.info("Archive rejected as music") - self.done.add(path) - else: - #logging.info("update.") - self.watching[path] = size - else: - self.watching[path] = size - #logging.info("add.") - - elif min_age < 60 \ - and os.path.isdir(path) \ - and path not in quick_import_done \ - and "encode-output" not in path: - try: - size = get_folder_size(path) - except FileNotFoundError: - logging.warning(f"Failed to find watched folder {path}, deleting from watchlist") - if path in self.watching: - del self.watching[path] - continue - except Exception: - logging.exception("Unknown error getting folder size") - if path in self.watching: - # Check if size is stable, then scan for audio files - if size == self.watching[path]: - del self.watching[path] - if folder_file_scan(path, DA_Formats) > 0.5: - - # Check if folder not already imported - imported = False - for pl in pctl.multi_playlist: - for i in pl.playlist_ids: - if path.replace("\\", "/") == pctl.master_library[i].fullpath[:len(path)]: - imported = True - if imported: - break - if imported: - break - else: - self.ready.add(path) - gui.update += 1 - self.done.add(path) - else: - self.watching[path] = size - else: - self.watching[path] = size - else: - self.done.add(path) - - if len(self.ready) > 0: - temp = set() - #logging.info(quick_import_done) - #logging.info(self.ready) - for item in self.ready: - if item not in quick_import_done: - if os.path.exists(path): - temp.add(item) - # else: - # logging.info("FILE IMPORTED") - self.ready = temp - - if len(self.watching) > 0: - gui.update += 1 - - -dl_mon = DLMon() -tauon.dl_mon = dl_mon - - -def dismiss_dl(): - dl_mon.ready.clear() - dl_mon.done.update(dl_mon.watching) - dl_mon.watching.clear() - - -dl_menu.add(MenuItem("Dismiss", dismiss_dl)) - - -class Fader: - - def __init__(self): - - self.total_timer = Timer() - self.timer = Timer() - self.ani_duration = 0.3 - self.state = 0 # 0 = Want off, 1 = Want fade on - self.a = 0 # The fade progress (0-1) - - def render(self): - - if self.total_timer.get() > self.ani_duration: - self.a = self.state - elif self.state == 0: - t = self.timer.hit() - self.a -= t / self.ani_duration - self.a = max(0, self.a) - elif self.state == 1: - t = self.timer.hit() - self.a += t / self.ani_duration - self.a = min(1, self.a) - - rect = [0, 0, window_size[0], window_size[1]] - ddt.rect(rect, [0, 0, 0, int(110 * self.a)]) - - if not (self.a == 0 or self.a == 1): - gui.update += 1 - - def rise(self): - - self.state = 1 - self.timer.hit() - self.total_timer.set() - - def fall(self): - - self.state = 0 - self.timer.hit() - self.total_timer.set() - - -fader = Fader() - - -class EdgePulse: - - def __init__(self): - - self.timer = Timer() - self.timer.force_set(10) - self.ani_duration = 0.5 - - def render(self, x, y, w, h, r=200, g=120, b=0) -> bool: - r = colours.pluse_colour[0] - g = colours.pluse_colour[1] - b = colours.pluse_colour[2] - time = self.timer.get() - if time < self.ani_duration: - alpha = 255 - int(255 * (time / self.ani_duration)) - ddt.rect((x, y, w, h), [r, g, b, alpha]) - gui.update = 2 - return True - return False - - def pulse(self): - self.timer.set() - - -class EdgePulse2: - - def __init__(self): - - self.timer = Timer() - self.timer.force_set(10) - self.ani_duration = 0.22 - - def render(self, x, y, w, h, bottom=False) -> bool | None: - - time = self.timer.get() - if time < self.ani_duration: - - if bottom: - if mouse_wheel > 0: - self.timer.force_set(10) - return None - elif mouse_wheel < 0: - self.timer.force_set(10) - return None - - alpha = 30 - int(25 * (time / self.ani_duration)) - h_off = (h // 5) * (time / self.ani_duration) * 4 - - if colours.lm: - colour = (0, 0, 0, alpha) - else: - colour = (255, 255, 255, alpha) - - if not bottom: - ddt.rect((x, y, w, h - h_off), colour) - else: - ddt.rect((x, y - (h - h_off), w, h - h_off), colour) - gui.update = 2 - return True - return False - - def pulse(self): - self.timer.set() - - -edge_playlist2 = EdgePulse2() -bottom_playlist2 = EdgePulse2() -gallery_pulse_top = EdgePulse2() -tab_pulse = EdgePulse() -lyric_side_top_pulse = EdgePulse2() -lyric_side_bottom_pulse = EdgePulse2() - - -def download_img(link: str, target_folder: str, track: TrackClass) -> None: - try: - response = urllib.request.urlopen(link, context=ssl_context) - info = response.info() - if info.get_content_maintype() == "image": - if info.get_content_subtype() == "jpeg": - save_target = os.path.join(target_dir, "image.jpg") - with open(save_target, "wb") as f: - f.write(response.read()) - # clear_img_cache() - clear_track_image_cache(track) - - elif info.get_content_subtype() == "png": - save_target = os.path.join(target_dir, "image.png") - with open(save_target, "wb") as f: - f.write(response.read()) - # clear_img_cache() - clear_track_image_cache(track) - else: - show_message(_("Image types other than PNG or JPEG are currently not supported"), mode="warning") - else: - show_message(_("The link does not appear to refer to an image file."), mode="warning") - gui.image_downloading = False - - except Exception as e: - logging.exception("Image download failed") - show_message(_("Image download failed."), str(e), mode="warning") - gui.image_downloading = False - - -def display_you_heart(x: int, yy: int, just: int = 0) -> None: - rect = [x - 1 * gui.scale, yy - 4 * gui.scale, 15 * gui.scale, 17 * gui.scale] - gui.heart_fields.append(rect) - fields.add(rect, update_playlist_call) - if coll(rect) and not track_box: - gui.pl_update += 1 - w = ddt.get_text_w(_("You"), 13) - xx = (x - w) - 5 * gui.scale - - if just == 1: - xx += w + 15 * gui.scale - - ty = yy - 28 * gui.scale - tx = xx - if ty < gui.panelY + 5 * gui.scale: - ty = gui.panelY + 5 * gui.scale - tx -= 20 * gui.scale - - # ddt.rect_r((xx - 1 * gui.scale, yy - 26 * gui.scale - 1 * gui.scale, w + 10 * gui.scale + 2 * gui.scale, 19 * gui.scale + 2 * gui.scale), [50, 50, 50, 255], True) - ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [15, 15, 15, 255]) - ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [35, 35, 35, 255]) - ddt.text((tx + 5 * gui.scale, ty + 4 * gui.scale), _("You"), [250, 250, 250, 255], 13, bg=[15, 15, 15, 255]) - - heart_row_icon.render(x, yy, [244, 100, 100, 255]) - -def display_spot_heart(x: int, yy: int, just: int = 0) -> None: - rect = [x - 1 * gui.scale, yy - 4 * gui.scale, 15 * gui.scale, 17 * gui.scale] - gui.heart_fields.append(rect) - fields.add(rect, update_playlist_call) - if coll(rect) and not track_box: - gui.pl_update += 1 - w = ddt.get_text_w(_("Liked on Spotify"), 13) - xx = (x - w) - 5 * gui.scale - - if just == 1: - xx += w + 15 * gui.scale - - ty = yy - 28 * gui.scale - tx = xx - if ty < gui.panelY + 5 * gui.scale: - ty = gui.panelY + 5 * gui.scale - tx -= 20 * gui.scale - - # ddt.rect_r((xx - 1 * gui.scale, yy - 26 * gui.scale - 1 * gui.scale, w + 10 * gui.scale + 2 * gui.scale, 19 * gui.scale + 2 * gui.scale), [50, 50, 50, 255], True) - ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [15, 15, 15, 255]) - ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [35, 35, 35, 255]) - ddt.text((tx + 5 * gui.scale, ty + 4 * gui.scale), _("Liked on Spotify"), [250, 250, 250, 255], 13, bg=[15, 15, 15, 255]) - - heart_row_icon.render(x, yy, [100, 244, 100, 255]) - -def display_friend_heart(x: int, yy: int, name: str, just: int = 0) -> None: - heart_row_icon.render(x, yy, heart_colours.get(name)) - - rect = [x - 1, yy - 4, 15 * gui.scale, 17 * gui.scale] - gui.heart_fields.append(rect) - fields.add(rect, update_playlist_call) - if coll(rect) and not track_box: - gui.pl_update += 1 - w = ddt.get_text_w(name, 13) - xx = (x - w) - 5 * gui.scale - - if just == 1: - xx += w + 15 * gui.scale - - ty = yy - 28 * gui.scale - tx = xx - if ty < gui.panelY + 5 * gui.scale: - ty = gui.panelY + 5 * gui.scale - tx -= 20 * gui.scale - - ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [15, 15, 15, 255]) - ddt.rect((tx - 5 * gui.scale, ty, w + 20 * gui.scale, 24 * gui.scale), [35, 35, 35, 255]) - ddt.text((tx + 5 * gui.scale, ty + 4 * gui.scale), name, [250, 250, 250, 255], 13, bg=[15, 15, 15, 255]) - - -# Set SDL window drag areas -# if system != 'windows': - -def hit_callback(win, point, data): - x = point.contents.x / logical_size[0] * window_size[0] - y = point.contents.y / logical_size[0] * window_size[0] - - # Special layout modes - if gui.mode == 3: - - if key_shift_down or key_shiftr_down: - return SDL_HITTEST_NORMAL - - # if prefs.mini_mode_mode == 5: - # return SDL_HITTEST_NORMAL - - if prefs.mini_mode_mode in (4, 5) and x > window_size[1] - 5 * gui.scale and y > window_size[1] - 12 * gui.scale: - return SDL_HITTEST_NORMAL - - if y < gui.window_control_hit_area_h and x > window_size[ - 0] - gui.window_control_hit_area_w: - return SDL_HITTEST_NORMAL - - # Square modes - y1 = window_size[0] - # if prefs.mini_mode_mode == 5: - # y1 = window_size[1] - y0 = 0 - if macos: - y0 = round(35 * gui.scale) - if window_size[0] == window_size[1]: - y1 = window_size[1] - 79 * gui.scale - if y0 < y < y1 and not search_over.active: - return SDL_HITTEST_DRAGGABLE - - return SDL_HITTEST_NORMAL - - # Standard player mode - if not gui.maximized: - if y < 0 and x > window_size[0]: - return SDL_HITTEST_RESIZE_TOPRIGHT - - if y < 0 and x < 1: - return SDL_HITTEST_RESIZE_TOPLEFT - - # if draw_border and y < 3 * gui.scale and x < window_size[0] - 40 * gui.scale and not gui.maximized: - # return SDL_HITTEST_RESIZE_TOP - - if y < gui.panelY: - - if gui.top_bar_mode2: - - if y < gui.panelY - gui.panelY2: - if prefs.left_window_control and x < 100 * gui.scale: - return SDL_HITTEST_NORMAL - - if x > window_size[0] - 100 * gui.scale and y < 30 * gui.scale: - return SDL_HITTEST_NORMAL - return SDL_HITTEST_DRAGGABLE - if top_panel.drag_zone_start_x > x or tab_menu.active: - return SDL_HITTEST_NORMAL - return SDL_HITTEST_DRAGGABLE - - if top_panel.drag_zone_start_x < x < window_size[0] - (gui.offset_extra + 5): - - if tab_menu.active or mouse_up or mouse_down: # mouse up/down is workaround for Wayland - return SDL_HITTEST_NORMAL - - if (prefs.left_window_control and x > window_size[0] - (100 * gui.scale) and ( - macos or system == "Windows" or msys)) or (not prefs.left_window_control and x > window_size[0] - (160 * gui.scale) and ( - macos or system == "Windows" or msys)): - return SDL_HITTEST_NORMAL - - return SDL_HITTEST_DRAGGABLE - - if not gui.maximized: - if x > window_size[0] - 20 * gui.scale and y > window_size[1] - 20 * gui.scale: - return SDL_HITTEST_RESIZE_BOTTOMRIGHT - if x < 5 and y > window_size[1] - 5: - return SDL_HITTEST_RESIZE_BOTTOMLEFT - if y > window_size[1] - 5 * gui.scale: - return SDL_HITTEST_RESIZE_BOTTOM - - if x > window_size[0] - 3 * gui.scale and y > 20 * gui.scale: - return SDL_HITTEST_RESIZE_RIGHT - if x < 5 * gui.scale and y > 10 * gui.scale: - return SDL_HITTEST_RESIZE_LEFT - return SDL_HITTEST_NORMAL - return SDL_HITTEST_NORMAL - - -c_hit_callback = SDL_HitTest(hit_callback) -SDL_SetWindowHitTest(t_window, c_hit_callback, 0) - - -# -------------------------------------------------------------------------------------------- - - -# caster = threading.Thread(target=enc, args=[tauon]) -# caster.daemon = True -# caster.start() - -tauon.thread_manager.ready_playback() - -try: - tauon.thread_manager.d["caster"] = [lambda: x, [tauon], None] -except Exception: - logging.exception("Failed to cast") - -tauon.thread_manager.d["worker"] = [worker1, (), None] -tauon.thread_manager.d["search"] = [worker2, (), None] -tauon.thread_manager.d["gallery"] = [worker3, (), None] -tauon.thread_manager.d["style"] = [worker4, (), None] -tauon.thread_manager.d["radio-thumb"] = [radio_thumb_gen.loader, (), None] - -tauon.thread_manager.ready("search") -tauon.thread_manager.ready("gallery") -tauon.thread_manager.ready("worker") - -# thread = threading.Thread(target=worker1) -# thread.daemon = True -# thread.start() -# # # -# thread = threading.Thread(target=worker2) -# thread.daemon = True -# thread.start() -# # # -# thread = threading.Thread(target=worker3) -# thread.daemon = True -# thread.start() -# -# thread = threading.Thread(target=worker4) -# thread.daemon = True -# thread.start() - - -gui.playlist_view_length = int(((window_size[1] - gui.playlist_top) / 16) - 1) - -ab_click = False -d_border = 1 - -update_layout = True - -event = SDL_Event() - -mouse_moved = False - -power = 0 - -for item in sys.argv: - if (os.path.isdir(item) or os.path.isfile(item) or "file://" in item) \ - and not item.endswith(".py") and not item.endswith("tauon.exe") and not item.endswith("tauonmb") \ - and not item.startswith("-"): - open_uri(item) - -sv = SDL_version() -SDL_GetVersion(sv) -sdl_version = sv.major * 100 + sv.minor * 10 + sv.patch -logging.info("Using SDL version: " + str(sv.major) + "." + str(sv.minor) + "." + str(sv.patch)) - -# C-ML -# if prefs.backend == 2: -# logging.warning("Using GStreamer as fallback. Some functions disabled") -if prefs.backend == 0: - show_message(_("ERROR: No backend found"), mode="error") - - -class Undo: - - def __init__(self): - - self.e = [] - - def undo(self): - - if not self.e: - show_message(_("There are no more steps to undo.")) - return - - job = self.e.pop() + save = [ + None, + pctl.master_count, + pctl.playlist_playing_position, + pctl.active_playlist_viewing, + pctl.playlist_view_position, + tauonplaylist_jar, # pctl.multi_playlist, # list[TauonPlaylist] + pctl.player_volume, + pctl.track_queue, + pctl.queue_step, + default_playlist, + None, # pctl.playlist_playing_position, + None, # Was cue list + "", # radio_field.text, + theme, + folder_image_offsets, + None, # lfm_username, + None, # lfm_hash, + latest_db_version, # Used for upgrading + view_prefs, + gui.save_size, + None, # old side panel size + 0, # save time (unused) + gui.vis_want, # gui.vis + pctl.selected_in_playlist, + album_mode_art_size, + draw_border, + prefs.enable_web, + prefs.allow_remote, + prefs.expose_web, + prefs.enable_transcode, + prefs.show_rym, + None, # was combo mode art size + gui.maximized, + prefs.prefer_bottom_title, + gui.display_time_mode, + prefs.transcode_mode, + prefs.transcode_codec, + prefs.transcode_bitrate, + 1, # prefs.line_style, + prefs.cache_gallery, + prefs.playlist_font_size, + prefs.use_title, + gui.pl_st, + None, # gui.set_mode, + None, + prefs.playlist_row_height, + prefs.show_wiki, + prefs.auto_extract, + prefs.colour_from_image, + gui.set_bar, + gui.gallery_show_text, + gui.bb_show_art, + False, # Was show stars + prefs.auto_lfm, + prefs.scrobble_mark, + prefs.replay_gain, + True, # Was radio lyrics + prefs.show_gimage, + prefs.end_setting, + prefs.show_gen, + [], # was old radio urls + prefs.auto_del_zip, + gui.level_meter_colour_mode, + prefs.ui_scale, + prefs.show_lyrics_side, + None, #prefs.last_device, + album_mode, + None, # album_playlist_width + prefs.transcode_opus_as, + gui.star_mode, + prefs.prefer_side, # gui.rsp, + gui.lsp, + gui.rspw, + gui.pref_gallery_w, + gui.pref_rspw, + gui.show_hearts, + prefs.monitor_downloads, # 76 + gui.artist_info_panel, # 77 + prefs.extract_to_music, # 78 + lb.enable, + None, # lb.key, + rename_files.text, + rename_folder.text, + prefs.use_jump_crossfade, + prefs.use_transition_crossfade, + prefs.show_notifications, + prefs.true_shuffle, + gui.set_mode, + None, # prefs.show_queue, # 88 + None, # prefs.show_transfer, + tauonqueueitem_jar, # pctl.force_queue, # 90 + prefs.use_pause_fade, # 91 + prefs.append_total_time, # 92 + None, # prefs.backend, + pctl.album_shuffle_mode, + pctl.album_repeat_mode, # 95 + prefs.finish_current, # Not used + prefs.reload_state, # 97 + None, # prefs.reload_play_state, + prefs.last_fm_token, + prefs.last_fm_username, + prefs.use_card_style, + prefs.auto_lyrics, + prefs.auto_lyrics_checked, + prefs.show_side_art, + prefs.window_opacity, + prefs.gallery_single_click, + prefs.tabs_on_top, + prefs.showcase_vis, + prefs.spec2_colour_mode, + prefs.device_buffer, # moved to config file + prefs.use_eq, + prefs.eq, + prefs.bio_large, + prefs.discord_show, + prefs.min_to_tray, + prefs.guitar_chords, + None, # prefs.playback_follow_cursor, + prefs.art_bg, + pctl.random_mode, + pctl.repeat_mode, + prefs.art_bg_stronger, + prefs.art_bg_always_blur, + prefs.failed_artists, + prefs.artist_list, + None, # prefs.auto_sort, + prefs.lyrics_enables, + prefs.fanart_notify, + prefs.bg_showcase_only, + None, # prefs.discogs_pat, + prefs.mini_mode_mode, + after_scan, + gui.gallery_positions, + prefs.chart_bg, + prefs.left_panel_mode, + gui.last_left_panel_mode, + None, #prefs.gst_device, + search_string_cache, + search_dia_string_cache, + pctl.gen_codes, + gui.show_ratings, + gui.show_album_ratings, + prefs.radio_urls, + gui.showcase_mode, # gui.combo_mode, + top_panel.prime_tab, + top_panel.prime_side, + prefs.sync_playlist, + prefs.spot_client, + prefs.spot_secret, + prefs.show_band, + prefs.download_playlist, + tauon.spot_ctl.cache_saved_albums, + prefs.auto_rec, + prefs.spotify_token, + prefs.use_libre_fm, + playlist_box.scroll_on, + prefs.artist_list_sort_mode, + prefs.phazor_device_selected, + prefs.failed_background_artists, + prefs.bg_flips, + prefs.tray_show_title, + prefs.artist_list_style, + trackclass_jar, + prefs.premium, + gui.radio_view, + pctl.radio_playlists, + pctl.radio_playlist_viewing, + prefs.radio_thumb_bans, + prefs.playlist_exports, + prefs.show_chromecast, + prefs.cache_list, + prefs.shuffle_lock, + prefs.album_shuffle_lock_mode, + gui.was_radio, + prefs.spot_username, + "", #prefs.spot_password, # No longer used + prefs.artist_list_threshold, + prefs.tray_theme, + prefs.row_title_format, + prefs.row_title_genre, + prefs.row_title_separator_type, + prefs.replay_preamp, # 181 + prefs.gallery_combine_disc, + ] - if job[0] == "playlist": - pctl.multi_playlist.append(job[1]) - switch_playlist(len(pctl.multi_playlist) - 1) - elif job[0] == "tracks": + try: + with (user_directory / "state.p.backup").open("wb") as file: + pickle.dump(save, file, protocol=pickle.HIGHEST_PROTOCOL) + # if not pctl.running: + with (user_directory / "state.p").open("wb") as file: + pickle.dump(save, file, protocol=pickle.HIGHEST_PROTOCOL) - uid = job[1] - li = job[2] + old_position = old_window_position + if not prefs.save_window_position: + old_position = None - for i, playlist in enumerate(pctl.multi_playlist): - if playlist.uuid_int == uid: - pl = playlist.playlist_ids - switch_playlist(i) - break - else: - logging.info("No matching playlist ID to restore tracks to") - return + save = [ + draw_border, + gui.save_size, + prefs.window_opacity, + gui.scale, + gui.maximized, + old_position, + ] - for i, ref in reversed(li): + if not fs_mode: + with (user_directory / "window.p").open("wb") as file: + pickle.dump(save, file, protocol=pickle.HIGHEST_PROTOCOL) - if i > len(pl): - logging.error("restore track error - playlist not correct length") - continue - pl.insert(i, ref) - - if not pctl.playlist_view_position < i < pctl.playlist_view_position + gui.playlist_view_length: - pctl.playlist_view_position = i - logging.debug("Position changed by undo") - elif job[0] == "ptt": - j, fr, fr_s, fr_scr, so, to_s, to_scr = job - star_store.insert(fr.index, fr_s) - star_store.insert(to.index, to_s) - to.lfm_scrobbles = to_scr - fr.lfm_scrobbles = fr_scr + tauon.spot_ctl.save_token() - gui.pl_update = 1 + with (user_directory / "lyrics_substitutions.json").open("w") as file: + json.dump(prefs.lyrics_subs, file) - def bk_playlist(self, pl_index: int) -> None: + save_prefs() - self.e.append(("playlist", pctl.multi_playlist[pl_index])) + for key, item in prefs.playlist_exports.items(): + pl = id_to_pl(key) + if pl is None: + continue + if item["auto"] is False: + continue + export_playlist_box.run_export(item, key, warnings=False) - def bk_tracks(self, pl_index: int, indis) -> None: + logging.info("Done writing database") - uid = pctl.multi_playlist[pl_index].uuid_int - self.e.append(("tracks", uid, indis)) + except PermissionError: + logging.exception("Permission error encountered while writing database") + show_message(_("Permission error encountered while writing database"), "error") + except Exception: + logging.exception("Unknown error encountered while writing database") - def bk_playtime_transfer(self, fr, fr_s, fr_scr, so, to_s, to_scr) -> None: - self.e.append(("ptt", fr, fr_s, fr_scr, so, to_s, to_scr)) +def test_show_add_home_music() -> None: + gui.add_music_folder_ready = True + if music_directory is None: + gui.add_music_folder_ready = False + return -undo = Undo() + for item in pctl.multi_playlist: + if item.last_folder == str(music_directory): + gui.add_music_folder_ready = False + break +def menu_is_open(): + for menu in Menu.instances: + if menu.active: + return True + return False -def reload_scale(): - auto_scale() +def is_level_zero(include_menus: bool = True) -> bool: + if include_menus: + for menu in Menu.instances: + if menu.active: + return False - scale = prefs.scale_want + return not gui.rename_folder_box \ + and not track_box \ + and not rename_track_box.active \ + and not radiobox.active \ + and not pref_box.enabled \ + and not quick_search_mode \ + and not gui.rename_playlist_box \ + and not search_over.active \ + and not gui.box_over \ + and not trans_edit_box.active - gui.scale = scale - ddt.scale = gui.scale - prime_fonts() - ddt.clear_text_cache() - scale_assets(scale_want=scale, force=True) - img_slide_update_gall(album_mode_art_size) +def drop_file(target): + global new_playlist_cooldown + global mouse_down + global drag_mode - for item in WhiteModImageAsset.assets: - item.reload() - for item in LoadImageAsset.assets: - item.reload() - for menu in Menu.instances: - menu.rescale() - bottom_bar1.__init__() - bottom_bar_ao1.__init__() - top_panel.__init__() - view_box.__init__(reload=True) - queue_box.recalc() - playlist_box.recalc() + if system != "windows" and sdl_version >= 204: + gmp = get_global_mouse() + gwp = get_window_position() + i_x = gmp[0] - gwp[0] + i_x = max(i_x, 0) + i_x = min(i_x, window_size[0]) + i_y = gmp[1] - gwp[1] + i_y = max(i_y, 0) + i_y = min(i_y, window_size[1]) + else: + i_y = pointer(c_int(0)) + i_x = pointer(c_int(0)) -def update_layout_do(): - if prefs.scale_want != gui.scale: - reload_scale() + SDL_GetMouseState(i_x, i_y) + i_y = i_y.contents.value / logical_size[0] * window_size[0] + i_x = i_x.contents.value / logical_size[0] * window_size[0] - w = window_size[0] - h = window_size[1] + #logging.info((i_x, i_y)) + gui.drop_playlist_target = 0 + #logging.info(event.drop) - if gui.switch_showcase_off: - ddt.force_gray = False - gui.switch_showcase_off = False - exit_combo(restore=True) + if i_y < gui.panelY and not new_playlist_cooldown and gui.mode == 1: + x = top_panel.tabs_left_x + for tab in top_panel.shown_tabs: + wid = top_panel.tab_text_spaces[tab] + top_panel.tab_extra_width - global draw_max_button - if draw_max_button and prefs.force_hide_max_button: - draw_max_button = False + if x < i_x < x + wid: + gui.drop_playlist_target = tab + tab_pulse.pulse() + gui.update += 1 + gui.pl_pulse = True + logging.info("Direct drop") + break - if gui.theme_name != prefs.theme_name: - gui.reload_theme = True - global theme - theme = get_theme_number(prefs.theme_name) - #logging.info("Config reload theme...") + x += wid + else: + logging.info("MISS") + if new_playlist_cooldown: + gui.drop_playlist_target = pctl.active_playlist_viewing + else: + if not target.lower().endswith(".xspf"): + gui.drop_playlist_target = new_playlist() + new_playlist_cooldown = True - # Restore in case of error - if gui.rspw < 30 * gui.scale: + elif gui.lsp and gui.panelY < i_y < window_size[1] - gui.panelBY and i_x < gui.lspw and gui.mode == 1: - gui.rspw = 100 * gui.scale + y = gui.panelY + y += 5 * gui.scale + y += playlist_box.tab_h + playlist_box.gap - # Lock right side panel to full size if fully extended ----- - if prefs.side_panel_layout == 0 and not album_mode: - max_w = round( - ((window_size[1] - gui.panelY - gui.panelBY - 17 * gui.scale) * gui.art_max_ratio_lock) + 17 * gui.scale) - # 17 here is the art box inset value + for i, pl in enumerate(pctl.multi_playlist): + if i_y < y: + gui.drop_playlist_target = i + tab_pulse.pulse() + gui.update += 1 + gui.pl_pulse = True + logging.info("Direct drop") + break + y += playlist_box.tab_h + playlist_box.gap + else: + if new_playlist_cooldown: + gui.drop_playlist_target = pctl.active_playlist_viewing + else: + if not target.lower().endswith(".xspf"): + gui.drop_playlist_target = new_playlist() + new_playlist_cooldown = True - if not album_mode and gui.rspw > max_w - 12 * gui.scale and side_drag: - gui.rsp_full_lock = True - # ---------------------------------------------------------- - # Auto shrink left side panel -------------- - pl_width = window_size[0] - pl_width_a = pl_width - if gui.rsp: - pl_width_a = pl_width - gui.rspw - pl_width -= gui.rspw - 300 * gui.scale # More sensitivity for compact with rsp for better visual balancing + else: + gui.drop_playlist_target = pctl.active_playlist_viewing - if pl_width < 900 * gui.scale and not gui.hide_tracklist_in_gallery: - gui.lspw = 180 * gui.scale + if not os.path.exists(target) and flatpak_mode: + show_message( + _("Could not access! Possible insufficient Flatpak permissions."), + _(" For details, see {link}").format(link="https://github.com/Taiko2k/TauonMusicBox/wiki/Flatpak-Extra-Steps"), + mode="bubble") - if pl_width < 700 * gui.scale: - gui.lspw = 150 * gui.scale + load_order = LoadClass() + load_order.target = target.replace("\\", "/") - if prefs.left_panel_mode == "artist list" and prefs.artist_list_style == 1: - gui.compact_artist_list = True - gui.lspw = 75 * gui.scale - if gui.force_side_on_drag: - gui.lspw = 180 * gui.scale - else: - gui.lspw = 220 * gui.scale - gui.compact_artist_list = False - if prefs.left_panel_mode == "artist list": - gui.lspw = 230 * gui.scale + if os.path.isdir(load_order.target): + quick_import_done.append(load_order.target) - if gui.lsp and prefs.left_panel_mode == "folder view": - gui.lspw = 260 * gui.scale - max_insets = 0 - for item in tree_view_box.rows: - max_insets = max(item[2], max_insets) + # if not pctl.multi_playlist[gui.drop_playlist_target].last_folder: + pctl.multi_playlist[gui.drop_playlist_target].last_folder.append(load_order.target) + reduce_paths(pctl.multi_playlist[gui.drop_playlist_target].last_folder) - p = (pl_width_a * 0.15) - round(200 * gui.scale) - if gui.hide_tracklist_in_gallery: - p = ((window_size[0] - gui.lspw) * 0.15) - round(170 * gui.scale) + load_order.playlist = pctl.multi_playlist[gui.drop_playlist_target].uuid_int + load_orders.append(copy.deepcopy(load_order)) - p = min(round(200 * gui.scale), p) - if p > 0: - gui.lspw += p - if max_insets > 1: - gui.lspw = max(gui.lspw, 260 * gui.scale + round(15 * gui.scale) * max_insets) + #logging.info('dropped: ' + str(dropped_file)) + gui.update += 1 + mouse_down = False + drag_mode = False - # ----- +def main(holder: Holder): + t_window = holder.t_window + renderer = holder.renderer + logical_size = holder.logical_size + window_size = holder.window_size + maximized = holder.maximized + scale = holder.scale + window_opacity = holder.window_opacity + draw_border = holder.draw_border + transfer_args_and_exit = holder.transfer_args_and_exit + old_window_position = holder.old_window_position + install_directory = holder.install_directory + user_directory = holder.user_directory + pyinstaller_mode = holder.pyinstaller_mode + phone = holder.phone + window_default_size = holder.window_default_size + window_title = holder.window_title + fs_mode = holder.fs_mode + t_title = holder.t_title + n_version = holder.n_version + t_version = holder.t_version + t_id = holder.t_id + t_agent = holder.t_agent + dev_mode = holder.dev_mode + instance_lock = holder.instance_lock + log = holder.log + logging.info(f"Window size: {window_size}") + + should_save_state = True + + # Detect platform + windows_native = False + macos = False + msys = False + system = "Linux" + arch = platform.machine() + platform_release = platform.release() + platform_system = platform.system() + win_ver = 0 - # Set bg art strength according to setting ---- - if prefs.art_bg_stronger == 3: - prefs.art_bg_opacity = 29 - elif prefs.art_bg_stronger == 2: - prefs.art_bg_opacity = 19 - else: - prefs.art_bg_opacity = 10 + try: + import pylast + last_fm_enable = True + except Exception: + logging.exception("PyLast module not found, last fm will be disabled.") + last_fm_enable = False + + if not windows_native: + import gi + from gi.repository import GLib + + font_folder = str(install_directory / "fonts") + if os.path.isdir(font_folder): + logging.info(f"Fonts directory: {font_folder}") + import ctypes + + fc = ctypes.cdll.LoadLibrary("libfontconfig-1.dll") + fc.FcConfigReference.restype = ctypes.c_void_p + fc.FcConfigReference.argtypes = (ctypes.c_void_p,) + fc.FcConfigAppFontAddDir.argtypes = (ctypes.c_void_p, ctypes.c_char_p) + config = ctypes.c_void_p() + config.contents = fc.FcConfigGetCurrent() + fc.FcConfigAppFontAddDir(config.value, font_folder.encode()) + + # Log to debug as we don't care at all when user does not have this + try: + import colored_traceback.always + logging.debug("Found colored_traceback for colored crash tracebacks") + except ModuleNotFoundError: + logging.debug("Unable to import colored_traceback, tracebacks will be dull.") + except Exception: + logging.exception("Unknown error trying to import colored_traceback, tracebacks will be dull.") - if prefs.bg_showcase_only: - prefs.art_bg_opacity += 21 + try: + from jxlpy import JXLImagePlugin + # We've already logged this once to INFO from t_draw, so just log to DEBUG + logging.debug("Found jxlpy for JPEG XL support") + except ModuleNotFoundError: + logging.warning("Unable to import jxlpy, JPEG XL support will be disabled.") + except Exception: + logging.exception("Unknown error trying to import jxlpy, JPEG XL support will be disabled.") - # ----- + try: + import setproctitle + except ModuleNotFoundError: + logging.warning("Unable to import setproctitle, won't be setting process title.") + except Exception: + logging.exception("Unknown error trying to import setproctitle, won't be setting process title.") + else: + setproctitle.setproctitle("tauonmb") - # Adjust for for compact window sizes ---- - if (prefs.always_art_header or (w < 600 * gui.scale and not gui.rsp and prefs.art_in_top_panel)) and not album_mode: - gui.top_bar_mode2 = True - gui.panelY = round(100 * gui.scale) - gui.playlist_top = gui.panelY + (8 * gui.scale) - gui.playlist_top_bk = gui.playlist_top + # try: + # import rpc + # discord_allow = True + # except Exception: + # logging.exception("Unable to import rpc, Discord Rich Presence will be disabled.") + discord_allow = False + try: + from pypresence import Presence + except ModuleNotFoundError: + logging.warning("Unable to import pypresence, Discord Rich Presence will be disabled.") + except Exception: + logging.exception("Unknown error trying to import pypresence, Discord Rich Presence will be disabled.") + else: + import asyncio + discord_allow = True + use_cc = False + try: + import opencc + except ModuleNotFoundError: + logging.warning("Unable to import opencc, Traditional and Simplified Chinese searches will not be usable interchangeably.") + except Exception: + logging.exception("Unknown error trying to import opencc, Traditional and Simplified Chinese searches will not be usable interchangeably.") else: - gui.top_bar_mode2 = False - gui.panelY = round(30 * gui.scale) - gui.playlist_top = gui.panelY + (8 * gui.scale) - gui.playlist_top_bk = gui.playlist_top + s2t = opencc.OpenCC("s2t") + t2s = opencc.OpenCC("t2s") + use_cc = True - gui.show_playlist = True - if w < 750 * gui.scale and album_mode: - gui.show_playlist = False + use_natsort = False + try: + import natsort + except ModuleNotFoundError: + logging.warning("Unable to import natsort, playlists may not sort as intended!") + except Exception: + logging.exception("Unknown error trying to import natsort, playlists may not sort as intended!") + else: + use_natsort = True - # Set bio panel size according to setting - if prefs.bio_large: - gui.artist_panel_height = 320 * gui.scale - if window_size[0] < 600 * gui.scale: - gui.artist_panel_height = 200 * gui.scale + if platform_system == "Windows": + try: + win_ver = int(platform_release) + except Exception: + logging.exception("Failed getting Windows version from platform.release()") + if sys.platform == "win32": + # system = 'Windows' + # windows_native = False + system = "Linux" + msys = True else: - gui.artist_panel_height = 200 * gui.scale - if window_size[0] < 600 * gui.scale: - gui.artist_panel_height = 150 * gui.scale + system = "Linux" + import fcntl - # Trigger artist bio reload if panel size has changed - if gui.artist_info_panel: - if gui.last_artist_panel_height != gui.artist_panel_height: - artist_info_box.get_data(artist_info_box.artist_on) - gui.last_artist_panel_height = gui.artist_panel_height + if sys.platform == "darwin": + macos = True - # prefs.art_bg_blur = 9 - # if prefs.bg_showcase_only: - # prefs.art_bg_blur = 15 - # - # if w / h == 16 / 9: - # logging.info("YEP") - # elif w / h < 16 / 9: - # logging.info("too low") - # else: - # logging.info("too high") - #logging.info((w, h)) + if system == "Windows": + import win32con + import win32api + import win32gui + import win32ui + import comtypes + import atexit - # input.mouse_click = False + if system == "Linux": + from tauon.t_modules import t_topchart - global renderer + if system == "Linux" and not macos and not msys: + from tauon.t_modules.t_dbus import Gnome - if prefs.spec2_colour_mode == 0: - prefs.spec2_base = [10, 10, 100] - prefs.spec2_multiply = [0.5, 1, 1] - elif prefs.spec2_colour_mode == 1: - prefs.spec2_base = [10, 10, 10] - prefs.spec2_multiply = [2, 1.2, 5] - # elif prefs.spec2_colour_mode == 2: - # prefs.spec2_base = [10, 100, 10] - # prefs.spec2_multiply = [1, -1, 0.4] + #1REWORK - gui.draw_vis4_top = False + ssl_context = setup_ssl(holder) + #2REWORK - if gui.combo_mode and gui.showcase_mode and prefs.showcase_vis and gui.mode != 3 and prefs.backend == 4: - gui.vis = 4 - gui.turbo = True - elif gui.vis_want == 0: - gui.turbo = False - gui.vis = 0 - else: - gui.vis = gui.vis_want - if gui.vis > 0: - gui.turbo = True + # Set data folders (portable mode) + config_directory = user_directory + cache_directory = user_directory / "cache" + home_directory = os.path.join(os.path.expanduser("~")) + + asset_directory = install_directory / "assets" + svg_directory = install_directory / "assets" / "svg" + scaled_asset_directory = asset_directory + + music_directory = Path("~").expanduser() / "Music" + if not music_directory.is_dir(): + music_directory = Path("~").expanduser() / "music" + + download_directory = Path("~").expanduser() / "Downloads" + + # Detect if we are installed or running portable + install_mode = False + flatpak_mode = False + snap_mode = False + if str(install_directory).startswith(("/opt/", "/usr/", "/app/", "/snap/")): + install_mode = True + if str(install_directory)[:6] == "/snap/": + snap_mode = True + if str(install_directory)[:5] == "/app/": + # Flatpak mode + logging.info("Detected running as Flatpak") + + # [old / no longer used] Symlink fontconfig from host system as workaround for poor font rendering + if os.path.exists(os.path.join(home_directory, ".var/app/com.github.taiko2k.tauonmb/config")): + + host_fcfg = os.path.join(home_directory, ".config/fontconfig/") + flatpak_fcfg = os.path.join(home_directory, ".var/app/com.github.taiko2k.tauonmb/config/fontconfig") + + if os.path.exists(host_fcfg): + + # if os.path.isdir(flatpak_fcfg) and not os.path.islink(flatpak_fcfg): + # shutil.rmtree(flatpak_fcfg) + if os.path.islink(flatpak_fcfg): + logging.info("-- Symlink to fonconfig exists, removing") + os.unlink(flatpak_fcfg) + # else: + # logging.info("-- Symlinking user fonconfig") + # #os.symlink(host_fcfg, flatpak_fcfg) - # Disable vis when in compact view - if gui.mode == 3 or gui.top_bar_mode2: # or prefs.backend == 2: - if not gui.combo_mode: - gui.vis = 0 - gui.turbo = False + flatpak_mode = True - if gui.mode == 1: - if not gui.maximized and not gui.lowered and gui.mode != 3: - gui.save_size[0] = logical_size[0] - gui.save_size[1] = logical_size[1] + # If we're installed, use home data locations + if (install_mode and system == "Linux") or macos or msys: + cache_directory = Path(GLib.get_user_cache_dir()) / "TauonMusicBox" + #user_directory = Path(GLib.get_user_data_dir()) / "TauonMusicBox" + config_directory = user_directory - bottom_bar1.update() + # if not user_directory.is_dir(): + # os.makedirs(user_directory) - # if system != 'windows': - # if draw_border: - # gui.panelY = 30 * gui.scale + 3 * gui.scale - # top_panel.ty = 3 * gui.scale - # else: - # gui.panelY = 30 * gui.scale - # top_panel.ty = 0 + if not config_directory.is_dir(): + os.makedirs(config_directory) - if gui.set_bar and gui.set_mode: - gui.playlist_top = gui.playlist_top_bk + gui.set_height - 6 * gui.scale + if snap_mode: + logging.info("Installed as Snap") + elif flatpak_mode: + logging.info("Installed as Flatpak") else: - gui.playlist_top = gui.playlist_top_bk + logging.info("Running from installed location") - if gui.artist_info_panel: - gui.playlist_top += gui.artist_panel_height + if not (user_directory / "encoder").is_dir(): + os.makedirs(user_directory / "encoder") - gui.offset_extra = 0 - if draw_border and not prefs.left_window_control: - offset = 61 * gui.scale - if not draw_min_button: - offset -= 35 * gui.scale - if draw_max_button: - offset += 33 * gui.scale - if gui.macstyle: - offset = 24 - if draw_min_button: - offset += 20 - if draw_max_button: - offset += 20 - offset = round(offset * gui.scale) - gui.offset_extra = offset + # elif (system == 'Windows' or msys) and ( + # 'Program Files' in install_directory or + # os.path.isfile(install_directory + '\\unins000.exe')): + # + # user_directory = os.path.expanduser('~').replace("\\", '/') + "/Music/TauonMusicBox" + # config_directory = user_directory + # cache_directory = user_directory / "cache" + # logging.info(f"User Directory: {user_directory}") + # install_mode = True + # if not os.path.isdir(user_directory): + # os.makedirs(user_directory) + + else: + logging.info("Running in portable mode") + config_directory = user_directory + + if not (user_directory / "state.p").is_file() and cache_directory.is_dir(): + logging.info("Clearing old cache directory") + logging.info(cache_directory) + shutil.rmtree(str(cache_directory)) + + n_cache_dir = str(cache_directory / "network") + e_cache_dir = str(cache_directory / "export") + g_cache_dir = str(cache_directory / "gallery") + a_cache_dir = str(cache_directory / "artist") + r_cache_dir = str(cache_directory / "radio-thumbs") + b_cache_dir = str(user_directory / "artist-backgrounds") + + if not os.path.isdir(n_cache_dir): + os.makedirs(n_cache_dir) + if not os.path.isdir(e_cache_dir): + os.makedirs(e_cache_dir) + if not os.path.isdir(g_cache_dir): + os.makedirs(g_cache_dir) + if not os.path.isdir(a_cache_dir): + os.makedirs(a_cache_dir) + if not os.path.isdir(b_cache_dir): + os.makedirs(b_cache_dir) + if not os.path.isdir(r_cache_dir): + os.makedirs(r_cache_dir) + + if not (user_directory / "artist-pictures").is_dir(): + os.makedirs(user_directory / "artist-pictures") + + if not (user_directory / "theme").is_dir(): + os.makedirs(user_directory / "theme") + + + if platform_system == "Linux": + system_config_directory = Path(GLib.get_user_config_dir()) + xdg_dir_file = system_config_directory / "user-dirs.dirs" + + if xdg_dir_file.is_file(): + with xdg_dir_file.open() as f: + for line in f: + if line.startswith("XDG_MUSIC_DIR="): + music_directory = Path(os.path.expandvars(line.split("=")[1].strip().replace('"', ""))).expanduser() + logging.debug(f"Found XDG-Music: {music_directory} in {xdg_dir_file}") + if line.startswith("XDG_DOWNLOAD_DIR="): + target = Path(os.path.expandvars(line.split("=")[1].strip().replace('"', ""))).expanduser() + if Path(target).is_dir(): + download_directory = target + logging.debug(f"Found XDG-Downloads: {download_directory} in {xdg_dir_file}") + + + if os.getenv("XDG_MUSIC_DIR"): + music_directory = Path(os.getenv("XDG_MUSIC_DIR")) + logging.debug("Override music to: " + music_directory) + + if os.getenv("XDG_DOWNLOAD_DIR"): + download_directory = Path(os.getenv("XDG_DOWNLOAD_DIR")) + logging.debug("Override downloads to: " + download_directory) + + if music_directory: + music_directory = Path(os.path.expandvars(music_directory)) + if download_directory: + download_directory = Path(os.path.expandvars(download_directory)) + + if not music_directory.is_dir(): + music_directory = None + + locale_directory = install_directory / "locale" + #if flatpak_mode: + # locale_directory = Path("/app/share/locale") + #elif str(install_directory).startswith(("/opt/", "/usr/")): + # locale_directory = Path("/usr/share/locale") + + dirs = Directories( + install_directory=install_directory, + svg_directory=svg_directory, + asset_directory=asset_directory, + scaled_asset_directory=scaled_asset_directory, + locale_directory=locale_directory, + user_directory=user_directory, + config_directory=config_directory, + cache_directory=cache_directory, + home_directory=home_directory, + music_directory=music_directory, + download_directory=download_directory, + ) + + logging.critical(dirs.download_directory) + + + + + + + logging.info(f"Install directory: {install_directory}") + #logging.info(f"SVG directory: {svg_directory}") + logging.info(f"Asset directory: {asset_directory}") + #logging.info(f"Scaled Asset Directory: {scaled_asset_directory}") + if locale_directory.exists(): + logging.info(f"Locale directory: {locale_directory}") + else: + logging.error(f"Locale directory MISSING: {locale_directory}") + logging.info(f"Userdata directory: {user_directory}") + logging.info(f"Config directory: {config_directory}") + logging.info(f"Cache directory: {cache_directory}") + logging.info(f"Home directory: {home_directory}") + logging.info(f"Music directory: {music_directory}") + logging.info(f"Downloads directory: {download_directory}") + + #3REWORK - TODO(Martin): Move this one to a separate dir func? + + launch_prefix = "" + if flatpak_mode: + launch_prefix = "flatpak-spawn --host " + + pid = os.getpid() - global album_v_gap - global album_h_gap - global album_v_slide_value + if not macos: + icon = IMG_Load(str(asset_directory / "icon-64.png").encode()) + else: + icon = IMG_Load(str(asset_directory / "tau-mac.png").encode()) - album_v_slide_value = round(50 * gui.scale) - if gui.gallery_show_text: - album_h_gap = 30 * gui.scale - album_v_gap = 66 * gui.scale + SDL_SetWindowIcon(t_window, icon) + + if not phone: + if window_size[0] != logical_size[0]: + SDL_SetWindowMinimumSize(t_window, 560, 330) else: - album_h_gap = 30 * gui.scale - album_v_gap = 25 * gui.scale + SDL_SetWindowMinimumSize(t_window, round(560 * scale), round(330 * scale)) - if prefs.thin_gallery_borders: + max_window_tex = 1000 + if window_size[0] > max_window_tex or window_size[1] > max_window_tex: - if gui.gallery_show_text: - album_h_gap = 20 * gui.scale - album_v_gap = 55 * gui.scale - else: - album_h_gap = 17 * gui.scale - album_v_gap = 15 * gui.scale + while window_size[0] > max_window_tex: + max_window_tex += 1000 + while window_size[1] > max_window_tex: + max_window_tex += 1000 - album_v_slide_value = round(45 * gui.scale) + main_texture = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, max_window_tex, + max_window_tex) + main_texture_overlay_temp = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, + max_window_tex, max_window_tex) - if prefs.increase_gallery_row_spacing: - album_v_gap = round(album_v_gap * 1.3) + overlay_texture_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, 300, 300) + SDL_SetTextureBlendMode(overlay_texture_texture, SDL_BLENDMODE_BLEND) + SDL_SetRenderTarget(renderer, overlay_texture_texture) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_RenderClear(renderer) + SDL_SetRenderTarget(renderer, None) - gui.gallery_scroll_field_left = window_size[0] - round(40 * gui.scale) + tracklist_texture = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, max_window_tex, + max_window_tex) + tracklist_texture_rect = SDL_Rect(0, 0, max_window_tex, max_window_tex) + SDL_SetTextureBlendMode(tracklist_texture, SDL_BLENDMODE_BLEND) - # gui.spec_rect[0] = window_size[0] - gui.offset_extra - 90 - gui.spec1_rec.x = int(round(window_size[0] - gui.offset_extra - 90 * gui.scale)) + SDL_SetRenderTarget(renderer, None) - # gui.spec_x = window_size[0] - gui.offset_extra - 90 + # Paint main texture + SDL_SetRenderTarget(renderer, main_texture) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) - gui.spec2_rec.x = int(round(window_size[0] - gui.spec2_rec.w - 10 * gui.scale - gui.offset_extra)) + SDL_SetRenderTarget(renderer, main_texture_overlay_temp) + SDL_SetTextureBlendMode(main_texture_overlay_temp, SDL_BLENDMODE_BLEND) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) + SDL_RenderClear(renderer) - gui.scroll_hide_box = (1, gui.panelY, 28 * gui.scale, window_size[1] - gui.panelBY - gui.panelY) - # Tracklist row size and text positioning --------------------------------- - gui.playlist_row_height = prefs.playlist_row_height - gui.row_font_size = prefs.playlist_font_size # 13 + # + # SDL_SetRenderTarget(renderer, None) + # SDL_SetRenderDrawColor(renderer, 7, 7, 7, 255) + # SDL_RenderClear(renderer) + # #SDL_RenderPresent(renderer) + # + # SDL_SetWindowOpacity(t_window, window_opacity) + #4REWORK - gui.playlist_text_offset = round(gui.playlist_row_height * 0.55) + 4 - 13 * gui.scale + loaded_asset_dc: dict[str, WhiteModImageAsset | LoadImageAsset] = {} + # loading_image = asset_loader(scaled_asset_directory, loaded_asset_dc, "loading.png") - if gui.scale != 1: - real_font_px = ddt.f_dict[gui.row_font_size][2] - # gui.playlist_text_offset = (round(gui.playlist_row_height - real_font_px) / 2) - ddt.get_y_offset("AbcD", gui.row_font_size, 100) + round(1.3 * gui.scale) + if maximized: + i_x = pointer(c_int(0)) + i_y = pointer(c_int(0)) - if gui.scale < 1.3: - gui.playlist_text_offset = round(((gui.playlist_row_height - real_font_px) / 2) - 1.9 * gui.scale) - elif gui.scale < 1.5: - gui.playlist_text_offset = round(((gui.playlist_row_height - real_font_px) / 2) - 1.3 * gui.scale) - elif gui.scale < 1.75: - gui.playlist_text_offset = round(((gui.playlist_row_height - real_font_px) / 2) - 1.1 * gui.scale) - elif gui.scale < 2.3: - gui.playlist_text_offset = round(((gui.playlist_row_height - real_font_px) / 2) - 1.5 * gui.scale) - else: - gui.playlist_text_offset = round(((gui.playlist_row_height - real_font_px) / 2) - 1.8 * gui.scale) + time.sleep(0.02) + SDL_PumpEvents() + SDL_GetWindowSize(t_window, i_x, i_y) + logical_size[0] = i_x.contents.value + logical_size[1] = i_y.contents.value + SDL_GL_GetDrawableSize(t_window, i_x, i_y) + window_size[0] = i_x.contents.value + window_size[1] = i_y.contents.value - gui.playlist_text_offset += prefs.tracklist_y_text_offset + # loading_image.render(window_size[0] // 2 - loading_image.w // 2, window_size[1] // 2 - loading_image.h // 2) + # SDL_RenderPresent(renderer) - gui.pl_title_real_height = round(gui.playlist_row_height * 0.55) + 4 - 12 + if install_directory != config_directory and not (config_directory / "input.txt").is_file(): + logging.warning("Input config file is missing, first run? Copying input.txt template from templates directory") + #logging.warning(install_directory) + #logging.warning(config_directory) + shutil.copy(install_directory / "templates" / "input.txt", config_directory) - # ------------------------------------------------------------------------- - gui.playlist_view_length = int( - (window_size[1] - gui.panelBY - gui.playlist_top - 12 * gui.scale) // gui.playlist_row_height) - box_r = gui.rspw / (window_size[1] - gui.panelBY - gui.panelY) + if snap_mode: + discord_allow = False - if gui.art_aspect_ratio > 1.01: - gui.art_unlock_ratio = True - gui.art_max_ratio_lock = max(gui.art_aspect_ratio, gui.art_max_ratio_lock) + musicbrainzngs.set_useragent("TauonMusicBox", n_version, "https://github.com/Taiko2k/Tauon") + #5REWORK + if system == "Windows": + os.environ["PYSDL2_DLL_PATH"] = str(install_directory / "lib") + elif not msys and not macos: + try: + gi.require_version("Notify", "0.7") + except Exception: + logging.exception("Failed importing gi Notify 0.7, will try 0.8") + gi.require_version("Notify", "0.8") + from gi.repository import Notify + #6REWORK + album_mode_art_size = int(200 * scale) + #7REWORK + b_info_y = int(window_size[1] * 0.7) # For future possible panel below playlist + #8REWORK + prefs = Prefs( + user_directory=user_directory, + music_directory=music_directory, + cache_directory=cache_directory, + macos=macos, + phone=phone, + left_window_control=left_window_control, + detect_macstyle=detect_macstyle, + gtk_settings=gtk_settings, + discord_allow=discord_allow, + flatpak_mode=flatpak_mode, + desktop=desktop, + window_opacity=window_opacity, + scale=scale, + ) + #9REWORK + gui = GuiVar(prefs=prefs) + #10REWORK + # STATE LOADING + # Loading of program data from previous run + gbc.disable() + ggc = 2 + + star_path1 = user_directory / "star.p" + star_path2 = user_directory / "star.p.backup" + star_size1 = 0 + star_size2 = 0 + to_load = star_path1 + if star_path1.is_file(): + star_size1 = star_path1.stat().st_size + if star_path2.is_file(): + star_size2 = star_path2.stat().st_size + if star_size2 > star_size1: + logging.warning("Loading backup star.p as it was bigger than regular file!") + to_load = star_path2 + if star_size1 == 0 and star_size2 == 0: + logging.warning("Star database file is missing, first run? Will create one anew!") + else: + try: + with to_load.open("rb") as file: + star_store.db = pickle.load(file) + except Exception: + logging.exception("Unknown error loading star.p file") - #logging.info("Avaliabe: " + str(box_r)) - elif box_r <= 1: - gui.art_unlock_ratio = False - gui.art_max_ratio_lock = 1 - if side_drag and key_shift_down: - gui.art_unlock_ratio = True - gui.art_max_ratio_lock = 5 + album_star_path = user_directory / "album-star.p" + if album_star_path.is_file(): + try: + with album_star_path.open("rb") as file: + album_star_store.db = pickle.load(file) + except Exception: + logging.exception("Unknown error loading album-star.p file") + else: + logging.warning("Album star database file is missing, first run? Will create one anew!") - gui.rspw = gui.pref_rspw - if album_mode: - gui.rspw = gui.pref_gallery_w + if (user_directory / "lyrics_substitutions.json").is_file(): + try: + with (user_directory / "lyrics_substitutions.json").open() as f: + prefs.lyrics_subs = json.load(f) + except FileNotFoundError: + logging.error("No existing lyrics_substitutions.json file") + except Exception: + logging.exception("Unknown error loading lyrics_substitutions.json") + #11REWORK + state_path1 = user_directory / "state.p" + state_path2 = user_directory / "state.p.backup" + for t in range(2): + # os.path.getsize(user_directory / "state.p") < 100 + try: + if t == 0: + if not state_path1.is_file(): + continue + with state_path1.open("rb") as file: + save = pickle.load(file) + if t == 1: + if not state_path2.is_file(): + logging.warning("State database file is missing, first run? Will create one anew!") + break + logging.warning("Loading backup state.p!") + with state_path2.open("rb") as file: + save = pickle.load(file) + + # def tt(): + # while True: + # logging.info(state_file.tell()) + # time.sleep(0.01) + # shooter(tt) + + db_version = save[17] + if db_version != latest_db_version: + if db_version > latest_db_version: + logging.critical(f"Loaded DB version: '{db_version}' is newer than latest known DB version '{latest_db_version}', refusing to load!\nAre you running an out of date Tauon version using Configuration directory from a newer one?") + sys.exit(42) + logging.warning(f"Loaded older DB version: {db_version}") + if save[63] is not None: + prefs.ui_scale = save[63] + # prefs.ui_scale = 1.3 + # gui.__init__() + + if save[0] is not None: + master_library = save[0] + master_count = save[1] + playlist_playing = save[2] + playlist_active = save[3] + playlist_view_position = save[4] + if save[5] is not None: + if db_version > 68: + multi_playlist = [] + tauonplaylist_jar = save[5] + for d in tauonplaylist_jar: + nt = TauonPlaylist(**d) + multi_playlist.append(nt) + else: + multi_playlist = save[5] + volume = save[6] + track_queue = save[7] + playing_in_queue = save[8] + default_playlist = save[9] + # playlist_playing = save[10] + # cue_list = save[11] + # radio_field_text = save[12] + theme = save[13] + folder_image_offsets = save[14] + # lfm_username = save[15] + # lfm_hash = save[16] + view_prefs = save[18] + # window_size = save[19] + gui.save_size = copy.copy(save[19]) + gui.rspw = save[20] + # savetime = save[21] + gui.vis_want = save[22] + selected_in_playlist = save[23] + if save[24] is not None: + album_mode_art_size = save[24] + if save[25] is not None: + draw_border = save[25] + if save[26] is not None: + prefs.enable_web = save[26] + if save[27] is not None: + prefs.allow_remote = save[27] + if save[28] is not None: + prefs.expose_web = save[28] + if save[29] is not None: + prefs.enable_transcode = save[29] + if save[30] is not None: + prefs.show_rym = save[30] + # if save[31] is not None: + # combo_mode_art_size = save[31] + if save[32] is not None: + gui.maximized = save[32] + if save[33] is not None: + prefs.prefer_bottom_title = save[33] + if save[34] is not None: + gui.display_time_mode = save[34] + # if save[35] is not None: + # prefs.transcode_mode = save[35] + if save[36] is not None: + prefs.transcode_codec = save[36] + if save[37] is not None: + prefs.transcode_bitrate = save[37] + # if save[38] is not None: + # prefs.line_style = save[38] + # if save[39] is not None: + # prefs.cache_gallery = save[39] + if save[40] is not None: + prefs.playlist_font_size = save[40] + if save[41] is not None: + prefs.use_title = save[41] + if save[42] is not None: + gui.pl_st = save[42] + # if save[43] is not None: + # gui.set_mode = save[43] + # gui.set_bar = gui.set_mode + if save[45] is not None: + prefs.playlist_row_height = save[45] + if save[46] is not None: + prefs.show_wiki = save[46] + if save[47] is not None: + prefs.auto_extract = save[47] + if save[48] is not None: + prefs.colour_from_image = save[48] + if save[49] is not None: + gui.set_bar = save[49] + if save[50] is not None: + gui.gallery_show_text = save[50] + if save[51] is not None: + gui.bb_show_art = save[51] + # if save[52] is not None: + # gui.show_stars = save[52] + if save[53] is not None: + prefs.auto_lfm = save[53] + if save[54] is not None: + prefs.scrobble_mark = save[54] + if save[55] is not None: + prefs.replay_gain = save[55] + # if save[56] is not None: + # prefs.radio_page_lyrics = save[56] + if save[57] is not None: + prefs.show_gimage = save[57] + if save[58] is not None: + prefs.end_setting = save[58] + if save[59] is not None: + prefs.show_gen = save[59] + # if save[60] is not None: + # url_saves = save[60] + if save[61] is not None: + prefs.auto_del_zip = save[61] + if save[62] is not None: + gui.level_meter_colour_mode = save[62] + if save[64] is not None: + prefs.show_lyrics_side = save[64] + # if save[65] is not None: + # prefs.last_device = save[65] + if save[66] is not None: + gui.restart_album_mode = save[66] + if save[67] is not None: + album_playlist_width = save[67] + if save[68] is not None: + prefs.transcode_opus_as = save[68] + if save[69] is not None: + gui.star_mode = save[69] + if save[70] is not None: + gui.rsp = save[70] + if save[71] is not None: + gui.lsp = save[71] + if save[72] is not None: + gui.rspw = save[72] + if save[73] is not None: + gui.pref_gallery_w = save[73] + if save[74] is not None: + gui.pref_rspw = save[74] + if save[75] is not None: + gui.show_hearts = save[75] + if save[76] is not None: + prefs.monitor_downloads = save[76] + if save[77] is not None: + gui.artist_info_panel = save[77] + if save[78] is not None: + prefs.extract_to_music = save[78] + if save[79] is not None: + prefs.enable_lb = save[79] + # if save[80] is not None: + # prefs.lb_token = save[80] + # if prefs.lb_token is None: + # prefs.lb_token = "" + if save[81] is not None: + rename_files_previous = save[81] + if save[82] is not None: + rename_folder_previous = save[82] + if save[83] is not None: + prefs.use_jump_crossfade = save[83] + if save[84] is not None: + prefs.use_transition_crossfade = save[84] + if save[85] is not None: + prefs.show_notifications = save[85] + # if save[86] is not None: + # prefs.true_shuffle = save[86] + if save[87] is not None: + gui.remember_library_mode = save[87] + # if save[88] is not None: + # prefs.show_queue = save[88] + # if save[89] is not None: + # prefs.show_transfer = save[89] + if save[90] is not None: + if db_version > 68: + tauonqueueitem_jar = save[90] + for d in tauonqueueitem_jar: + nt = TauonQueueItem(**d) + p_force_queue.append(nt) + else: + p_force_queue = save[90] + if save[91] is not None: + prefs.use_pause_fade = save[91] + if save[92] is not None: + prefs.append_total_time = save[92] + if save[93] is not None: + prefs.backend = save[93] # moved to config file + if save[94] is not None: + prefs.album_shuffle_mode = save[94] + if save[95] is not None: + prefs.album_repeat_mode = save[95] + # if save[96] is not None: + # prefs.finish_current = save[96] + if save[97] is not None: + reload_state = save[97] + # if save[98] is not None: + # prefs.reload_play_state = save[98] + if save[99] is not None: + prefs.last_fm_token = save[99] + if save[100] is not None: + prefs.last_fm_username = save[100] + # if save[101] is not None: + # prefs.use_card_style = save[101] + # if save[102] is not None: + # prefs.auto_lyrics = save[102] + if save[103] is not None: + prefs.auto_lyrics_checked = save[103] + if save[104] is not None: + prefs.show_side_art = save[104] + if save[105] is not None: + prefs.window_opacity = save[105] + if save[106] is not None: + prefs.gallery_single_click = save[106] + if save[107] is not None: + prefs.tabs_on_top = save[107] + if save[108] is not None: + prefs.showcase_vis = save[108] + if save[109] is not None: + prefs.spec2_colour_mode = save[109] + # if save[110] is not None: + # prefs.device_buffer = save[110] + if save[111] is not None: + prefs.use_eq = save[111] + if save[112] is not None: + prefs.eq = save[112] + if save[113] is not None: + prefs.bio_large = save[113] + if save[114] is not None: + prefs.discord_show = save[114] + if save[115] is not None: + prefs.min_to_tray = save[115] + if save[116] is not None: + prefs.guitar_chords = save[116] + if save[117] is not None: + prefs.playback_follow_cursor = save[117] + if save[118] is not None: + prefs.art_bg = save[118] + if save[119] is not None: + prefs.random_mode = save[119] + if save[120] is not None: + prefs.repeat_mode = save[120] + if save[121] is not None: + prefs.art_bg_stronger = save[121] + if save[122] is not None: + prefs.art_bg_always_blur = save[122] + if save[123] is not None: + prefs.failed_artists = save[123] + if save[124] is not None: + prefs.artist_list = save[124] + if save[125] is not None: + prefs.auto_sort = save[125] + if save[126] is not None: + prefs.lyrics_enables = save[126] + if save[127] is not None: + prefs.fanart_notify = save[127] + if save[128] is not None: + prefs.bg_showcase_only = save[128] + if save[129] is not None: + prefs.discogs_pat = save[129] + if save[130] is not None: + prefs.mini_mode_mode = save[130] + if save[131] is not None: + after_scan = save[131] + if save[132] is not None: + gui.gallery_positions = save[132] + if save[133] is not None: + prefs.chart_bg = save[133] + if save[134] is not None: + prefs.left_panel_mode = save[134] + if save[135] is not None: + gui.last_left_panel_mode = save[135] + # if save[136] is not None: + # prefs.gst_device = save[136] + if save[137] is not None: + search_string_cache = save[137] + if save[138] is not None: + search_dia_string_cache = save[138] + if save[139] is not None: + gen_codes = save[139] + if save[140] is not None: + gui.show_ratings = save[140] + if save[141] is not None: + gui.show_album_ratings = save[141] + if save[142] is not None: + prefs.radio_urls = save[142] + if save[143] is not None: + gui.restore_showcase_view = save[143] + if save[144] is not None: + gui.saved_prime_tab = save[144] + if save[145] is not None: + gui.saved_prime_direction = save[145] + if save[146] is not None: + prefs.sync_playlist = save[146] + if save[147] is not None: + prefs.spot_client = save[147] + if save[148] is not None: + prefs.spot_secret = save[148] + if save[149] is not None: + prefs.show_band = save[149] + if save[150] is not None: + prefs.download_playlist = save[150] + if save[151] is not None: + spot_cache_saved_albums = save[151] + if save[152] is not None: + prefs.auto_rec = save[152] + if save[153] is not None: + prefs.spotify_token = save[153] + if save[154] is not None: + prefs.use_libre_fm = save[154] + if save[155] is not None: + prefs.old_playlist_box_position = save[155] + if save[156] is not None: + prefs.artist_list_sort_mode = save[156] + if save[157] is not None: + prefs.phazor_device_selected = save[157] + if save[158] is not None: + prefs.failed_background_artists = save[158] + if save[159] is not None: + prefs.bg_flips = save[159] + if save[160] is not None: + prefs.tray_show_title = save[160] + if save[161] is not None: + prefs.artist_list_style = save[161] + if save[162] is not None: + trackclass_jar = save[162] + for d in trackclass_jar: + nt = TrackClass() + nt.__dict__.update(d) + master_library[d["index"]] = nt + if save[163] is not None: + prefs.premium = save[163] + if save[164] is not None: + gui.restore_radio_view = save[164] + if save[165] is not None: + radio_playlists = save[165] + if save[166] is not None: + radio_playlist_viewing = save[166] + if save[167] is not None: + prefs.radio_thumb_bans = save[167] + if save[168] is not None: + prefs.playlist_exports = save[168] + if save[169] is not None: + prefs.show_chromecast = save[169] + if save[170] is not None: + prefs.cache_list = save[170] + if save[171] is not None: + prefs.shuffle_lock = save[171] + if save[172] is not None: + prefs.album_shuffle_lock_mode = save[172] + if save[173] is not None: + gui.was_radio = save[173] + if save[174] is not None: + prefs.spot_username = save[174] + # if save[175] is not None: + # prefs.spot_password = save[175] + if save[176] is not None: + prefs.artist_list_threshold = save[176] + if save[177] is not None: + prefs.tray_theme = save[177] + if save[178] is not None: + prefs.row_title_format = save[178] + if save[179] is not None: + prefs.row_title_genre = save[179] + if save[180] is not None: + prefs.row_title_separator_type = save[180] + if save[181] is not None: + prefs.replay_preamp = save[181] + if save[182] is not None: + prefs.gallery_combine_disc = save[182] + + del save + break - # Limit the right side panel width to height of area - if gui.rsp and prefs.side_panel_layout == 0: - if album_mode: - pass - else: + except IndexError: + logging.exception("Index error") + break + except Exception: + logging.exception("Failed to load save file") + logging.info(f"Database loaded in {round(perf_timer.get(), 3)} seconds.") + #12REWORK + # temporary + if window_size is None: + window_size = window_default_size + gui.rspw = 200 + #13REWORK + if download_directory.is_dir(): + download_directories.append(str(download_directory)) + + if music_directory is not None and music_directory.is_dir(): + download_directories.append(str(music_directory)) + #14REWORK + load_prefs() + save_prefs() - if not gui.art_unlock_ratio: + # Temporary + if 0 < db_version <= 34: + prefs.theme_name = get_theme_name(theme) + if 0 < db_version <= 66: + prefs.device_buffer = 80 + if 0 < db_version <= 53: + logging.info("Resetting fonts to defaults") + prefs.linux_font = "Noto Sans" + prefs.linux_font_semibold = "Noto Sans Medium" + prefs.linux_font_bold = "Noto Sans Bold" + save_prefs() - if gui.rsp_full_lock and not side_drag: - gui.rspw = window_size[0] + # Auto detect lang + lang: list[str] | None = None + if prefs.ui_lang != "auto" or prefs.ui_lang == "": + # Force set lang + lang = [prefs.ui_lang] - gui.rspw = min(gui.rspw, window_size[1] - gui.panelY - gui.panelBY) + f = gettext.find("tauon", localedir=str(locale_directory), languages=lang) + if f: + translation = gettext.translation("tauon", localedir=str(locale_directory), languages=lang) + translation.install() + builtins._ = translation.gettext - # Determine how wide the playlist need to be - gui.plw = window_size[0] - gui.playlist_left = 0 - if gui.lsp: - # if gui.plw > gui.lspw: - gui.plw -= gui.lspw - gui.playlist_left = gui.lspw - if gui.rsp: - gui.plw -= gui.rspw + logging.info(f"Translation file for '{lang}' loaded") + elif lang: + logging.error(f"No translation file available for '{lang}'") - # Shrink side panel if playlist gets too small - if window_size[0] > 100 and not gui.hide_tracklist_in_gallery: + # ---- - if gui.plw < 300: - if gui.rsp: + sss = SDL_SysWMinfo() + SDL_GetWindowWMInfo(t_window, sss) - l = 0 - if gui.lsp: - l = gui.lspw + if prefs.use_gamepad: + SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) - gui.rspw = max(window_size[0] - l - 300, 110) - # if album_mode and window_size[0] > 750 * gui.scale: - # gui.pref_gallery_w = gui.rspw + smtc = False - # Determine how wide the playlist need to be (again) - gui.plw = window_size[0] - gui.playlist_left = 0 - if gui.lsp: - # if gui.plw > gui.lspw: - gui.plw -= gui.lspw - gui.playlist_left = gui.lspw - if gui.rsp: - gui.plw -= gui.rspw + if msys and win_ver >= 10: - if window_size[0] < 630 * gui.scale: - gui.compact_bar = True + #logging.info(sss.info.win.window) + SMTC_path = install_directory / "lib" / "TauonSMTC.dll" + if SMTC_path.exists(): + try: + sm = ctypes.cdll.LoadLibrary(str(SMTC_path)) + + def SMTC_button_callback(button: int) -> None: + logging.debug(f"SMTC sent key ID: {button}") + if button == 1: + inp.media_key = "Play" + if button == 2: + inp.media_key = "Pause" + if button == 3: + inp.media_key = "Next" + if button == 4: + inp.media_key = "Previous" + if button == 5: + inp.media_key = "Stop" + gui.update += 1 + tauon.wake() + + close_callback = ctypes.WINFUNCTYPE(ctypes.c_void_p, ctypes.c_int)(SMTC_button_callback) + smtc = sm.init(close_callback) == 0 + except Exception: + logging.exception("Failed to load TauonSMTC.dll - Media keys will not work!") else: - gui.compact_bar = False + logging.warning("Failed to load TauonSMTC.dll - Media keys will not work!") + #15REWORK + auto_scale(prefs) + #16REWORK + scale_assets(scale_want=prefs.scale_want) - gui.pl_update = 1 + try: + #star_lines = view_prefs['star-lines'] + update_title = view_prefs["update-title"] + prefs.prefer_side = view_prefs["side-panel"] + prefs.dim_art = False # view_prefs['dim-art'] + #gui.turbo = view_prefs['level-meter'] + #pl_follow = view_prefs['pl-follow'] + scroll_enable = view_prefs["scroll-enable"] + if "break-enable" in view_prefs: + break_enable = view_prefs["break-enable"] + else: + logging.warning("break-enable not found in view_prefs[] when trying to load settings! First run?") + #dd_index = view_prefs['dd-index'] + #custom_line_mode = view_prefs['custom-line'] + #thick_lines = view_prefs['thick-lines'] + if "append-date" in view_prefs: + prefs.append_date = view_prefs["append-date"] + else: + logging.warning("append-date not found in view_prefs[] when trying to load settings! First run?") + except KeyError: + logging.exception("Failed to load settings - pref not found!") + except Exception: + logging.exception("Failed to load settings!") - # Tracklist sizing ---------------------------------------------------- - left = gui.playlist_left - width = gui.plw + if prefs.prefer_side is False: + gui.rsp = False + #17REWORK + pctl = PlayerCtl(prefs) + #18REWORK + lb = ListenBrainz(prefs) + #19REWORK - center_mode = True - if gui.lsp or gui.rsp or gui.set_mode: - center_mode = False + if system == "Linux" and not macos and not msys: + try: + Notify.init("Tauon Music Box") + g_tc_notify = Notify.Notification.new( + "Tauon Music Box", + "Transcoding has finished.") + value = GLib.Variant("s", t_id) + g_tc_notify.set_hint("desktop-entry", value) + + g_tc_notify.add_action( + "action_click", + "Open Output Folder", + g_open_encode_out, + None, + ) + + de_notify_support = True - if gui.set_mode and window_size[0] < 600: - center_mode = False + except Exception: + logging.exception("Failed init notifications") - highlight_left = 0 - highlight_width = width + if de_notify_support: + song_notification = Notify.Notification.new("Next track notification") + value = GLib.Variant("s", t_id) + song_notification.set_hint("desktop-entry", value) + lastfm = LastFMapi() - inset_left = highlight_left + 23 * gui.scale - inset_width = highlight_width - 32 * gui.scale + QuickThumbnail.renderer = holder.renderer - if gui.lsp and not gui.rsp: - inset_width -= 10 * gui.scale + #20REWORK + lfm_scrobbler = LastScrob() + strings = Strings() + signal.signal(signal.SIGINT, signal_handler) - if gui.lsp: - inset_left -= 10 * gui.scale - inset_width += 10 * gui.scale + if system == "Windows" or msys: + from lynxtray import SysTrayIcon - if center_mode: - if gui.set_mode: - highlight_left = int(pow((window_size[0] / gui.scale * 0.005), 2) * gui.scale) - else: - highlight_left = int(pow((window_size[0] / gui.scale * 0.01), 2) * gui.scale) + tray = STray() - if window_size[0] < 600 * gui.scale: - highlight_left = 3 * gui.scale + stats_gen = GStats() - highlight_width -= highlight_left * 2 - inset_left = highlight_left + 18 * gui.scale - inset_width = highlight_width - 25 * gui.scale + tauon = Tauon(holder) + #21REWORK + deco = Deco(tauon) + deco.get_themes = get_themes + deco.renderer = renderer - if window_size[0] < 600 and gui.lsp: - inset_width = highlight_width - 18 * gui.scale + if prefs.backend != 4: + prefs.backend = 4 - gui.tracklist_center_mode = center_mode - gui.tracklist_inset_left = inset_left - gui.tracklist_inset_width = inset_width - gui.tracklist_highlight_left = highlight_left - gui.tracklist_highlight_width = highlight_width + chrome = None - if album_mode and gui.hide_tracklist_in_gallery: - gui.show_playlist = False - gui.rspw = window_size[0] - 20 * gui.scale - if gui.lsp: - gui.rspw -= gui.lspw + try: + from tauon.t_modules.t_chrome import Chrome + chrome = Chrome(tauon) + except ModuleNotFoundError as e: + logging.debug(f"pychromecast import error: {e}") + logging.warning("Unable to import Chrome(pychromecast), chromecast support will be disabled.") + except Exception: + logging.exception("Unknown error trying to import Chrome(pychromecast), chromecast support will be disabled.") + finally: + logging.debug("Found Chrome(pychromecast) for chromecast support") - # -------------------------------------------------------------------- + tauon.chrome = chrome - if window_size[0] > gui.max_window_tex or window_size[1] > gui.max_window_tex: + plex = PlexService() + tauon.plex = plex - while window_size[0] > gui.max_window_tex: - gui.max_window_tex += 1000 - while window_size[1] > gui.max_window_tex: - gui.max_window_tex += 1000 + jellyfin = Jellyfin(tauon) + tauon.jellyfin = jellyfin - gui.tracklist_texture_rect = SDL_Rect(0, 0, gui.max_window_tex, gui.max_window_tex) + subsonic = SubsonicService() - SDL_DestroyTexture(gui.tracklist_texture) - SDL_RenderClear(renderer) - gui.tracklist_texture = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, - gui.max_window_tex, - gui.max_window_tex) + koel = KoelService() + tauon.koel = koel - SDL_SetRenderTarget(renderer, gui.tracklist_texture) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_RenderClear(renderer) - SDL_SetTextureBlendMode(gui.tracklist_texture, SDL_BLENDMODE_BLEND) + tau = TauService() + tauon.tau = tau + #22REWORK + if system == "Linux" and not macos and not msys: + gnome = Gnome(tauon) - # SDL_SetRenderTarget(renderer, gui.main_texture) - # SDL_RenderClear(renderer) + try: + gnomeThread = threading.Thread(target=gnome.main) + gnomeThread.daemon = True + gnomeThread.start() + except Exception: + logging.exception("Could not start Dbus thread") + + if (system == "Windows" or msys): + tray.start() + + if win_ver < 10: + logging.warning("Unsupported Windows version older than W10, hooking media keys the old way without SMTC!") + import keyboard + + def key_callback(event): + + if event.event_type == "down": + if event.scan_code == -179: + inp.media_key = "Play" + elif event.scan_code == -178: + inp.media_key = "Stop" + elif event.scan_code == -177: + inp.media_key = "Previous" + elif event.scan_code == -176: + inp.media_key = "Next" + gui.update += 1 + tauon.wake() + + keyboard.hook_key(-179, key_callback) + keyboard.hook_key(-178, key_callback) + keyboard.hook_key(-177, key_callback) + keyboard.hook_key(-176, key_callback) + #23REWORK + mac_circle = asset_loader(scaled_asset_directory, loaded_asset_dc, "macstyle.png", True) + #24REWORK + if not maximized and gui.maximized: + SDL_MaximizeWindow(t_window) - SDL_DestroyTexture(gui.main_texture) - gui.main_texture = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, - gui.max_window_tex, - gui.max_window_tex) - SDL_SetTextureBlendMode(gui.main_texture, SDL_BLENDMODE_BLEND) - SDL_SetRenderTarget(renderer, gui.main_texture) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_SetRenderTarget(renderer, gui.main_texture) - SDL_RenderClear(renderer) + # logging.error(SDL_GetError()) - SDL_DestroyTexture(gui.main_texture_overlay_temp) - gui.main_texture_overlay_temp = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_ARGB8888, - SDL_TEXTUREACCESS_TARGET, gui.max_window_tex, - gui.max_window_tex) - SDL_SetTextureBlendMode(gui.main_texture_overlay_temp, SDL_BLENDMODE_BLEND) - SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) - SDL_RenderClear(renderer) + # t_window = SDL_CreateShapedWindow( + # window_title, + # SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + # window_size[0], window_size[1], + # flags) - update_set() + # logging.error(SDL_GetError()) + + if system == "Windows" or msys: + gui.window_id = sss.info.win.window + + + + # ------------------------------------------------------------------------------------------- + # initiate SDL2 --------------------------------------------------------------------C-IS----- + + cursor_hand = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND) + cursor_standard = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW) + cursor_shift = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEWE) + cursor_text = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM) + + cursor_br_corner = cursor_standard + cursor_right_side = cursor_standard + cursor_top_side = cursor_standard + cursor_left_side = cursor_standard + cursor_bottom_side = cursor_standard + + if msys: + cursor_br_corner = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZENWSE) + cursor_right_side = cursor_shift + cursor_left_side = cursor_shift + cursor_top_side = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZENS) + cursor_bottom_side = cursor_top_side + elif not msys and system == "Linux" and "XCURSOR_THEME" in os.environ and "XCURSOR_SIZE" in os.environ: + try: + class XcursorImage(ctypes.Structure): + _fields_ = [ + ("version", c_uint32), + ("size", c_uint32), + ("width", c_uint32), + ("height", c_uint32), + ("xhot", c_uint32), + ("yhot", c_uint32), + ("delay", c_uint32), + ("pixels", c_void_p), + ] - if prefs.art_bg: - tauon.thread_manager.ready("style") + try: + xcu = ctypes.cdll.LoadLibrary("libXcursor.so") + except Exception: + logging.exception("Failed to load libXcursor.so, will try libXcursor.so.1") + xcu = ctypes.cdll.LoadLibrary("libXcursor.so.1") + xcu.XcursorLibraryLoadImage.restype = ctypes.POINTER(XcursorImage) + + def get_xcursor(name: str): + if "XCURSOR_THEME" not in os.environ: + raise ValueError("Missing XCURSOR_THEME in env") + if "XCURSOR_SIZE" not in os.environ: + raise ValueError("Missing XCURSOR_SIZE in env") + xcursor_theme = os.environ["XCURSOR_THEME"] + xcursor_size = os.environ["XCURSOR_SIZE"] + c1 = xcu.XcursorLibraryLoadImage(c_char_p(name.encode()), c_char_p(xcursor_theme.encode()), c_int(int(xcursor_size))).contents + sdl_surface = SDL_CreateRGBSurfaceWithFormatFrom(c1.pixels, c1.width, c1.height, 32, c1.width * 4, SDL_PIXELFORMAT_ARGB8888) + cursor = SDL_CreateColorCursor(sdl_surface, round(c1.xhot), round(c1.yhot)) + xcu.XcursorImageDestroy(ctypes.byref(c1)) + SDL_FreeSurface(sdl_surface) + return cursor + + cursor_br_corner = get_xcursor("se-resize") + cursor_right_side = get_xcursor("right_side") + cursor_top_side = get_xcursor("top_side") + cursor_left_side = get_xcursor("left_side") + cursor_bottom_side = get_xcursor("bottom_side") + + if SDL_GetCurrentVideoDriver() == b"wayland": + cursor_standard = get_xcursor("left_ptr") + cursor_text = get_xcursor("xterm") + cursor_shift = get_xcursor("sb_h_double_arrow") + cursor_hand = get_xcursor("hand2") + SDL_SetCursor(cursor_standard) -# SDL_RenderClear(renderer) -# SDL_RenderPresent(renderer) + except Exception: + logging.exception("Error loading xcursor") + #25REWORK -# SDL_ShowWindow(t_window) + # try: + # SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, b"1") + # + # except Exception: + # logging.exception("old version of SDL detected") -# Clear spectogram texture -SDL_SetRenderTarget(renderer, gui.spec2_tex) -SDL_RenderClear(renderer) -ddt.rect((0, 0, 1000, 1000), [7, 7, 7, 255]) + # get window surface and set up renderer + # renderer = SDL_CreateRenderer(t_window, 0, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC) -SDL_SetRenderTarget(renderer, gui.spec1_tex) -SDL_RenderClear(renderer) -ddt.rect((0, 0, 1000, 1000), [7, 7, 7, 255]) + # renderer = SDL_CreateRenderer(t_window, 0, SDL_RENDERER_ACCELERATED) + # + # # window_surface = SDL_GetWindowSurface(t_window) + # + # SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + # + # display_index = SDL_GetWindowDisplayIndex(t_window) + # display_bounds = SDL_Rect(0, 0) + # SDL_GetDisplayBounds(display_index, display_bounds) + # + # icon = IMG_Load(os.path.join(asset_directory, "icon-64.png").encode()) + # SDL_SetWindowIcon(t_window, icon) + # SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best".encode()) + # + # SDL_SetWindowMinimumSize(t_window, round(560 * gui.scale), round(330 * gui.scale)) + # + # + # gui.max_window_tex = 1000 + # if window_size[0] > gui.max_window_tex or window_size[1] > gui.max_window_tex: + # + # while window_size[0] > gui.max_window_tex: + # gui.max_window_tex += 1000 + # while window_size[1] > gui.max_window_tex: + # gui.max_window_tex += 1000 + # + # gui.ttext = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.max_window_tex, gui.max_window_tex) + # + # # gui.pl_surf = SDL_CreateRGBSurfaceWithFormat(0, gui.max_window_tex, gui.max_window_tex, 32, SDL_PIXELFORMAT_RGB888) + # + # SDL_SetTextureBlendMode(gui.ttext, SDL_BLENDMODE_BLEND) + # + # gui.spec2_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.spec2_w, gui.spec2_y) + # gui.spec1_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.spec_w, gui.spec_h) + # gui.spec4_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.spec4_w, gui.spec4_h) + # gui.spec_level_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.level_ww, gui.level_hh) + # + # SDL_SetTextureBlendMode(gui.spec4_tex, SDL_BLENDMODE_BLEND) + # + # SDL_SetRenderTarget(renderer, None) + # + # gui.main_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.max_window_tex, gui.max_window_tex) + # gui.main_texture_overlay_temp = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.max_window_tex, gui.max_window_tex) + # + # SDL_SetRenderTarget(renderer, gui.main_texture) + # SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) + # + # SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) + # SDL_SetTextureBlendMode(gui.main_texture_overlay_temp, SDL_BLENDMODE_BLEND) + # SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) + # + # SDL_RenderClear(renderer) + # + # gui.abc = SDL_Rect(0, 0, gui.max_window_tex, gui.max_window_tex) + # gui.pl_update = 2 + # + # SDL_SetWindowOpacity(t_window, prefs.window_opacity) + + # gui.spec1_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.spec_w, gui.spec_h) + # gui.spec4_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.spec4_w, gui.spec4_h) + # gui.spec_level_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gui.level_ww, gui.level_hh) + # SDL_SetTextureBlendMode(gui.spec4_tex, SDL_BLENDMODE_BLEND) + + + if (system == "Windows" or msys) and taskbar_progress: + + class WinTask: + + def __init__(self): + self.start = time.time() + self.updated_state = 0 + self.window_id = gui.window_id + import comtypes.client as cc + cc.GetModule(str(install_directory / "TaskbarLib.tlb")) + import comtypes.gen.TaskbarLib as tbl + self.taskbar = cc.CreateObject( + "{56FDF344-FD6D-11d0-958A-006097C9A090}", + interface=tbl.ITaskbarList3) + self.taskbar.HrInit() + + self.d_timer = Timer() + + def update(self, force=False): + if self.d_timer.get() > 2 or force: + self.d_timer.set() + + if pctl.playing_state == 1 and self.updated_state != 1: + self.taskbar.SetProgressState(self.window_id, 0x2) + + if pctl.playing_state == 1: + self.updated_state = 1 + if pctl.playing_length > 2: + perc = int(pctl.playing_time * 100 / int(pctl.playing_length)) + if perc < 2: + perc = 1 + elif perc > 100: + prec = 100 + else: + perc = 0 -SDL_SetRenderTarget(renderer, gui.spec_level_tex) -SDL_RenderClear(renderer) -ddt.rect((0, 0, 1000, 1000), [7, 7, 7, 255]) + self.taskbar.SetProgressValue(self.window_id, perc, 100) -SDL_SetRenderTarget(renderer, None) + elif pctl.playing_state == 2 and self.updated_state != 2: + self.updated_state = 2 + self.taskbar.SetProgressState(self.window_id, 0x8) + elif pctl.playing_state == 0 and self.updated_state != 0: + self.updated_state = 0 + self.taskbar.SetProgressState(self.window_id, 0x2) + self.taskbar.SetProgressValue(self.window_id, 0, 100) -# SDL_RenderPresent(renderer) -# time.sleep(3) + if (install_directory / "TaskbarLib.tlb").is_file(): + logging.info("Taskbar progress enabled") + pctl.windows_progress = WinTask() -class GetSDLInput: + else: + pctl.taskbar_progress = False + logging.warning("Could not find TaskbarLib.tlb") - def __init__(self): - self.i_y = pointer(c_int(0)) - self.i_x = pointer(c_int(0)) + #25REWORK - self.mouse_capture_want = False - self.mouse_capture = False + ddt = TDraw(renderer) + ddt.scale = gui.scale + ddt.force_subpixel_text = prefs.force_subpixel_text - def mouse(self): - SDL_PumpEvents() - SDL_GetMouseState(self.i_x, self.i_y) - return int(self.i_x.contents.value / logical_size[0] * window_size[0]), int( - self.i_y.contents.value / logical_size[0] * window_size[0]) + launch = Launch(tauon, pctl, gui, ddt) + draw = Drawing() + #26REWORK + if system == "Linux": + prime_fonts(prefs) + else: + # standard_font = "Meiryo" + standard_font = "Arial" + # semibold_font = "Meiryo Semibold" + semibold_font = "Arial Bold" + standard_weight = 500 + bold_weight = 600 + ddt.win_prime_font(standard_font, 14, 10, weight=standard_weight, y_offset=0) + ddt.win_prime_font(standard_font, 15, 11, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 15, 11.5, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 15, 12, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 15, 13, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 16, 14, weight=standard_weight, y_offset=0) + ddt.win_prime_font(standard_font, 16, 14.5, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 17, 15, weight=standard_weight, y_offset=-1) + ddt.win_prime_font(standard_font, 20, 16, weight=standard_weight, y_offset=-2) + ddt.win_prime_font(standard_font, 20, 17, weight=standard_weight, y_offset=-1) + + ddt.win_prime_font(standard_font, 30 + 4, 30, weight=standard_weight, y_offset=-12) + ddt.win_prime_font(semibold_font, 9, 209, weight=bold_weight, y_offset=1) + ddt.win_prime_font("Arial", 10 + 4, 210, weight=600, y_offset=2) + ddt.win_prime_font("Arial", 11 + 3, 211, weight=600, y_offset=2) + ddt.win_prime_font(semibold_font, 12 + 4, 212, weight=bold_weight, y_offset=1) + ddt.win_prime_font(semibold_font, 13 + 3, 213, weight=bold_weight, y_offset=-1) + ddt.win_prime_font(semibold_font, 14 + 2, 214, weight=bold_weight, y_offset=1) + ddt.win_prime_font(semibold_font, 15 + 2, 215, weight=bold_weight, y_offset=1) + ddt.win_prime_font(semibold_font, 16 + 2, 216, weight=bold_weight, y_offset=1) + ddt.win_prime_font(semibold_font, 17 + 2, 218, weight=bold_weight, y_offset=1) + ddt.win_prime_font(semibold_font, 18 + 2, 218, weight=bold_weight, y_offset=1) + ddt.win_prime_font(semibold_font, 19 + 2, 220, weight=bold_weight, y_offset=1) + ddt.win_prime_font(semibold_font, 28 + 2, 228, weight=bold_weight, y_offset=1) + + standard_weight = 550 + ddt.win_prime_font(standard_font, 14, 310, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 15, 311, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 16, 312, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 17, 313, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 18, 314, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 19, 315, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 20, 316, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 21, 317, weight=standard_weight, y_offset=1) + + standard_font = "Arial Narrow" + standard_weight = 500 + + ddt.win_prime_font(standard_font, 14, 410, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 15, 411, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 16, 412, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 17, 413, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 18, 414, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 19, 415, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 20, 416, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 21, 417, weight=standard_weight, y_offset=1) + + standard_weight = 600 + + ddt.win_prime_font(standard_font, 14, 510, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 15, 511, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 16, 512, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 17, 513, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 18, 514, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 19, 515, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 20, 516, weight=standard_weight, y_offset=1) + ddt.win_prime_font(standard_font, 21, 517, weight=standard_weight, y_offset=1) + drop_shadow = DropShadow(gui) + lyrics_ren_mini = LyricsRenMini() + lyrics_ren = LyricsRen() + tauon.synced_to_static_lyrics = TimedLyricsToStatic() + timed_lyrics_ren = TimedLyricsRen() + text_box_canvas_rect = SDL_Rect(0, 0, round(2000 * gui.scale), round(40 * gui.scale)) + text_box_canvas_hide_rect = SDL_Rect(0, 0, round(2000 * gui.scale), round(40 * gui.scale)) + text_box_canvas = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, text_box_canvas_rect.w, text_box_canvas_rect.h) + SDL_SetTextureBlendMode(text_box_canvas, SDL_BLENDMODE_BLEND) + + rename_text_area = TextBox() + gst_output_field = TextBox2() + gst_output_field.text = prefs.gst_output + search_text = TextBox() + rename_files = TextBox2() + sub_lyrics_a = TextBox2() + sub_lyrics_b = TextBox2() + sync_target = TextBox2() + edit_artist = TextBox2() + edit_album = TextBox2() + edit_title = TextBox2() + edit_album_artist = TextBox2() + + rename_files.text = prefs.rename_tracks_template + if rename_files_previous: + rename_files.text = rename_files_previous + + text_plex_usr = TextBox2() + text_plex_pas = TextBox2() + text_plex_ser = TextBox2() + + text_jelly_usr = TextBox2() + text_jelly_pas = TextBox2() + text_jelly_ser = TextBox2() + + text_koel_usr = TextBox2() + text_koel_pas = TextBox2() + text_koel_ser = TextBox2() + + text_air_usr = TextBox2() + text_air_pas = TextBox2() + text_air_ser = TextBox2() + + text_spot_client = TextBox2() + text_spot_secret = TextBox2() + text_spot_username = TextBox2() + text_spot_password = TextBox2() + + text_maloja_url = TextBox2() + text_maloja_key = TextBox2() + + text_sat_url = TextBox2() + text_sat_playlist = TextBox2() + + rename_folder = TextBox2() + rename_folder.text = prefs.rename_folder_template + if rename_folder_previous: + rename_folder.text = rename_folder_previous + + temp_dest = SDL_Rect(0, 0) + + album_art_gen = AlbumArt() + + # 0 - blank + # 1 - preparing first + # 2 - render first + # 3 - preparing 2nd + + style_overlay = StyleOverlay() + #27REWORK + message_info_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "notice.png") + message_warning_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "warning.png") + message_tick_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "done.png") + message_arrow_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "ext.png") + message_error_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "error.png") + message_bubble_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "bubble.png") + message_download_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "ddl.png") + + + + + tool_tip = ToolTip() + tool_tip2 = ToolTip() + tool_tip2.trigger = 1.8 + track_box_path_tool_timer = Timer() + + columns_tool_tip = ToolTip3() + tool_tip_instant = ToolTip3() + + # Create empty area menu + playlist_menu = Menu(tauon, 130) + radio_entry_menu = Menu(tauon, 125) + showcase_menu = Menu(tauon, 135) + center_info_menu = Menu(tauon, 125) + cancel_menu = Menu(tauon, 100) + gallery_menu = Menu(tauon, 175, show_icons=True) + artist_info_menu = Menu(tauon, 135) + queue_menu = Menu(tauon, 150) + repeat_menu = Menu(tauon, 120) + shuffle_menu = Menu(tauon, 120) + artist_list_menu = Menu(tauon, 165, show_icons=True) + lightning_menu = Menu(tauon, 165) + lsp_menu = Menu(tauon, 145) + folder_tree_menu = Menu(tauon, 175, show_icons=True) + folder_tree_stem_menu = Menu(tauon, 190, show_icons=True) + overflow_menu = Menu(tauon, 175) + spotify_playlist_menu = Menu(tauon, 175) + radio_context_menu = Menu(tauon, 175) + #chrome_menu = Menu(tauon, 175) + + # . Menu entry: A side panel view layout + lsp_menu.add(MenuItem(_("Playlists + Queue"), enable_playlist_list, disable_test=lsp_menu_test_playlist)) + lsp_menu.add(MenuItem(_("Queue"), enable_queue_panel, disable_test=lsp_menu_test_queue)) + # . Menu entry: Side panel view layout showing a list of artists with thumbnails + lsp_menu.add(MenuItem(_("Artist List"), enable_artist_list, disable_test=lsp_menu_test_artist)) + # . Menu entry: A side panel view layout. Alternative name: Folder Tree + lsp_menu.add(MenuItem(_("Folder Navigator"), enable_folder_list, disable_test=lsp_menu_test_tree)) + + radio_entry_menu.add(MenuItem(_("Visit Website"), visit_radio_site, visit_radio_site_deco, pass_ref=True, pass_ref_deco=True)) + radio_entry_menu.add(MenuItem(_("Save"), save_to_radios, pass_ref=True)) + + rename_track_box = RenameTrackBox() + trans_edit_box = TransEditBox() + sub_lyrics_box = SubLyricsBox() + export_playlist_box = ExportPlaylistBox() + rename_playlist_box = RenamePlaylistBox() + playlist_box = PlaylistBox() + + tauon.toggle_repeat = toggle_repeat + tauon.menu_album_repeat = menu_album_repeat + tauon.menu_repeat_off = menu_repeat_off + tauon.menu_set_repeat = menu_set_repeat + tauon.toggle_random = toggle_random + + repeat_menu.add(MenuItem(_("Repeat OFF"), menu_repeat_off)) + repeat_menu.add(MenuItem(_("Repeat Track"), menu_set_repeat)) + repeat_menu.add(MenuItem(_("Repeat Album"), menu_album_repeat)) + + artist_list_menu.add_to_sub(0, MenuItem(_("Sort Alphabetically"), aa_sort_alpha)) + artist_list_menu.add_to_sub(0, MenuItem(_("Sort by Popularity"), aa_sort_popular)) + artist_list_menu.add_to_sub(0, MenuItem(_("Sort by Playtime"), aa_sort_play)) + artist_list_menu.add_to_sub(0, MenuItem(_("Toggle Thumbnails"), toggle_artist_list_style)) + artist_list_menu.add_to_sub(0, MenuItem(_("Toggle Filter"), toggle_artist_list_threshold, toggle_artist_list_threshold_deco)) + + shuffle_menu.add(MenuItem(_("Shuffle Lockdown"), toggle_shuffle_layout)) + shuffle_menu.add(MenuItem(_("Shuffle Lockdown Albums"), toggle_shuffle_layout_albums)) + shuffle_menu.br() + shuffle_menu.add(MenuItem(_("Shuffle OFF"), menu_shuffle_off)) + shuffle_menu.add(MenuItem(_("Shuffle Tracks"), menu_set_random)) + shuffle_menu.add(MenuItem(_("Random Albums"), menu_album_random)) + + artist_info_menu.add(MenuItem(_("Close Panel"), artist_info_panel_close)) + artist_info_menu.add(MenuItem(_("Make Large"), toggle_bio_size, toggle_bio_size_deco)) + + filter_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "filter.png", True)) + filter_icon.colour = [43, 213, 255, 255] + filter_icon.xoff = 1 + + folder_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "folder.png", True)) + info_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "info.png", True)) + + folder_icon.colour = [244, 220, 66, 255] + info_icon.colour = [61, 247, 163, 255] + + power_bar_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "power.png", True) + + move_jobs = [] + move_in_progress = False + + folder_tree_stem_menu.add(MenuItem(_("Open Folder"), open_folder_stem, pass_ref=True, icon=folder_icon)) + folder_tree_menu.add(MenuItem(_("Open Folder"), open_folder, pass_ref=True, pass_ref_deco=True, icon=folder_icon, disable_test=open_folder_disable_test)) + + lightning_menu.add(MenuItem(_("Filter to New Playlist"), tag_to_new_playlist, pass_ref=True, icon=filter_icon)) + folder_tree_menu.add(MenuItem(_("Filter to New Playlist"), folder_to_new_playlist_by_track_id, pass_ref=True, icon=filter_icon)) + folder_tree_stem_menu.add(MenuItem(_("Filter to New Playlist"), stem_to_new_playlist, pass_ref=True, icon=filter_icon)) + folder_tree_stem_menu.add(MenuItem(_("Rescan Folder"), re_import3, pass_ref=True)) + folder_tree_menu.add(MenuItem(_("Rescan Folder"), re_import4, pass_ref=True)) + lightning_menu.add(MenuItem(_("Move Playing Folder Here"), move_playing_folder_to_tag, pass_ref=True)) + + folder_tree_stem_menu.add(MenuItem(_("Move Playing Folder Here"), move_playing_folder_to_tree_stem, pass_ref=True)) + + folder_tree_stem_menu.br() + + folder_tree_stem_menu.add(MenuItem(_("Collapse All"), collapse_tree, collapse_tree_deco)) + + folder_tree_stem_menu.add(MenuItem("lock", lock_folder_tree, lock_folder_tree_deco)) + # folder_tree_menu.add("lock", lock_folder_tree, lock_folder_tree_deco) + + gallery_menu.add(MenuItem(_("Open Folder"), open_folder, pass_ref=True, pass_ref_deco=True, icon=folder_icon, disable_test=open_folder_disable_test)) + gallery_menu.add(MenuItem(_("Show in Playlist"), show_in_playlist)) + gallery_menu.add_sub(_("Image…"), 160) + gallery_menu.add(MenuItem(_("Add Album to Queue"), add_album_to_queue, pass_ref=True)) + gallery_menu.add(MenuItem(_("Enqueue Album Next"), add_album_to_queue_fc, pass_ref=True)) + + cancel_menu.add(MenuItem(_("Cancel"), cancel_import)) + + showcase_menu.add(MenuItem(_("Search for Lyrics"), get_lyric_wiki, search_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + showcase_menu.add(MenuItem("Toggle synced", toggle_synced_lyrics, toggle_synced_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + showcase_menu.add(MenuItem(_("Toggle Lyrics"), toggle_lyrics, toggle_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + showcase_menu.add_sub(_("Misc…"), 150) + showcase_menu.add_to_sub(0, MenuItem(_("Substitute Search..."), show_sub_search, pass_ref=True)) + showcase_menu.add_to_sub(0, MenuItem(_("Paste Lyrics"), paste_lyrics, paste_lyrics_deco, pass_ref=True)) + showcase_menu.add_to_sub(0, MenuItem(_("Copy Lyrics"), copy_lyrics, copy_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + showcase_menu.add_to_sub(0, MenuItem(_("Clear Lyrics"), clear_lyrics, clear_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + showcase_menu.add_to_sub(0, MenuItem(_("Toggle art panel"), toggle_side_art, toggle_side_art_deco, show_test=lyrics_in_side_show)) + showcase_menu.add_to_sub(0, MenuItem(_("Toggle art position"), + toggle_lyrics_panel_position, toggle_lyrics_panel_position_deco, show_test=lyrics_in_side_show)) + + center_info_menu.add(MenuItem(_("Search for Lyrics"), get_lyric_wiki, search_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + center_info_menu.add(MenuItem(_("Toggle Lyrics"), toggle_lyrics, toggle_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + center_info_menu.add_sub(_("Misc…"), 150) + center_info_menu.add_to_sub(0, MenuItem(_("Substitute Search..."), show_sub_search, pass_ref=True)) + center_info_menu.add_to_sub(0, MenuItem(_("Paste Lyrics"), paste_lyrics, paste_lyrics_deco, pass_ref=True)) + center_info_menu.add_to_sub(0, MenuItem(_("Copy Lyrics"), copy_lyrics, copy_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + center_info_menu.add_to_sub(0, MenuItem(_("Clear Lyrics"), clear_lyrics, clear_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + center_info_menu.add_to_sub(0, MenuItem(_("Toggle art panel"), toggle_side_art, toggle_side_art_deco, show_test=lyrics_in_side_show)) + center_info_menu.add_to_sub(0, MenuItem(_("Toggle art position"), + toggle_lyrics_panel_position, toggle_lyrics_panel_position_deco, show_test=lyrics_in_side_show)) + + picture_menu = Menu(tauon, 175) + picture_menu.add(MenuItem(_("Open Image"), open_image, open_image_deco, pass_ref=True, pass_ref_deco=True, disable_test=open_image_disable_test)) + # Next and previous pictures + picture_menu.add(MenuItem(_("Next Image"), cycle_offset, cycle_image_deco, pass_ref=True, pass_ref_deco=True)) + #picture_menu.add(_("Previous"), cycle_offset_back, cycle_image_deco, pass_ref=True, pass_ref_deco=True) + + # Extract embedded artwork from file + picture_menu.add(MenuItem(_("Extract Image"), save_embed_img, extract_image_deco, pass_ref=True, pass_ref_deco=True, disable_test=save_embed_img_disable_test)) + + del_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "del.png", True) + delete_icon = MenuIcon(del_icon) + + picture_menu.add( + MenuItem(_("Delete Image File"), delete_track_image, delete_track_image_deco, pass_ref=True, + pass_ref_deco=True, icon=delete_icon)) + + picture_menu.add(MenuItem(_("Quick-Fetch Cover Art"), download_art1_fire, dl_art_deco, pass_ref=True, pass_ref_deco=True, disable_test=download_art1_fire_disable_test)) + # picture_menu.add(_('Search Google for Images'), ser_gimage, search_image_deco, pass_ref=True, pass_ref_deco=True, show_test=toggle_gimage) + + # picture_menu.add(_('Toggle art box'), toggle_side_art, toggle_side_art_deco) + + picture_menu.add(MenuItem(_("Search for Lyrics"), get_lyric_wiki, search_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + picture_menu.add(MenuItem(_("Toggle Lyrics"), toggle_lyrics, toggle_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + + gallery_menu.add_to_sub(0, MenuItem(_("Next"), cycle_offset, cycle_image_gal_deco, pass_ref=True, pass_ref_deco=True)) + gallery_menu.add_to_sub(0, MenuItem(_("Previous"), cycle_offset_back, cycle_image_gal_deco, pass_ref=True, pass_ref_deco=True)) + gallery_menu.add_to_sub(0, MenuItem(_("Open Image"), open_image, open_image_deco, pass_ref=True, pass_ref_deco=True, disable_test=open_image_disable_test)) + gallery_menu.add_to_sub(0, MenuItem(_("Extract Image"), save_embed_img, extract_image_deco, pass_ref=True, pass_ref_deco=True, disable_test=save_embed_img_disable_test)) + gallery_menu.add_to_sub(0, MenuItem(_("Delete Image <combined>"), delete_track_image, delete_track_image_deco, pass_ref=True, pass_ref_deco=True)) #, icon=delete_icon) + gallery_menu.add_to_sub(0, MenuItem(_("Quick-Fetch Cover Art"), download_art1_fire, dl_art_deco, pass_ref=True, pass_ref_deco=True, disable_test=download_art1_fire_disable_test)) + # playlist_menu.add('Paste', append_here, paste_deco) + + # Create playlist tab menu + tab_menu = Menu(tauon, 160, show_icons=True) + tab_menu.add(MenuItem(_("Rename"), rename_playlist, pass_ref=True, hint="Ctrl+R")) + + radio_tab_menu = Menu(tauon, 160, show_icons=True) + radio_tab_menu.add(MenuItem(_("Rename"), rename_playlist, pass_ref=True, hint="Ctrl+R")) + tab_menu.add(MenuItem("Pin", pin_playlist_toggle, pl_pin_deco, pass_ref=True, pass_ref_deco=True)) + + lock_asset = asset_loader(scaled_asset_directory, loaded_asset_dc, "lock.png", True) + lock_icon = MenuIcon(lock_asset) + lock_icon.base_asset_mod = asset_loader(scaled_asset_directory, loaded_asset_dc, "unlock.png", True) + lock_icon.colour = [240, 190, 10, 255] + lock_icon.colour_callback = lock_colour_callback + lock_icon.xoff = 4 + lock_icon.yoff = -1 + + tab_menu.add(MenuItem(_("Lock"), lock_playlist_toggle, pl_lock_deco, + pass_ref=True, pass_ref_deco=True, icon=lock_icon, show_test=test_shift)) + + # Clear playlist + tab_menu.add(MenuItem(_("Clear"), clear_playlist, pass_ref=True, disable_test=test_pl_tab_locked, pass_ref_deco=True)) + + to_scan = [] + + tauon.sort_track_2 = sort_track_2 + + delete_icon.xoff = 3 + delete_icon.colour = [249, 70, 70, 255] + + tab_menu.add(MenuItem(_("Delete"), + delete_playlist_force, pass_ref=True, hint="Ctrl+W", icon=delete_icon, disable_test=test_pl_tab_locked, pass_ref_deco=True)) + radio_tab_menu.add(MenuItem(_("Delete"), + delete_playlist_force, pass_ref=True, hint="Ctrl+W", icon=delete_icon, disable_test=test_pl_tab_locked, pass_ref_deco=True)) + + heartx_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-menu.png", True)) + spot_heartx_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-menu.png", True)) + transcode_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "transcode.png", True)) + mod_folder_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "mod_folder.png", True)) + settings_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "settings2.png", True)) + rename_tracks_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "pen.png", True)) + add_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "new.png", True)) + spot_asset = asset_loader(scaled_asset_directory, loaded_asset_dc, "spot.png", True) + spot_icon = MenuIcon(spot_asset) + spot_icon.colour = [30, 215, 96, 255] + spot_icon.xoff = 5 + spot_icon.yoff = 2 + + jell_icon = MenuIcon(spot_asset) + jell_icon.colour = [190, 100, 210, 255] + jell_icon.xoff = 5 + jell_icon.yoff = 2 + + tab_menu.br() + + column_names = ( + "Artist", + "Album Artist", + "Album", + "Title", + "Composer", + "Time", + "Date", + "Genre", + "#", + "P", + "Starline", + "Rating", + "Comment", + "Codec", + "Lyrics", + "Bitrate", + "S", + "Filename", + "Disc", + "CUE", + ) + + extra_tab_menu = Menu(tauon, 155, show_icons=True) + + extra_tab_menu.add(MenuItem(_("New Playlist"), new_playlist, icon=add_icon)) + + tab_menu.add(MenuItem(_("Upload"), + upload_spotify_playlist, pass_ref=True, pass_ref_deco=True, icon=jell_icon, show_test=spotify_show_test)) + tab_menu.add(MenuItem(_("Upload"), + upload_jellyfin_playlist, pass_ref=True, pass_ref_deco=True, icon=spot_icon, show_test=jellyfin_show_test)) + + tab_menu.add(MenuItem(_("Regenerate"), regen_playlist_async, regenerate_deco, pass_ref=True, pass_ref_deco=True, hint="Alt+R")) + tab_menu.add_sub(_("Generate…"), 150) + tab_menu.add_sub(_("Sort…"), 170) + extra_tab_menu.add_sub(_("From Current…"), 133) + # tab_menu.add(_("Sort by Filepath"), standard_sort, pass_ref=True, disable_test=test_pl_tab_locked, pass_ref_deco=True) + # tab_menu.add(_("Sort Track Numbers"), sort_track_2, pass_ref=True) + # tab_menu.add(_("Sort Year per Artist"), year_sort, pass_ref=True) + + tab_menu.add_to_sub(1, MenuItem(_("Sort by Imported Tracks"), imported_sort, pass_ref=True)) + tab_menu.add_to_sub(1, MenuItem(_("Sort by Imported Folders"), imported_sort_folders, pass_ref=True)) + tab_menu.add_to_sub(1, MenuItem(_("Sort by Filepath"), standard_sort, pass_ref=True)) + tab_menu.add_to_sub(1, MenuItem(_("Sort Track Numbers"), sort_track_2, pass_ref=True)) + tab_menu.add_to_sub(1, MenuItem(_("Sort Year per Artist"), year_sort, pass_ref=True)) + tab_menu.add_to_sub(1, MenuItem(_("Make Playlist Auto-Sorting"), make_auto_sorting, pass_ref=True)) + + tab_menu.br() + + tab_menu.add(MenuItem(_("Rescan Folder"), re_import2, rescan_deco, pass_ref=True, pass_ref_deco=True)) + + tab_menu.add(MenuItem(_("Paste"), s_append, paste_deco, pass_ref=True)) + tab_menu.add(MenuItem(_("Append Playing"), append_current_playing, append_deco, pass_ref=True)) + tab_menu.br() + + # tab_menu.add("Sort By Filepath", sort_path_pl, pass_ref=True) + + tab_menu.add(MenuItem(_("Export…"), export_playlist_box.activate, pass_ref=True)) + + tab_menu.add_sub(_("Misc…"), 175) + tab_menu.add_to_sub(2, MenuItem(_("Export Playlist Stats"), export_stats, pass_ref=True)) + tab_menu.add_to_sub(2, MenuItem(_("Export Albums CSV"), export_playlist_albums, pass_ref=True)) + tab_menu.add_to_sub(2, MenuItem(_("Transcode All"), convert_playlist, pass_ref=True)) + tab_menu.add_to_sub(2, MenuItem(_("Rescan Tags"), rescan_tags, pass_ref=True)) + # tab_menu.add_to_sub(_('Forget Import Folder'), 2, forget_pl_import_folder, rescan_deco, pass_ref=True, pass_ref_deco=True) + # tab_menu.add_to_sub(_('Re-Import Last Folder'), 1, re_import, pass_ref=True) + # tab_menu.add_to_sub(_('Quick Export XSPF'), 2, export_xspf, pass_ref=True) + # tab_menu.add_to_sub(_('Quick Export M3U'), 2, export_m3u, pass_ref=True) + tab_menu.add_to_sub(2, MenuItem(_("Toggle Breaks"), pl_toggle_playlist_break, pass_ref=True)) + tab_menu.add_to_sub(2, MenuItem(_("Edit Generator..."), edit_generator_box, pass_ref=True)) + tab_menu.add_to_sub(2, MenuItem(_("Engage Gallery Quick Add"), start_quick_add, pass_ref=True)) + tab_menu.add_to_sub(2, MenuItem(_("Set as Sync Playlist"), set_sync_playlist, sync_playlist_deco, pass_ref_deco=True, pass_ref=True)) + tab_menu.add_to_sub(2, MenuItem(_("Set as Downloads Playlist"), set_download_playlist, set_download_deco, pass_ref_deco=True, pass_ref=True)) + tab_menu.add_to_sub(2, MenuItem(_("Set podcast mode"), set_podcast_playlist, set_podcast_deco, pass_ref_deco=True, pass_ref=True)) + tab_menu.add_to_sub(2, MenuItem(_("Remove Duplicates"), remove_duplicates, pass_ref=True)) + tab_menu.add_to_sub(2, MenuItem(_("Toggle Console"), console.toggle)) + + # tab_menu.add_to_sub("Empty Playlist", 0, new_playlist) + tab_menu.add_to_sub(0, MenuItem(_("Top Played Tracks"), gen_top_100, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Top Played Tracks"), gen_top_100, pass_ref=True)) + + tab_menu.add_to_sub(0, MenuItem(_("Top Played Albums"), gen_folder_top, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Top Played Albums"), gen_folder_top, pass_ref=True)) + + tab_menu.add_to_sub(0, MenuItem(_("Top Rated Tracks"), gen_top_rating, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Top Rated Tracks"), gen_top_rating, pass_ref=True)) + + tab_menu.add_to_sub(0, MenuItem(_("Top Rated Albums"), gen_folder_top_rating, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Top Rated Albums"), gen_folder_top_rating, pass_ref=True)) - def test_capture_mouse(self): - if not self.mouse_capture and self.mouse_capture_want: - SDL_CaptureMouse(SDL_TRUE) - self.mouse_capture = True - elif self.mouse_capture and not self.mouse_capture_want: - SDL_CaptureMouse(SDL_FALSE) - self.mouse_capture = False + tab_menu.add_to_sub(0, MenuItem(_("File Modified"), gen_last_modified, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("File Modified"), gen_last_modified, pass_ref=True)) + # tab_menu.add_to_sub(_("File Path"), 0, standard_sort, pass_ref=True) + # extra_tab_menu.add_to_sub(_("File Path"), 0, standard_sort, pass_ref=True) -gal_up = False -gal_down = False -gal_left = False -gal_right = False + tab_menu.add_to_sub(0, MenuItem(_("Longest Tracks"), gen_sort_len, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Longest Tracks"), gen_sort_len, pass_ref=True)) -get_sdl_input = GetSDLInput() + tab_menu.add_to_sub(0, MenuItem(_("Longest Albums"), gen_folder_duration, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Longest Albums"), gen_folder_duration, pass_ref=True)) + tab_menu.add_to_sub(0, MenuItem(_("Year by Oldest"), gen_sort_date, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Year by Oldest"), gen_sort_date, pass_ref=True)) -def window_is_focused() -> bool: - """Thread safe?""" - if SDL_GetWindowFlags(t_window) & SDL_WINDOW_INPUT_FOCUS: - return True - return False + tab_menu.add_to_sub(0, MenuItem(_("Year by Latest"), gen_sort_date_new, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Year by Latest"), gen_sort_date_new, pass_ref=True)) + # tab_menu.add_to_sub(_("Year by Artist"), 0, year_sort, pass_ref=True) + # extra_tab_menu.add_to_sub(_("Year by Artist"), 0, year_sort, pass_ref=True) -def save_state() -> None: - if should_save_state: - logging.info("Writing database to disk... ") - else: - logging.warning("Dev mode, not saving state... ") - return + tab_menu.add_to_sub(0, MenuItem(_("Shuffled Tracks"), gen_500_random, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Shuffled Tracks"), gen_500_random, pass_ref=True)) - # view_prefs['star-lines'] = star_lines - view_prefs["update-title"] = update_title - view_prefs["side-panel"] = prefs.prefer_side - view_prefs["dim-art"] = prefs.dim_art - #view_prefs['level-meter'] = gui.turbo - # view_prefs['pl-follow'] = pl_follow - view_prefs["scroll-enable"] = scroll_enable - view_prefs["break-enable"] = break_enable - # view_prefs['dd-index'] = dd_index - view_prefs["append-date"] = prefs.append_date + tab_menu.add_to_sub(0, MenuItem(_("Shuffled Albums"), gen_folder_shuffle, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Shuffled Albums"), gen_folder_shuffle, pass_ref=True)) - tauonplaylist_jar = [] - tauonqueueitem_jar = [] -# if db_version > 68: - for v in pctl.multi_playlist: -# logging.warning(f"Playlist: {v}") - tauonplaylist_jar.append(v.__dict__) - for v in pctl.force_queue: -# logging.warning(f"Queue: {v}") - tauonqueueitem_jar.append(v.__dict__) -# else: -# tauonplaylist_jar = pctl.multi_playlist -# tauonqueueitem_jar = pctl.track_queue - trackclass_jar = [] - for v in pctl.master_library.values(): - trackclass_jar.append(v.__dict__) + tab_menu.add_to_sub(0, MenuItem(_("Lucky Random"), gen_best_random, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Lucky Random"), gen_best_random, pass_ref=True)) - save = [ - None, - pctl.master_count, - pctl.playlist_playing_position, - pctl.active_playlist_viewing, - pctl.playlist_view_position, - tauonplaylist_jar, # pctl.multi_playlist, # list[TauonPlaylist] - pctl.player_volume, - pctl.track_queue, - pctl.queue_step, - default_playlist, - None, # pctl.playlist_playing_position, - None, # Was cue list - "", # radio_field.text, - theme, - folder_image_offsets, - None, # lfm_username, - None, # lfm_hash, - latest_db_version, # Used for upgrading - view_prefs, - gui.save_size, - None, # old side panel size - 0, # save time (unused) - gui.vis_want, # gui.vis - pctl.selected_in_playlist, - album_mode_art_size, - draw_border, - prefs.enable_web, - prefs.allow_remote, - prefs.expose_web, - prefs.enable_transcode, - prefs.show_rym, - None, # was combo mode art size - gui.maximized, - prefs.prefer_bottom_title, - gui.display_time_mode, - prefs.transcode_mode, - prefs.transcode_codec, - prefs.transcode_bitrate, - 1, # prefs.line_style, - prefs.cache_gallery, - prefs.playlist_font_size, - prefs.use_title, - gui.pl_st, - None, # gui.set_mode, - None, - prefs.playlist_row_height, - prefs.show_wiki, - prefs.auto_extract, - prefs.colour_from_image, - gui.set_bar, - gui.gallery_show_text, - gui.bb_show_art, - False, # Was show stars - prefs.auto_lfm, - prefs.scrobble_mark, - prefs.replay_gain, - True, # Was radio lyrics - prefs.show_gimage, - prefs.end_setting, - prefs.show_gen, - [], # was old radio urls - prefs.auto_del_zip, - gui.level_meter_colour_mode, - prefs.ui_scale, - prefs.show_lyrics_side, - None, #prefs.last_device, - album_mode, - None, # album_playlist_width - prefs.transcode_opus_as, - gui.star_mode, - prefs.prefer_side, # gui.rsp, - gui.lsp, - gui.rspw, - gui.pref_gallery_w, - gui.pref_rspw, - gui.show_hearts, - prefs.monitor_downloads, # 76 - gui.artist_info_panel, # 77 - prefs.extract_to_music, # 78 - lb.enable, - None, # lb.key, - rename_files.text, - rename_folder.text, - prefs.use_jump_crossfade, - prefs.use_transition_crossfade, - prefs.show_notifications, - prefs.true_shuffle, - gui.set_mode, - None, # prefs.show_queue, # 88 - None, # prefs.show_transfer, - tauonqueueitem_jar, # pctl.force_queue, # 90 - prefs.use_pause_fade, # 91 - prefs.append_total_time, # 92 - None, # prefs.backend, - pctl.album_shuffle_mode, - pctl.album_repeat_mode, # 95 - prefs.finish_current, # Not used - prefs.reload_state, # 97 - None, # prefs.reload_play_state, - prefs.last_fm_token, - prefs.last_fm_username, - prefs.use_card_style, - prefs.auto_lyrics, - prefs.auto_lyrics_checked, - prefs.show_side_art, - prefs.window_opacity, - prefs.gallery_single_click, - prefs.tabs_on_top, - prefs.showcase_vis, - prefs.spec2_colour_mode, - prefs.device_buffer, # moved to config file - prefs.use_eq, - prefs.eq, - prefs.bio_large, - prefs.discord_show, - prefs.min_to_tray, - prefs.guitar_chords, - None, # prefs.playback_follow_cursor, - prefs.art_bg, - pctl.random_mode, - pctl.repeat_mode, - prefs.art_bg_stronger, - prefs.art_bg_always_blur, - prefs.failed_artists, - prefs.artist_list, - None, # prefs.auto_sort, - prefs.lyrics_enables, - prefs.fanart_notify, - prefs.bg_showcase_only, - None, # prefs.discogs_pat, - prefs.mini_mode_mode, - after_scan, - gui.gallery_positions, - prefs.chart_bg, - prefs.left_panel_mode, - gui.last_left_panel_mode, - None, #prefs.gst_device, - search_string_cache, - search_dia_string_cache, - pctl.gen_codes, - gui.show_ratings, - gui.show_album_ratings, - prefs.radio_urls, - gui.showcase_mode, # gui.combo_mode, - top_panel.prime_tab, - top_panel.prime_side, - prefs.sync_playlist, - prefs.spot_client, - prefs.spot_secret, - prefs.show_band, - prefs.download_playlist, - tauon.spot_ctl.cache_saved_albums, - prefs.auto_rec, - prefs.spotify_token, - prefs.use_libre_fm, - playlist_box.scroll_on, - prefs.artist_list_sort_mode, - prefs.phazor_device_selected, - prefs.failed_background_artists, - prefs.bg_flips, - prefs.tray_show_title, - prefs.artist_list_style, - trackclass_jar, - prefs.premium, - gui.radio_view, - pctl.radio_playlists, - pctl.radio_playlist_viewing, - prefs.radio_thumb_bans, - prefs.playlist_exports, - prefs.show_chromecast, - prefs.cache_list, - prefs.shuffle_lock, - prefs.album_shuffle_lock_mode, - gui.was_radio, - prefs.spot_username, - "", #prefs.spot_password, # No longer used - prefs.artist_list_threshold, - prefs.tray_theme, - prefs.row_title_format, - prefs.row_title_genre, - prefs.row_title_separator_type, - prefs.replay_preamp, # 181 - prefs.gallery_combine_disc, - ] + tab_menu.add_to_sub(0, MenuItem(_("Reverse Tracks"), gen_reverse, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Reverse Tracks"), gen_reverse, pass_ref=True)) - try: - with (user_directory / "state.p.backup").open("wb") as file: - pickle.dump(save, file, protocol=pickle.HIGHEST_PROTOCOL) - # if not pctl.running: - with (user_directory / "state.p").open("wb") as file: - pickle.dump(save, file, protocol=pickle.HIGHEST_PROTOCOL) + tab_menu.add_to_sub(0, MenuItem(_("Reverse Albums"), gen_folder_reverse, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Reverse Albums"), gen_folder_reverse, pass_ref=True)) - old_position = old_window_position - if not prefs.save_window_position: - old_position = None + tab_menu.add_to_sub(0, MenuItem(_("Duplicate"), gen_dupe, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Duplicate"), gen_dupe, pass_ref=True)) - save = [ - draw_border, - gui.save_size, - prefs.window_opacity, - gui.scale, - gui.maximized, - old_position, - ] + # tab_menu.add_to_sub("Filepath", 1, gen_sort_path, pass_ref=True) - if not fs_mode: - with (user_directory / "window.p").open("wb") as file: - pickle.dump(save, file, protocol=pickle.HIGHEST_PROTOCOL) + # tab_menu.add_to_sub("Artist → gui.abc", 0, gen_sort_artist, pass_ref=True) - tauon.spot_ctl.save_token() + # tab_menu.add_to_sub("Album → gui.abc", 0, gen_sort_album, pass_ref=True) + tab_menu.add_to_sub(0, MenuItem(_("Loved"), gen_love, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Loved"), gen_love, pass_ref=True)) + tab_menu.add_to_sub(0, MenuItem(_("Has Comment"), gen_comment, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Has Comment"), gen_comment, pass_ref=True)) + tab_menu.add_to_sub(0, MenuItem(_("Has Lyrics"), gen_lyrics, pass_ref=True)) + extra_tab_menu.add_to_sub(0, MenuItem(_("Has Lyrics"), gen_lyrics, pass_ref=True)) - with (user_directory / "lyrics_substitutions.json").open("w") as file: - json.dump(prefs.lyrics_subs, file) + playlist_menu.add(MenuItem("Paste", paste, paste_deco)) - save_prefs() + playlist_menu.add(MenuItem(_("Add Playing Spotify Album"), paste_playlist_coast_album, paste_playlist_coast_album_deco, + show_test=spotify_show_test)) + playlist_menu.add(MenuItem(_("Add Playing Spotify Track"), paste_playlist_coast_track, paste_playlist_coast_album_deco, + show_test=spotify_show_test)) - for key, item in prefs.playlist_exports.items(): - pl = id_to_pl(key) - if pl is None: - continue - if item["auto"] is False: - continue - export_playlist_box.run_export(item, key, warnings=False) + # Create track context menu + track_menu = Menu(tauon, 195, show_icons=True) - logging.info("Done writing database") + track_menu.add(MenuItem(_("Open Folder"), open_folder, pass_ref=True, pass_ref_deco=True, icon=folder_icon, disable_test=open_folder_disable_test)) + track_menu.add(MenuItem(_("Track Info…"), activate_track_box, pass_ref=True, icon=info_icon)) - except PermissionError: - logging.exception("Permission error encountered while writing database") - show_message(_("Permission error encountered while writing database"), "error") - except Exception: - logging.exception("Unknown error encountered while writing database") + heartx_icon.colour = [55, 55, 55, 255] + heartx_icon.xoff = 1 + heartx_icon.yoff = 0 + heartx_icon.colour_callback = heart_xmenu_colour + spot_heartx_icon.colour = [30, 215, 96, 255] + spot_heartx_icon.xoff = 3 + spot_heartx_icon.yoff = 0 + spot_heartx_icon.colour_callback = spot_heart_xmenu_colour -SDL_StartTextInput() + # Mark track as 'liked' + track_menu.add(MenuItem("Love", love_index, love_decox, icon=heartx_icon)) + heart_spot_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-menu.png", True)) + heart_spot_icon.colour = [30, 215, 96, 255] + heart_spot_icon.xoff = 1 + heart_spot_icon.yoff = 0 + heart_spot_icon.colour_callback = spot_heart_menu_colour -# SDL_SetHint(SDL_HINT_IME_INTERNAL_EDITING, b"1") -# SDL_EventState(SDL_SYSWMEVENT, 1) + track_menu.add(MenuItem("Spotify Like Track", toggle_spotify_like_ref, toggle_spotify_like_row_deco, show_test=spot_like_show_test, icon=heart_spot_icon)) + # def toggle_queue(mode: int = 0) -> bool: + # if mode == 1: + # return prefs.show_queue + # prefs.show_queue ^= True + # prefs.show_queue ^= True -def test_show_add_home_music() -> None: - gui.add_music_folder_ready = True - if music_directory is None: - gui.add_music_folder_ready = False - return + track_menu.add(MenuItem(_("Add to Queue"), add_to_queue, pass_ref=True, hint="MB3")) - for item in pctl.multi_playlist: - if item.last_folder == str(music_directory): - gui.add_music_folder_ready = False - break + track_menu.add(MenuItem(_("↳ After Current Track"), add_to_queue_next, pass_ref=True, show_test=test_shift)) + track_menu.add(MenuItem(_("Show in Gallery"), show_in_gal, pass_ref=True, show_test=test_show)) -test_show_add_home_music() + track_menu.add_sub(_("Meta…"), 160) -if gui.restart_album_mode: - toggle_album_mode(True) + track_menu.br() + # track_menu.add('Cut', s_cut, pass_ref=False) + # track_menu.add('Remove', del_selected) + track_menu.add(MenuItem(_("Copy"), s_copy, pass_ref=False)) -if gui.remember_library_mode: - toggle_library_mode() + # track_menu.add(_('Paste + Transfer Folder'), lightning_paste, pass_ref=False, show_test=lightning_move_test) -quick_import_done = [] + track_menu.add(MenuItem(_("Paste"), menu_paste, paste_deco, pass_ref=True)) -if reload_state: - if reload_state[0] == 1: - pctl.jump_time = reload_state[1] - pctl.play() + track_menu.add(MenuItem(_("Delete Track File"), delete_track, pass_ref=True, icon=delete_icon, show_test=test_shift)) -pctl.notify_update() + track_menu.br() -key_focused = 0 + # rename_tracks_icon.colour = [244, 241, 66, 255] + # rename_tracks_icon.colour = [204, 255, 66, 255] + rename_tracks_icon.colour = [204, 100, 205, 255] + rename_tracks_icon.xoff = 1 + track_menu.add_to_sub(0, MenuItem(_("Rename Tracks…"), rename_track_box.activate, rename_tracks_deco, pass_ref=True, + pass_ref_deco=True, icon=rename_tracks_icon, disable_test=rename_track_box.disable_test)) -theme = get_theme_number(prefs.theme_name) + track_menu.add_to_sub(0, MenuItem(_("Edit fields…"), activate_trans_editor)) -if pl_to_id(pctl.active_playlist_viewing) in gui.gallery_positions: - gui.album_scroll_px = gui.gallery_positions[pl_to_id(pctl.active_playlist_viewing)] + mod_folder_icon.colour = [229, 98, 98, 255] + track_menu.add_to_sub(0, MenuItem(_("Modify Folder…"), rename_folders, pass_ref=True, pass_ref_deco=True, icon=mod_folder_icon, disable_test=rename_folders_disable_test)) -def menu_is_open(): - for menu in Menu.instances: - if menu.active: - return True - return False + # track_menu.add_to_sub("Reset Track Play Count", 0, reset_play_count, pass_ref=True) + # track_menu.add('Reload Metadata', reload_metadata, pass_ref=True) + track_menu.add_to_sub(0, MenuItem(_("Rescan Tags"), reload_metadata, pass_ref=True)) -def is_level_zero(include_menus: bool = True) -> bool: - if include_menus: - for menu in Menu.instances: - if menu.active: - return False + mbp_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "mbp-g.png")) + mbp_icon.base_asset = asset_loader(scaled_asset_directory, loaded_asset_dc, "mbp-gs.png") - return not gui.rename_folder_box \ - and not track_box \ - and not rename_track_box.active \ - and not radiobox.active \ - and not pref_box.enabled \ - and not quick_search_mode \ - and not gui.rename_playlist_box \ - and not search_over.active \ - and not gui.box_over \ - and not trans_edit_box.active + mbp_icon.xoff = 2 + mbp_icon.yoff = -1 + if gui.scale == 1.25: + mbp_icon.yoff = 0 -# Hold the splash/loading screen for a minimum duration -# while core_timer.get() < 0.5: -# time.sleep(0.01) + edit_icon = None + if prefs.tag_editor_name == "Picard": + edit_icon = mbp_icon -# Resize menu widths to text length (length can vary due to translations) -for menu in Menu.instances: + track_menu.add_to_sub(0, MenuItem(_("Edit with"), launch_editor, pass_ref=True, pass_ref_deco=True, icon=edit_icon, render_func=edit_deco, disable_test=launch_editor_disable_test)) + track_menu.add_to_sub(0, MenuItem(_("Lyrics..."), show_lyrics_menu, pass_ref=True)) + track_menu.add_to_sub(0, MenuItem(_("Fix Mojibake"), intel_moji, pass_ref=True)) + # track_menu.add_to_sub("Copy Playlist", 1, transfer, pass_ref=True, args=[1, 3]) - w = 0 - icon_space = 0 + selection_menu = Menu(tauon, 200, show_icons=False) + folder_menu = Menu(tauon, 193, show_icons=True) - if menu.show_icons: - icon_space = 25 * gui.scale + folder_menu.add(MenuItem(_("Open Folder"), open_folder, pass_ref=True, pass_ref_deco=True, icon=folder_icon, disable_test=open_folder_disable_test)) - for item in menu.items: - if item is None: - continue - test_width = ddt.get_text_w(item.title, menu.font) + icon_space + 21 * gui.scale - if not item.is_sub_menu and item.hint: - test_width += ddt.get_text_w(item.hint, menu.font) + 4 * gui.scale - - w = max(test_width, w) - - # sub - if item.is_sub_menu: - ww = 0 - sub_icon_space = 0 - for sub_item in menu.subs[item.sub_menu_number]: - if sub_item.icon is not None: - sub_icon_space = 25 * gui.scale - break - for sub_item in menu.subs[item.sub_menu_number]: + folder_menu.add(MenuItem(_("Modify Folder…"), rename_folders, pass_ref=True, pass_ref_deco=True, icon=mod_folder_icon, disable_test=rename_folders_disable_test)) + folder_tree_menu.add(MenuItem(_("Modify Folder…"), rename_folders, pass_ref=True, pass_ref_deco=True, icon=mod_folder_icon, disable_test=rename_folders_disable_test)) + # folder_menu.add(_("Add Album to Queue"), add_album_to_queue, pass_ref=True) + folder_menu.add(MenuItem(_("Add Album to Queue"), add_album_to_queue, pass_ref=True)) + folder_menu.add(MenuItem(_("Enqueue Album Next"), add_album_to_queue_fc, pass_ref=True)) - test_width = ddt.get_text_w(sub_item.title, menu.font) + sub_icon_space + 23 * gui.scale - ww = max(test_width, ww) + gallery_menu.add(MenuItem(_("Modify Folder…"), rename_folders, pass_ref=True, pass_ref_deco=True, icon=mod_folder_icon, disable_test=rename_folders_disable_test)) - item.sub_menu_width = max(ww, item.sub_menu_width) + folder_menu.add(MenuItem(_("Rename Tracks…"), rename_track_box.activate, rename_tracks_deco, + pass_ref=True, pass_ref_deco=True, icon=rename_tracks_icon, disable_test=rename_track_box.disable_test)) + folder_tree_menu.add(MenuItem(_("Rename Tracks…"), rename_track_box.activate, pass_ref=True, pass_ref_deco=True, icon=rename_tracks_icon, disable_test=rename_track_box.disable_test)) - menu.w = max(w, menu.w) + if not snap_mode: + folder_menu.add(MenuItem("Edit with", launch_editor_selection, pass_ref=True, + pass_ref_deco=True, icon=edit_icon, render_func=edit_deco, disable_test=launch_editor_selection_disable_test)) + folder_tree_menu.add(MenuItem(_("Add Album to Queue"), add_album_to_queue, pass_ref=True)) + folder_tree_menu.add(MenuItem(_("Enqueue Album Next"), add_album_to_queue_fc, pass_ref=True)) -def drop_file(target): - global new_playlist_cooldown - global mouse_down - global drag_mode + folder_tree_menu.br() + folder_tree_menu.add(MenuItem(_("Collapse All"), collapse_tree, collapse_tree_deco)) + folder_tree_menu.add(MenuItem("lock", lock_folder_tree, lock_folder_tree_deco)) - if system != "windows" and sdl_version >= 204: - gmp = get_global_mouse() - gwp = get_window_position() - i_x = gmp[0] - gwp[0] - i_x = max(i_x, 0) - i_x = min(i_x, window_size[0]) - i_y = gmp[1] - gwp[1] - i_y = max(i_y, 0) - i_y = min(i_y, window_size[1]) - else: - i_y = pointer(c_int(0)) - i_x = pointer(c_int(0)) - SDL_GetMouseState(i_x, i_y) - i_y = i_y.contents.value / logical_size[0] * window_size[0] - i_x = i_x.contents.value / logical_size[0] * window_size[0] + # selection_menu.br() + transcode_icon.colour = [239, 74, 157, 255] + folder_menu.add(MenuItem(_("Rescan Tags"), reload_metadata, pass_ref=True)) + folder_menu.add(MenuItem(_("Edit fields…"), activate_trans_editor)) + folder_menu.add(MenuItem(_("Vacuum Playtimes"), vacuum_playtimes, pass_ref=True, show_test=test_shift)) + folder_menu.add(MenuItem(_("Transcode Folder"), convert_folder, transcode_deco, pass_ref=True, icon=transcode_icon, + show_test=toggle_transcode)) + gallery_menu.add(MenuItem(_("Transcode Folder"), convert_folder, transcode_deco, pass_ref=True, icon=transcode_icon, + show_test=toggle_transcode)) + folder_menu.br() + + tauon.spot_ctl.cache_saved_albums = spot_cache_saved_albums + + # Copy album title text to clipboard + folder_menu.add(MenuItem(_('Copy "Artist - Album"'), clip_title, pass_ref=True)) + + folder_menu.add(MenuItem("Lookup Spotify Album URL", get_album_spot_url, get_album_spot_url_deco, pass_ref=True, + pass_ref_deco=True, show_test=spotify_show_test, icon=spot_icon)) + + folder_menu.add(MenuItem("Add to Spotify Library", add_to_spotify_library, add_to_spotify_library_deco, pass_ref=True, + pass_ref_deco=True, show_test=spotify_show_test, icon=spot_icon)) + + + # Copy artist name text to clipboard + # folder_menu.add(_('Copy "Artist"'), clip_ar, pass_ref=True) + + selection_menu.add(MenuItem(_("Add to queue"), add_selected_to_queue_multi, selection_queue_deco)) + selection_menu.br() + selection_menu.add(MenuItem(_("Rescan Tags"), reload_metadata_selection)) + selection_menu.add(MenuItem(_("Edit fields…"), activate_trans_editor)) + selection_menu.add(MenuItem(_("Edit with "), launch_editor_selection, pass_ref=True, pass_ref_deco=True, icon=edit_icon, render_func=edit_deco, disable_test=launch_editor_selection_disable_test)) + + selection_menu.br() + folder_menu.br() + + # It's complicated + # folder_menu.add(_('Copy Folder From Library'), lightning_copy) + + selection_menu.add(MenuItem(_("Copy"), s_copy)) + selection_menu.add(MenuItem(_("Cut"), s_cut)) + selection_menu.add(MenuItem(_("Remove"), del_selected)) + selection_menu.add(MenuItem(_("Delete Files"), force_del_selected, show_test=test_shift, icon=delete_icon)) + + folder_menu.add(MenuItem(_("Copy"), s_copy)) + gallery_menu.add(MenuItem(_("Copy"), s_copy)) + # folder_menu.add(_('Cut'), s_cut) + # folder_menu.add(_('Paste + Transfer Folder'), lightning_paste, pass_ref=False, show_test=lightning_move_test) + # gallery_menu.add(_('Paste + Transfer Folder'), lightning_paste, pass_ref=False, show_test=lightning_move_test) + folder_menu.add(MenuItem(_("Remove"), del_selected)) + gallery_menu.add(MenuItem(_("Remove"), del_selected)) + + + track_menu.add(MenuItem(_("Search Artist on Wikipedia"), ser_wiki, pass_ref=True, show_test=toggle_wiki)) + track_menu.add(MenuItem(_("Search Track on Genius"), ser_gen, pass_ref=True, show_test=toggle_gen)) + + son_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "sonemic-g.png")) + son_icon.base_asset = asset_loader(scaled_asset_directory, loaded_asset_dc, "sonemic-gs.png") + + son_icon.xoff = 1 + track_menu.add(MenuItem(_("Search Artist on Sonemic"), ser_rym, pass_ref=True, icon=son_icon, show_test=toggle_rym)) + + band_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "band.png", True)) + band_icon.xoff = 0 + band_icon.yoff = 1 + band_icon.colour = [96, 147, 158, 255] + + track_menu.add(MenuItem(_("Search Artist on Bandcamp"), ser_band, pass_ref=True, icon=band_icon, show_test=toggle_band)) + + # Copy metadata to clipboard + # track_menu.add(_('Copy "Artist - Album"'), clip_aar_al, pass_ref=True) + # Copy metadata to clipboard + track_menu.add(MenuItem(_('Copy "Artist - Track"'), clip_ar_tr, pass_ref=True)) + track_menu.add(MenuItem(_("Copy TIDAL Album URL"), tidal_copy_album, show_test=is_tidal_track, pass_ref=True)) + track_menu.add_sub(_("Spotify…"), 190, show_test=spotify_show_test) + track_menu.add_to_sub(1, MenuItem(_("Show Full Artist"), get_spot_artist_track, pass_ref=True, icon=spot_icon)) + track_menu.add_to_sub(1, MenuItem(_("Show Full Album"), get_spot_album_track, pass_ref=True, icon=spot_icon)) + track_menu.add_to_sub(1, MenuItem(_("Copy Track URL"), get_track_spot_url, get_track_spot_url_deco, pass_ref=True, + icon=spot_icon)) + # track_menu.add_to_sub(1, MenuItem(_("Get Recommended"), get_spot_recs_track, pass_ref=True, icon=spot_icon)) + + track_menu.br() + track_menu.add(MenuItem(_("Transcode Folder"), convert_folder, transcode_deco, pass_ref=True, icon=transcode_icon, + show_test=toggle_transcode)) + + + # Create top menu + x_menu: Menu = Menu(tauon, 190, show_icons=True) + view_menu = Menu(tauon, 170) + set_menu = Menu(tauon, 150) + set_menu_hidden = Menu(tauon, 100) + vis_menu = Menu(tauon, 140) + window_menu = Menu(tauon, 140) + field_menu = Menu(tauon, 140) + dl_menu = Menu(tauon, 90) + + window_menu = Menu(tauon, 140) + window_menu.add(MenuItem(_("Minimize"), do_minimize_button)) + window_menu.add(MenuItem(_("Maximize"), do_maximize_button)) + window_menu.add(MenuItem(_("Exit"), do_exit_button)) + + # Copy text + field_menu.add(MenuItem(_("Copy"), field_copy, pass_ref=True)) + # Paste text + field_menu.add(MenuItem(_("Paste"), field_paste, pass_ref=True)) + # Clear text + field_menu.add(MenuItem(_("Clear"), field_clear, pass_ref=True)) + + vis_menu.add(MenuItem(_("Off"), vis_off)) + vis_menu.add(MenuItem(_("Level Meter"), level_on)) + vis_menu.add(MenuItem(_("Spectrum Visualizer"), spec_on)) + # vis_menu.add(_("Spectrogram"), spec2_def) + + # Mark for translation + _("Time") + _("Filepath") + + # set_menu.add(_("Sort Ascending"), sort_ass, pass_ref=True, disable_test=view_pl_is_locked, pass_ref_deco=True) + # set_menu.add(_("Sort Decending"), sort_dec, pass_ref=True, disable_test=view_pl_is_locked, pass_ref_deco=True) + # set_menu.br() + set_menu.add(MenuItem(_("Auto Resize"), auto_size_columns)) + set_menu.add(MenuItem(_("Hide bar"), hide_set_bar)) + set_menu_hidden.add(MenuItem(_("Show bar"), show_set_bar)) + set_menu.br() + set_menu.add(MenuItem("- " + _("Remove This"), sa_remove, pass_ref=True)) + set_menu.br() + set_menu.add(MenuItem("+ " + _("Artist"), sa_artist)) + set_menu.add(MenuItem("+ " + _("Title"), sa_title)) + set_menu.add(MenuItem("+ " + _("Album"), sa_album)) + set_menu.add(MenuItem("+ " + _("Duration"), sa_time)) + set_menu.add(MenuItem("+ " + _("Date"), sa_date)) + set_menu.add(MenuItem("+ " + _("Genre"), sa_genre)) + set_menu.add(MenuItem("+ " + _("Track Number"), sa_track)) + set_menu.add(MenuItem("+ " + _("Play Count"), sa_count)) + set_menu.add(MenuItem("+ " + _("Codec"), sa_codec)) + set_menu.add(MenuItem("+ " + _("Bitrate"), sa_bitrate)) + set_menu.add(MenuItem("+ " + _("Filename"), sa_filename)) + set_menu.add(MenuItem("+ " + _("Starline"), sa_star)) + set_menu.add(MenuItem("+ " + _("Rating"), sa_rating)) + set_menu.add(MenuItem("+ " + _("Loved"), sa_love)) + + set_menu.add_sub("+ " + _("More…"), 150) + + set_menu.add_to_sub(0, MenuItem("+ " + _("Album Artist"), sa_album_artist)) + set_menu.add_to_sub(0, MenuItem("+ " + _("Comment"), sa_comment)) + set_menu.add_to_sub(0, MenuItem("+ " + _("Filepath"), sa_file)) + set_menu.add_to_sub(0, MenuItem("+ " + _("Scrobble Count"), sa_scrobbles)) + set_menu.add_to_sub(0, MenuItem("+ " + _("Composer"), sa_composer)) + set_menu.add_to_sub(0, MenuItem("+ " + _("Disc Number"), sa_disc)) + set_menu.add_to_sub(0, MenuItem("+ " + _("Has Lyrics"), sa_lyrics)) + set_menu.add_to_sub(0, MenuItem("+ " + _("Is CUE Sheet"), sa_cue)) + + add_icon.xoff = 3 + add_icon.yoff = 0 + add_icon.colour = [237, 80, 221, 255] + add_icon.colour_callback = new_playlist_colour_callback + + x_menu.add(MenuItem(_("New Playlist"), new_playlist, new_playlist_deco, icon=add_icon)) + + x_menu.add(MenuItem(_("Clean Database!"), clean_db_fast, clean_db_deco, show_test=clean_db_show_test)) + + # x_menu.add(_("Internet Radio…"), activate_radio_box) + + tauon.switch_playlist = switch_playlist + + x_menu.add(MenuItem(_("Paste Spotify Playlist"), import_spotify_playlist, import_spotify_playlist_deco, icon=spot_icon, + show_test=spotify_show_test)) + + x_menu.add(MenuItem(_("Import Music Folder"), import_music, show_test=show_import_music)) + + x_menu.br() + + settings_icon.xoff = 0 + settings_icon.yoff = 2 + settings_icon.colour = [232, 200, 96, 255] # [230, 152, 118, 255]#[173, 255, 47, 255] #[198, 237, 56, 255] + # settings_icon.colour = [180, 140, 255, 255] + x_menu.add(MenuItem(_("Settings"), activate_info_box, icon=settings_icon)) + x_menu.add_sub(_("Database…"), 190) + + if dev_mode: + def dev_mode_enable_save_state() -> None: + global should_save_state + should_save_state = True + show_message(_("Enabled saving state")) + + def dev_mode_disable_save_state() -> None: + global should_save_state + should_save_state = False + show_message(_("Disabled saving state")) + + x_menu.add_sub(_("Dev Mode"), 190) + x_menu.add_to_sub(1, MenuItem(_("Enable Saving State"), dev_mode_enable_save_state)) + x_menu.add_to_sub(1, MenuItem(_("Disable Saving State"), dev_mode_disable_save_state)) + x_menu.br() + + x_menu.add_to_sub(0, MenuItem(_("Export as CSV"), export_database)) + x_menu.add_to_sub(0, MenuItem(_("Rescan All Folders"), rescan_all_folders)) + x_menu.add_to_sub(0, MenuItem(_("Play History to Playlist"), q_to_playlist)) + x_menu.add_to_sub(0, MenuItem(_("Reset Image Cache"), clear_img_cache)) + + cm_clean_db = False + # x_menu.add('Toggle Side panel', toggle_combo_view, combo_deco) + + x_menu.add_to_sub(0, MenuItem(_("Remove Network Tracks"), clean_db2)) + x_menu.add_to_sub(0, MenuItem(_("Remove Missing Tracks"), clean_db)) + + x_menu.add_to_sub(0, MenuItem(_("Import FMPS Ratings"), import_fmps)) + x_menu.add_to_sub(0, MenuItem(_("Import POPM Ratings"), import_popm)) + x_menu.add_to_sub(0, MenuItem(_("Reset User Ratings"), clear_ratings)) + x_menu.add_to_sub(0, MenuItem(_("Find Incomplete Albums"), find_incomplete)) + x_menu.add_to_sub(0, MenuItem(_("Mark Missing as Found"), pctl.reset_missing_flags, show_test=test_shift)) + + if chrome: + x_menu.add_sub(_("Chromecast…"), 220) + shooter(cast_search2) - #logging.info((i_x, i_y)) - gui.drop_playlist_target = 0 - #logging.info(event.drop) + tauon.chrome_menu = x_menu - if i_y < gui.panelY and not new_playlist_cooldown and gui.mode == 1: - x = top_panel.tabs_left_x - for tab in top_panel.shown_tabs: - wid = top_panel.tab_text_spaces[tab] + top_panel.tab_extra_width + #x_menu.add(_("Cast…"), cast_search, cast_deco) - if x < i_x < x + wid: - gui.drop_playlist_target = tab - tab_pulse.pulse() - gui.update += 1 - gui.pl_pulse = True - logging.info("Direct drop") - break + mode_menu = Menu(tauon, 175) - x += wid - else: - logging.info("MISS") - if new_playlist_cooldown: - gui.drop_playlist_target = pctl.active_playlist_viewing - else: - if not target.lower().endswith(".xspf"): - gui.drop_playlist_target = new_playlist() - new_playlist_cooldown = True + mode_menu.add(MenuItem(_("Tab"), set_mini_mode_D)) + mode_menu.add(MenuItem(_("Mini"), set_mini_mode_A1)) + # mode_menu.add(_('Mini Mode Large'), set_mini_mode_A2) + mode_menu.add(MenuItem(_("Slate"), set_mini_mode_C1)) + mode_menu.add(MenuItem(_("Square"), set_mini_mode_B1)) + mode_menu.add(MenuItem(_("Square Large"), set_mini_mode_B2)) - elif gui.lsp and gui.panelY < i_y < window_size[1] - gui.panelBY and i_x < gui.lspw and gui.mode == 1: + mode_menu.br() + mode_menu.add(MenuItem(_("Copy Title to Clipboard"), copy_bb_metadata)) - y = gui.panelY - y += 5 * gui.scale - y += playlist_box.tab_h + playlist_box.gap + extra_menu = Menu(tauon, 175, show_icons=True) + extra_menu.add(MenuItem(_("Random Track"), random_track, hint=";")) - for i, pl in enumerate(pctl.multi_playlist): - if i_y < y: - gui.drop_playlist_target = i - tab_pulse.pulse() - gui.update += 1 - gui.pl_pulse = True - logging.info("Direct drop") - break - y += playlist_box.tab_h + playlist_box.gap - else: - if new_playlist_cooldown: - gui.drop_playlist_target = pctl.active_playlist_viewing - else: - if not target.lower().endswith(".xspf"): - gui.drop_playlist_target = new_playlist() - new_playlist_cooldown = True + radiorandom_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "radiorandom.png", True)) + revert_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "revert.png", True)) + + radiorandom_icon.xoff = 1 + radiorandom_icon.yoff = 0 + radiorandom_icon.colour = [153, 229, 133, 255] + extra_menu.add(MenuItem(_("Radio Random"), radio_random, hint="/", icon=radiorandom_icon)) + + revert_icon.xoff = 1 + revert_icon.yoff = 0 + revert_icon.colour = [229, 102, 59, 255] + extra_menu.add(MenuItem(_("Revert"), pctl.revert, hint="Shift+/", icon=revert_icon)) + + # extra_menu.add('Toggle Repeat', toggle_repeat, hint='COMMA') + + + # extra_menu.add('Toggle Random', toggle_random, hint='PERIOD') + extra_menu.add(MenuItem(_("Clear Queue"), clear_queue, queue_deco, hint="Alt+Shift+Q")) + + heart_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-menu.png", True)) + heart_row_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-track.png", True) + heart_notify_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-notify.png", True) + heart_notify_break_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "heart-notify-break.png", True) + # spotify_row_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "spotify-row.png", True) + star_pc_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "star-pc.png", True) + star_row_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "star.png", True) + star_half_row_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "star-half.png", True) + + heart_colours = ColourGenCache(0.7, 0.7) + + heart_icon.colour = [245, 60, 60, 255] + heart_icon.xoff = 3 + heart_icon.yoff = 0 + + if gui.scale == 1.25: + heart_icon.yoff = 1 + + heart_icon.colour_callback = heart_menu_colour + extra_menu.add(MenuItem("Love", bar_love_notify, love_deco, icon=heart_icon)) + extra_menu.add(MenuItem(_("Global Search"), activate_search_overlay, hint="Ctrl+G")) + extra_menu.add(MenuItem(_("Locate Artist"), locate_artist)) + extra_menu.add(MenuItem(_("Go To Playing"), goto_playing_extra, hint="'")) + extra_menu.br() + extra_menu.add(MenuItem("Spotify Like Track", toggle_spotify_like_active, toggle_spotify_like_active_deco, + show_test=spotify_show_test, icon=spot_heartx_icon)) + extra_menu.add_sub(_("Import Spotify…"), 140, show_test=spotify_show_test) + extra_menu.add_to_sub(0, MenuItem(_("Liked Albums"), spot_import_albums, show_test=spotify_show_test, icon=spot_icon)) + extra_menu.add_to_sub(0, MenuItem(_("Liked Tracks"), spot_import_tracks, show_test=spotify_show_test, icon=spot_icon)) + #extra_menu.add_to_sub(_("Import All Playlists"), 0, spot_import_playlists, show_test=spotify_show_test, icon=spot_icon) + extra_menu.add_to_sub(0, MenuItem(_("Playlist…"), spot_import_playlist_menu, show_test=spotify_show_test, icon=spot_icon)) + extra_menu.add_to_sub(0, MenuItem(_("Current Context"), spot_import_context, show_spot_coasting_deco, show_test=spotify_show_test, icon=spot_icon)) + extra_menu.add(MenuItem("Show Full Album", get_album_spot_active, get_album_spot_deco, + show_test=spotify_show_test, icon=spot_icon)) + extra_menu.add(MenuItem(_("Show Full Artist"), get_artist_spot, + show_test=spotify_show_test, icon=spot_icon)) + + extra_menu.add(MenuItem(_("Start Spotify Remote"), show_spot_playing, show_spot_playing_deco, show_test=spotify_show_test, + icon=spot_icon)) + + extra_menu.add(MenuItem("Transfer audio here", spot_transfer_playback_here, show_test=lambda x:spotify_show_test(0) and tauon.enable_librespot and prefs.launch_spotify_local and not pctl.spot_playing and (tauon.spot_ctl.coasting or tauon.spot_ctl.playing), + icon=spot_icon)) + + theme_files = os.listdir(str(install_directory / "theme")) + theme_files.sort() + + + last_fm_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "as.png", True) + lastfm_icon = MenuIcon(last_fm_icon) + + if gui.scale == 2 or gui.scale == 1.25: + lastfm_icon.xoff = 0 else: - gui.drop_playlist_target = pctl.active_playlist_viewing + lastfm_icon.xoff = -1 - if not os.path.exists(target) and flatpak_mode: - show_message( - _("Could not access! Possible insufficient Flatpak permissions."), - _(" For details, see {link}").format(link="https://github.com/Taiko2k/TauonMusicBox/wiki/Flatpak-Extra-Steps"), - mode="bubble") + lastfm_icon.yoff = 1 - load_order = LoadClass() - load_order.target = target.replace("\\", "/") + lastfm_icon.colour = [249, 70, 70, 255] + lastfm_icon.colour_callback = lastfm_colour - if os.path.isdir(load_order.target): - quick_import_done.append(load_order.target) - # if not pctl.multi_playlist[gui.drop_playlist_target].last_folder: - pctl.multi_playlist[gui.drop_playlist_target].last_folder.append(load_order.target) - reduce_paths(pctl.multi_playlist[gui.drop_playlist_target].last_folder) + lb_icon = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "lb-g.png")) + lb_icon.base_asset = asset_loader(scaled_asset_directory, loaded_asset_dc, "lb-gs.png") - load_order.playlist = pctl.multi_playlist[gui.drop_playlist_target].uuid_int - load_orders.append(copy.deepcopy(load_order)) - #logging.info('dropped: ' + str(dropped_file)) - gui.update += 1 - mouse_down = False - drag_mode = False + lb_icon.mode_callback = lb_mode + + lb_icon.xoff = 3 + lb_icon.yoff = -1 + if gui.scale == 1.25: + lb_icon.yoff = 0 -if gui.restore_showcase_view: - enter_showcase_view() -if gui.restore_radio_view: - enter_radio_view() + if prefs.auto_lfm: + listen_icon = lastfm_icon + elif lb.enable: + listen_icon = lb_icon + else: + listen_icon = None -# switch_playlist(len(pctl.multi_playlist) - 1) + x_menu.add(MenuItem("LFM", lastfm.toggle, last_fm_menu_deco, icon=listen_icon, show_test=lastfm_menu_test)) + x_menu.add(MenuItem(_("Exit Shuffle Lockdown"), toggle_shuffle_layout, show_test=exit_shuffle_layout)) + x_menu.add(MenuItem(_("Donate"), open_donate_link)) + x_menu.add(MenuItem(_("Exit"), tauon.exit, hint="Alt+F4", set_ref="User clicked menu exit button", pass_ref=+True)) + x_menu.add(MenuItem(_("Disengage Quick Add"), stop_quick_add, show_test=show_stop_quick_add)) -SDL_SetRenderTarget(renderer, overlay_texture_texture) + added = [] + search_over = SearchOverlay() + message_box = MessageBox() + nagbox = NagBox() -block_size = 3 + worker2_lock = threading.Lock() + spot_search_rate_timer = Timer() + + album_info_cache = {} + perfs = [] + album_info_cache_key = (-1, -1) + tauon.get_album_info = get_album_info + + power_tag_colours = ColourGenCache(0.5, 0.8) + + gui.pt_on = Timer() + gui.pt_off = Timer() + gui.pt = 0 + + tauon.reload_albums = reload_albums + + # ------------------------------------------------------------------------------------ + # WEBSERVER + if prefs.enable_web is True: + webThread = threading.Thread( + target=webserve, args=[pctl, prefs, gui, album_art_gen, str(install_directory), strings, tauon]) + webThread.daemon = True + webThread.start() + + ctlThread = threading.Thread(target=controller, args=[tauon]) + ctlThread.daemon = True + ctlThread.start() + + if prefs.enable_remote: + tauon.start_remote() + tauon.remote_limited = False + # -------------------------------------------------------------- + + key_shiftr_down = False + key_ctrl_down = False + key_rctrl_down = False + key_meta = False + key_ralt = False + key_lalt = False + + fields = Fields() + + pref_box = Over() + + inc_arrow = asset_loader(scaled_asset_directory, loaded_asset_dc, "inc.png", True) + dec_arrow = asset_loader(scaled_asset_directory, loaded_asset_dc, "dec.png", True) + corner_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "corner.png", True) + + top_panel = TopPanel() + bottom_bar1 = BottomBarType1() + bottom_bar_ao1 = BottomBarType_ao1() + mini_mode = MiniMode() + mini_mode2 = MiniMode2() + mini_mode3 = MiniMode3() + + restore_ignore_timer = Timer() + restore_ignore_timer.force_set(100) + + pl_bg = None + if (user_directory / "bg.png").exists(): + pl_bg = LoadImageAsset( + scaled_asset_directory=scaled_asset_directory, path=str(user_directory / "bg.png"), is_full_path=True) + + playlist_render = StandardPlaylist() + art_box = ArtBox() + mini_lyrics_scroll = ScrollBox() + playlist_panel_scroll = ScrollBox() + artist_info_scroll = ScrollBox() + device_scroll = ScrollBox() + artist_list_scroll = ScrollBox() + gallery_scroll = ScrollBox() + tree_view_scroll = ScrollBox() + radio_view_scroll = ScrollBox() + radiobox = RadioBox() + tauon.radiobox = radiobox + tauon.dummy_track = radiobox.dummy_track + + artist_list_menu.add(MenuItem(_("Filter to New Playlist"), create_artist_pl, pass_ref=True, icon=filter_icon)) + artist_list_menu.add_sub(_("View..."), 140) + artist_list_box = ArtistList() + tree_view_box = TreeView() + + queue_box = QueueBox() + + meta_box = MetaBox() + artist_picture_render = PictureRender() + artist_preview_render = PictureRender() + + # artist info box def + artist_info_box = ArtistInfoBox() + + artist_info_menu.add(MenuItem(_("Download Artist Data"), artist_info_box.manual_dl, artist_dl_deco, show_test=test_artist_dl)) + artist_info_menu.add(MenuItem(_("Clear Bio"), flush_artist_bio, pass_ref=True, show_test=test_shift)) + + radio_thumb_gen = RadioThumbGen() + + radio_context_menu.add(MenuItem(_("Edit..."), rename_station, pass_ref=True)) + radio_context_menu.add( + MenuItem(_("Visit Website"), visit_radio_station, visit_radio_station_site_deco, pass_ref=True, pass_ref_deco=True)) + radio_context_menu.add(MenuItem(_("Remove"), remove_station, pass_ref=True)) + + radio_view = RadioView() + showcase = Showcase() + cctest = ColourPulse2() + view_box = ViewBox() + dl_mon = DLMon() + tauon.dl_mon = dl_mon + dl_menu.add(MenuItem("Dismiss", dismiss_dl)) + + fader = Fader() + edge_playlist2 = EdgePulse2() + bottom_playlist2 = EdgePulse2() + gallery_pulse_top = EdgePulse2() + tab_pulse = EdgePulse() + lyric_side_top_pulse = EdgePulse2() + lyric_side_bottom_pulse = EdgePulse2() + + c_hit_callback = SDL_HitTest(hit_callback) + SDL_SetWindowHitTest(t_window, c_hit_callback, 0) + + # -------------------------------------------------------------------------------------------- + + # caster = threading.Thread(target=enc, args=[tauon]) + # caster.daemon = True + # caster.start() + + tauon.thread_manager.ready_playback() + + try: + tauon.thread_manager.d["caster"] = [lambda: x, [tauon], None] + except Exception: + logging.exception("Failed to cast") + + tauon.thread_manager.d["worker"] = [worker1, (), None] + tauon.thread_manager.d["search"] = [worker2, (), None] + tauon.thread_manager.d["gallery"] = [worker3, (), None] + tauon.thread_manager.d["style"] = [worker4, (), None] + tauon.thread_manager.d["radio-thumb"] = [radio_thumb_gen.loader, (), None] + + tauon.thread_manager.ready("search") + tauon.thread_manager.ready("gallery") + tauon.thread_manager.ready("worker") + + # thread = threading.Thread(target=worker1) + # thread.daemon = True + # thread.start() + # # # + # thread = threading.Thread(target=worker2) + # thread.daemon = True + # thread.start() + # # # + # thread = threading.Thread(target=worker3) + # thread.daemon = True + # thread.start() + # + # thread = threading.Thread(target=worker4) + # thread.daemon = True + # thread.start() + + + gui.playlist_view_length = int(((window_size[1] - gui.playlist_top) / 16) - 1) + + ab_click = False + d_border = 1 + + update_layout = True + + event = SDL_Event() -x = 0 -y = 0 -while y < 300: - x = 0 - while x < 300: - ddt.rect((x, y, 1, 1), [0, 0, 0, 70]) - ddt.rect((x + 2, y + 0, 1, 1), [0, 0, 0, 70]) - ddt.rect((x + 2, y + 2, 1, 1), [0, 0, 0, 70]) - ddt.rect((x + 0, y + 2, 1, 1), [0, 0, 0, 70]) - - x += block_size - y += block_size - -sync_target.text = prefs.sync_target -SDL_SetRenderTarget(renderer, None) - -if msys: - SDL_SetWindowResizable(t_window, True) # Not sure why this is needed - -# Generate theme buttons -pref_box.themes.append((ColoursClass(), "Mindaro", 0)) -theme_files = get_themes() -for i, theme in enumerate(theme_files): - c = ColoursClass() - load_theme(c, Path(theme[0])) - pref_box.themes.append((c, theme[1], i + 1)) - -pctl.total_playtime = star_store.get_total() - -mouse_up = False -mouse_wheel = 0 -reset_render = False -c_yax = 0 -c_yax_timer = Timer() -c_xax = 0 -c_xax_timer = Timer() -c_xay = 0 -c_xay_timer = Timer() -rt = 0 - -# MAIN LOOP - -while pctl.running: - # bm.get('main') - # time.sleep(100) - if k_input: - - keymaps.hits.clear() - - d_mouse_click = False - right_click = False - level_2_right_click = False - inp.mouse_click = False - middle_click = False - mouse_up = False - inp.key_return_press = False - key_down_press = False - key_up_press = False - key_right_press = False - key_left_press = False - key_esc_press = False - key_del = False - inp.backspace_press = 0 - key_backspace_press = False - inp.key_tab_press = False - key_c_press = False - key_v_press = False - key_a_press = False - key_z_press = False - key_x_press = False - key_home_press = False - key_end_press = False - mouse_wheel = 0 - pref_box.scroll = 0 - new_playlist_cooldown = False - input_text = "" - inp.level_2_enter = False - - mouse_enter_window = False - gui.mouse_in_window = True - if key_focused: - key_focused -= 1 - - # f not mouse_down: - k_input = False - clicked = False - focused = False mouse_moved = False - gui.level_2_click = False - # gui.update = 2 + power = 0 + + for item in sys.argv: + if (os.path.isdir(item) or os.path.isfile(item) or "file://" in item) \ + and not item.endswith(".py") and not item.endswith("tauon.exe") and not item.endswith("tauonmb") \ + and not item.startswith("-"): + open_uri(item) + + sv = SDL_version() + SDL_GetVersion(sv) + sdl_version = sv.major * 100 + sv.minor * 10 + sv.patch + logging.info("Using SDL version: " + str(sv.major) + "." + str(sv.minor) + "." + str(sv.patch)) + + # C-ML + # if prefs.backend == 2: + # logging.warning("Using GStreamer as fallback. Some functions disabled") + if prefs.backend == 0: + show_message(_("ERROR: No backend found"), mode="error") + + undo = Undo() + + # SDL_RenderClear(renderer) + # SDL_RenderPresent(renderer) + + # SDL_ShowWindow(t_window) + + # Clear spectogram texture + SDL_SetRenderTarget(renderer, gui.spec2_tex) + SDL_RenderClear(renderer) + ddt.rect((0, 0, 1000, 1000), [7, 7, 7, 255]) + + SDL_SetRenderTarget(renderer, gui.spec1_tex) + SDL_RenderClear(renderer) + ddt.rect((0, 0, 1000, 1000), [7, 7, 7, 255]) + + SDL_SetRenderTarget(renderer, gui.spec_level_tex) + SDL_RenderClear(renderer) + ddt.rect((0, 0, 1000, 1000), [7, 7, 7, 255]) + + SDL_SetRenderTarget(renderer, None) + + + # SDL_RenderPresent(renderer) + + # time.sleep(3) + + gal_up = False + gal_down = False + gal_left = False + gal_right = False + + get_sdl_input = GetSDLInput() + + SDL_StartTextInput() + + # SDL_SetHint(SDL_HINT_IME_INTERNAL_EDITING, b"1") + # SDL_EventState(SDL_SYSWMEVENT, 1) + test_show_add_home_music() + + if gui.restart_album_mode: + toggle_album_mode(True) + + if gui.remember_library_mode: + toggle_library_mode() + + quick_import_done = [] + + if reload_state: + if reload_state[0] == 1: + pctl.jump_time = reload_state[1] + pctl.play() + + pctl.notify_update() + + key_focused = 0 + + theme = get_theme_number(prefs.theme_name) + + if pl_to_id(pctl.active_playlist_viewing) in gui.gallery_positions: + gui.album_scroll_px = gui.gallery_positions[pl_to_id(pctl.active_playlist_viewing)] + + + # Hold the splash/loading screen for a minimum duration + # while core_timer.get() < 0.5: + # time.sleep(0.01) + + # Resize menu widths to text length (length can vary due to translations) + for menu in Menu.instances: + + w = 0 + icon_space = 0 + + if menu.show_icons: + icon_space = 25 * gui.scale + + for item in menu.items: + if item is None: + continue + test_width = ddt.get_text_w(item.title, menu.font) + icon_space + 21 * gui.scale + if not item.is_sub_menu and item.hint: + test_width += ddt.get_text_w(item.hint, menu.font) + 4 * gui.scale + + w = max(test_width, w) + + # sub + if item.is_sub_menu: + ww = 0 + sub_icon_space = 0 + for sub_item in menu.subs[item.sub_menu_number]: + if sub_item.icon is not None: + sub_icon_space = 25 * gui.scale + break + for sub_item in menu.subs[item.sub_menu_number]: + + test_width = ddt.get_text_w(sub_item.title, menu.font) + sub_icon_space + 23 * gui.scale + ww = max(test_width, ww) + + item.sub_menu_width = max(ww, item.sub_menu_width) + + menu.w = max(w, menu.w) + + if gui.restore_showcase_view: + enter_showcase_view() + if gui.restore_radio_view: + enter_radio_view() + + # switch_playlist(len(pctl.multi_playlist) - 1) + + SDL_SetRenderTarget(renderer, overlay_texture_texture) + + block_size = 3 + + x = 0 + y = 0 + while y < 300: + x = 0 + while x < 300: + ddt.rect((x, y, 1, 1), [0, 0, 0, 70]) + ddt.rect((x + 2, y + 0, 1, 1), [0, 0, 0, 70]) + ddt.rect((x + 2, y + 2, 1, 1), [0, 0, 0, 70]) + ddt.rect((x + 0, y + 2, 1, 1), [0, 0, 0, 70]) + + x += block_size + y += block_size + + sync_target.text = prefs.sync_target + SDL_SetRenderTarget(renderer, None) + + if msys: + SDL_SetWindowResizable(t_window, True) # Not sure why this is needed + + # Generate theme buttons + pref_box.themes.append((ColoursClass(), "Mindaro", 0)) + theme_files = get_themes() + for i, theme in enumerate(theme_files): + c = ColoursClass() + load_theme(c, Path(theme[0])) + pref_box.themes.append((c, theme[1], i + 1)) + + pctl.total_playtime = star_store.get_total() + + mouse_up = False + mouse_wheel = 0 + reset_render = False + c_yax = 0 + c_yax_timer = Timer() + c_xax = 0 + c_xax_timer = Timer() + c_xay = 0 + c_xay_timer = Timer() + rt = 0 + + # MAIN LOOP + + while pctl.running: + # bm.get('main') + # time.sleep(100) + if k_input: + + keymaps.hits.clear() - while SDL_PollEvent(ctypes.byref(event)) != 0: + d_mouse_click = False + right_click = False + level_2_right_click = False + inp.mouse_click = False + middle_click = False + mouse_up = False + inp.key_return_press = False + key_down_press = False + key_up_press = False + key_right_press = False + key_left_press = False + key_esc_press = False + key_del = False + inp.backspace_press = 0 + key_backspace_press = False + inp.key_tab_press = False + key_c_press = False + key_v_press = False + key_a_press = False + key_z_press = False + key_x_press = False + key_home_press = False + key_end_press = False + mouse_wheel = 0 + pref_box.scroll = 0 + new_playlist_cooldown = False + input_text = "" + inp.level_2_enter = False - # if event.type == SDL_SYSWMEVENT: - # logging.info(event.syswm.msg.contents) # Not implemented by pysdl2 + mouse_enter_window = False + gui.mouse_in_window = True + if key_focused: + key_focused -= 1 - if event.type == SDL_CONTROLLERDEVICEADDED and prefs.use_gamepad: - if SDL_IsGameController(event.cdevice.which): - SDL_GameControllerOpen(event.cdevice.which) - try: - logging.info(f"Found game controller: {SDL_GameControllerNameForIndex(event.cdevice.which).decode()}") - except Exception: - logging.exception("Error getting game controller") - - if event.type == SDL_CONTROLLERAXISMOTION and prefs.use_gamepad: - if event.caxis.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT: - rt = event.caxis.value > 5000 - if event.caxis.axis == SDL_CONTROLLER_AXIS_LEFTY: - if event.caxis.value < -10000: - new = -1 - elif event.caxis.value > 10000: - new = 1 - else: - new = 0 - if new != c_yax: - c_yax_timer.force_set(1) - c_yax = new - power += 5 - gui.update += 1 - if event.caxis.axis == SDL_CONTROLLER_AXIS_RIGHTX: - if event.caxis.value < -15000: - new = -1 - elif event.caxis.value > 15000: - new = 1 - else: - new = 0 - if new != c_xax: - c_xax_timer.force_set(1) - c_xax = new - power += 5 - gui.update += 1 - if event.caxis.axis == SDL_CONTROLLER_AXIS_RIGHTY: - if event.caxis.value < -15000: - new = -1 - elif event.caxis.value > 15000: - new = 1 - else: - new = 0 - if new != c_xay: - c_xay_timer.force_set(1) - c_xay = new - power += 5 - gui.update += 1 + # f not mouse_down: + k_input = False + clicked = False + focused = False + mouse_moved = False + gui.level_2_click = False - if event.type == SDL_CONTROLLERBUTTONDOWN and prefs.use_gamepad: - k_input = True - power += 5 - gui.update += 2 - if event.cbutton.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: - if rt: - toggle_random() - else: - pctl.advance() - if event.cbutton.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER: - if rt: - toggle_repeat() - else: - pctl.back() - if event.cbutton.button == SDL_CONTROLLER_BUTTON_A: - if rt: - pctl.show_current(highlight=True) - elif pctl.playing_ready() and pctl.active_playlist_playing == pctl.active_playlist_viewing and \ - pctl.selected_ready() and default_playlist[ - pctl.selected_in_playlist] == pctl.playing_object().index: - pctl.play_pause() - else: - inp.key_return_press = True - if event.cbutton.button == SDL_CONTROLLER_BUTTON_X: - if rt: - random_track() - else: - toggle_gallery_keycontrol(always_exit=True) - if event.cbutton.button == SDL_CONTROLLER_BUTTON_Y: - if rt: - pctl.advance(rr=True) - else: - pctl.play_pause() - if event.cbutton.button == SDL_CONTROLLER_BUTTON_B: - if rt: - pctl.revert() - elif is_level_zero(): - pctl.stop() - else: - key_esc_press = True - if event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_UP: - key_up_press = True - if event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN: - key_down_press = True - if event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT: - if gui.album_tab_mode: - key_left_press = True - elif is_level_zero() or quick_search_mode: - cycle_playlist_pinned(1) - if event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT: - if gui.album_tab_mode: - key_right_press = True - elif is_level_zero() or quick_search_mode: - cycle_playlist_pinned(-1) + # gui.update = 2 - if event.type == SDL_RENDER_TARGETS_RESET and not msys: - reset_render = True + while SDL_PollEvent(ctypes.byref(event)) != 0: - if event.type == SDL_DROPTEXT: + # if event.type == SDL_SYSWMEVENT: + # logging.info(event.syswm.msg.contents) # Not implemented by pysdl2 - power += 5 + if event.type == SDL_CONTROLLERDEVICEADDED and prefs.use_gamepad: + if SDL_IsGameController(event.cdevice.which): + SDL_GameControllerOpen(event.cdevice.which) + try: + logging.info(f"Found game controller: {SDL_GameControllerNameForIndex(event.cdevice.which).decode()}") + except Exception: + logging.exception("Error getting game controller") + + if event.type == SDL_CONTROLLERAXISMOTION and prefs.use_gamepad: + if event.caxis.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT: + rt = event.caxis.value > 5000 + if event.caxis.axis == SDL_CONTROLLER_AXIS_LEFTY: + if event.caxis.value < -10000: + new = -1 + elif event.caxis.value > 10000: + new = 1 + else: + new = 0 + if new != c_yax: + c_yax_timer.force_set(1) + c_yax = new + power += 5 + gui.update += 1 + if event.caxis.axis == SDL_CONTROLLER_AXIS_RIGHTX: + if event.caxis.value < -15000: + new = -1 + elif event.caxis.value > 15000: + new = 1 + else: + new = 0 + if new != c_xax: + c_xax_timer.force_set(1) + c_xax = new + power += 5 + gui.update += 1 + if event.caxis.axis == SDL_CONTROLLER_AXIS_RIGHTY: + if event.caxis.value < -15000: + new = -1 + elif event.caxis.value > 15000: + new = 1 + else: + new = 0 + if new != c_xay: + c_xay_timer.force_set(1) + c_xay = new + power += 5 + gui.update += 1 - link = event.drop.file.decode() - #logging.info(link) + if event.type == SDL_CONTROLLERBUTTONDOWN and prefs.use_gamepad: + k_input = True + power += 5 + gui.update += 2 + if event.cbutton.button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + if rt: + toggle_random() + else: + pctl.advance() + if event.cbutton.button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + if rt: + toggle_repeat() + else: + pctl.back() + if event.cbutton.button == SDL_CONTROLLER_BUTTON_A: + if rt: + pctl.show_current(highlight=True) + elif pctl.playing_ready() and pctl.active_playlist_playing == pctl.active_playlist_viewing and \ + pctl.selected_ready() and default_playlist[ + pctl.selected_in_playlist] == pctl.playing_object().index: + pctl.play_pause() + else: + inp.key_return_press = True + if event.cbutton.button == SDL_CONTROLLER_BUTTON_X: + if rt: + random_track() + else: + toggle_gallery_keycontrol(always_exit=True) + if event.cbutton.button == SDL_CONTROLLER_BUTTON_Y: + if rt: + pctl.advance(rr=True) + else: + pctl.play_pause() + if event.cbutton.button == SDL_CONTROLLER_BUTTON_B: + if rt: + pctl.revert() + elif is_level_zero(): + pctl.stop() + else: + key_esc_press = True + if event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_UP: + key_up_press = True + if event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN: + key_down_press = True + if event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT: + if gui.album_tab_mode: + key_left_press = True + elif is_level_zero() or quick_search_mode: + cycle_playlist_pinned(1) + if event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + if gui.album_tab_mode: + key_right_press = True + elif is_level_zero() or quick_search_mode: + cycle_playlist_pinned(-1) + + if event.type == SDL_RENDER_TARGETS_RESET and not msys: + reset_render = True + + if event.type == SDL_DROPTEXT: - if pctl.playing_ready() and link.startswith("http"): - if system != "windows" and sdl_version >= 204: - gmp = get_global_mouse() - gwp = get_window_position() - i_x = gmp[0] - gwp[0] - i_x = max(i_x, 0) - i_x = min(i_x, window_size[0]) - i_y = gmp[1] - gwp[1] - i_y = max(i_y, 0) - i_y = min(i_y, window_size[1]) - else: - i_y = pointer(c_int(0)) - i_x = pointer(c_int(0)) + power += 5 - SDL_GetMouseState(i_x, i_y) - i_y = i_y.contents.value / logical_size[0] * window_size[0] - i_x = i_x.contents.value / logical_size[0] * window_size[0] + link = event.drop.file.decode() + #logging.info(link) + + if pctl.playing_ready() and link.startswith("http"): + if system != "windows" and sdl_version >= 204: + gmp = get_global_mouse() + gwp = get_window_position() + i_x = gmp[0] - gwp[0] + i_x = max(i_x, 0) + i_x = min(i_x, window_size[0]) + i_y = gmp[1] - gwp[1] + i_y = max(i_y, 0) + i_y = min(i_y, window_size[1]) + else: + i_y = pointer(c_int(0)) + i_x = pointer(c_int(0)) - if coll_point((i_x, i_y), gui.main_art_box): - logging.info("Drop picture...") - #logging.info(link) - gui.image_downloading = True - track = pctl.playing_object() - target_dir = track.parent_folder_path + SDL_GetMouseState(i_x, i_y) + i_y = i_y.contents.value / logical_size[0] * window_size[0] + i_x = i_x.contents.value / logical_size[0] * window_size[0] - shoot_dl = threading.Thread(target=download_img, args=(link, target_dir, track)) - shoot_dl.daemon = True - shoot_dl.start() + if coll_point((i_x, i_y), gui.main_art_box): + logging.info("Drop picture...") + #logging.info(link) + gui.image_downloading = True + track = pctl.playing_object() + target_dir = track.parent_folder_path - gui.update = True + shoot_dl = threading.Thread(target=download_img, args=(link, target_dir, track)) + shoot_dl.daemon = True + shoot_dl.start() - elif link.startswith("file:///"): - link = link.replace("\r", "") - for line in link.split("\n"): - target = str(urllib.parse.unquote(line)).replace("file:///", "/") - drop_file(target) + gui.update = True - if event.type == SDL_DROPFILE: + elif link.startswith("file:///"): + link = link.replace("\r", "") + for line in link.split("\n"): + target = str(urllib.parse.unquote(line)).replace("file:///", "/") + drop_file(target) - power += 5 - dropped_file_sdl = event.drop.file - #logging.info(dropped_file_sdl) - target = str(urllib.parse.unquote( - dropped_file_sdl.decode("utf-8", errors="surrogateescape"))).replace("file:///", "/").replace("\r", "") - #logging.info(target) - drop_file(target) + if event.type == SDL_DROPFILE: + power += 5 + dropped_file_sdl = event.drop.file + #logging.info(dropped_file_sdl) + target = str(urllib.parse.unquote( + dropped_file_sdl.decode("utf-8", errors="surrogateescape"))).replace("file:///", "/").replace("\r", "") + #logging.info(target) + drop_file(target) - elif event.type == 8192: - gui.pl_update = 1 - gui.update += 2 - elif event.type == SDL_QUIT: - power += 5 + elif event.type == 8192: + gui.pl_update = 1 + gui.update += 2 - if gui.tray_active and prefs.min_to_tray and not key_shift_down: - tauon.min_to_tray() - else: - tauon.exit("Window received exit signal") - break - elif event.type == SDL_TEXTEDITING: - power += 5 - #logging.info("edit text") - editline = event.edit.text - #logging.info(editline) - editline = editline.decode("utf-8", "ignore") - k_input = True - gui.update += 1 + elif event.type == SDL_QUIT: + power += 5 - elif event.type == SDL_MOUSEMOTION: + if gui.tray_active and prefs.min_to_tray and not key_shift_down: + tauon.min_to_tray() + else: + tauon.exit("Window received exit signal") + break + elif event.type == SDL_TEXTEDITING: + power += 5 + #logging.info("edit text") + editline = event.edit.text + #logging.info(editline) + editline = editline.decode("utf-8", "ignore") + k_input = True + gui.update += 1 - mouse_position[0] = int(event.motion.x / logical_size[0] * window_size[0]) - mouse_position[1] = int(event.motion.y / logical_size[0] * window_size[0]) - mouse_moved = True - gui.mouse_unknown = False - elif event.type == SDL_MOUSEBUTTONDOWN: + elif event.type == SDL_MOUSEMOTION: - k_input = True - focused = True - power += 5 - gui.update += 1 - gui.mouse_in_window = True + mouse_position[0] = int(event.motion.x / logical_size[0] * window_size[0]) + mouse_position[1] = int(event.motion.y / logical_size[0] * window_size[0]) + mouse_moved = True + gui.mouse_unknown = False + elif event.type == SDL_MOUSEBUTTONDOWN: - if ggc == 2: # dont click on first full frame - continue + k_input = True + focused = True + power += 5 + gui.update += 1 + gui.mouse_in_window = True - if event.button.button == SDL_BUTTON_RIGHT: - right_click = True - right_down = True - #logging.info("RIGHT DOWN") - elif event.button.button == SDL_BUTTON_LEFT: - #logging.info("LEFT DOWN") + if ggc == 2: # dont click on first full frame + continue - # if mouse_position[1] > 1 and mouse_position[0] > 1: - # mouse_down = True + if event.button.button == SDL_BUTTON_RIGHT: + right_click = True + right_down = True + #logging.info("RIGHT DOWN") + elif event.button.button == SDL_BUTTON_LEFT: + #logging.info("LEFT DOWN") - inp.mouse_click = True + # if mouse_position[1] > 1 and mouse_position[0] > 1: + # mouse_down = True - mouse_down = True - elif event.button.button == SDL_BUTTON_MIDDLE: - if not search_over.active: - middle_click = True - gui.update += 1 - elif event.button.button == SDL_BUTTON_X1: - keymaps.hits.append("MB4") - elif event.button.button == SDL_BUTTON_X2: - keymaps.hits.append("MB5") - elif event.type == SDL_MOUSEBUTTONUP: - k_input = True - power += 5 - gui.update += 1 - if event.button.button == SDL_BUTTON_RIGHT: - right_down = False - elif event.button.button == SDL_BUTTON_LEFT: - if mouse_down: - mouse_up = True - mouse_up_position[0] = event.motion.x / logical_size[0] * window_size[0] - mouse_up_position[1] = event.motion.y / logical_size[0] * window_size[0] + inp.mouse_click = True - mouse_down = False + mouse_down = True + elif event.button.button == SDL_BUTTON_MIDDLE: + if not search_over.active: + middle_click = True + gui.update += 1 + elif event.button.button == SDL_BUTTON_X1: + keymaps.hits.append("MB4") + elif event.button.button == SDL_BUTTON_X2: + keymaps.hits.append("MB5") + elif event.type == SDL_MOUSEBUTTONUP: + k_input = True + power += 5 gui.update += 1 - elif event.type == SDL_KEYDOWN and key_focused == 0: - k_input = True - power += 5 - gui.update += 2 - if prefs.use_scancodes: - keymaps.hits.append(event.key.keysym.scancode) - else: - keymaps.hits.append(event.key.keysym.sym) + if event.button.button == SDL_BUTTON_RIGHT: + right_down = False + elif event.button.button == SDL_BUTTON_LEFT: + if mouse_down: + mouse_up = True + mouse_up_position[0] = event.motion.x / logical_size[0] * window_size[0] + mouse_up_position[1] = event.motion.y / logical_size[0] * window_size[0] - if prefs.use_scancodes: - if event.key.keysym.scancode == SDL_SCANCODE_V: + mouse_down = False + gui.update += 1 + elif event.type == SDL_KEYDOWN and key_focused == 0: + k_input = True + power += 5 + gui.update += 2 + if prefs.use_scancodes: + keymaps.hits.append(event.key.keysym.scancode) + else: + keymaps.hits.append(event.key.keysym.sym) + + if prefs.use_scancodes: + if event.key.keysym.scancode == SDL_SCANCODE_V: + key_v_press = True + elif event.key.keysym.scancode == SDL_SCANCODE_A: + key_a_press = True + elif event.key.keysym.scancode == SDL_SCANCODE_C: + key_c_press = True + elif event.key.keysym.scancode == SDL_SCANCODE_Z: + key_z_press = True + elif event.key.keysym.scancode == SDL_SCANCODE_X: + key_x_press = True + elif event.key.keysym.sym == SDLK_v: key_v_press = True - elif event.key.keysym.scancode == SDL_SCANCODE_A: + elif event.key.keysym.sym == SDLK_a: key_a_press = True - elif event.key.keysym.scancode == SDL_SCANCODE_C: + elif event.key.keysym.sym == SDLK_c: key_c_press = True - elif event.key.keysym.scancode == SDL_SCANCODE_Z: + elif event.key.keysym.sym == SDLK_z: key_z_press = True - elif event.key.keysym.scancode == SDL_SCANCODE_X: + elif event.key.keysym.sym == SDLK_x: key_x_press = True - elif event.key.keysym.sym == SDLK_v: - key_v_press = True - elif event.key.keysym.sym == SDLK_a: - key_a_press = True - elif event.key.keysym.sym == SDLK_c: - key_c_press = True - elif event.key.keysym.sym == SDLK_z: - key_z_press = True - elif event.key.keysym.sym == SDLK_x: - key_x_press = True - - if event.key.keysym.sym == (SDLK_RETURN or SDLK_RETURN2) and len(editline) == 0: - inp.key_return_press = True - elif event.key.keysym.sym == SDLK_KP_ENTER and len(editline) == 0: - inp.key_return_press = True - elif event.key.keysym.sym == SDLK_TAB: - inp.key_tab_press = True - elif event.key.keysym.sym == SDLK_BACKSPACE: - inp.backspace_press += 1 - key_backspace_press = True - elif event.key.keysym.sym == SDLK_DELETE: - key_del = True - elif event.key.keysym.sym == SDLK_RALT: - key_ralt = True - elif event.key.keysym.sym == SDLK_LALT: - key_lalt = True - elif event.key.keysym.sym == SDLK_DOWN: - key_down_press = True - elif event.key.keysym.sym == SDLK_UP: - key_up_press = True - elif event.key.keysym.sym == SDLK_LEFT: - key_left_press = True - elif event.key.keysym.sym == SDLK_RIGHT: - key_right_press = True - elif event.key.keysym.sym == SDLK_LSHIFT: - key_shift_down = True - elif event.key.keysym.sym == SDLK_RSHIFT: - key_shiftr_down = True - elif event.key.keysym.sym == SDLK_LCTRL: - key_ctrl_down = True - elif event.key.keysym.sym == SDLK_RCTRL: - key_rctrl_down = True - elif event.key.keysym.sym == SDLK_HOME: - key_home_press = True - elif event.key.keysym.sym == SDLK_END: - key_end_press = True - elif event.key.keysym.sym == SDLK_LGUI: - if macos: + + if event.key.keysym.sym == (SDLK_RETURN or SDLK_RETURN2) and len(editline) == 0: + inp.key_return_press = True + elif event.key.keysym.sym == SDLK_KP_ENTER and len(editline) == 0: + inp.key_return_press = True + elif event.key.keysym.sym == SDLK_TAB: + inp.key_tab_press = True + elif event.key.keysym.sym == SDLK_BACKSPACE: + inp.backspace_press += 1 + key_backspace_press = True + elif event.key.keysym.sym == SDLK_DELETE: + key_del = True + elif event.key.keysym.sym == SDLK_RALT: + key_ralt = True + elif event.key.keysym.sym == SDLK_LALT: + key_lalt = True + elif event.key.keysym.sym == SDLK_DOWN: + key_down_press = True + elif event.key.keysym.sym == SDLK_UP: + key_up_press = True + elif event.key.keysym.sym == SDLK_LEFT: + key_left_press = True + elif event.key.keysym.sym == SDLK_RIGHT: + key_right_press = True + elif event.key.keysym.sym == SDLK_LSHIFT: + key_shift_down = True + elif event.key.keysym.sym == SDLK_RSHIFT: + key_shiftr_down = True + elif event.key.keysym.sym == SDLK_LCTRL: key_ctrl_down = True - else: - key_meta = True - key_focused = 1 + elif event.key.keysym.sym == SDLK_RCTRL: + key_rctrl_down = True + elif event.key.keysym.sym == SDLK_HOME: + key_home_press = True + elif event.key.keysym.sym == SDLK_END: + key_end_press = True + elif event.key.keysym.sym == SDLK_LGUI: + if macos: + key_ctrl_down = True + else: + key_meta = True + key_focused = 1 + + elif event.type == SDL_KEYUP: - elif event.type == SDL_KEYUP: - - k_input = True - power += 5 - gui.update += 2 - if event.key.keysym.sym == SDLK_LSHIFT: - key_shift_down = False - elif event.key.keysym.sym == SDLK_LCTRL: - key_ctrl_down = False - elif event.key.keysym.sym == SDLK_RCTRL: - key_rctrl_down = False - elif event.key.keysym.sym == SDLK_RSHIFT: - key_shiftr_down = False - elif event.key.keysym.sym == SDLK_RALT: - gui.album_tab_mode = False - key_ralt = False - elif event.key.keysym.sym == SDLK_LALT: - gui.album_tab_mode = False - key_lalt = False - elif event.key.keysym.sym == SDLK_LGUI: - if macos: + k_input = True + power += 5 + gui.update += 2 + if event.key.keysym.sym == SDLK_LSHIFT: + key_shift_down = False + elif event.key.keysym.sym == SDLK_LCTRL: key_ctrl_down = False - else: - key_meta = False - key_focused = 1 + elif event.key.keysym.sym == SDLK_RCTRL: + key_rctrl_down = False + elif event.key.keysym.sym == SDLK_RSHIFT: + key_shiftr_down = False + elif event.key.keysym.sym == SDLK_RALT: + gui.album_tab_mode = False + key_ralt = False + elif event.key.keysym.sym == SDLK_LALT: + gui.album_tab_mode = False + key_lalt = False + elif event.key.keysym.sym == SDLK_LGUI: + if macos: + key_ctrl_down = False + else: + key_meta = False + key_focused = 1 + + elif event.type == SDL_TEXTINPUT: + k_input = True + power += 5 + input_text += event.text.text.decode("utf-8") - elif event.type == SDL_TEXTINPUT: - k_input = True - power += 5 - input_text += event.text.text.decode("utf-8") + gui.update += 1 + #logging.info(input_text) - gui.update += 1 - #logging.info(input_text) + elif event.type == SDL_MOUSEWHEEL: + k_input = True + power += 6 + mouse_wheel += event.wheel.y + gui.update += 1 + elif event.type == SDL_WINDOWEVENT: - elif event.type == SDL_MOUSEWHEEL: - k_input = True - power += 6 - mouse_wheel += event.wheel.y - gui.update += 1 - elif event.type == SDL_WINDOWEVENT: + power += 5 + #logging.info(event.window.event) - power += 5 - #logging.info(event.window.event) + if event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED: + #logging.info("SDL_WINDOWEVENT_FOCUS_GAINED") - if event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED: - #logging.info("SDL_WINDOWEVENT_FOCUS_GAINED") + if system == "Linux" and not macos and not msys: + gnome.focus() + k_input = True - if system == "Linux" and not macos and not msys: - gnome.focus() - k_input = True + mouse_enter_window = True + focused = True + gui.lowered = False + key_focused = 1 + mouse_down = False + gui.album_tab_mode = False + gui.pl_update = 1 + gui.update += 1 - mouse_enter_window = True - focused = True - gui.lowered = False - key_focused = 1 - mouse_down = False - gui.album_tab_mode = False - gui.pl_update = 1 - gui.update += 1 + elif event.window.event == SDL_WINDOWEVENT_FOCUS_LOST: + close_all_menus() + key_focused = 1 + gui.update += 1 - elif event.window.event == SDL_WINDOWEVENT_FOCUS_LOST: - close_all_menus() - key_focused = 1 - gui.update += 1 + elif event.window.event == SDL_WINDOWEVENT_DISPLAY_CHANGED: + # SDL_WINDOWEVENT_DISPLAY_CHANGED logs new display ID as data1 (0 or 1 or 2...), it not width, and data 2 is always 0 + pass + elif event.window.event == SDL_WINDOWEVENT_RESIZED: + # SDL_WINDOWEVENT_RESIZED logs width to data1 and height to data2 + if event.window.data1 < 500: + logging.error("Window width is less than 500, grrr why does this happen, stupid bug") + SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) + elif restore_ignore_timer.get() > 1: # Hacky + gui.update = 2 - elif event.window.event == SDL_WINDOWEVENT_DISPLAY_CHANGED: - # SDL_WINDOWEVENT_DISPLAY_CHANGED logs new display ID as data1 (0 or 1 or 2...), it not width, and data 2 is always 0 - pass - elif event.window.event == SDL_WINDOWEVENT_RESIZED: - # SDL_WINDOWEVENT_RESIZED logs width to data1 and height to data2 - if event.window.data1 < 500: - logging.error("Window width is less than 500, grrr why does this happen, stupid bug") - SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - elif restore_ignore_timer.get() > 1: # Hacky - gui.update = 2 - - logical_size[0] = event.window.data1 - logical_size[1] = event.window.data2 - - if gui.mode != 3: - logical_size[0] = max(300, logical_size[0]) - logical_size[1] = max(300, logical_size[1]) - - i_x = pointer(c_int(0)) - i_y = pointer(c_int(0)) - SDL_GL_GetDrawableSize(t_window, i_x, i_y) - window_size[0] = i_x.contents.value - window_size[1] = i_y.contents.value - - auto_scale() - update_layout = True + logical_size[0] = event.window.data1 + logical_size[1] = event.window.data2 + if gui.mode != 3: + logical_size[0] = max(300, logical_size[0]) + logical_size[1] = max(300, logical_size[1]) - elif event.window.event == SDL_WINDOWEVENT_ENTER: - #logging.info("ENTER") - mouse_enter_window = True - gui.mouse_in_window = True - gui.update += 1 + i_x = pointer(c_int(0)) + i_y = pointer(c_int(0)) + SDL_GL_GetDrawableSize(t_window, i_x, i_y) + window_size[0] = i_x.contents.value + window_size[1] = i_y.contents.value - # elif event.window.event == SDL_WINDOWEVENT_HIDDEN: - # - elif event.window.event == SDL_WINDOWEVENT_EXPOSED: - #logging.info("expose") - gui.lowered = False + auto_scale(prefs) + update_layout = True - elif event.window.event == SDL_WINDOWEVENT_MINIMIZED: - gui.lowered = True - # if prefs.min_to_tray: - # tray.down() - # tauon.thread_manager.sleep() - elif event.window.event == SDL_WINDOWEVENT_RESTORED: + elif event.window.event == SDL_WINDOWEVENT_ENTER: + #logging.info("ENTER") + mouse_enter_window = True + gui.mouse_in_window = True + gui.update += 1 - gui.lowered = False - gui.maximized = False - gui.pl_update = 1 - gui.update += 2 + # elif event.window.event == SDL_WINDOWEVENT_HIDDEN: + # + elif event.window.event == SDL_WINDOWEVENT_EXPOSED: + #logging.info("expose") + gui.lowered = False - if update_title: - update_title_do() - #logging.info("restore") + elif event.window.event == SDL_WINDOWEVENT_MINIMIZED: + gui.lowered = True + # if prefs.min_to_tray: + # tray.down() + # tauon.thread_manager.sleep() - elif event.window.event == SDL_WINDOWEVENT_SHOWN: - focused = True - gui.pl_update = 1 - gui.update += 1 + elif event.window.event == SDL_WINDOWEVENT_RESTORED: - # elif event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED: - # logging.info("FOCUS GAINED") - # # input.mouse_enter_event = True - # # gui.update += 1 - # # k_input = True + gui.lowered = False + gui.maximized = False + gui.pl_update = 1 + gui.update += 2 - elif event.window.event == SDL_WINDOWEVENT_MAXIMIZED: - if gui.mode != 3: # workaround. sdl bug? gives event on window size set - gui.maximized = True - update_layout = True - gui.pl_update = 1 - gui.update += 1 + if update_title: + update_title_do() + #logging.info("restore") - elif event.window.event == SDL_WINDOWEVENT_LEAVE: - gui.mouse_in_window = False - gui.update += 1 - power = 1000 + elif event.window.event == SDL_WINDOWEVENT_SHOWN: + focused = True + gui.pl_update = 1 + gui.update += 1 - if mouse_moved: - if fields.test(): - gui.update += 1 + # elif event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED: + # logging.info("FOCUS GAINED") + # # input.mouse_enter_event = True + # # gui.update += 1 + # # k_input = True - if gui.request_raise: - gui.request_raise = False - logging.info("Raise") - SDL_ShowWindow(t_window) - SDL_RestoreWindow(t_window) - SDL_RaiseWindow(t_window) - gui.lowered = False - - # if tauon.thread_manager.sleeping: - # if not gui.lowered: - # tauon.thread_manager.wake() - if gui.lowered: - gui.update = 0 - # ---------------- - # This section of code controls the internal processing speed or 'frame-rate' - # It's pretty messy - # if not gui.pl_update and gui.rendered_playlist_position != playlist_view_position: - # logging.warning("The playlist failed to render at the latest position!!!!") - - power += 1 - - if pctl.playerCommandReady: - if tauon.thread_manager.player_lock.locked(): - try: - tauon.thread_manager.player_lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked player_lock") - else: - logging.exception("Unknown RuntimeError trying to release player_lock") - except Exception: - logging.exception("Unknown exception trying to release player_lock") + elif event.window.event == SDL_WINDOWEVENT_MAXIMIZED: + if gui.mode != 3: # workaround. sdl bug? gives event on window size set + gui.maximized = True + update_layout = True + gui.pl_update = 1 + gui.update += 1 - if gui.frame_callback_list: - i = len(gui.frame_callback_list) - 1 - while i >= 0: - if gui.frame_callback_list[i].test(): - gui.update = 1 - power = 1000 - del gui.frame_callback_list[i] - i -= 1 + elif event.window.event == SDL_WINDOWEVENT_LEAVE: + gui.mouse_in_window = False + gui.update += 1 + power = 1000 - if animate_monitor_timer.get() < 1 or load_orders: + if mouse_moved: + if fields.test(): + gui.update += 1 - if cursor_blink_timer.get() > 0.65: - cursor_blink_timer.set() - TextBox.cursor ^= True - gui.update = 1 + if gui.request_raise: + gui.request_raise = False + logging.info("Raise") + SDL_ShowWindow(t_window) + SDL_RestoreWindow(t_window) + SDL_RaiseWindow(t_window) + gui.lowered = False + + # if tauon.thread_manager.sleeping: + # if not gui.lowered: + # tauon.thread_manager.wake() + if gui.lowered: + gui.update = 0 + # ---------------- + # This section of code controls the internal processing speed or 'frame-rate' + # It's pretty messy + # if not gui.pl_update and gui.rendered_playlist_position != playlist_view_position: + # logging.warning("The playlist failed to render at the latest position!!!!") + + power += 1 + + if pctl.playerCommandReady: + if tauon.thread_manager.player_lock.locked(): + try: + tauon.thread_manager.player_lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked player_lock") + else: + logging.exception("Unknown RuntimeError trying to release player_lock") + except Exception: + logging.exception("Unknown exception trying to release player_lock") - if k_input: - cursor_blink_timer.set() - TextBox.cursor = True + if gui.frame_callback_list: + i = len(gui.frame_callback_list) - 1 + while i >= 0: + if gui.frame_callback_list[i].test(): + gui.update = 1 + power = 1000 + del gui.frame_callback_list[i] + i -= 1 - SDL_Delay(3) - power = 1000 + if animate_monitor_timer.get() < 1 or load_orders: - if mouse_wheel or k_input or gui.pl_update or gui.update or top_panel.adds: # or mouse_moved: - power = 1000 + if cursor_blink_timer.get() > 0.65: + cursor_blink_timer.set() + TextBox.cursor ^= True + gui.update = 1 - if prefs.art_bg and core_timer.get() < 3: - power = 1000 + if k_input: + cursor_blink_timer.set() + TextBox.cursor = True - if mouse_down and mouse_moved: - power = 1000 - if gui.update_on_drag: - gui.update += 1 - if gui.pl_update_on_drag: - gui.pl_update += 1 + SDL_Delay(3) + power = 1000 - if pctl.wake_past_time: + if mouse_wheel or k_input or gui.pl_update or gui.update or top_panel.adds: # or mouse_moved: + power = 1000 - if get_real_time() > pctl.wake_past_time: - pctl.wake_past_time = 0 + if prefs.art_bg and core_timer.get() < 3: power = 1000 - gui.update += 1 - if gui.level_update and not album_scroll_hold and not scroll_hold: - power = 500 + if mouse_down and mouse_moved: + power = 1000 + if gui.update_on_drag: + gui.update += 1 + if gui.pl_update_on_drag: + gui.pl_update += 1 - # if gui.vis == 3 and (pctl.playing_state == 1 or pctl.playing_state == 3): - # power = 500 - # if len(gui.spec2_buffers) > 0 and gui.spec2_timer.get() > 0.04: - # gui.spec2_timer.set() - # gui.level_update = True - # vis_update = True - # else: - # SDL_Delay(5) + if pctl.wake_past_time: - if not pctl.running: - break + if get_real_time() > pctl.wake_past_time: + pctl.wake_past_time = 0 + power = 1000 + gui.update += 1 - if pctl.playing_state > 0: - power += 400 + if gui.level_update and not album_scroll_hold and not scroll_hold: + power = 500 - if power < 500: + # if gui.vis == 3 and (pctl.playing_state == 1 or pctl.playing_state == 3): + # power = 500 + # if len(gui.spec2_buffers) > 0 and gui.spec2_timer.get() > 0.04: + # gui.spec2_timer.set() + # gui.level_update = True + # vis_update = True + # else: + # SDL_Delay(5) - time.sleep(0.03) + if not pctl.running: + break - if ( - pctl.playing_state == 0 or pctl.playing_state == 2) and not load_orders and gui.update == 0 and not tauon.gall_ren.queue and not transcode_list and not gui.frame_callback_list: - pass - else: - sleep_timer.set() - if sleep_timer.get() > 2: - SDL_WaitEventTimeout(None, 1000) - continue + if pctl.playing_state > 0: + power += 400 - else: - power = 0 + if power < 500: - gui.pl_update = min(gui.pl_update, 2) + time.sleep(0.03) - new_playlist_cooldown = False + if ( + pctl.playing_state == 0 or pctl.playing_state == 2) and not load_orders and gui.update == 0 and not tauon.gall_ren.queue and not transcode_list and not gui.frame_callback_list: + pass + else: + sleep_timer.set() + if sleep_timer.get() > 2: + SDL_WaitEventTimeout(None, 1000) + continue - if prefs.auto_extract and prefs.monitor_downloads: - dl_mon.scan() + else: + power = 0 - if mouse_down and not coll((2, 2, window_size[0] - 4, window_size[1] - 4)): - #logging.info(SDL_GetMouseState(None, None)) - if SDL_GetGlobalMouseState(None, None) == 0: - mouse_down = False - mouse_up = True - quick_drag = False + gui.pl_update = min(gui.pl_update, 2) - #logging.info(window_size) - # if window_size[0] / window_size[1] == 16 / 9: - # logging.info('OK') - # if window_size[0] / window_size[1] > 16 / 9: - # logging.info("A") + new_playlist_cooldown = False - if key_meta: - input_text = "" - k_input = False - inp.key_return_press = False - inp.key_tab_press = False + if prefs.auto_extract and prefs.monitor_downloads: + dl_mon.scan() - if k_input: - if inp.mouse_click or right_click or mouse_up: - last_click_location = copy.deepcopy(click_location) - click_location = copy.deepcopy(mouse_position) + if mouse_down and not coll((2, 2, window_size[0] - 4, window_size[1] - 4)): + #logging.info(SDL_GetMouseState(None, None)) + if SDL_GetGlobalMouseState(None, None) == 0: + mouse_down = False + mouse_up = True + quick_drag = False - if key_focused != 0: - keymaps.hits.clear() + #logging.info(window_size) + # if window_size[0] / window_size[1] == 16 / 9: + # logging.info('OK') + # if window_size[0] / window_size[1] > 16 / 9: + # logging.info("A") - # d_mouse_click = False - # right_click = False - # level_2_right_click = False - # inp.mouse_click = False - # middle_click = False - mouse_up = False + if key_meta: + input_text = "" + k_input = False inp.key_return_press = False - key_down_press = False - key_up_press = False - key_right_press = False - key_left_press = False - key_esc_press = False - key_del = False - inp.backspace_press = 0 - key_backspace_press = False inp.key_tab_press = False - key_c_press = False - key_v_press = False - # key_f_press = False - key_a_press = False - # key_t_press = False - key_z_press = False - key_x_press = False - key_home_press = False - key_end_press = False - mouse_wheel = 0 - pref_box.scroll = 0 - input_text = "" - inp.level_2_enter = False - if c_yax != 0: - if c_yax_timer.get() >= 0: - if c_yax == -1: - key_up_press = True - if c_yax == 1: - key_down_press = True - c_yax_timer.force_set(-0.01) - gui.delay_frame(0.02) - k_input = True - if c_xax != 0: - if c_xax_timer.get() >= 0: - if c_xax == 1: - pctl.seek_time(pctl.playing_time + 2) - if c_xax == -1: - pctl.seek_time(pctl.playing_time - 2) - c_xax_timer.force_set(-0.01) - gui.delay_frame(0.02) - k_input = True - if c_xay != 0: - if c_xay_timer.get() >= 0: - if c_xay == -1: - pctl.player_volume += 1 - pctl.player_volume = min(pctl.player_volume, 100) - pctl.set_volume() - if c_xay == 1: - if pctl.player_volume > 1: - pctl.player_volume -= 1 - else: - pctl.player_volume = 0 - pctl.set_volume() - c_xay_timer.force_set(-0.01) - gui.delay_frame(0.02) - k_input = True - - if k_input and key_focused == 0: - - if keymaps.hits: - n = 1 - while n < 10: - if keymaps.test(f"jump-playlist-{n}"): - if len(pctl.multi_playlist) > n - 1: - switch_playlist(n - 1) - n += 1 - - if keymaps.test("cycle-playlist-left"): - if gui.album_tab_mode and key_left_press: - pass - elif is_level_zero() or quick_search_mode: - cycle_playlist_pinned(1) - if keymaps.test("cycle-playlist-right"): - if gui.album_tab_mode and key_right_press: - pass - elif is_level_zero() or quick_search_mode: - cycle_playlist_pinned(-1) - - if keymaps.test("toggle-console"): - console.toggle() - - if keymaps.test("toggle-fullscreen"): - if not gui.fullscreen and gui.mode != 3: - gui.fullscreen = True - SDL_SetWindowFullscreen(t_window, SDL_WINDOW_FULLSCREEN_DESKTOP) - elif gui.fullscreen: - gui.fullscreen = False - SDL_SetWindowFullscreen(t_window, 0) - - if keymaps.test("playlist-toggle-breaks"): - # Toggle force off folder break for viewed playlist - pctl.multi_playlist[pctl.active_playlist_viewing].hide_title ^= 1 - gui.pl_update = 1 + if k_input: + if inp.mouse_click or right_click or mouse_up: + last_click_location = copy.deepcopy(click_location) + click_location = copy.deepcopy(mouse_position) + + if key_focused != 0: + keymaps.hits.clear() + + # d_mouse_click = False + # right_click = False + # level_2_right_click = False + # inp.mouse_click = False + # middle_click = False + mouse_up = False + inp.key_return_press = False + key_down_press = False + key_up_press = False + key_right_press = False + key_left_press = False + key_esc_press = False + key_del = False + inp.backspace_press = 0 + key_backspace_press = False + inp.key_tab_press = False + key_c_press = False + key_v_press = False + # key_f_press = False + key_a_press = False + # key_t_press = False + key_z_press = False + key_x_press = False + key_home_press = False + key_end_press = False + mouse_wheel = 0 + pref_box.scroll = 0 + input_text = "" + inp.level_2_enter = False + + if c_yax != 0: + if c_yax_timer.get() >= 0: + if c_yax == -1: + key_up_press = True + if c_yax == 1: + key_down_press = True + c_yax_timer.force_set(-0.01) + gui.delay_frame(0.02) + k_input = True + if c_xax != 0: + if c_xax_timer.get() >= 0: + if c_xax == 1: + pctl.seek_time(pctl.playing_time + 2) + if c_xax == -1: + pctl.seek_time(pctl.playing_time - 2) + c_xax_timer.force_set(-0.01) + gui.delay_frame(0.02) + k_input = True + if c_xay != 0: + if c_xay_timer.get() >= 0: + if c_xay == -1: + pctl.player_volume += 1 + pctl.player_volume = min(pctl.player_volume, 100) + pctl.set_volume() + if c_xay == 1: + if pctl.player_volume > 1: + pctl.player_volume -= 1 + else: + pctl.player_volume = 0 + pctl.set_volume() + c_xay_timer.force_set(-0.01) + gui.delay_frame(0.02) + k_input = True + + if k_input and key_focused == 0: + + if keymaps.hits: + n = 1 + while n < 10: + if keymaps.test(f"jump-playlist-{n}"): + if len(pctl.multi_playlist) > n - 1: + switch_playlist(n - 1) + n += 1 + + if keymaps.test("cycle-playlist-left"): + if gui.album_tab_mode and key_left_press: + pass + elif is_level_zero() or quick_search_mode: + cycle_playlist_pinned(1) + if keymaps.test("cycle-playlist-right"): + if gui.album_tab_mode and key_right_press: + pass + elif is_level_zero() or quick_search_mode: + cycle_playlist_pinned(-1) + + if keymaps.test("toggle-console"): + console.toggle() + + if keymaps.test("toggle-fullscreen"): + if not gui.fullscreen and gui.mode != 3: + gui.fullscreen = True + SDL_SetWindowFullscreen(t_window, SDL_WINDOW_FULLSCREEN_DESKTOP) + elif gui.fullscreen: + gui.fullscreen = False + SDL_SetWindowFullscreen(t_window, 0) + + if keymaps.test("playlist-toggle-breaks"): + # Toggle force off folder break for viewed playlist + pctl.multi_playlist[pctl.active_playlist_viewing].hide_title ^= 1 + gui.pl_update = 1 + + if keymaps.test("find-playing-artist"): + # standard_size() + if len(pctl.track_queue) > 0: + quick_search_mode = True + search_text.text = "" + input_text = pctl.playing_object().artist - if keymaps.test("find-playing-artist"): - # standard_size() - if len(pctl.track_queue) > 0: - quick_search_mode = True - search_text.text = "" - input_text = pctl.playing_object().artist + if keymaps.test("show-encode-folder"): + open_encode_out() - if keymaps.test("show-encode-folder"): - open_encode_out() + if keymaps.test("toggle-left-panel"): + gui.lsp ^= True + update_layout_do() - if keymaps.test("toggle-left-panel"): - gui.lsp ^= True - update_layout_do() + if keymaps.test("toggle-last-left-panel"): + toggle_left_last() + update_layout_do() - if keymaps.test("toggle-last-left-panel"): - toggle_left_last() - update_layout_do() + if keymaps.test("escape"): + key_esc_press = True - if keymaps.test("escape"): - key_esc_press = True + if key_ctrl_down: + gui.pl_update += 1 - if key_ctrl_down: - gui.pl_update += 1 + if mouse_enter_window: + inp.key_return_press = False - if mouse_enter_window: - inp.key_return_press = False + if gui.fullscreen and key_esc_press: + gui.fullscreen = False + SDL_SetWindowFullscreen(t_window, 0) + + # Disable keys for text cursor control + if not gui.rename_folder_box and not rename_track_box.active and not gui.rename_playlist_box and not radiobox.active and not pref_box.enabled and not trans_edit_box.active: + + if not quick_search_mode and not search_over.active: + if album_mode and gui.album_tab_mode \ + and not key_ctrl_down \ + and not key_meta \ + and not key_lalt: + if key_left_press: + gal_left = True + key_left_press = False + if key_right_press: + gal_right = True + key_right_press = False + if key_up_press: + gal_up = True + key_up_press = False + if key_down_press: + gal_down = True + key_down_press = False - if gui.fullscreen and key_esc_press: - gui.fullscreen = False - SDL_SetWindowFullscreen(t_window, 0) + if not search_over.active: + if key_del: + close_all_menus() + del_selected() - # Disable keys for text cursor control - if not gui.rename_folder_box and not rename_track_box.active and not gui.rename_playlist_box and not radiobox.active and not pref_box.enabled and not trans_edit_box.active: + # Arrow keys to change playlist + if (key_left_press or key_right_press) and len(pctl.multi_playlist) > 1: + gui.pl_update = 1 + gui.update += 1 - if not quick_search_mode and not search_over.active: - if album_mode and gui.album_tab_mode \ - and not key_ctrl_down \ - and not key_meta \ - and not key_lalt: - if key_left_press: - gal_left = True - key_left_press = False - if key_right_press: - gal_right = True - key_right_press = False - if key_up_press: - gal_up = True - key_up_press = False - if key_down_press: - gal_down = True - key_down_press = False - - if not search_over.active: - if key_del: - close_all_menus() - del_selected() + if keymaps.test("start"): + if pctl.playing_time < 4: + pctl.back() + else: + pctl.new_time = 0 + pctl.playing_time = 0 + pctl.decode_time = 0 + pctl.playerCommand = "seek" + pctl.playerCommandReady = True - # Arrow keys to change playlist - if (key_left_press or key_right_press) and len(pctl.multi_playlist) > 1: + if keymaps.test("goto-top"): + pctl.playlist_view_position = 0 + logging.debug("Position changed by key") + pctl.selected_in_playlist = 0 gui.pl_update = 1 - gui.update += 1 - if keymaps.test("start"): - if pctl.playing_time < 4: - pctl.back() - else: - pctl.new_time = 0 - pctl.playing_time = 0 - pctl.decode_time = 0 - pctl.playerCommand = "seek" - pctl.playerCommandReady = True - - if keymaps.test("goto-top"): - pctl.playlist_view_position = 0 - logging.debug("Position changed by key") - pctl.selected_in_playlist = 0 - gui.pl_update = 1 + if keymaps.test("goto-bottom"): + n = len(default_playlist) - gui.playlist_view_length + 1 + n = max(n, 0) + pctl.playlist_view_position = n + logging.debug("Position changed by key") + pctl.selected_in_playlist = len(default_playlist) - 1 + gui.pl_update = 1 - if keymaps.test("goto-bottom"): - n = len(default_playlist) - gui.playlist_view_length + 1 - n = max(n, 0) - pctl.playlist_view_position = n - logging.debug("Position changed by key") - pctl.selected_in_playlist = len(default_playlist) - 1 - gui.pl_update = 1 + if not pref_box.enabled and not radiobox.active and not rename_track_box.active \ + and not gui.rename_folder_box \ + and not gui.rename_playlist_box and not search_over.active and not gui.box_over and not trans_edit_box.active: - if not pref_box.enabled and not radiobox.active and not rename_track_box.active \ - and not gui.rename_folder_box \ - and not gui.rename_playlist_box and not search_over.active and not gui.box_over and not trans_edit_box.active: + if quick_search_mode: + if keymaps.test("add-to-queue") and pctl.selected_ready(): + add_selected_to_queue() + input_text = "" - if quick_search_mode: - if keymaps.test("add-to-queue") and pctl.selected_ready(): - add_selected_to_queue() - input_text = "" + else: - else: + if key_c_press and key_ctrl_down: + gui.pl_update = 1 + s_copy() - if key_c_press and key_ctrl_down: - gui.pl_update = 1 - s_copy() + if key_x_press and key_ctrl_down: + gui.pl_update = 1 + s_cut() - if key_x_press and key_ctrl_down: - gui.pl_update = 1 - s_cut() + if key_v_press and key_ctrl_down: + gui.pl_update = 1 + paste() - if key_v_press and key_ctrl_down: - gui.pl_update = 1 - paste() + if keymaps.test("playpause"): + pctl.play_pause() - if keymaps.test("playpause"): - pctl.play_pause() + if inp.key_return_press and (gui.rename_folder_box or rename_track_box.active or radiobox.active): + inp.key_return_press = False + inp.level_2_enter = True - if inp.key_return_press and (gui.rename_folder_box or rename_track_box.active or radiobox.active): - inp.key_return_press = False - inp.level_2_enter = True + if key_ctrl_down and key_z_press: + undo.undo() - if key_ctrl_down and key_z_press: - undo.undo() + if keymaps.test("quit"): + tauon.exit("Quit keyboard shortcut pressed") - if keymaps.test("quit"): - tauon.exit("Quit keyboard shortcut pressed") + if keymaps.test("testkey"): # F7: test + pass - if keymaps.test("testkey"): # F7: test - pass + if gui.mode < 3: + if keymaps.test("toggle-auto-theme"): + prefs.colour_from_image ^= True + if prefs.colour_from_image: + show_message(_("Enabled auto theme")) + else: + show_message(_("Disabled auto theme")) + gui.reload_theme = True + gui.theme_temp_current = -1 + + if keymaps.test("transfer-playtime-to"): + if len(cargo) == 1 and tauon.copied_track is not None and -1 < pctl.selected_in_playlist < len( + default_playlist): + fr = pctl.get_track(tauon.copied_track) + to = pctl.get_track(default_playlist[pctl.selected_in_playlist]) + + fr_s = star_store.full_get(fr.index) + to_s = star_store.full_get(to.index) + + fr_scr = fr.lfm_scrobbles + to_scr = to.lfm_scrobbles + + undo.bk_playtime_transfer(fr, fr_s, fr_scr, to, to_s, to_scr) + + if to_s is None: + to_s = star_store.new_object() + if fr_s is None: + fr_s = star_store.new_object() + + new = star_store.new_object() + + new[0] = fr_s[0] + to_s[0] # playtime + new[1] = fr_s[1] # flags + if to_s[1]: + new[1] = to_s[1] # keep target flags + new[2] = fr_s[2] # raiting + if to_s[2] > 0 and fr_s[2] == 0: + new[2] = to_s[2] # keep target rating + to.lfm_scrobbles = fr.lfm_scrobbles + + star_store.remove(fr.index) + star_store.remove(to.index) + if new[0] or new[1] or new[2]: + star_store.insert(to.index, new) + + tauon.copied_track = None + gui.pl_update += 1 + logging.info("Transferred track stats!") + elif tauon.copied_track is None: + show_message(_("First select a source track by copying it into clipboard")) - if gui.mode < 3: - if keymaps.test("toggle-auto-theme"): - prefs.colour_from_image ^= True - if prefs.colour_from_image: - show_message(_("Enabled auto theme")) - else: - show_message(_("Disabled auto theme")) - gui.reload_theme = True - gui.theme_temp_current = -1 - - if keymaps.test("transfer-playtime-to"): - if len(cargo) == 1 and tauon.copied_track is not None and -1 < pctl.selected_in_playlist < len( - default_playlist): - fr = pctl.get_track(tauon.copied_track) - to = pctl.get_track(default_playlist[pctl.selected_in_playlist]) - - fr_s = star_store.full_get(fr.index) - to_s = star_store.full_get(to.index) - - fr_scr = fr.lfm_scrobbles - to_scr = to.lfm_scrobbles - - undo.bk_playtime_transfer(fr, fr_s, fr_scr, to, to_s, to_scr) - - if to_s is None: - to_s = star_store.new_object() - if fr_s is None: - fr_s = star_store.new_object() - - new = star_store.new_object() - - new[0] = fr_s[0] + to_s[0] # playtime - new[1] = fr_s[1] # flags - if to_s[1]: - new[1] = to_s[1] # keep target flags - new[2] = fr_s[2] # raiting - if to_s[2] > 0 and fr_s[2] == 0: - new[2] = to_s[2] # keep target rating - to.lfm_scrobbles = fr.lfm_scrobbles - - star_store.remove(fr.index) - star_store.remove(to.index) - if new[0] or new[1] or new[2]: - star_store.insert(to.index, new) - - tauon.copied_track = None - gui.pl_update += 1 - logging.info("Transferred track stats!") - elif tauon.copied_track is None: - show_message(_("First select a source track by copying it into clipboard")) - - if keymaps.test("toggle-gallery"): - toggle_album_mode() - - if keymaps.test("toggle-right-panel"): - if gui.combo_mode: - exit_combo() - elif not album_mode: - toggle_side_panel() - else: + if keymaps.test("toggle-gallery"): toggle_album_mode() - if keymaps.test("toggle-minimode"): - set_mini_mode() - gui.update += 1 + if keymaps.test("toggle-right-panel"): + if gui.combo_mode: + exit_combo() + elif not album_mode: + toggle_side_panel() + else: + toggle_album_mode() - if keymaps.test("cycle-layouts"): + if keymaps.test("toggle-minimode"): + set_mini_mode() + gui.update += 1 - if view_box.tracks(): - view_box.side(True) - elif view_box.side(): - view_box.gallery1(True) - elif view_box.gallery1(): - view_box.lyrics(True) - else: - view_box.tracks(True) + if keymaps.test("cycle-layouts"): - if keymaps.test("cycle-layouts-reverse"): + if view_box.tracks(): + view_box.side(True) + elif view_box.side(): + view_box.gallery1(True) + elif view_box.gallery1(): + view_box.lyrics(True) + else: + view_box.tracks(True) - if view_box.tracks(): - view_box.lyrics(True) - elif view_box.lyrics(): - view_box.gallery1(True) - elif view_box.gallery1(): - view_box.side(True) - else: - view_box.tracks(True) + if keymaps.test("cycle-layouts-reverse"): + + if view_box.tracks(): + view_box.lyrics(True) + elif view_box.lyrics(): + view_box.gallery1(True) + elif view_box.gallery1(): + view_box.side(True) + else: + view_box.tracks(True) - if keymaps.test("toggle-columns"): - view_box.col(True) + if keymaps.test("toggle-columns"): + view_box.col(True) - if keymaps.test("toggle-artistinfo"): - view_box.artist_info(True) + if keymaps.test("toggle-artistinfo"): + view_box.artist_info(True) - if keymaps.test("toggle-showcase"): - view_box.lyrics(True) + if keymaps.test("toggle-showcase"): + view_box.lyrics(True) - if keymaps.test("toggle-gallery-keycontrol"): - toggle_gallery_keycontrol() + if keymaps.test("toggle-gallery-keycontrol"): + toggle_gallery_keycontrol() - if keymaps.test("toggle-show-art"): - toggle_side_art() + if keymaps.test("toggle-show-art"): + toggle_side_art() - elif gui.mode == 3: - if keymaps.test("toggle-minimode"): - restore_full_mode() - gui.update += 1 + elif gui.mode == 3: + if keymaps.test("toggle-minimode"): + restore_full_mode() + gui.update += 1 - ab_click = False + ab_click = False - if keymaps.test("new-playlist"): - new_playlist() + if keymaps.test("new-playlist"): + new_playlist() - if keymaps.test("edit-generator"): - edit_generator_box(pctl.active_playlist_viewing) + if keymaps.test("edit-generator"): + edit_generator_box(pctl.active_playlist_viewing) - if keymaps.test("new-generator-playlist"): - new_playlist() - edit_generator_box(pctl.active_playlist_viewing) + if keymaps.test("new-generator-playlist"): + new_playlist() + edit_generator_box(pctl.active_playlist_viewing) - if keymaps.test("delete-playlist"): - delete_playlist(pctl.active_playlist_viewing) + if keymaps.test("delete-playlist"): + delete_playlist(pctl.active_playlist_viewing) - if keymaps.test("delete-playlist-force"): - delete_playlist(pctl.active_playlist_viewing, force=True) + if keymaps.test("delete-playlist-force"): + delete_playlist(pctl.active_playlist_viewing, force=True) - if keymaps.test("rename-playlist"): - if gui.radio_view: - rename_playlist(pctl.radio_playlist_viewing) - else: - rename_playlist(pctl.active_playlist_viewing) - rename_playlist_box.x = 60 * gui.scale - rename_playlist_box.y = 60 * gui.scale + if keymaps.test("rename-playlist"): + if gui.radio_view: + rename_playlist(pctl.radio_playlist_viewing) + else: + rename_playlist(pctl.active_playlist_viewing) + rename_playlist_box.x = 60 * gui.scale + rename_playlist_box.y = 60 * gui.scale - # Transfer click register to menus - if inp.mouse_click: - for instance in Menu.instances: - if instance.active: - instance.click() - inp.mouse_click = False - ab_click = True - if view_box.active: - view_box.clicked = True + # Transfer click register to menus + if inp.mouse_click: + for instance in Menu.instances: + if instance.active: + instance.click() + inp.mouse_click = False + ab_click = True + if view_box.active: + view_box.clicked = True - if inp.mouse_click and ( - prefs.show_nag or gui.box_over or radiobox.active or search_over.active or gui.rename_folder_box or gui.rename_playlist_box or rename_track_box.active or view_box.active or trans_edit_box.active): # and not gui.message_box: - inp.mouse_click = False - gui.level_2_click = True - else: - gui.level_2_click = False - - if track_box and inp.mouse_click: - w = 540 - h = 240 - x = int(window_size[0] / 2) - int(w / 2) - y = int(window_size[1] / 2) - int(h / 2) - if coll([x, y, w, h]): + if inp.mouse_click and ( + prefs.show_nag or gui.box_over or radiobox.active or search_over.active or gui.rename_folder_box or gui.rename_playlist_box or rename_track_box.active or view_box.active or trans_edit_box.active): # and not gui.message_box: inp.mouse_click = False gui.level_2_click = True + else: + gui.level_2_click = False - if right_click: - level_2_right_click = True + if track_box and inp.mouse_click: + w = 540 + h = 240 + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) + if coll([x, y, w, h]): + inp.mouse_click = False + gui.level_2_click = True - if pref_box.enabled: + if right_click: + level_2_right_click = True - if pref_box.inside(): - if inp.mouse_click: # and not gui.message_box: - pref_box.click = True - inp.mouse_click = False - if right_click: - right_click = False - pref_box.right_click = True + if pref_box.enabled: - pref_box.scroll = mouse_wheel - mouse_wheel = 0 - else: - if inp.mouse_click: - pref_box.close() - if right_click: - pref_box.close() - if pref_box.lock is False: - pass + if pref_box.inside(): + if inp.mouse_click: # and not gui.message_box: + pref_box.click = True + inp.mouse_click = False + if right_click: + right_click = False + pref_box.right_click = True - if right_click and ( - radiobox.active or rename_track_box.active or gui.rename_playlist_box or gui.rename_folder_box or search_over.active): - right_click = False + pref_box.scroll = mouse_wheel + mouse_wheel = 0 + else: + if inp.mouse_click: + pref_box.close() + if right_click: + pref_box.close() + if pref_box.lock is False: + pass - if mouse_wheel != 0: - gui.update += 1 - if mouse_down is True: - gui.update += 1 + if right_click and ( + radiobox.active or rename_track_box.active or gui.rename_playlist_box or gui.rename_folder_box or search_over.active): + right_click = False - if keymaps.test("pagedown"): # key_PGD: - if len(default_playlist) > 10: - pctl.playlist_view_position += gui.playlist_view_length - 4 - if pctl.playlist_view_position > len(default_playlist): - pctl.playlist_view_position = len(default_playlist) - 2 - gui.pl_update = 1 - pctl.selected_in_playlist = pctl.playlist_view_position - logging.debug("Position changed by page key") - shift_selection.clear() - if keymaps.test("pageup"): - if len(default_playlist) > 0: - pctl.playlist_view_position -= gui.playlist_view_length - 4 - pctl.playlist_view_position = max(pctl.playlist_view_position, 0) - gui.pl_update = 1 - pctl.selected_in_playlist = pctl.playlist_view_position - logging.debug("Position changed by page key") - shift_selection.clear() + if mouse_wheel != 0: + gui.update += 1 + if mouse_down is True: + gui.update += 1 - if quick_search_mode is False and rename_track_box.active is False and gui.rename_folder_box is False and gui.rename_playlist_box is False and not pref_box.enabled and not radiobox.active: + if keymaps.test("pagedown"): # key_PGD: + if len(default_playlist) > 10: + pctl.playlist_view_position += gui.playlist_view_length - 4 + if pctl.playlist_view_position > len(default_playlist): + pctl.playlist_view_position = len(default_playlist) - 2 + gui.pl_update = 1 + pctl.selected_in_playlist = pctl.playlist_view_position + logging.debug("Position changed by page key") + shift_selection.clear() + if keymaps.test("pageup"): + if len(default_playlist) > 0: + pctl.playlist_view_position -= gui.playlist_view_length - 4 + pctl.playlist_view_position = max(pctl.playlist_view_position, 0) + gui.pl_update = 1 + pctl.selected_in_playlist = pctl.playlist_view_position + logging.debug("Position changed by page key") + shift_selection.clear() - if keymaps.test("info-playing"): - if pctl.selected_in_playlist < len(default_playlist): - r_menu_index = pctl.get_track(default_playlist[pctl.selected_in_playlist]).index - track_box = True + if quick_search_mode is False and rename_track_box.active is False and gui.rename_folder_box is False and gui.rename_playlist_box is False and not pref_box.enabled and not radiobox.active: - if keymaps.test("info-show"): - if pctl.selected_in_playlist < len(default_playlist): - r_menu_index = pctl.get_track(default_playlist[pctl.selected_in_playlist]).index - track_box = True + if keymaps.test("info-playing"): + if pctl.selected_in_playlist < len(default_playlist): + r_menu_index = pctl.get_track(default_playlist[pctl.selected_in_playlist]).index + track_box = True - # These need to be disabled when text fields are active - if not search_over.active and not gui.box_over and not radiobox.active and not gui.rename_folder_box and not rename_track_box.active and not gui.rename_playlist_box and not trans_edit_box.active: - if keymaps.test("advance"): - key_right_press = False - pctl.advance() + if keymaps.test("info-show"): + if pctl.selected_in_playlist < len(default_playlist): + r_menu_index = pctl.get_track(default_playlist[pctl.selected_in_playlist]).index + track_box = True - if keymaps.test("previous"): - key_left_press = False - pctl.back() + # These need to be disabled when text fields are active + if not search_over.active and not gui.box_over and not radiobox.active and not gui.rename_folder_box and not rename_track_box.active and not gui.rename_playlist_box and not trans_edit_box.active: + if keymaps.test("advance"): + key_right_press = False + pctl.advance() - if key_a_press and key_ctrl_down: - gui.pl_update = 1 - shift_selection = range(len(default_playlist)) # TODO(Martin): This can under some circumstances end up doing a range.clear() + if keymaps.test("previous"): + key_left_press = False + pctl.back() + + if key_a_press and key_ctrl_down: + gui.pl_update = 1 + shift_selection = range(len(default_playlist)) # TODO(Martin): This can under some circumstances end up doing a range.clear() - if keymaps.test("revert"): - pctl.revert() + if keymaps.test("revert"): + pctl.revert() - if keymaps.test("random-track-start"): - pctl.advance(rr=True) + if keymaps.test("random-track-start"): + pctl.advance(rr=True) - if keymaps.test("vol-down"): - if pctl.player_volume > 3: - pctl.player_volume -= 3 - else: - pctl.player_volume = 0 - pctl.set_volume() + if keymaps.test("vol-down"): + if pctl.player_volume > 3: + pctl.player_volume -= 3 + else: + pctl.player_volume = 0 + pctl.set_volume() - if keymaps.test("toggle-mute"): - pctl.toggle_mute() + if keymaps.test("toggle-mute"): + pctl.toggle_mute() - if keymaps.test("vol-up"): - pctl.player_volume += 3 - pctl.player_volume = min(pctl.player_volume, 100) - pctl.set_volume() + if keymaps.test("vol-up"): + pctl.player_volume += 3 + pctl.player_volume = min(pctl.player_volume, 100) + pctl.set_volume() + + if keymaps.test("shift-down") and len(default_playlist) > 0: + gui.pl_update += 1 + if pctl.selected_in_playlist > len(default_playlist) - 1: + pctl.selected_in_playlist = 0 - if keymaps.test("shift-down") and len(default_playlist) > 0: - gui.pl_update += 1 - if pctl.selected_in_playlist > len(default_playlist) - 1: - pctl.selected_in_playlist = 0 - - if not shift_selection: - shift_selection.append(pctl.selected_in_playlist) - if pctl.selected_in_playlist < len(default_playlist) - 1: - r = pctl.selected_in_playlist - pctl.selected_in_playlist += 1 - if pctl.selected_in_playlist not in shift_selection: + if not shift_selection: shift_selection.append(pctl.selected_in_playlist) - else: - shift_selection.remove(r) - - if keymaps.test("shift-up") and pctl.selected_in_playlist > -1: - gui.pl_update += 1 - if pctl.selected_in_playlist > len(default_playlist) - 1: - pctl.selected_in_playlist = 0 - - if not shift_selection: - shift_selection.append(pctl.selected_in_playlist) - if pctl.selected_in_playlist < len(default_playlist) - 1: - r = pctl.selected_in_playlist - pctl.selected_in_playlist -= 1 - if pctl.selected_in_playlist not in shift_selection: - shift_selection.insert(0, pctl.selected_in_playlist) - else: - shift_selection.remove(r) + if pctl.selected_in_playlist < len(default_playlist) - 1: + r = pctl.selected_in_playlist + pctl.selected_in_playlist += 1 + if pctl.selected_in_playlist not in shift_selection: + shift_selection.append(pctl.selected_in_playlist) + else: + shift_selection.remove(r) - if keymaps.test("toggle-shuffle"): - # pctl.random_mode ^= True - toggle_random() + if keymaps.test("shift-up") and pctl.selected_in_playlist > -1: + gui.pl_update += 1 + if pctl.selected_in_playlist > len(default_playlist) - 1: + pctl.selected_in_playlist = 0 - if keymaps.test("goto-playing"): - pctl.show_current() - if keymaps.test("goto-previous"): - if pctl.queue_step > 1: - pctl.show_current(index=pctl.track_queue[pctl.queue_step - 1]) + if not shift_selection: + shift_selection.append(pctl.selected_in_playlist) + if pctl.selected_in_playlist < len(default_playlist) - 1: + r = pctl.selected_in_playlist + pctl.selected_in_playlist -= 1 + if pctl.selected_in_playlist not in shift_selection: + shift_selection.insert(0, pctl.selected_in_playlist) + else: + shift_selection.remove(r) - if keymaps.test("toggle-repeat"): - toggle_repeat() + if keymaps.test("toggle-shuffle"): + # pctl.random_mode ^= True + toggle_random() - if keymaps.test("random-track"): - random_track() + if keymaps.test("goto-playing"): + pctl.show_current() + if keymaps.test("goto-previous"): + if pctl.queue_step > 1: + pctl.show_current(index=pctl.track_queue[pctl.queue_step - 1]) - if keymaps.test("random-album"): - random_album() + if keymaps.test("toggle-repeat"): + toggle_repeat() - if keymaps.test("opacity-up"): - prefs.window_opacity += .05 - prefs.window_opacity = min(prefs.window_opacity, 1) - SDL_SetWindowOpacity(t_window, prefs.window_opacity) + if keymaps.test("random-track"): + random_track() - if keymaps.test("opacity-down"): - prefs.window_opacity -= .05 - prefs.window_opacity = max(prefs.window_opacity, .30) - SDL_SetWindowOpacity(t_window, prefs.window_opacity) + if keymaps.test("random-album"): + random_album() - if keymaps.test("seek-forward"): - pctl.seek_time(pctl.playing_time + prefs.seek_interval) + if keymaps.test("opacity-up"): + prefs.window_opacity += .05 + prefs.window_opacity = min(prefs.window_opacity, 1) + SDL_SetWindowOpacity(t_window, prefs.window_opacity) - if keymaps.test("seek-back"): - pctl.seek_time(pctl.playing_time - prefs.seek_interval) + if keymaps.test("opacity-down"): + prefs.window_opacity -= .05 + prefs.window_opacity = max(prefs.window_opacity, .30) + SDL_SetWindowOpacity(t_window, prefs.window_opacity) - if keymaps.test("play"): - pctl.play() + if keymaps.test("seek-forward"): + pctl.seek_time(pctl.playing_time + prefs.seek_interval) - if keymaps.test("stop"): - pctl.stop() + if keymaps.test("seek-back"): + pctl.seek_time(pctl.playing_time - prefs.seek_interval) - if keymaps.test("pause"): - pctl.pause_only() + if keymaps.test("play"): + pctl.play() - if keymaps.test("love-playing"): - bar_love(notify=True) + if keymaps.test("stop"): + pctl.stop() - if keymaps.test("love-selected"): - select_love(notify=True) + if keymaps.test("pause"): + pctl.pause_only() - if keymaps.test("search-lyrics-selected"): - if pctl.selected_ready(): - track = pctl.get_track(default_playlist[pctl.selected_in_playlist]) - if track.lyrics: - show_message(_("Track already has lyrics")) - else: - get_lyric_wiki(track) + if keymaps.test("love-playing"): + bar_love(notify=True) - if keymaps.test("substitute-search-selected"): - if pctl.selected_ready(): - show_sub_search(pctl.get_track(default_playlist[pctl.selected_in_playlist])) + if keymaps.test("love-selected"): + select_love(notify=True) - if keymaps.test("global-search"): - activate_search_overlay() + if keymaps.test("search-lyrics-selected"): + if pctl.selected_ready(): + track = pctl.get_track(default_playlist[pctl.selected_in_playlist]) + if track.lyrics: + show_message(_("Track already has lyrics")) + else: + get_lyric_wiki(track) - if keymaps.test("add-to-queue") and pctl.selected_ready(): - add_selected_to_queue() + if keymaps.test("substitute-search-selected"): + if pctl.selected_ready(): + show_sub_search(pctl.get_track(default_playlist[pctl.selected_in_playlist])) - if keymaps.test("clear-queue"): - clear_queue() + if keymaps.test("global-search"): + activate_search_overlay() - if keymaps.test("regenerate-playlist"): - regenerate_playlist(pctl.active_playlist_viewing) + if keymaps.test("add-to-queue") and pctl.selected_ready(): + add_selected_to_queue() - if keymaps.test("cycle-theme"): - gui.reload_theme = True - gui.theme_temp_current = -1 - gui.temp_themes.clear() - theme += 1 + if keymaps.test("clear-queue"): + clear_queue() - if keymaps.test("cycle-theme-reverse"): - gui.theme_temp_current = -1 - gui.temp_themes.clear() - pref_box.devance_theme() + if keymaps.test("regenerate-playlist"): + regenerate_playlist(pctl.active_playlist_viewing) - if keymaps.test("reload-theme"): - gui.reload_theme = True + if keymaps.test("cycle-theme"): + gui.reload_theme = True + gui.theme_temp_current = -1 + gui.temp_themes.clear() + theme += 1 - # if mouse_position[1] < 1: - # mouse_down = False + if keymaps.test("cycle-theme-reverse"): + gui.theme_temp_current = -1 + gui.temp_themes.clear() + pref_box.devance_theme() - if mouse_down is False: - scroll_hold = False + if keymaps.test("reload-theme"): + gui.reload_theme = True - # if focused is True: - # mouse_down = False + # if mouse_position[1] < 1: + # mouse_down = False - if inp.media_key: - if inp.media_key == "Play": - if pctl.playing_state == 0: - pctl.play() - else: - pctl.pause() - elif inp.media_key == "Pause": - pctl.pause_only() - elif inp.media_key == "Stop": - pctl.stop() - elif inp.media_key == "Next": - pctl.advance() - elif inp.media_key == "Previous": - pctl.back() - - elif inp.media_key == "Rewind": - pctl.seek_time(pctl.playing_time - 10) - elif inp.media_key == "FastForward": - pctl.seek_time(pctl.playing_time + 10) - elif inp.media_key == "Repeat": - toggle_repeat() - elif inp.media_key == "Shuffle": - toggle_random() - - inp.media_key = "" - - if len(load_orders) > 0: - loading_in_progress = True - pctl.after_import_flag = True - tauon.thread_manager.ready("worker") - if loaderCommand == LC_None: + if mouse_down is False: + scroll_hold = False - # Fliter out files matching CUE filenames - # This isnt the only mechanism that does this. This one helps in the situation - # where the user drags and drops multiple files at onec. CUEs in folders are handled elsewhere - if len(load_orders) > 1: + # if focused is True: + # mouse_down = False + + if inp.media_key: + if inp.media_key == "Play": + if pctl.playing_state == 0: + pctl.play() + else: + pctl.pause() + elif inp.media_key == "Pause": + pctl.pause_only() + elif inp.media_key == "Stop": + pctl.stop() + elif inp.media_key == "Next": + pctl.advance() + elif inp.media_key == "Previous": + pctl.back() + + elif inp.media_key == "Rewind": + pctl.seek_time(pctl.playing_time - 10) + elif inp.media_key == "FastForward": + pctl.seek_time(pctl.playing_time + 10) + elif inp.media_key == "Repeat": + toggle_repeat() + elif inp.media_key == "Shuffle": + toggle_random() + + inp.media_key = "" + + if len(load_orders) > 0: + loading_in_progress = True + pctl.after_import_flag = True + tauon.thread_manager.ready("worker") + if loaderCommand == LC_None: + + # Fliter out files matching CUE filenames + # This isnt the only mechanism that does this. This one helps in the situation + # where the user drags and drops multiple files at onec. CUEs in folders are handled elsewhere + if len(load_orders) > 1: + for order in load_orders: + if order.stage == 0 and order.target.endswith(".cue"): + for order2 in load_orders: + if not order2.target.endswith(".cue") and\ + os.path.splitext(order2.target)[0] == os.path.splitext(order.target)[0] and\ + os.path.isfile(order2.target): + order2.stage = -1 + for i in reversed(range(len(load_orders))): + order = load_orders[i] + if order.stage == -1: + del load_orders[i] + + # Prepare loader thread with load order for order in load_orders: - if order.stage == 0 and order.target.endswith(".cue"): - for order2 in load_orders: - if not order2.target.endswith(".cue") and\ - os.path.splitext(order2.target)[0] == os.path.splitext(order.target)[0] and\ - os.path.isfile(order2.target): - order2.stage = -1 - for i in reversed(range(len(load_orders))): - order = load_orders[i] - if order.stage == -1: - del load_orders[i] - - # Prepare loader thread with load order - for order in load_orders: - if order.stage == 0: - order.traget = order.target.replace("\\", "/") - order.stage = 1 - if os.path.isdir(order.traget): - loaderCommand = LC_Folder - else: - loaderCommand = LC_File - if order.traget.endswith(".xspf"): - to_got = "xspf" - to_get = 0 + if order.stage == 0: + order.traget = order.target.replace("\\", "/") + order.stage = 1 + if os.path.isdir(order.traget): + loaderCommand = LC_Folder else: - to_got = 1 - to_get = 1 - loaderCommandReady = True - tauon.thread_manager.ready("worker") - break + loaderCommand = LC_File + if order.traget.endswith(".xspf"): + to_got = "xspf" + to_get = 0 + else: + to_got = 1 + to_get = 1 + loaderCommandReady = True + tauon.thread_manager.ready("worker") + break - elif loading_in_progress is True: - loading_in_progress = False - pctl.notify_change() + elif loading_in_progress is True: + loading_in_progress = False + pctl.notify_change() - if loaderCommand == LC_Done: - loaderCommand = LC_None - gui.update += 1 - # gui.pl_update = 1 - # loading_in_progress = False + if loaderCommand == LC_Done: + loaderCommand = LC_None + gui.update += 1 + # gui.pl_update = 1 + # loading_in_progress = False - if update_layout: - update_layout_do() - update_layout = False + if update_layout: + update_layout_do() + update_layout = False - # if tauon.worker_save_state and\ - # not gui.pl_pulse and\ - # not loading_in_progress and\ - # not to_scan and\ - # not plex.scanning and\ - # not cm_clean_db and\ - # not lastfm.scanning_friends and\ - # not move_in_progress: - # save_state() - # cue_list.clear() - # tauon.worker_save_state = False + # if tauon.worker_save_state and\ + # not gui.pl_pulse and\ + # not loading_in_progress and\ + # not to_scan and\ + # not plex.scanning and\ + # not cm_clean_db and\ + # not lastfm.scanning_friends and\ + # not move_in_progress: + # save_state() + # cue_list.clear() + # tauon.worker_save_state = False - # ----------------------------------------------------- - # THEME SWITCHER-------------------------------------------------------------------- + # ----------------------------------------------------- + # THEME SWITCHER-------------------------------------------------------------------- - if gui.reload_theme is True: + if gui.reload_theme is True: - gui.pl_update = 1 - theme_files = get_themes() + gui.pl_update = 1 + theme_files = get_themes() - if theme > len(theme_files): # sic - theme = 0 + if theme > len(theme_files): # sic + theme = 0 - if theme > 0: - theme_number = theme - 1 - try: + if theme > 0: + theme_number = theme - 1 + try: + + colours.column_colours.clear() + colours.column_colours_playing.clear() + + theme_item = theme_files[theme_number] + + gui.theme_name = theme_item[1] + colours.lm = False + colours.__init__() - colours.column_colours.clear() - colours.column_colours_playing.clear() + load_theme(colours, Path(theme_item[0])) + deco.load(colours.deco) + logging.info("Applying theme: " + gui.theme_name) - theme_item = theme_files[theme_number] + if colours.lm: + info_icon.colour = [60, 60, 60, 255] + else: + info_icon.colour = [61, 247, 163, 255] + + if colours.lm: + folder_icon.colour = [255, 190, 80, 255] + else: + folder_icon.colour = [244, 220, 66, 255] + + if colours.lm: + settings_icon.colour = [85, 187, 250, 255] + else: + settings_icon.colour = [232, 200, 96, 255] - gui.theme_name = theme_item[1] + if colours.lm: + radiorandom_icon.colour = [120, 200, 120, 255] + else: + radiorandom_icon.colour = [153, 229, 133, 255] + + except Exception: + logging.exception("Error loading theme file") + raise + show_message(_("Error loading theme file"), "", mode="warning") + + if theme == 0: + gui.theme_name = "Mindaro" + logging.info("Applying default theme: Mindaro") colours.lm = False colours.__init__() + colours.post_config() + deco.unload() - load_theme(colours, Path(theme_item[0])) - deco.load(colours.deco) - logging.info("Applying theme: " + gui.theme_name) + prefs.theme_name = gui.theme_name - if colours.lm: - info_icon.colour = [60, 60, 60, 255] - else: - info_icon.colour = [61, 247, 163, 255] + #logging.info("Theme number: " + str(theme)) + gui.reload_theme = False + ddt.text_background_colour = colours.playlist_panel_background - if colours.lm: - folder_icon.colour = [255, 190, 80, 255] - else: - folder_icon.colour = [244, 220, 66, 255] + # --------------------------------------------------------------------------------------------------------- + # GUI DRAWING------ + #logging.info(gui.update) + #logging.info(gui.lowered) + if gui.mode == 3: + gui.pl_update = 0 - if colours.lm: - settings_icon.colour = [85, 187, 250, 255] - else: - settings_icon.colour = [232, 200, 96, 255] + if gui.pl_update and not gui.update: + gui.update = 1 - if colours.lm: - radiorandom_icon.colour = [120, 200, 120, 255] - else: - radiorandom_icon.colour = [153, 229, 133, 255] + if gui.update > 0 and not resize_mode: + gui.update = min(gui.update, 2) - except Exception: - logging.exception("Error loading theme file") - raise - show_message(_("Error loading theme file"), "", mode="warning") - - if theme == 0: - gui.theme_name = "Mindaro" - logging.info("Applying default theme: Mindaro") - colours.lm = False - colours.__init__() - colours.post_config() - deco.unload() - - prefs.theme_name = gui.theme_name - - #logging.info("Theme number: " + str(theme)) - gui.reload_theme = False - ddt.text_background_colour = colours.playlist_panel_background - - # --------------------------------------------------------------------------------------------------------- - # GUI DRAWING------ - #logging.info(gui.update) - #logging.info(gui.lowered) - if gui.mode == 3: - gui.pl_update = 0 + if reset_render: + logging.info("Reset render targets!") + clear_img_cache(delete_disk=False) + ddt.clear_text_cache() + for item in WhiteModImageAsset.assets: + item.reload() + reset_render = False - if gui.pl_update and not gui.update: - gui.update = 1 + SDL_SetRenderTarget(renderer, None) + SDL_SetRenderDrawColor( + renderer, colours.top_panel_background[0], colours.top_panel_background[1], + colours.top_panel_background[2], colours.top_panel_background[3]) + SDL_RenderClear(renderer) + SDL_SetRenderTarget(renderer, gui.main_texture) + SDL_RenderClear(renderer) - if gui.update > 0 and not resize_mode: - gui.update = min(gui.update, 2) + # perf_timer.set() + gui.update_on_drag = False + gui.pl_update_on_drag = False - if reset_render: - logging.info("Reset render targets!") - clear_img_cache(delete_disk=False) - ddt.clear_text_cache() - for item in WhiteModImageAsset.assets: - item.reload() - reset_render = False + # mouse_position[0], mouse_position[1] = get_sdl_input.mouse() + gui.showed_title = False - SDL_SetRenderTarget(renderer, None) - SDL_SetRenderDrawColor( - renderer, colours.top_panel_background[0], colours.top_panel_background[1], - colours.top_panel_background[2], colours.top_panel_background[3]) - SDL_RenderClear(renderer) - SDL_SetRenderTarget(renderer, gui.main_texture) - SDL_RenderClear(renderer) + if not gui.mouse_in_window and not bottom_bar1.volume_bar_being_dragged and not bottom_bar1.volume_hit and not bottom_bar1.seek_hit: + mouse_position[0] = -300 + mouse_position[1] = -300 - # perf_timer.set() - gui.update_on_drag = False - gui.pl_update_on_drag = False + if gui.clear_image_cache_next: + gui.clear_image_cache_next -= 1 + album_art_gen.clear_cache() + style_overlay.radio_meta = None + if prefs.art_bg: + tauon.thread_manager.ready("style") - # mouse_position[0], mouse_position[1] = get_sdl_input.mouse() - gui.showed_title = False + fields.clear() + gui.cursor_want = 0 - if not gui.mouse_in_window and not bottom_bar1.volume_bar_being_dragged and not bottom_bar1.volume_hit and not bottom_bar1.seek_hit: - mouse_position[0] = -300 - mouse_position[1] = -300 + gui.layer_focus = 0 - if gui.clear_image_cache_next: - gui.clear_image_cache_next -= 1 - album_art_gen.clear_cache() - style_overlay.radio_meta = None - if prefs.art_bg: - tauon.thread_manager.ready("style") + if inp.mouse_click or mouse_wheel or right_click: + mouse_position[0], mouse_position[1] = get_sdl_input.mouse() - fields.clear() - gui.cursor_want = 0 + if inp.mouse_click: + n_click_time = time.time() + if n_click_time - click_time < 0.42: + d_mouse_click = True + click_time = n_click_time - gui.layer_focus = 0 + # Don't register bottom level click when closing message box + if gui.message_box and pref_box.enabled and not key_focused and not coll(message_box.get_rect()): + inp.mouse_click = False + gui.message_box = False - if inp.mouse_click or mouse_wheel or right_click: - mouse_position[0], mouse_position[1] = get_sdl_input.mouse() + # Enable the garbage collecter (since we disabled it during startup) + if ggc > 0: + if ggc == 2: + ggc = 1 + elif ggc == 1: + ggc = 0 + gbc.enable() + #logging.info("Enabling garbage collecting") - if inp.mouse_click: - n_click_time = time.time() - if n_click_time - click_time < 0.42: - d_mouse_click = True - click_time = n_click_time + if gui.mode == 4: + launch.render() + elif gui.mode == 1 or gui.mode == 2: - # Don't register bottom level click when closing message box - if gui.message_box and pref_box.enabled and not key_focused and not coll(message_box.get_rect()): - inp.mouse_click = False - gui.message_box = False + ddt.text_background_colour = colours.playlist_panel_background - # Enable the garbage collecter (since we disabled it during startup) - if ggc > 0: - if ggc == 2: - ggc = 1 - elif ggc == 1: - ggc = 0 - gbc.enable() - #logging.info("Enabling garbage collecting") + # Side Bar Draging---------- - if gui.mode == 4: - launch.render() - elif gui.mode == 1 or gui.mode == 2: + if not mouse_down: + side_drag = False - ddt.text_background_colour = colours.playlist_panel_background + rect = (window_size[0] - gui.rspw - 5 * gui.scale, gui.panelY, 12 * gui.scale, + window_size[1] - gui.panelY - gui.panelBY) + fields.add(rect) - # Side Bar Draging---------- + if (coll(rect) or side_drag is True) \ + and rename_track_box.active is False \ + and radiobox.active is False \ + and gui.rename_playlist_box is False \ + and gui.message_box is False \ + and pref_box.enabled is False \ + and track_box is False \ + and not gui.rename_folder_box \ + and not Menu.active \ + and (gui.rsp or album_mode) \ + and not artist_info_scroll.held \ + and gui.layer_focus == 0 and gui.show_playlist: + + if side_drag is True: + draw_sep_hl = True + # gui.update += 1 + gui.update_on_drag = True - if not mouse_down: - side_drag = False + if inp.mouse_click: + side_drag = True + gui.side_bar_drag_source = mouse_position[0] + gui.side_bar_drag_original = gui.rspw - rect = (window_size[0] - gui.rspw - 5 * gui.scale, gui.panelY, 12 * gui.scale, - window_size[1] - gui.panelY - gui.panelBY) - fields.add(rect) + if not quick_drag: + gui.cursor_want = 1 - if (coll(rect) or side_drag is True) \ - and rename_track_box.active is False \ - and radiobox.active is False \ - and gui.rename_playlist_box is False \ - and gui.message_box is False \ - and pref_box.enabled is False \ - and track_box is False \ - and not gui.rename_folder_box \ - and not Menu.active \ - and (gui.rsp or album_mode) \ - and not artist_info_scroll.held \ - and gui.layer_focus == 0 and gui.show_playlist: - - if side_drag is True: - draw_sep_hl = True - # gui.update += 1 - gui.update_on_drag = True - - if inp.mouse_click: - side_drag = True - gui.side_bar_drag_source = mouse_position[0] - gui.side_bar_drag_original = gui.rspw - - if not quick_drag: - gui.cursor_want = 1 - - # side drag update - if side_drag: - - offset = gui.side_bar_drag_source - mouse_position[0] - - target = gui.side_bar_drag_original + offset - - # Snap to album mode position if close - if not album_mode and prefs.side_panel_layout == 1: - if abs(target - gui.pref_gallery_w) < 35 * gui.scale: - target = gui.pref_gallery_w - - # Reset max ratio if drag drops below ratio width - if prefs.side_panel_layout == 0: - if target < round((window_size[1] - gui.panelY - gui.panelBY) * gui.art_aspect_ratio): - gui.art_max_ratio_lock = gui.art_aspect_ratio - - max_w = round(((window_size[ - 1] - gui.panelY - gui.panelBY - 17 * gui.scale) * gui.art_max_ratio_lock) + 17 * gui.scale) - # 17 here is the art box inset value + # side drag update + if side_drag: - else: - max_w = window_size[0] + offset = gui.side_bar_drag_source - mouse_position[0] - if not album_mode and target > max_w - 12 * gui.scale: - target = max_w - gui.rspw = target - gui.rsp_full_lock = True + target = gui.side_bar_drag_original + offset - else: - gui.rspw = target - gui.rsp_full_lock = False + # Snap to album mode position if close + if not album_mode and prefs.side_panel_layout == 1: + if abs(target - gui.pref_gallery_w) < 35 * gui.scale: + target = gui.pref_gallery_w - if album_mode: - pass - # gui.rspw = target + # Reset max ratio if drag drops below ratio width + if prefs.side_panel_layout == 0: + if target < round((window_size[1] - gui.panelY - gui.panelBY) * gui.art_aspect_ratio): + gui.art_max_ratio_lock = gui.art_aspect_ratio - if album_mode and gui.rspw < album_mode_art_size + 50 * gui.scale: - target = album_mode_art_size + 50 * gui.scale + max_w = round(((window_size[ + 1] - gui.panelY - gui.panelBY - 17 * gui.scale) * gui.art_max_ratio_lock) + 17 * gui.scale) + # 17 here is the art box inset value - # Prevent side bar getting too small - target = max(target, 120 * gui.scale) + else: + max_w = window_size[0] - # Remember size for this view mode - if not album_mode: - gui.pref_rspw = target - else: - gui.pref_gallery_w = target + if not album_mode and target > max_w - 12 * gui.scale: + target = max_w + gui.rspw = target + gui.rsp_full_lock = True - update_layout_do() + else: + gui.rspw = target + gui.rsp_full_lock = False - # ALBUM GALLERY RENDERING: - # Gallery view - # C-AR + if album_mode: + pass + # gui.rspw = target - if album_mode: - try: - # Arrow key input - if gal_right: - gal_right = False - gal_jump_select(False, 1) - goto_album(pctl.selected_in_playlist) - pctl.playlist_view_position = pctl.selected_in_playlist - logging.debug("Position changed by gallery key press") - gui.pl_update = 1 - if gal_down: - gal_down = False - gal_jump_select(False, row_len) - goto_album(pctl.selected_in_playlist, down=True) - pctl.playlist_view_position = pctl.selected_in_playlist - logging.debug("Position changed by gallery key press") - gui.pl_update = 1 - if gal_left: - gal_left = False - gal_jump_select(True, 1) - goto_album(pctl.selected_in_playlist) - pctl.playlist_view_position = pctl.selected_in_playlist - logging.debug("Position changed by gallery key press") - gui.pl_update = 1 - if gal_up: - gal_up = False - gal_jump_select(True, row_len) - goto_album(pctl.selected_in_playlist) - pctl.playlist_view_position = pctl.selected_in_playlist - logging.debug("Position changed by gallery key press") - gui.pl_update = 1 + if album_mode and gui.rspw < album_mode_art_size + 50 * gui.scale: + target = album_mode_art_size + 50 * gui.scale + + # Prevent side bar getting too small + target = max(target, 120 * gui.scale) + + # Remember size for this view mode + if not album_mode: + gui.pref_rspw = target + else: + gui.pref_gallery_w = target + + update_layout_do() + + # ALBUM GALLERY RENDERING: + # Gallery view + # C-AR + + if album_mode: + try: + # Arrow key input + if gal_right: + gal_right = False + gal_jump_select(False, 1) + goto_album(pctl.selected_in_playlist) + pctl.playlist_view_position = pctl.selected_in_playlist + logging.debug("Position changed by gallery key press") + gui.pl_update = 1 + if gal_down: + gal_down = False + gal_jump_select(False, row_len) + goto_album(pctl.selected_in_playlist, down=True) + pctl.playlist_view_position = pctl.selected_in_playlist + logging.debug("Position changed by gallery key press") + gui.pl_update = 1 + if gal_left: + gal_left = False + gal_jump_select(True, 1) + goto_album(pctl.selected_in_playlist) + pctl.playlist_view_position = pctl.selected_in_playlist + logging.debug("Position changed by gallery key press") + gui.pl_update = 1 + if gal_up: + gal_up = False + gal_jump_select(True, row_len) + goto_album(pctl.selected_in_playlist) + pctl.playlist_view_position = pctl.selected_in_playlist + logging.debug("Position changed by gallery key press") + gui.pl_update = 1 - w = gui.rspw + w = gui.rspw - if window_size[0] < 750 * gui.scale: - w = window_size[0] - 20 * gui.scale - if gui.lsp: - w -= gui.lspw + if window_size[0] < 750 * gui.scale: + w = window_size[0] - 20 * gui.scale + if gui.lsp: + w -= gui.lspw - x = window_size[0] - w - h = window_size[1] - gui.panelY - gui.panelBY + x = window_size[0] - w + h = window_size[1] - gui.panelY - gui.panelBY - if not gui.show_playlist and inp.mouse_click: - left = 0 - if gui.lsp: - left = gui.lspw + if not gui.show_playlist and inp.mouse_click: + left = 0 + if gui.lsp: + left = gui.lspw - if left < mouse_position[0] < left + 20 * gui.scale and window_size[1] - gui.panelBY > \ - mouse_position[1] > gui.panelY: - toggle_album_mode() - inp.mouse_click = False - mouse_down = False + if left < mouse_position[0] < left + 20 * gui.scale and window_size[1] - gui.panelBY > \ + mouse_position[1] > gui.panelY: + toggle_album_mode() + inp.mouse_click = False + mouse_down = False - rect = [x, gui.panelY, w, h] - ddt.rect(rect, colours.gallery_background) - # ddt.rect_r(rect, [255, 0, 0, 200], True) + rect = [x, gui.panelY, w, h] + ddt.rect(rect, colours.gallery_background) + # ddt.rect_r(rect, [255, 0, 0, 200], True) - area_x = w + 38 * gui.scale - # area_x = w - 40 * gui.scale + area_x = w + 38 * gui.scale + # area_x = w - 40 * gui.scale - row_len = int((area_x - album_h_gap) / (album_mode_art_size + album_h_gap)) + row_len = int((area_x - album_h_gap) / (album_mode_art_size + album_h_gap)) - #logging.info(row_len) + #logging.info(row_len) - compact = 40 * gui.scale - a_offset = 7 * gui.scale + compact = 40 * gui.scale + a_offset = 7 * gui.scale - l_area = x - r_area = w - c_area = r_area // 2 + l_area + l_area = x + r_area = w + c_area = r_area // 2 + l_area - ddt.text_background_colour = colours.gallery_background + ddt.text_background_colour = colours.gallery_background - line1_colour = colours.gallery_artist_line - line2_colour = colours.grey(240) # colours.side_bar_line1 + line1_colour = colours.gallery_artist_line + line2_colour = colours.grey(240) # colours.side_bar_line1 - if colours.side_panel_background != colours.gallery_background: - line2_colour = [240, 240, 240, 255] - line1_colour = alpha_mod([220, 220, 220, 255], 120) + if colours.side_panel_background != colours.gallery_background: + line2_colour = [240, 240, 240, 255] + line1_colour = alpha_mod([220, 220, 220, 255], 120) - if test_lumi(colours.gallery_background) < 0.5 or (prefs.use_card_style and colours.lm): - line1_colour = colours.grey(80) - line2_colour = colours.grey(40) + if test_lumi(colours.gallery_background) < 0.5 or (prefs.use_card_style and colours.lm): + line1_colour = colours.grey(80) + line2_colour = colours.grey(40) - if row_len == 0: - row_len = 1 + if row_len == 0: + row_len = 1 - dev = int((r_area - compact) / (row_len + 0)) + dev = int((r_area - compact) / (row_len + 0)) - render_pos = 0 - album_on = 0 + render_pos = 0 + album_on = 0 - max_scroll = round( - (math.ceil((len(album_dex)) / row_len) - 1) * (album_mode_art_size + album_v_gap)) - round( - 50 * gui.scale) + max_scroll = round( + (math.ceil((len(album_dex)) / row_len) - 1) * (album_mode_art_size + album_v_gap)) - round( + 50 * gui.scale) - # Mouse wheel scrolling - if not search_over.active and not radiobox.active \ - and mouse_position[0] > window_size[0] - w and gui.panelY < mouse_position[1] < window_size[ - 1] - gui.panelBY: + # Mouse wheel scrolling + if not search_over.active and not radiobox.active \ + and mouse_position[0] > window_size[0] - w and gui.panelY < mouse_position[1] < window_size[ + 1] - gui.panelBY: - if mouse_wheel != 0: - scroll_gallery_hide_timer.set() - gui.frame_callback_list.append(TestTimer(0.9)) + if mouse_wheel != 0: + scroll_gallery_hide_timer.set() + gui.frame_callback_list.append(TestTimer(0.9)) - if prefs.gallery_row_scroll: - gui.album_scroll_px -= mouse_wheel * (album_mode_art_size + album_v_gap) # 90 - else: - gui.album_scroll_px -= mouse_wheel * prefs.gallery_scroll_wheel_px + if prefs.gallery_row_scroll: + gui.album_scroll_px -= mouse_wheel * (album_mode_art_size + album_v_gap) # 90 + else: + gui.album_scroll_px -= mouse_wheel * prefs.gallery_scroll_wheel_px - if gui.album_scroll_px < round(album_v_slide_value * -1): - gui.album_scroll_px = round(album_v_slide_value * -1) - if album_dex: - gallery_pulse_top.pulse() + if gui.album_scroll_px < round(album_v_slide_value * -1): + gui.album_scroll_px = round(album_v_slide_value * -1) + if album_dex: + gallery_pulse_top.pulse() - if gui.album_scroll_px > max_scroll: - gui.album_scroll_px = max_scroll - gui.album_scroll_px = max(gui.album_scroll_px, round(album_v_slide_value * -1)) + if gui.album_scroll_px > max_scroll: + gui.album_scroll_px = max_scroll + gui.album_scroll_px = max(gui.album_scroll_px, round(album_v_slide_value * -1)) - rect = ( - gui.gallery_scroll_field_left, gui.panelY, window_size[0] - gui.gallery_scroll_field_left - 2, h) + rect = ( + gui.gallery_scroll_field_left, gui.panelY, window_size[0] - gui.gallery_scroll_field_left - 2, h) - card_mode = False - if prefs.use_card_style and colours.lm and gui.gallery_show_text: - card_mode = True + card_mode = False + if prefs.use_card_style and colours.lm and gui.gallery_show_text: + card_mode = True - rect = (window_size[0] - 40 * gui.scale, gui.panelY, 38 * gui.scale, h) - fields.add(rect) + rect = (window_size[0] - 40 * gui.scale, gui.panelY, 38 * gui.scale, h) + fields.add(rect) - # Show scroll area - if coll(rect) or gallery_scroll.held or scroll_gallery_hide_timer.get() < 0.9 or gui.album_tab_mode: + # Show scroll area + if coll(rect) or gallery_scroll.held or scroll_gallery_hide_timer.get() < 0.9 or gui.album_tab_mode: - if gallery_scroll.held: - while len(tauon.gall_ren.queue) > 2: - tauon.gall_ren.queue.pop() + if gallery_scroll.held: + while len(tauon.gall_ren.queue) > 2: + tauon.gall_ren.queue.pop() - # Draw power bar button - if gui.pt == 0 and gui.power_bar is not None and len(gui.power_bar) > 3: - rect = (window_size[0] - (15 + 20) * gui.scale, gui.panelY + 3 * gui.scale, 18 * gui.scale, - 24 * gui.scale) - fields.add(rect) - colour = [255, 255, 255, 35] - if colours.lm: - colour = [0, 0, 0, 30] - if coll(rect) and not gallery_scroll.held: - colour = [255, 220, 100, 245] + # Draw power bar button + if gui.pt == 0 and gui.power_bar is not None and len(gui.power_bar) > 3: + rect = (window_size[0] - (15 + 20) * gui.scale, gui.panelY + 3 * gui.scale, 18 * gui.scale, + 24 * gui.scale) + fields.add(rect) + colour = [255, 255, 255, 35] if colours.lm: - colour = [250, 100, 0, 255] - if inp.mouse_click: - gui.pt = 1 + colour = [0, 0, 0, 30] + if coll(rect) and not gallery_scroll.held: + colour = [255, 220, 100, 245] + if colours.lm: + colour = [250, 100, 0, 255] + if inp.mouse_click: + gui.pt = 1 + + power_bar_icon.render(rect[0] + round(5 * gui.scale), rect[1] + round(3 * gui.scale), colour) + + # Draw scroll bar + if gui.pt == 0: + gui.album_scroll_px = gallery_scroll.draw( + window_size[0] - 16 * gui.scale, gui.panelY, + 15 * gui.scale, + window_size[1] - (gui.panelY + gui.panelBY), + gui.album_scroll_px + album_v_slide_value, + max_scroll + album_v_slide_value, + jump_distance=1400 * gui.scale, + r_click=right_click, + extend_field=15 * gui.scale) - album_v_slide_value + + if last_row != row_len: + last_row = row_len + + if pctl.selected_in_playlist < len(pctl.playing_playlist()): + goto_album(pctl.selected_in_playlist) + # else: + # goto_album(pctl.playlist_playing_position) + + extend = 0 + if card_mode: # gui.gallery_show_text: + extend = 40 * gui.scale + + # Process inputs first + if (inp.mouse_click or right_click or middle_click or mouse_down or mouse_up) and default_playlist: + while render_pos < gui.album_scroll_px + window_size[1]: + + if b_info_bar and render_pos > gui.album_scroll_px + b_info_y: + break - power_bar_icon.render(rect[0] + round(5 * gui.scale), rect[1] + round(3 * gui.scale), colour) + if render_pos < gui.album_scroll_px - album_mode_art_size - album_v_gap: + # Skip row + render_pos += album_mode_art_size + album_v_gap + album_on += row_len + else: + # render row + y = render_pos - gui.album_scroll_px + row_x = 0 + for a in range(row_len): + if album_on > len(album_dex) - 1: + break - # Draw scroll bar - if gui.pt == 0: - gui.album_scroll_px = gallery_scroll.draw( - window_size[0] - 16 * gui.scale, gui.panelY, - 15 * gui.scale, - window_size[1] - (gui.panelY + gui.panelBY), - gui.album_scroll_px + album_v_slide_value, - max_scroll + album_v_slide_value, - jump_distance=1400 * gui.scale, - r_click=right_click, - extend_field=15 * gui.scale) - album_v_slide_value + x = (l_area + dev * a) - int(album_mode_art_size / 2) + int(dev / 2) + int( + compact / 2) - a_offset - if last_row != row_len: - last_row = row_len + if album_dex[album_on] > len(default_playlist): + break - if pctl.selected_in_playlist < len(pctl.playing_playlist()): - goto_album(pctl.selected_in_playlist) - # else: - # goto_album(pctl.playlist_playing_position) + rect = (x, y, album_mode_art_size, album_mode_art_size + extend * gui.scale) + # fields.add(rect) + m_in = coll(rect) and gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY - extend = 0 - if card_mode: # gui.gallery_show_text: - extend = 40 * gui.scale + # if m_in: + # ddt.rect_r((x - 7, y - 7, album_mode_art_size + 14, album_mode_art_size + extend + 55), [80, 80, 80, 80], True) - # Process inputs first - if (inp.mouse_click or right_click or middle_click or mouse_down or mouse_up) and default_playlist: - while render_pos < gui.album_scroll_px + window_size[1]: + # Quick drag and drop + if mouse_up and (playlist_hold and m_in) and not side_drag and shift_selection: - if b_info_bar and render_pos > gui.album_scroll_px + b_info_y: - break + info = get_album_info(album_dex[album_on]) + if info[1]: - if render_pos < gui.album_scroll_px - album_mode_art_size - album_v_gap: - # Skip row - render_pos += album_mode_art_size + album_v_gap - album_on += row_len - else: - # render row - y = render_pos - gui.album_scroll_px - row_x = 0 - for a in range(row_len): - if album_on > len(album_dex) - 1: - break + track_position = info[1][0] - x = (l_area + dev * a) - int(album_mode_art_size / 2) + int(dev / 2) + int( - compact / 2) - a_offset + if track_position > shift_selection[0]: + track_position = info[1][-1] + 1 - if album_dex[album_on] > len(default_playlist): - break + ref = [] + for item in shift_selection: + ref.append(default_playlist[item]) - rect = (x, y, album_mode_art_size, album_mode_art_size + extend * gui.scale) - # fields.add(rect) - m_in = coll(rect) and gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY + for item in shift_selection: + default_playlist[item] = "old" - # if m_in: - # ddt.rect_r((x - 7, y - 7, album_mode_art_size + 14, album_mode_art_size + extend + 55), [80, 80, 80, 80], True) + for item in shift_selection: + default_playlist.insert(track_position, "new") - # Quick drag and drop - if mouse_up and (playlist_hold and m_in) and not side_drag and shift_selection: + for b in reversed(range(len(default_playlist))): + if default_playlist[b] == "old": + del default_playlist[b] + shift_selection = [] + for b in range(len(default_playlist)): + if default_playlist[b] == "new": + shift_selection.append(b) + default_playlist[b] = ref.pop(0) - info = get_album_info(album_dex[album_on]) - if info[1]: + pctl.selected_in_playlist = shift_selection[0] + gui.pl_update += 1 + playlist_hold = False - track_position = info[1][0] + reload_albums(True) + pctl.notify_change() - if track_position > shift_selection[0]: - track_position = info[1][-1] + 1 + elif not side_drag and is_level_zero(): - ref = [] - for item in shift_selection: - ref.append(default_playlist[item]) + if coll_point(click_location, rect) and gui.panelY < mouse_position[1] < \ + window_size[1] - gui.panelBY: + info = get_album_info(album_dex[album_on]) - for item in shift_selection: - default_playlist[item] = "old" + if m_in and mouse_up and prefs.gallery_single_click: - for item in shift_selection: - default_playlist.insert(track_position, "new") + if is_level_zero() and gui.d_click_ref == album_dex[album_on]: - for b in reversed(range(len(default_playlist))): - if default_playlist[b] == "old": - del default_playlist[b] - shift_selection = [] - for b in range(len(default_playlist)): - if default_playlist[b] == "new": - shift_selection.append(b) - default_playlist[b] = ref.pop(0) + if info[0] == 1 and pctl.playing_state == 2: + pctl.play() + elif info[0] == 1 and pctl.playing_state > 0: + pctl.playlist_view_position = album_dex[album_on] + logging.debug("Position changed by gallery click") + else: + pctl.playlist_view_position = album_dex[album_on] + logging.debug("Position changed by gallery click") + pctl.jump(default_playlist[album_dex[album_on]], album_dex[album_on]) - pctl.selected_in_playlist = shift_selection[0] - gui.pl_update += 1 - playlist_hold = False + pctl.show_current() - reload_albums(True) - pctl.notify_change() + elif mouse_down and not m_in: + info = get_album_info(album_dex[album_on]) + quick_drag = True + if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: + playlist_hold = True + shift_selection = info[1] + gui.pl_update += 1 + click_location = [0, 0] - elif not side_drag and is_level_zero(): + if m_in: - if coll_point(click_location, rect) and gui.panelY < mouse_position[1] < \ - window_size[1] - gui.panelBY: info = get_album_info(album_dex[album_on]) + if inp.mouse_click: - if m_in and mouse_up and prefs.gallery_single_click: - - if is_level_zero() and gui.d_click_ref == album_dex[album_on]: - - if info[0] == 1 and pctl.playing_state == 2: - pctl.play() - elif info[0] == 1 and pctl.playing_state > 0: - pctl.playlist_view_position = album_dex[album_on] - logging.debug("Position changed by gallery click") - else: - pctl.playlist_view_position = album_dex[album_on] - logging.debug("Position changed by gallery click") - pctl.jump(default_playlist[album_dex[album_on]], album_dex[album_on]) - - pctl.show_current() - - elif mouse_down and not m_in: - info = get_album_info(album_dex[album_on]) - quick_drag = True - if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: - playlist_hold = True - shift_selection = info[1] - gui.pl_update += 1 - click_location = [0, 0] - - if m_in: - - info = get_album_info(album_dex[album_on]) - if inp.mouse_click: + if prefs.gallery_single_click: + gui.d_click_ref = album_dex[album_on] - if prefs.gallery_single_click: - gui.d_click_ref = album_dex[album_on] + else: - else: + if d_click_timer.get() < 0.5 and gui.d_click_ref == album_dex[album_on]: - if d_click_timer.get() < 0.5 and gui.d_click_ref == album_dex[album_on]: + if info[0] == 1 and pctl.playing_state == 2: + pctl.play() + elif info[0] == 1 and pctl.playing_state > 0: + pctl.playlist_view_position = album_dex[album_on] + logging.debug("Position changed by gallery click") + else: + pctl.playlist_view_position = album_dex[album_on] + logging.debug("Position changed by gallery click") + pctl.jump(default_playlist[album_dex[album_on]], album_dex[album_on]) - if info[0] == 1 and pctl.playing_state == 2: - pctl.play() - elif info[0] == 1 and pctl.playing_state > 0: - pctl.playlist_view_position = album_dex[album_on] - logging.debug("Position changed by gallery click") else: - pctl.playlist_view_position = album_dex[album_on] - logging.debug("Position changed by gallery click") - pctl.jump(default_playlist[album_dex[album_on]], album_dex[album_on]) + gui.d_click_ref = album_dex[album_on] + d_click_timer.set() + + pctl.playlist_view_position = album_dex[album_on] + logging.debug("Position changed by gallery click") + pctl.selected_in_playlist = album_dex[album_on] + gui.pl_update += 1 + + elif middle_click and is_level_zero(): + # Middle click to add album to queue + if key_ctrl_down: + # Add to queue ungrouped + album = get_album_info(album_dex[album_on])[1] + for item in album: + pctl.force_queue.append( + queue_item_gen(default_playlist[item], item, pl_to_id( + pctl.active_playlist_viewing))) + queue_timer_set(plural=True) + if prefs.stop_end_queue: + pctl.auto_stop = False + else: + # Add to queue grouped + add_album_to_queue(default_playlist[album_dex[album_on]]) + + elif right_click: + if pctl.quick_add_target: + + pl = id_to_pl(pctl.quick_add_target) + if pl is not None: + parent = pctl.get_track( + default_playlist[album_dex[album_on]]).parent_folder_path + # remove from target pl + if default_playlist[album_dex[album_on]] in pctl.multi_playlist[pl].playlist_ids: + for i in reversed(range(len(pctl.multi_playlist[pl].playlist_ids))): + if pctl.get_track(pctl.multi_playlist[pl].playlist_ids[i]).parent_folder_path == parent: + del pctl.multi_playlist[pl].playlist_ids[i] + else: + # add + for i in range(len(default_playlist)): + if pctl.get_track(default_playlist[i]).parent_folder_path == parent: + pctl.multi_playlist[pl].playlist_ids.append(default_playlist[i]) + + reload_albums(True) else: - gui.d_click_ref = album_dex[album_on] - d_click_timer.set() + pctl.selected_in_playlist = album_dex[album_on] + # playlist_position = pctl.playlist_selected + shift_selection = [pctl.selected_in_playlist] + gallery_menu.activate(default_playlist[pctl.selected_in_playlist]) + r_menu_position = pctl.selected_in_playlist + + shift_selection = [] + u = pctl.selected_in_playlist + while u < len(default_playlist) and pctl.master_library[ + default_playlist[u]].parent_folder_path == \ + pctl.master_library[ + default_playlist[pctl.selected_in_playlist]].parent_folder_path: + shift_selection.append(u) + u += 1 + pctl.render_playlist() + + album_on += 1 + + if album_on > len(album_dex): + break + render_pos += album_mode_art_size + album_v_gap - pctl.playlist_view_position = album_dex[album_on] - logging.debug("Position changed by gallery click") - pctl.selected_in_playlist = album_dex[album_on] - gui.pl_update += 1 + render_pos = 0 + album_on = 0 + album_count = 0 - elif middle_click and is_level_zero(): - # Middle click to add album to queue - if key_ctrl_down: - # Add to queue ungrouped - album = get_album_info(album_dex[album_on])[1] - for item in album: - pctl.force_queue.append( - queue_item_gen(default_playlist[item], item, pl_to_id( - pctl.active_playlist_viewing))) - queue_timer_set(plural=True) - if prefs.stop_end_queue: - pctl.auto_stop = False - else: - # Add to queue grouped - add_album_to_queue(default_playlist[album_dex[album_on]]) - - elif right_click: - if pctl.quick_add_target: - - pl = id_to_pl(pctl.quick_add_target) - if pl is not None: - parent = pctl.get_track( - default_playlist[album_dex[album_on]]).parent_folder_path - # remove from target pl - if default_playlist[album_dex[album_on]] in pctl.multi_playlist[pl].playlist_ids: - for i in reversed(range(len(pctl.multi_playlist[pl].playlist_ids))): - if pctl.get_track(pctl.multi_playlist[pl].playlist_ids[i]).parent_folder_path == parent: - del pctl.multi_playlist[pl].playlist_ids[i] - else: - # add - for i in range(len(default_playlist)): - if pctl.get_track(default_playlist[i]).parent_folder_path == parent: - pctl.multi_playlist[pl].playlist_ids.append(default_playlist[i]) + if not pref_box.enabled or mouse_wheel != 0: + gui.first_in_grid = None - reload_albums(True) + # Render album grid + while render_pos < gui.album_scroll_px + window_size[1] and default_playlist: - else: - pctl.selected_in_playlist = album_dex[album_on] - # playlist_position = pctl.playlist_selected - shift_selection = [pctl.selected_in_playlist] - gallery_menu.activate(default_playlist[pctl.selected_in_playlist]) - r_menu_position = pctl.selected_in_playlist + if b_info_bar and render_pos > gui.album_scroll_px + b_info_y: + break - shift_selection = [] - u = pctl.selected_in_playlist - while u < len(default_playlist) and pctl.master_library[ - default_playlist[u]].parent_folder_path == \ - pctl.master_library[ - default_playlist[pctl.selected_in_playlist]].parent_folder_path: - shift_selection.append(u) - u += 1 - pctl.render_playlist() + if render_pos < gui.album_scroll_px - album_mode_art_size - album_v_gap: + # Skip row + render_pos += album_mode_art_size + album_v_gap + album_on += row_len + else: + # render row + y = render_pos - gui.album_scroll_px - album_on += 1 + row_x = 0 - if album_on > len(album_dex): + if y > window_size[1] - gui.panelBY - 30 * gui.scale and window_size[1] < 340 * gui.scale: break - render_pos += album_mode_art_size + album_v_gap + # if y > - render_pos = 0 - album_on = 0 - album_count = 0 + for a in range(row_len): - if not pref_box.enabled or mouse_wheel != 0: - gui.first_in_grid = None + if album_on > len(album_dex) - 1: + break - # Render album grid - while render_pos < gui.album_scroll_px + window_size[1] and default_playlist: + x = (l_area + dev * a) - int(album_mode_art_size / 2) + int(dev / 2) + int( + compact / 2) - a_offset - if b_info_bar and render_pos > gui.album_scroll_px + b_info_y: - break + if album_dex[album_on] > len(default_playlist): + break - if render_pos < gui.album_scroll_px - album_mode_art_size - album_v_gap: - # Skip row - render_pos += album_mode_art_size + album_v_gap - album_on += row_len - else: - # render row - y = render_pos - gui.album_scroll_px + track = pctl.master_library[default_playlist[album_dex[album_on]]] - row_x = 0 + info = get_album_info(album_dex[album_on]) + album = info[1] + # info = (0, 0, 0) - if y > window_size[1] - gui.panelBY - 30 * gui.scale and window_size[1] < 340 * gui.scale: - break - # if y > + # rect = (x, y, album_mode_art_size, album_mode_art_size + extend * gui.scale) + # fields.add(rect) + # m_in = coll(rect) and gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY - for a in range(row_len): + if gui.first_in_grid is None and y > gui.panelY: # This marks what track is the first in the grid + gui.first_in_grid = album_dex[album_on] - if album_on > len(album_dex) - 1: - break + # artisttitle = colours.side_bar_line2 + # albumtitle = colours.side_bar_line1 # grey(220) - x = (l_area + dev * a) - int(album_mode_art_size / 2) + int(dev / 2) + int( - compact / 2) - a_offset + if card_mode: + ddt.text_background_colour = colours.grey(250) + drop_shadow.render( + x + 3 * gui.scale, y + 3 * gui.scale, + album_mode_art_size + 11 * gui.scale, + album_mode_art_size + 45 * gui.scale + 13 * gui.scale) + ddt.rect( + (x, y, album_mode_art_size, album_mode_art_size + 45 * gui.scale), colours.grey(250)) - if album_dex[album_on] > len(default_playlist): - break + # White background needs extra border + if colours.lm and not card_mode: + ddt.rect_a((x - 2, y - 2), (album_mode_art_size + 4, album_mode_art_size + 4), colours.grey(200)) - track = pctl.master_library[default_playlist[album_dex[album_on]]] + if a == row_len - 1: + gui.gallery_scroll_field_left = max( + x + album_mode_art_size, + window_size[0] - round(50 * gui.scale)) - info = get_album_info(album_dex[album_on]) - album = info[1] - # info = (0, 0, 0) + if info[0] == 1 and 0 < pctl.playing_state < 3: + ddt.rect_a( + (x - 4, y - 4), (album_mode_art_size + 8, album_mode_art_size + 8), + colours.gallery_highlight) + # ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), + # colours.gallery_background, True) - # rect = (x, y, album_mode_art_size, album_mode_art_size + extend * gui.scale) - # fields.add(rect) - # m_in = coll(rect) and gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY + # Draw quick add highlight + if pctl.quick_add_target: + pl = id_to_pl(pctl.quick_add_target) + if pl is not None and default_playlist[album_dex[album_on]] in \ + pctl.multi_playlist[pl].playlist_ids: + c = [110, 233, 90, 255] + if colours.lm: + c = [66, 244, 66, 255] + ddt.rect_a((x - 4, y - 4), (album_mode_art_size + 8, album_mode_art_size + 8), c) - if gui.first_in_grid is None and y > gui.panelY: # This marks what track is the first in the grid - gui.first_in_grid = album_dex[album_on] + # Draw transcode highlight + if transcode_list and os.path.isdir(prefs.encoder_output): - # artisttitle = colours.side_bar_line2 - # albumtitle = colours.side_bar_line1 # grey(220) + tr = False - if card_mode: - ddt.text_background_colour = colours.grey(250) - drop_shadow.render( - x + 3 * gui.scale, y + 3 * gui.scale, - album_mode_art_size + 11 * gui.scale, - album_mode_art_size + 45 * gui.scale + 13 * gui.scale) - ddt.rect( - (x, y, album_mode_art_size, album_mode_art_size + 45 * gui.scale), colours.grey(250)) - - # White background needs extra border - if colours.lm and not card_mode: - ddt.rect_a((x - 2, y - 2), (album_mode_art_size + 4, album_mode_art_size + 4), colours.grey(200)) - - if a == row_len - 1: - gui.gallery_scroll_field_left = max( - x + album_mode_art_size, - window_size[0] - round(50 * gui.scale)) - - if info[0] == 1 and 0 < pctl.playing_state < 3: - ddt.rect_a( - (x - 4, y - 4), (album_mode_art_size + 8, album_mode_art_size + 8), - colours.gallery_highlight) - # ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), - # colours.gallery_background, True) - - # Draw quick add highlight - if pctl.quick_add_target: - pl = id_to_pl(pctl.quick_add_target) - if pl is not None and default_playlist[album_dex[album_on]] in \ - pctl.multi_playlist[pl].playlist_ids: - c = [110, 233, 90, 255] - if colours.lm: - c = [66, 244, 66, 255] - ddt.rect_a((x - 4, y - 4), (album_mode_art_size + 8, album_mode_art_size + 8), c) - - # Draw transcode highlight - if transcode_list and os.path.isdir(prefs.encoder_output): - - tr = False - - if (encode_folder_name(track) in os.listdir(prefs.encoder_output)): - tr = True - else: - for folder in transcode_list: - if pctl.get_track(folder[0]).parent_folder_path == track.parent_folder_path: - tr = True - break - if tr: - c = [244, 212, 66, 255] - if colours.lm: - c = [244, 64, 244, 255] - ddt.rect_a((x - 4, y - 4), (album_mode_art_size + 8, album_mode_art_size + 8), c) + if (encode_folder_name(track) in os.listdir(prefs.encoder_output)): + tr = True + else: + for folder in transcode_list: + if pctl.get_track(folder[0]).parent_folder_path == track.parent_folder_path: + tr = True + break + if tr: + c = [244, 212, 66, 255] + if colours.lm: + c = [244, 64, 244, 255] + ddt.rect_a((x - 4, y - 4), (album_mode_art_size + 8, album_mode_art_size + 8), c) + # ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), + # colours.gallery_background, True) + + # Draw selection + + if (gui.album_tab_mode or gallery_menu.active) and info[2] is True: + c = colours.gallery_highlight + c = [c[1], c[2], c[0], c[3]] + ddt.rect_a((x - 4, y - 4), (album_mode_art_size + 8, album_mode_art_size + 8), c) # [150, 80, 222, 255] # ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), # colours.gallery_background, True) - # Draw selection - - if (gui.album_tab_mode or gallery_menu.active) and info[2] is True: - c = colours.gallery_highlight - c = [c[1], c[2], c[0], c[3]] - ddt.rect_a((x - 4, y - 4), (album_mode_art_size + 8, album_mode_art_size + 8), c) # [150, 80, 222, 255] - # ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), - # colours.gallery_background, True) - - # Draw selection animation - if gui.gallery_animate_highlight_on == album_dex[ - album_on] and gallery_select_animate_timer.get() < 1.5: - - t = gallery_select_animate_timer.get() - c = colours.gallery_highlight - if t < 0.2: - a = int(255 * (t / 0.2)) - elif t < 0.5: - a = 255 - else: - a = int(255 - 255 * (t - 0.5)) + # Draw selection animation + if gui.gallery_animate_highlight_on == album_dex[ + album_on] and gallery_select_animate_timer.get() < 1.5: + + t = gallery_select_animate_timer.get() + c = colours.gallery_highlight + if t < 0.2: + a = int(255 * (t / 0.2)) + elif t < 0.5: + a = 255 + else: + a = int(255 - 255 * (t - 0.5)) - c = [c[1], c[2], c[0], a] - ddt.rect_a((x - 5, y - 5), (album_mode_art_size + 10, album_mode_art_size + 10), c) # [150, 80, 222, 255] + c = [c[1], c[2], c[0], a] + ddt.rect_a((x - 5, y - 5), (album_mode_art_size + 10, album_mode_art_size + 10), c) # [150, 80, 222, 255] - gui.update += 1 + gui.update += 1 + + # Draw faint outline + ddt.rect( + (x - 1, y - 1, album_mode_art_size + 2, album_mode_art_size + 2), + [255, 255, 255, 11]) - # Draw faint outline - ddt.rect( - (x - 1, y - 1, album_mode_art_size + 2, album_mode_art_size + 2), - [255, 255, 255, 11]) + if gui.album_tab_mode or gallery_menu.active: + if info[2] is False and info[0] != 1 and not colours.lm: + ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), [0, 0, 0, 110]) + albumtitle = colours.grey(160) - if gui.album_tab_mode or gallery_menu.active: - if info[2] is False and info[0] != 1 and not colours.lm: + elif info[0] != 1 and pctl.playing_state != 0 and prefs.dim_art: ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), [0, 0, 0, 110]) albumtitle = colours.grey(160) - elif info[0] != 1 and pctl.playing_state != 0 and prefs.dim_art: - ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), [0, 0, 0, 110]) - albumtitle = colours.grey(160) - - # Determine meta info - singles = False - artists = 0 - last_album = "" - last_artist = "" - s = 0 - ones = 0 - for id in album: - tr = pctl.get_track(default_playlist[id]) - if tr.album != last_album: - if last_album: - s += 1 - last_album = tr.album - if str(tr.track_number) == "1": - ones += 1 - if tr.artist != last_artist: - artists += 1 - if s > 2 or ones > 2: - singles = True - - # Draw blank back colour - back_colour = [40, 40, 40, 50] - if colours.lm: - back_colour = [10, 10, 10, 15] - - back_colour = alpha_blend([10, 10, 10, 15], colours.gallery_background) - - ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), back_colour) - - # Draw album art - if singles: - dia = math.sqrt(album_mode_art_size * album_mode_art_size * 2) - ran = dia * 0.25 - off = (dia - ran) / 2 - albs = min(len(album), 5) - spacing = ran / (albs - 1) - size = round(album_mode_art_size * 0.5) - - i = 0 - for p in album[:albs]: - - pp = spacing * i - pp += off - xx = pp / math.sqrt(2) - - xx -= size / 2 - drawn_art = tauon.gall_ren.render( - pctl.get_track(default_playlist[p]), (x + xx, y + xx), - size=size, force_offset=0) - if not drawn_art: - g = 50 + round(100 / albs) * i - ddt.rect((x + xx, y + xx, size, size), [g, g, g, 100]) - drawn_art = True - i += 1 - - else: - album_count += 1 - if (album_count * 1.5) + 10 > tauon.gall_ren.limit: - tauon.gall_ren.limit = round((album_count * 1.5) + 30) - drawn_art = tauon.gall_ren.render(track, (x, y)) - - # Determine mouse collision - rect = (x, y, album_mode_art_size, album_mode_art_size + extend * gui.scale) - m_in = coll(rect) and gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY - fields.add(rect) + # Determine meta info + singles = False + artists = 0 + last_album = "" + last_artist = "" + s = 0 + ones = 0 + for id in album: + tr = pctl.get_track(default_playlist[id]) + if tr.album != last_album: + if last_album: + s += 1 + last_album = tr.album + if str(tr.track_number) == "1": + ones += 1 + if tr.artist != last_artist: + artists += 1 + if s > 2 or ones > 2: + singles = True + + # Draw blank back colour + back_colour = [40, 40, 40, 50] + if colours.lm: + back_colour = [10, 10, 10, 15] + + back_colour = alpha_blend([10, 10, 10, 15], colours.gallery_background) + + ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), back_colour) + + # Draw album art + if singles: + dia = math.sqrt(album_mode_art_size * album_mode_art_size * 2) + ran = dia * 0.25 + off = (dia - ran) / 2 + albs = min(len(album), 5) + spacing = ran / (albs - 1) + size = round(album_mode_art_size * 0.5) + + i = 0 + for p in album[:albs]: + + pp = spacing * i + pp += off + xx = pp / math.sqrt(2) + + xx -= size / 2 + drawn_art = tauon.gall_ren.render( + pctl.get_track(default_playlist[p]), (x + xx, y + xx), + size=size, force_offset=0) + if not drawn_art: + g = 50 + round(100 / albs) * i + ddt.rect((x + xx, y + xx, size, size), [g, g, g, 100]) + drawn_art = True + i += 1 - # Draw mouse-over highlight - if (not gallery_menu.active and m_in) or (gallery_menu.active and info[2]): - if is_level_zero(): - ddt.rect(rect, [255, 255, 255, 10]) - - if drawn_art is False and gui.gallery_show_text is False: - ddt.text( - (x + int(album_mode_art_size / 2), y + album_mode_art_size - 22 * gui.scale, 2), - pctl.master_library[default_playlist[album_dex[album_on]]].parent_folder_name, - colours.gallery_artist_line, - 13, - album_mode_art_size - 15 * gui.scale, - bg=alpha_blend(back_colour, colours.gallery_background)) - - if prefs.art_bg and drawn_art: - rect = SDL_Rect(round(x), round(y), album_mode_art_size, album_mode_art_size) - if rect.y < gui.panelY: - diff = round(gui.panelY - rect.y) - rect.y += diff - rect.h -= diff - elif (rect.y + rect.h) > window_size[1] - gui.panelBY: - diff = round((rect.y + rect.h) - (window_size[1] - gui.panelBY)) - rect.h -= diff - - if rect.h > 0: - style_overlay.hole_punches.append(rect) - - # # Drag over highlight - # if quick_drag and playlist_hold and mouse_down: - # rect = (x, y, album_mode_art_size, album_mode_art_size + extend * gui.scale) - # m_in = coll(rect) and gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY - # if m_in: - # ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), [120, 10, 255, 100], True) - - if gui.gallery_show_text: - c_index = default_playlist[album_dex[album_on]] - - if c_index in album_artist_dict: - pass else: - i = album_dex[album_on] - if pctl.master_library[default_playlist[i]].album_artist: - album_artist_dict[c_index] = pctl.master_library[ - default_playlist[i]].album_artist + album_count += 1 + if (album_count * 1.5) + 10 > tauon.gall_ren.limit: + tauon.gall_ren.limit = round((album_count * 1.5) + 30) + drawn_art = tauon.gall_ren.render(track, (x, y)) + + # Determine mouse collision + rect = (x, y, album_mode_art_size, album_mode_art_size + extend * gui.scale) + m_in = coll(rect) and gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY + fields.add(rect) + + # Draw mouse-over highlight + if (not gallery_menu.active and m_in) or (gallery_menu.active and info[2]): + if is_level_zero(): + ddt.rect(rect, [255, 255, 255, 10]) + + if drawn_art is False and gui.gallery_show_text is False: + ddt.text( + (x + int(album_mode_art_size / 2), y + album_mode_art_size - 22 * gui.scale, 2), + pctl.master_library[default_playlist[album_dex[album_on]]].parent_folder_name, + colours.gallery_artist_line, + 13, + album_mode_art_size - 15 * gui.scale, + bg=alpha_blend(back_colour, colours.gallery_background)) + + if prefs.art_bg and drawn_art: + rect = SDL_Rect(round(x), round(y), album_mode_art_size, album_mode_art_size) + if rect.y < gui.panelY: + diff = round(gui.panelY - rect.y) + rect.y += diff + rect.h -= diff + elif (rect.y + rect.h) > window_size[1] - gui.panelBY: + diff = round((rect.y + rect.h) - (window_size[1] - gui.panelBY)) + rect.h -= diff + + if rect.h > 0: + style_overlay.hole_punches.append(rect) + + # # Drag over highlight + # if quick_drag and playlist_hold and mouse_down: + # rect = (x, y, album_mode_art_size, album_mode_art_size + extend * gui.scale) + # m_in = coll(rect) and gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY + # if m_in: + # ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), [120, 10, 255, 100], True) + + if gui.gallery_show_text: + c_index = default_playlist[album_dex[album_on]] + + if c_index in album_artist_dict: + pass else: - while i < len(default_playlist) - 1: - if pctl.master_library[default_playlist[i]].parent_folder_name != \ - pctl.master_library[ - default_playlist[album_dex[album_on]]].parent_folder_name: + i = album_dex[album_on] + if pctl.master_library[default_playlist[i]].album_artist: + album_artist_dict[c_index] = pctl.master_library[ + default_playlist[i]].album_artist + else: + while i < len(default_playlist) - 1: + if pctl.master_library[default_playlist[i]].parent_folder_name != \ + pctl.master_library[ + default_playlist[album_dex[album_on]]].parent_folder_name: + album_artist_dict[c_index] = pctl.master_library[ + default_playlist[album_dex[album_on]]].artist + break + if pctl.master_library[default_playlist[i]].artist != \ + pctl.master_library[ + default_playlist[album_dex[album_on]]].artist: + album_artist_dict[c_index] = _("Various Artists") + + break + i += 1 + else: album_artist_dict[c_index] = pctl.master_library[ default_playlist[album_dex[album_on]]].artist - break - if pctl.master_library[default_playlist[i]].artist != \ - pctl.master_library[ - default_playlist[album_dex[album_on]]].artist: - album_artist_dict[c_index] = _("Various Artists") - break - i += 1 + line = album_artist_dict[c_index] + line2 = pctl.master_library[default_playlist[album_dex[album_on]]].album + if singles: + line2 = pctl.master_library[ + default_playlist[album_dex[album_on]]].parent_folder_name + if artists > 1: + line = _("Various Artists") + + text_align = 0 + if prefs.center_gallery_text: + x += album_mode_art_size // 2 + text_align = 2 + elif card_mode: + x += round(6 * gui.scale) + + if card_mode: + + if line2 == "": + + ddt.text( + (x, y + album_mode_art_size + 8 * gui.scale, text_align), + line, + line1_colour, + 310, + album_mode_art_size - 18 * gui.scale) else: - album_artist_dict[c_index] = pctl.master_library[ - default_playlist[album_dex[album_on]]].artist - - line = album_artist_dict[c_index] - line2 = pctl.master_library[default_playlist[album_dex[album_on]]].album - if singles: - line2 = pctl.master_library[ - default_playlist[album_dex[album_on]]].parent_folder_name - if artists > 1: - line = _("Various Artists") - - text_align = 0 - if prefs.center_gallery_text: - x += album_mode_art_size // 2 - text_align = 2 - elif card_mode: - x += round(6 * gui.scale) - if card_mode: - - if line2 == "": + ddt.text( + (x, y + album_mode_art_size + 7 * gui.scale, text_align), + line2, + line2_colour, + 311, + album_mode_art_size - 18 * gui.scale) + + ddt.text( + (x, y + album_mode_art_size + (10 + 14) * gui.scale, text_align), + line, + line1_colour, + 10, + album_mode_art_size - 18 * gui.scale) + elif line2 == "": ddt.text( - (x, y + album_mode_art_size + 8 * gui.scale, text_align), + (x, y + album_mode_art_size + 9 * gui.scale, text_align), line, line1_colour, - 310, - album_mode_art_size - 18 * gui.scale) + 311, + album_mode_art_size - 5 * gui.scale) else: ddt.text( - (x, y + album_mode_art_size + 7 * gui.scale, text_align), + (x, y + album_mode_art_size + 8 * gui.scale, text_align), line2, line2_colour, - 311, - album_mode_art_size - 18 * gui.scale) + 212, + album_mode_art_size) ddt.text( (x, y + album_mode_art_size + (10 + 14) * gui.scale, text_align), line, line1_colour, - 10, - album_mode_art_size - 18 * gui.scale) - elif line2 == "": - - ddt.text( - (x, y + album_mode_art_size + 9 * gui.scale, text_align), - line, - line1_colour, - 311, - album_mode_art_size - 5 * gui.scale) - else: - - ddt.text( - (x, y + album_mode_art_size + 8 * gui.scale, text_align), - line2, - line2_colour, - 212, - album_mode_art_size) - - ddt.text( - (x, y + album_mode_art_size + (10 + 14) * gui.scale, text_align), - line, - line1_colour, - 311, - album_mode_art_size - 5 * gui.scale) - - album_on += 1 + 311, + album_mode_art_size - 5 * gui.scale) - if album_on > len(album_dex): - break - render_pos += album_mode_art_size + album_v_gap + album_on += 1 - # POWER TAG BAR -------------- + if album_on > len(album_dex): + break + render_pos += album_mode_art_size + album_v_gap - if gui.pt > 0: # gui.pt > 0 or (gui.power_bar is not None and len(gui.power_bar) > 1): + # POWER TAG BAR -------------- - top = gui.panelY - run_y = top + 1 + if gui.pt > 0: # gui.pt > 0 or (gui.power_bar is not None and len(gui.power_bar) > 1): - hot_r = (window_size[0] - 47 * gui.scale, top, 45 * gui.scale, h) - fields.add(hot_r) + top = gui.panelY + run_y = top + 1 - if gui.pt == 0: # mouse moves in - if coll(hot_r) and window_is_focused(): - gui.pt_on.set() - gui.pt = 1 - elif gui.pt == 1: # wait then trigger if stays, reset if goes out - if not coll(hot_r): - gui.pt = 0 - elif gui.pt_on.get() > 0.2: - gui.pt = 2 + hot_r = (window_size[0] - 47 * gui.scale, top, 45 * gui.scale, h) + fields.add(hot_r) - off = 0 + if gui.pt == 0: # mouse moves in + if coll(hot_r) and window_is_focused(): + gui.pt_on.set() + gui.pt = 1 + elif gui.pt == 1: # wait then trigger if stays, reset if goes out + if not coll(hot_r): + gui.pt = 0 + elif gui.pt_on.get() > 0.2: + gui.pt = 2 + + off = 0 + for item in gui.power_bar: + item.ani_timer.force_set(off) + off -= 0.005 + + elif gui.pt == 2: # wait to turn off + + if coll(hot_r): + gui.pt_off.set() + if gui.pt_off.get() > 0.6 and not lightning_menu.active: + gui.pt = 3 + + off = 0 + for item in gui.power_bar: + item.ani_timer.force_set(off) + off -= 0.01 + + done = True + # Animate tages on + if gui.pt == 2: for item in gui.power_bar: - item.ani_timer.force_set(off) - off -= 0.005 - - elif gui.pt == 2: # wait to turn off - - if coll(hot_r): - gui.pt_off.set() - if gui.pt_off.get() > 0.6 and not lightning_menu.active: - gui.pt = 3 + t = item.ani_timer.get() + if t < 0: + break + if t > 0.2: + item.peak_x = 9 * gui.scale + else: + item.peak_x = (t / 0.2) * 9 * gui.scale - off = 0 + # Animate tags off + if gui.pt == 3: for item in gui.power_bar: - item.ani_timer.force_set(off) - off -= 0.01 - - done = True - # Animate tages on - if gui.pt == 2: - for item in gui.power_bar: - t = item.ani_timer.get() - if t < 0: - break - if t > 0.2: - item.peak_x = 9 * gui.scale - else: - item.peak_x = (t / 0.2) * 9 * gui.scale - - # Animate tags off - if gui.pt == 3: - for item in gui.power_bar: - t = item.ani_timer.get() - if t < 0: - done = False - break - if t > 0.2: - item.peak_x = 0 - else: - item.peak_x = 9 * gui.scale - ((t / 0.2) * 9 * gui.scale) - done = False - if done: - gui.pt = 0 - gui.update += 1 + t = item.ani_timer.get() + if t < 0: + done = False + break + if t > 0.2: + item.peak_x = 0 + else: + item.peak_x = 9 * gui.scale - ((t / 0.2) * 9 * gui.scale) + done = False + if done: + gui.pt = 0 + gui.update += 1 - # Keep draw loop running while on - if gui.pt > 0: - gui.update = 2 + # Keep draw loop running while on + if gui.pt > 0: + gui.update = 2 - # Draw tags + # Draw tags - block_h = round(27 * gui.scale) - block_gap = 1 * gui.scale - if gui.scale == 1.25: - block_gap = 1 + block_h = round(27 * gui.scale) + block_gap = 1 * gui.scale + if gui.scale == 1.25: + block_gap = 1 - if coll(hot_r) or gui.pt > 0: + if coll(hot_r) or gui.pt > 0: - for i, item in enumerate(gui.power_bar): + for i, item in enumerate(gui.power_bar): - if run_y + block_h > top + h: - break + if run_y + block_h > top + h: + break - rect = [window_size[0] - item.peak_x, run_y, 7 * gui.scale, block_h] - i_rect = [window_size[0] - 36 * gui.scale, run_y, 34 * gui.scale, block_h] - fields.add(i_rect) + rect = [window_size[0] - item.peak_x, run_y, 7 * gui.scale, block_h] + i_rect = [window_size[0] - 36 * gui.scale, run_y, 34 * gui.scale, block_h] + fields.add(i_rect) - if (coll(i_rect) or ( - lightning_menu.active and lightning_menu.reference == item)) and item.peak_x == 9 * gui.scale: + if (coll(i_rect) or ( + lightning_menu.active and lightning_menu.reference == item)) and item.peak_x == 9 * gui.scale: - if not lightning_menu.active or lightning_menu.reference == item or right_click: + if not lightning_menu.active or lightning_menu.reference == item or right_click: - minx = 100 * gui.scale - maxx = minx * 2 + minx = 100 * gui.scale + maxx = minx * 2 - ww = ddt.get_text_w(item.name, 213) + ww = ddt.get_text_w(item.name, 213) - w = max(minx, ww) - w = min(maxx, w) + w = max(minx, ww) + w = min(maxx, w) - ddt.rect( - (rect[0] - w - 25 * gui.scale, run_y, w + 26 * gui.scale, block_h), - [230, 230, 230, 255]) - ddt.text( - (rect[0] - 10 * gui.scale, run_y + 5 * gui.scale, 1), item.name, - [5, 5, 5, 255], 213, w, bg=[230, 230, 230, 255]) - - if inp.mouse_click: - goto_album(item.position) - if right_click: - lightning_menu.activate(item, position=( - window_size[0] - 180 * gui.scale, rect[1] + rect[3] + 5 * gui.scale)) - if middle_click: - path_stem_to_playlist(item.path, item.name) - - ddt.rect(rect, item.colour) - run_y += block_h + block_gap - - gallery_pulse_top.render( - window_size[0] - gui.rspw, gui.panelY, gui.rspw - round(16 * gui.scale), 20 * gui.scale) - except Exception: - logging.exception("Gallery render error!") - # END POWER BAR ------------------------ + ddt.rect( + (rect[0] - w - 25 * gui.scale, run_y, w + 26 * gui.scale, block_h), + [230, 230, 230, 255]) + ddt.text( + (rect[0] - 10 * gui.scale, run_y + 5 * gui.scale, 1), item.name, + [5, 5, 5, 255], 213, w, bg=[230, 230, 230, 255]) + + if inp.mouse_click: + goto_album(item.position) + if right_click: + lightning_menu.activate(item, position=( + window_size[0] - 180 * gui.scale, rect[1] + rect[3] + 5 * gui.scale)) + if middle_click: + path_stem_to_playlist(item.path, item.name) + + ddt.rect(rect, item.colour) + run_y += block_h + block_gap + + gallery_pulse_top.render( + window_size[0] - gui.rspw, gui.panelY, gui.rspw - round(16 * gui.scale), 20 * gui.scale) + except Exception: + logging.exception("Gallery render error!") + # END POWER BAR ------------------------ - # End of gallery view - # -------------------------------------------------------------------------- - # Main Playlist: - if len(load_orders) > 0: + # End of gallery view + # -------------------------------------------------------------------------- + # Main Playlist: + if len(load_orders) > 0: - for i, order in enumerate(load_orders): - if order.stage == 2: - target_pl = 0 + for i, order in enumerate(load_orders): + if order.stage == 2: + target_pl = 0 - # Sort the tracks by track number - sort_track_2(None, order.tracks) + # Sort the tracks by track number + sort_track_2(None, order.tracks) - for p, playlist in enumerate(pctl.multi_playlist): - if playlist.uuid_int == order.playlist: - target_pl = p + for p, playlist in enumerate(pctl.multi_playlist): + if playlist.uuid_int == order.playlist: + target_pl = p + break + else: + del load_orders[i] + logging.error("Target playlist lost") break - else: - del load_orders[i] - logging.error("Target playlist lost") - break - - if order.replace_stem: - for ii, id in reversed(list(enumerate(pctl.multi_playlist[target_pl].playlist_ids))): - pfp = pctl.get_track(id).parent_folder_path - if pfp.startswith(order.target.replace("\\", "/")): - if pfp.rstrip("/\\") == order.target.rstrip("/\\") or \ - (len(pfp) > len(order.target) and pfp[ - len(order.target.rstrip("/\\"))] in ("/", "\\")): - del pctl.multi_playlist[target_pl].playlist_ids[ii] - - #logging.info(order.tracks) - if order.playlist_position is not None: - #logging.info(order.playlist_position) - pctl.multi_playlist[target_pl].playlist_ids[ - order.playlist_position:order.playlist_position] = order.tracks - # else: - else: - pctl.multi_playlist[target_pl].playlist_ids += order.tracks + if order.replace_stem: + for ii, id in reversed(list(enumerate(pctl.multi_playlist[target_pl].playlist_ids))): + pfp = pctl.get_track(id).parent_folder_path + if pfp.startswith(order.target.replace("\\", "/")): + if pfp.rstrip("/\\") == order.target.rstrip("/\\") or \ + (len(pfp) > len(order.target) and pfp[ + len(order.target.rstrip("/\\"))] in ("/", "\\")): + del pctl.multi_playlist[target_pl].playlist_ids[ii] + + #logging.info(order.tracks) + if order.playlist_position is not None: + #logging.info(order.playlist_position) + pctl.multi_playlist[target_pl].playlist_ids[ + order.playlist_position:order.playlist_position] = order.tracks + # else: - pctl.update_shuffle_pool(pctl.multi_playlist[target_pl].uuid_int) + else: + pctl.multi_playlist[target_pl].playlist_ids += order.tracks - gui.update += 2 - gui.pl_update += 2 - if order.notify and gui.message_box and len(load_orders) == 1: - show_message(_("Rescan folders complete."), mode="done") - reload() - tree_view_box.clear_target_pl(target_pl) + pctl.update_shuffle_pool(pctl.multi_playlist[target_pl].uuid_int) - if order.play and order.tracks: + gui.update += 2 + gui.pl_update += 2 + if order.notify and gui.message_box and len(load_orders) == 1: + show_message(_("Rescan folders complete."), mode="done") + reload() + tree_view_box.clear_target_pl(target_pl) - for p, plst in enumerate(pctl.multi_playlist): - if order.tracks[0] in plst[2]: - target_pl = p - break + if order.play and order.tracks: - switch_playlist(target_pl) + for p, plst in enumerate(pctl.multi_playlist): + if order.tracks[0] in plst.playlist_ids: + target_pl = p + break - pctl.active_playlist_playing = pctl.active_playlist_viewing + switch_playlist(target_pl) - # If already in playlist, delete latest add - if pctl.multi_playlist[target_pl].title == "Default": - if default_playlist.count(order.tracks[0]) > 1: - for q in reversed(range(len(default_playlist))): - if default_playlist[q] == order.tracks[0]: - del default_playlist[q] - break + pctl.active_playlist_playing = pctl.active_playlist_viewing - pctl.jump(order.tracks[0], pl_position=default_playlist.index(order.tracks[0])) + # If already in playlist, delete latest add + if pctl.multi_playlist[target_pl].title == "Default": + if default_playlist.count(order.tracks[0]) > 1: + for q in reversed(range(len(default_playlist))): + if default_playlist[q] == order.tracks[0]: + del default_playlist[q] + break - pctl.show_current(True, True, True, True, True) + pctl.jump(order.tracks[0], pl_position=default_playlist.index(order.tracks[0])) - del load_orders[i] + pctl.show_current(True, True, True, True, True) - # Are there more orders for this playlist? - # If not, decide on a name for the playlist - for item in load_orders: - if item.playlist == order.playlist: - break - else: + del load_orders[i] - if _("New Playlist") in pctl.multi_playlist[target_pl].title: - auto_name_pl(target_pl) + # Are there more orders for this playlist? + # If not, decide on a name for the playlist + for item in load_orders: + if item.playlist == order.playlist: + break + else: - if prefs.auto_sort: - if pctl.multi_playlist[target_pl].locked: - show_message(_("Auto sort skipped because playlist is locked.")) - else: - logging.info("Auto sorting") - standard_sort(target_pl) - year_sort(target_pl) - - if not load_orders: - loading_in_progress = False - pctl.notify_change() - gui.auto_play_import = False - album_artist_dict.clear() - break + if _("New Playlist") in pctl.multi_playlist[target_pl].title: + auto_name_pl(target_pl) - if gui.show_playlist: + if prefs.auto_sort: + if pctl.multi_playlist[target_pl].locked: + show_message(_("Auto sort skipped because playlist is locked.")) + else: + logging.info("Auto sorting") + standard_sort(target_pl) + year_sort(target_pl) + + if not load_orders: + loading_in_progress = False + pctl.notify_change() + gui.auto_play_import = False + album_artist_dict.clear() + break - # playlist hit test - if coll(( - gui.playlist_left, gui.playlist_top, gui.plw, - window_size[1] - gui.panelY - gui.panelBY)) and not drag_mode and ( - inp.mouse_click or mouse_wheel != 0 or right_click or middle_click or mouse_up or mouse_down): - gui.pl_update = 1 + if gui.show_playlist: - if gui.combo_mode and mouse_wheel != 0: - gui.pl_update = 1 + # playlist hit test + if coll(( + gui.playlist_left, gui.playlist_top, gui.plw, + window_size[1] - gui.panelY - gui.panelBY)) and not drag_mode and ( + inp.mouse_click or mouse_wheel != 0 or right_click or middle_click or mouse_up or mouse_down): + gui.pl_update = 1 - # MAIN PLAYLIST - # C-PR + if gui.combo_mode and mouse_wheel != 0: + gui.pl_update = 1 - top = gui.panelY - if gui.artist_info_panel: - top += gui.artist_panel_height + # MAIN PLAYLIST + # C-PR - if gui.set_mode and not gui.set_bar: - left = 0 - if gui.lsp: - left = gui.lspw - rect = [left, top, gui.plw, 12 * gui.scale] - if right_click and coll(rect): - set_menu_hidden.activate() - right_click = False + top = gui.panelY + if gui.artist_info_panel: + top += gui.artist_panel_height - width = gui.plw - if gui.set_bar and gui.set_mode: - left = 0 - if gui.lsp: - left = gui.lspw + if gui.set_mode and not gui.set_bar: + left = 0 + if gui.lsp: + left = gui.lspw + rect = [left, top, gui.plw, 12 * gui.scale] + if right_click and coll(rect): + set_menu_hidden.activate() + right_click = False - if gui.tracklist_center_mode: - left = gui.tracklist_inset_left - round(20 * gui.scale) - width = gui.tracklist_inset_width + round(20 * gui.scale) + width = gui.plw + if gui.set_bar and gui.set_mode: + left = 0 + if gui.lsp: + left = gui.lspw - rect = [left, top, width, gui.set_height] - start = left + 16 * gui.scale - run = 0 - in_grip = False + if gui.tracklist_center_mode: + left = gui.tracklist_inset_left - round(20 * gui.scale) + width = gui.tracklist_inset_width + round(20 * gui.scale) - if not mouse_down and gui.set_hold != -1: - gui.set_hold = -1 + rect = [left, top, width, gui.set_height] + start = left + 16 * gui.scale + run = 0 + in_grip = False - for h, item in enumerate(gui.pl_st): - box = (start + run, rect[1], item[1], rect[3]) - grip = (start + run, rect[1], 3 * gui.scale, rect[3]) - m_grip = (grip[0] - 4 * gui.scale, grip[1], grip[2] + 8 * gui.scale, grip[3]) - l_grip = (grip[0] + 9 * gui.scale, grip[1], box[2] - 14 * gui.scale, grip[3]) - fields.add(m_grip) + if not mouse_down and gui.set_hold != -1: + gui.set_hold = -1 - if coll(l_grip): - if mouse_up and gui.set_label_hold != -1: - if point_distance(mouse_position, gui.set_label_point) < 8 * gui.scale: - sort_direction = 0 - if h != gui.column_d_click_on or gui.column_d_click_timer.get() > 2.5: - gui.column_d_click_timer.set() - gui.column_d_click_on = h + for h, item in enumerate(gui.pl_st): + box = (start + run, rect[1], item[1], rect[3]) + grip = (start + run, rect[1], 3 * gui.scale, rect[3]) + m_grip = (grip[0] - 4 * gui.scale, grip[1], grip[2] + 8 * gui.scale, grip[3]) + l_grip = (grip[0] + 9 * gui.scale, grip[1], box[2] - 14 * gui.scale, grip[3]) + fields.add(m_grip) - sort_direction = 1 + if coll(l_grip): + if mouse_up and gui.set_label_hold != -1: + if point_distance(mouse_position, gui.set_label_point) < 8 * gui.scale: + sort_direction = 0 + if h != gui.column_d_click_on or gui.column_d_click_timer.get() > 2.5: + gui.column_d_click_timer.set() + gui.column_d_click_on = h - gui.column_sort_ani_direction = 1 - gui.column_sort_ani_x = start + run + item[1] + sort_direction = 1 - elif gui.column_d_click_on == h: - gui.column_d_click_on = -1 - gui.column_d_click_timer.force_set(10) + gui.column_sort_ani_direction = 1 + gui.column_sort_ani_x = start + run + item[1] - sort_direction = -1 + elif gui.column_d_click_on == h: + gui.column_d_click_on = -1 + gui.column_d_click_timer.force_set(10) - gui.column_sort_ani_direction = -1 - gui.column_sort_ani_x = start + run + item[1] + sort_direction = -1 - if sort_direction: + gui.column_sort_ani_direction = -1 + gui.column_sort_ani_x = start + run + item[1] - if gui.pl_st[h][0] in {"Starline", "Rating", "❤", "P", "S", "Time", "Date"}: - sort_direction *= -1 + if sort_direction: - if sort_direction == 1: - sort_ass(h) - else: - sort_ass(h, True) - gui.column_sort_ani_timer.set() + if gui.pl_st[h][0] in {"Starline", "Rating", "❤", "P", "S", "Time", "Date"}: + sort_direction *= -1 - else: - gui.column_d_click_on = -1 - if h != gui.set_label_hold: - dest = h - if dest > gui.set_label_hold: - dest += 1 - temp = gui.pl_st[gui.set_label_hold] - gui.pl_st[gui.set_label_hold] = "old" - gui.pl_st.insert(dest, temp) - gui.pl_st.remove("old") - - gui.pl_update = 1 - gui.set_label_hold = -1 - #logging.info("MOVE") - break + if sort_direction == 1: + sort_ass(h) + else: + sort_ass(h, True) + gui.column_sort_ani_timer.set() - gui.set_label_hold = -1 + else: + gui.column_d_click_on = -1 + if h != gui.set_label_hold: + dest = h + if dest > gui.set_label_hold: + dest += 1 + temp = gui.pl_st[gui.set_label_hold] + gui.pl_st[gui.set_label_hold] = "old" + gui.pl_st.insert(dest, temp) + gui.pl_st.remove("old") + + gui.pl_update = 1 + gui.set_label_hold = -1 + #logging.info("MOVE") + break - if inp.mouse_click: - gui.set_label_hold = h - gui.set_label_point = copy.deepcopy(mouse_position) - if right_click: - set_menu.activate(h) + gui.set_label_hold = -1 - if h != 0: - if coll(m_grip): - in_grip = True if inp.mouse_click: - gui.set_hold = h - gui.set_point = mouse_position[0] - gui.set_old = gui.pl_st[h - 1][1] - - if mouse_down and gui.set_hold == h: - gui.pl_st[h - 1][1] = gui.set_old + (mouse_position[0] - gui.set_point) - gui.pl_st[h - 1][1] = max(gui.pl_st[h - 1][1], 25) - - gui.update = 1 - # gui.pl_update = 1 - - total = 0 - for i in range(len(gui.pl_st) - 1): - total += gui.pl_st[i][1] - - wid = gui.plw - round(16 * gui.scale) - if gui.tracklist_center_mode: - wid = gui.tracklist_highlight_width - round(16 * gui.scale) - gui.pl_st[len(gui.pl_st) - 1][1] = wid - total - - run += item[1] - - if not mouse_down: - gui.set_label_hold = -1 - #logging.info(in_grip) - if gui.set_label_hold == -1: - if in_grip and not x_menu.active and not view_menu.active and not tab_menu.active and not set_menu.active: - gui.cursor_want = 1 - if gui.set_hold != -1: - gui.cursor_want = 1 - gui.pl_update_on_drag = True - - # heart field test - if gui.heart_fields: - for field in gui.heart_fields: - fields.add(field, update_playlist_call) - - if gui.pl_update > 0: - gui.rendered_playlist_position = playlist_view_position - - gui.pl_update -= 1 - if gui.combo_mode: + gui.set_label_hold = h + gui.set_label_point = copy.deepcopy(mouse_position) + if right_click: + set_menu.activate(h) + + if h != 0: + if coll(m_grip): + in_grip = True + if inp.mouse_click: + gui.set_hold = h + gui.set_point = mouse_position[0] + gui.set_old = gui.pl_st[h - 1][1] + + if mouse_down and gui.set_hold == h: + gui.pl_st[h - 1][1] = gui.set_old + (mouse_position[0] - gui.set_point) + gui.pl_st[h - 1][1] = max(gui.pl_st[h - 1][1], 25) + + gui.update = 1 + # gui.pl_update = 1 + + total = 0 + for i in range(len(gui.pl_st) - 1): + total += gui.pl_st[i][1] + + wid = gui.plw - round(16 * gui.scale) + if gui.tracklist_center_mode: + wid = gui.tracklist_highlight_width - round(16 * gui.scale) + gui.pl_st[len(gui.pl_st) - 1][1] = wid - total + + run += item[1] + + if not mouse_down: + gui.set_label_hold = -1 + #logging.info(in_grip) + if gui.set_label_hold == -1: + if in_grip and not x_menu.active and not view_menu.active and not tab_menu.active and not set_menu.active: + gui.cursor_want = 1 + if gui.set_hold != -1: + gui.cursor_want = 1 + gui.pl_update_on_drag = True + + # heart field test + if gui.heart_fields: + for field in gui.heart_fields: + fields.add(field, update_playlist_call) + + if gui.pl_update > 0: + gui.rendered_playlist_position = playlist_view_position + + gui.pl_update -= 1 + if gui.combo_mode: + if gui.radio_view: + radio_view.render() + elif gui.showcase_mode: + showcase.render() + + + # else: + # combo_pl_render.full_render() + else: + gui.heart_fields.clear() + playlist_render.full_render() + + elif gui.combo_mode: if gui.radio_view: radio_view.render() elif gui.showcase_mode: showcase.render() - - # else: - # combo_pl_render.full_render() + # combo_pl_render.cache_render() else: - gui.heart_fields.clear() - playlist_render.full_render() - - elif gui.combo_mode: - if gui.radio_view: - radio_view.render() - elif gui.showcase_mode: - showcase.render() - # else: - # combo_pl_render.cache_render() - else: - playlist_render.cache_render() - - if gui.combo_mode and key_esc_press and is_level_zero(): - exit_combo() - - if not gui.set_bar and gui.set_mode and not gui.combo_mode: - width = gui.plw - left = 0 - if gui.lsp: - left = gui.lspw - if gui.tracklist_center_mode: - left = gui.tracklist_highlight_left - width = gui.tracklist_highlight_width - rect = [left, top, width, gui.set_height // 2.5] - fields.add(rect) - gui.delay_frame(0.26) - - if coll(rect) and gui.bar_hover_timer.get() > 0.25: - ddt.rect(rect, colours.column_bar_background) - if inp.mouse_click: - gui.set_bar = True - update_layout_do() - if not coll(rect): - gui.bar_hover_timer.set() - - if gui.set_bar and gui.set_mode and not gui.combo_mode: - - x = 0 - if gui.lsp: - x = gui.lspw - - width = gui.plw + playlist_render.cache_render() - if gui.tracklist_center_mode: - x = gui.tracklist_highlight_left - width = gui.tracklist_highlight_width + if gui.combo_mode and key_esc_press and is_level_zero(): + exit_combo() - rect = [x, top, width, gui.set_height] - - c_bar_background = colours.column_bar_background - - # if colours.lm: - # c_bar_background = [235, 110, 160, 255] + if not gui.set_bar and gui.set_mode and not gui.combo_mode: + width = gui.plw + left = 0 + if gui.lsp: + left = gui.lspw + if gui.tracklist_center_mode: + left = gui.tracklist_highlight_left + width = gui.tracklist_highlight_width + rect = [left, top, width, gui.set_height // 2.5] + fields.add(rect) + gui.delay_frame(0.26) - if gui.tracklist_center_mode: - ddt.rect((0, top, window_size[0], gui.set_height), c_bar_background) - else: - ddt.rect(rect, c_bar_background) + if coll(rect) and gui.bar_hover_timer.get() > 0.25: + ddt.rect(rect, colours.column_bar_background) + if inp.mouse_click: + gui.set_bar = True + update_layout_do() + if not coll(rect): + gui.bar_hover_timer.set() - start = x + 16 * gui.scale - c_width = width - 16 * gui.scale + if gui.set_bar and gui.set_mode and not gui.combo_mode: - run = 0 + x = 0 + if gui.lsp: + x = gui.lspw - for i, item in enumerate(gui.pl_st): + width = gui.plw - # if run > rect[2] - 55 * gui.scale: - # break + if gui.tracklist_center_mode: + x = gui.tracklist_highlight_left + width = gui.tracklist_highlight_width - wid = item[1] + rect = [x, top, width, gui.set_height] - if run + wid > c_width: - wid = c_width - run + c_bar_background = colours.column_bar_background - if run > c_width - 22 * gui.scale: - break + # if colours.lm: + # c_bar_background = [235, 110, 160, 255] - # if run > c_width - 20 * gui.scale: - # run = run - 20 * gui.scale + if gui.tracklist_center_mode: + ddt.rect((0, top, window_size[0], gui.set_height), c_bar_background) + else: + ddt.rect(rect, c_bar_background) - wid = max(0, wid) + start = x + 16 * gui.scale + c_width = width - 16 * gui.scale - # ddt.rect_r((run, 40, wid, 10), [255, 0, 0, 100]) - box = (start + run, rect[1], wid, rect[3]) + run = 0 - grip = (start + run, rect[1], 3 * gui.scale, rect[3]) + for i, item in enumerate(gui.pl_st): - bg = c_bar_background + # if run > rect[2] - 55 * gui.scale: + # break - if coll(box) and gui.set_label_hold != -1: - bg = [39, 39, 39, 255] + wid = item[1] - if i == gui.set_label_hold: - bg = [22, 22, 22, 255] + if run + wid > c_width: + wid = c_width - run - ddt.rect(box, bg) - ddt.rect(grip, colours.column_grip) + if run > c_width - 22 * gui.scale: + break - line = _(item[0]) - ddt.text_background_colour = bg + # if run > c_width - 20 * gui.scale: + # run = run - 20 * gui.scale - # # Remove columns if positioned out of view - # if box[0] + 10 * gui.scale > start + (gui.plw - 25 * gui.scale): - # - # if box[0] + 10 * gui.scale > start + gui.plw: - # del gui.pl_st[i] - # - # i += 1 - # while i < len(gui.pl_st): - # del gui.pl_st[i] - # i += 1 - # - # break - if line == "❤": - heart_row_icon.render(box[0] + 9 * gui.scale, top + 8 * gui.scale, colours.column_bar_text) - else: - ddt.text( - (box[0] + 10 * gui.scale, top + 4 * gui.scale), line, colours.column_bar_text, 312, - bg=bg, max_w=box[2] - 25 * gui.scale) + wid = max(0, wid) - run += box[2] + # ddt.rect_r((run, 40, wid, 10), [255, 0, 0, 100]) + box = (start + run, rect[1], wid, rect[3]) - t = gui.column_sort_ani_timer.get() - if t < 0.30: - gui.update += 1 - x = round(gui.column_sort_ani_x - 22 * gui.scale) - p = t / 0.30 + grip = (start + run, rect[1], 3 * gui.scale, rect[3]) - if gui.column_sort_ani_direction == 1: - y = top + 8 * p + 3 * gui.scale - gui.column_sort_down_icon.render(x, round(y), [255, 255, 255, 90]) - else: - p = 1 - p - y = top + 8 * p + 2 * gui.scale - gui.column_sort_up_icon.render(x, round(y), [255, 255, 255, 90]) - - # Switch Vis: - if right_click and coll( - (window_size[0] - 130 * gui.scale - gui.offset_extra, 0, 125 * gui.scale, - gui.panelY)) and not gui.top_bar_mode2: - vis_menu.activate(None, (window_size[0] - 100 * gui.scale - gui.offset_extra, 30 * gui.scale)) - elif right_click and top_panel.tabs_right_x < mouse_position[0] and \ - mouse_position[1] < gui.panelY and \ - mouse_position[0] > top_panel.tabs_right_x and \ - mouse_position[0] < window_size[0] - 130 * gui.scale - gui.offset_extra: - - window_menu.activate(None, (mouse_position[0], 30 * gui.scale)) - - elif middle_click and top_panel.tabs_right_x < mouse_position[0] and \ - mouse_position[1] < gui.panelY and \ - mouse_position[0] > top_panel.tabs_right_x and \ - mouse_position[0] < window_size[0] - gui.offset_extra: + bg = c_bar_background - do_minimize_button() + if coll(box) and gui.set_label_hold != -1: + bg = [39, 39, 39, 255] - # edge_playlist.render(gui.playlist_left, gui.panelY, gui.plw, 2 * gui.scale) + if i == gui.set_label_hold: + bg = [22, 22, 22, 255] - bottom_playlist2.render(gui.playlist_left, window_size[1] - gui.panelBY, gui.plw, 25 * gui.scale, - bottom=True) - # -------------------------------------------- - # ALBUM ART + ddt.rect(box, bg) + ddt.rect(grip, colours.column_grip) - # Right side panel drawing + line = _(item[0]) + ddt.text_background_colour = bg - if gui.rsp and not album_mode: - gui.showing_l_panel = False - target_track = pctl.show_object() + # # Remove columns if positioned out of view + # if box[0] + 10 * gui.scale > start + (gui.plw - 25 * gui.scale): + # + # if box[0] + 10 * gui.scale > start + gui.plw: + # del gui.pl_st[i] + # + # i += 1 + # while i < len(gui.pl_st): + # del gui.pl_st[i] + # i += 1 + # + # break + if line == "❤": + heart_row_icon.render(box[0] + 9 * gui.scale, top + 8 * gui.scale, colours.column_bar_text) + else: + ddt.text( + (box[0] + 10 * gui.scale, top + 4 * gui.scale), line, colours.column_bar_text, 312, + bg=bg, max_w=box[2] - 25 * gui.scale) - if middle_click: - if coll( - (window_size[0] - gui.rspw, gui.panelY, gui.rspw, - window_size[1] - gui.panelY - gui.panelBY)): + run += box[2] - if (target_track and target_track.lyrics and prefs.show_lyrics_side) or \ - ( - prefs.show_lyrics_side and prefs.prefer_synced_lyrics and target_track is not None and timed_lyrics_ren.generate( - target_track)): + t = gui.column_sort_ani_timer.get() + if t < 0.30: + gui.update += 1 + x = round(gui.column_sort_ani_x - 22 * gui.scale) + p = t / 0.30 - prefs.show_lyrics_side ^= True - prefs.side_panel_layout = 1 + if gui.column_sort_ani_direction == 1: + y = top + 8 * p + 3 * gui.scale + gui.column_sort_down_icon.render(x, round(y), [255, 255, 255, 90]) else: + p = 1 - p + y = top + 8 * p + 2 * gui.scale + gui.column_sort_up_icon.render(x, round(y), [255, 255, 255, 90]) + + # Switch Vis: + if right_click and coll( + (window_size[0] - 130 * gui.scale - gui.offset_extra, 0, 125 * gui.scale, + gui.panelY)) and not gui.top_bar_mode2: + vis_menu.activate(None, (window_size[0] - 100 * gui.scale - gui.offset_extra, 30 * gui.scale)) + elif right_click and top_panel.tabs_right_x < mouse_position[0] and \ + mouse_position[1] < gui.panelY and \ + mouse_position[0] > top_panel.tabs_right_x and \ + mouse_position[0] < window_size[0] - 130 * gui.scale - gui.offset_extra: + + window_menu.activate(None, (mouse_position[0], 30 * gui.scale)) + + elif middle_click and top_panel.tabs_right_x < mouse_position[0] and \ + mouse_position[1] < gui.panelY and \ + mouse_position[0] > top_panel.tabs_right_x and \ + mouse_position[0] < window_size[0] - gui.offset_extra: + + do_minimize_button() + + # edge_playlist.render(gui.playlist_left, gui.panelY, gui.plw, 2 * gui.scale) + + bottom_playlist2.render(gui.playlist_left, window_size[1] - gui.panelBY, gui.plw, 25 * gui.scale, + bottom=True) + # -------------------------------------------- + # ALBUM ART + + # Right side panel drawing + + if gui.rsp and not album_mode: + gui.showing_l_panel = False + target_track = pctl.show_object() + + if middle_click: + if coll( + (window_size[0] - gui.rspw, gui.panelY, gui.rspw, + window_size[1] - gui.panelY - gui.panelBY)): + + if (target_track and target_track.lyrics and prefs.show_lyrics_side) or \ + ( + prefs.show_lyrics_side and prefs.prefer_synced_lyrics and target_track is not None and timed_lyrics_ren.generate( + target_track)): + + prefs.show_lyrics_side ^= True + prefs.side_panel_layout = 1 + else: - if prefs.side_panel_layout == 0: + if prefs.side_panel_layout == 0: - if (target_track and target_track.lyrics and not prefs.show_lyrics_side) or \ - ( - prefs.prefer_synced_lyrics and target_track is not None and timed_lyrics_ren.generate( - target_track)): - prefs.show_lyrics_side = True - prefs.side_panel_layout = 1 + if (target_track and target_track.lyrics and not prefs.show_lyrics_side) or \ + ( + prefs.prefer_synced_lyrics and target_track is not None and timed_lyrics_ren.generate( + target_track)): + prefs.show_lyrics_side = True + prefs.side_panel_layout = 1 + else: + prefs.side_panel_layout = 1 else: - prefs.side_panel_layout = 1 - else: - prefs.side_panel_layout = 0 + prefs.side_panel_layout = 0 - if prefs.show_lyrics_side and prefs.prefer_synced_lyrics and target_track is not None and timed_lyrics_ren.generate( - target_track): + if prefs.show_lyrics_side and prefs.prefer_synced_lyrics and target_track is not None and timed_lyrics_ren.generate( + target_track): - if prefs.show_side_lyrics_art_panel: - l_panel_h = round(200 * gui.scale) - l_panel_y = window_size[1] - (gui.panelBY + l_panel_h) - gui.showing_l_panel = True + if prefs.show_side_lyrics_art_panel: + l_panel_h = round(200 * gui.scale) + l_panel_y = window_size[1] - (gui.panelBY + l_panel_h) + gui.showing_l_panel = True - if not prefs.lyric_metadata_panel_top: - timed_lyrics_ren.render(target_track.index, (window_size[0] - gui.rspw) + 9 * gui.scale, - gui.panelY + 25 * gui.scale, side_panel=True, w=gui.rspw, - h=window_size[1] - gui.panelY - gui.panelBY - l_panel_h) - meta_box.l_panel(window_size[0] - gui.rspw, l_panel_y, gui.rspw, l_panel_h, target_track) + if not prefs.lyric_metadata_panel_top: + timed_lyrics_ren.render(target_track.index, (window_size[0] - gui.rspw) + 9 * gui.scale, + gui.panelY + 25 * gui.scale, side_panel=True, w=gui.rspw, + h=window_size[1] - gui.panelY - gui.panelBY - l_panel_h) + meta_box.l_panel(window_size[0] - gui.rspw, l_panel_y, gui.rspw, l_panel_h, target_track) + else: + timed_lyrics_ren.render(target_track.index, (window_size[0] - gui.rspw) + 9 * gui.scale, + gui.panelY + 25 * gui.scale + l_panel_h, side_panel=True, + w=gui.rspw, + h=window_size[1] - gui.panelY - gui.panelBY - l_panel_h) + meta_box.l_panel(window_size[0] - gui.rspw, gui.panelY, gui.rspw, l_panel_h, target_track) else: timed_lyrics_ren.render(target_track.index, (window_size[0] - gui.rspw) + 9 * gui.scale, - gui.panelY + 25 * gui.scale + l_panel_h, side_panel=True, - w=gui.rspw, - h=window_size[1] - gui.panelY - gui.panelBY - l_panel_h) - meta_box.l_panel(window_size[0] - gui.rspw, gui.panelY, gui.rspw, l_panel_h, target_track) - else: - timed_lyrics_ren.render(target_track.index, (window_size[0] - gui.rspw) + 9 * gui.scale, - gui.panelY + 25 * gui.scale, side_panel=True, w=gui.rspw, - h=window_size[1] - gui.panelY - gui.panelBY) + gui.panelY + 25 * gui.scale, side_panel=True, w=gui.rspw, + h=window_size[1] - gui.panelY - gui.panelBY) - if right_click and coll( - (window_size[0] - gui.rspw, gui.panelY + 25 * gui.scale, gui.rspw, window_size[1] - (gui.panelBY + gui.panelY))): - center_info_menu.activate(target_track) + if right_click and coll( + (window_size[0] - gui.rspw, gui.panelY + 25 * gui.scale, gui.rspw, window_size[1] - (gui.panelBY + gui.panelY))): + center_info_menu.activate(target_track) - elif prefs.show_lyrics_side and target_track is not None and target_track.lyrics != "" and gui.rspw > 192 * gui.scale: + elif prefs.show_lyrics_side and target_track is not None and target_track.lyrics != "" and gui.rspw > 192 * gui.scale: - if prefs.show_side_lyrics_art_panel: - l_panel_h = round(200 * gui.scale) - l_panel_y = window_size[1] - (gui.panelBY + l_panel_h) - gui.showing_l_panel = True + if prefs.show_side_lyrics_art_panel: + l_panel_h = round(200 * gui.scale) + l_panel_y = window_size[1] - (gui.panelBY + l_panel_h) + gui.showing_l_panel = True - if not prefs.lyric_metadata_panel_top: - meta_box.lyrics( - window_size[0] - gui.rspw, gui.panelY, gui.rspw, - window_size[1] - gui.panelY - gui.panelBY - l_panel_h, target_track) - meta_box.l_panel(window_size[0] - gui.rspw, l_panel_y, gui.rspw, l_panel_h, target_track) + if not prefs.lyric_metadata_panel_top: + meta_box.lyrics( + window_size[0] - gui.rspw, gui.panelY, gui.rspw, + window_size[1] - gui.panelY - gui.panelBY - l_panel_h, target_track) + meta_box.l_panel(window_size[0] - gui.rspw, l_panel_y, gui.rspw, l_panel_h, target_track) + else: + meta_box.lyrics( + window_size[0] - gui.rspw, gui.panelY + l_panel_h, gui.rspw, + window_size[1] - (gui.panelY + gui.panelBY + l_panel_h), target_track) + + meta_box.l_panel( + window_size[0] - gui.rspw, gui.panelY, gui.rspw, l_panel_h, + target_track, top_border=False) else: meta_box.lyrics( - window_size[0] - gui.rspw, gui.panelY + l_panel_h, gui.rspw, - window_size[1] - (gui.panelY + gui.panelBY + l_panel_h), target_track) + window_size[0] - gui.rspw, gui.panelY, gui.rspw, + window_size[1] - gui.panelY - gui.panelBY, target_track) - meta_box.l_panel( - window_size[0] - gui.rspw, gui.panelY, gui.rspw, l_panel_h, - target_track, top_border=False) - else: - meta_box.lyrics( - window_size[0] - gui.rspw, gui.panelY, gui.rspw, - window_size[1] - gui.panelY - gui.panelBY, target_track) + elif prefs.side_panel_layout == 0: - elif prefs.side_panel_layout == 0: + boxw = gui.rspw + boxh = gui.rspw - boxw = gui.rspw - boxh = gui.rspw + if prefs.show_side_art: - if prefs.show_side_art: + meta_box.draw( + window_size[0] - gui.rspw, gui.panelY + boxh, gui.rspw, + window_size[1] - gui.panelY - gui.panelBY - boxh, track=target_track) - meta_box.draw( - window_size[0] - gui.rspw, gui.panelY + boxh, gui.rspw, - window_size[1] - gui.panelY - gui.panelBY - boxh, track=target_track) + boxh = min(boxh, window_size[1] - gui.panelY - gui.panelBY) - boxh = min(boxh, window_size[1] - gui.panelY - gui.panelBY) + art_box.draw(window_size[0] - gui.rspw, gui.panelY, boxw, boxh, target_track=target_track) - art_box.draw(window_size[0] - gui.rspw, gui.panelY, boxw, boxh, target_track=target_track) + else: + meta_box.draw( + window_size[0] - gui.rspw, gui.panelY, gui.rspw, + window_size[1] - gui.panelY - gui.panelBY, track=target_track) - else: - meta_box.draw( - window_size[0] - gui.rspw, gui.panelY, gui.rspw, - window_size[1] - gui.panelY - gui.panelBY, track=target_track) + elif prefs.side_panel_layout == 1: - elif prefs.side_panel_layout == 1: + h = window_size[1] - (gui.panelY + gui.panelBY) + x = window_size[0] - gui.rspw + y = gui.panelY + w = gui.rspw - h = window_size[1] - (gui.panelY + gui.panelBY) - x = window_size[0] - gui.rspw - y = gui.panelY - w = gui.rspw + ddt.rect((x, y, w, h), colours.side_panel_background) + test_auto_lyrics(target_track) + # Draw lyrics if avaliable + if prefs.show_lyrics_side and target_track and target_track.lyrics != "": # and not prefs.show_side_art: + # meta_box.lyrics(x, y, w, h, target_track) + if right_click and coll((x, y, w, h)) and target_track: + center_info_menu.activate(target_track) + else: - ddt.rect((x, y, w, h), colours.side_panel_background) - test_auto_lyrics(target_track) - # Draw lyrics if avaliable - if prefs.show_lyrics_side and target_track and target_track.lyrics != "": # and not prefs.show_side_art: - # meta_box.lyrics(x, y, w, h, target_track) - if right_click and coll((x, y, w, h)) and target_track: - center_info_menu.activate(target_track) - else: + box_wide_w = round(w * 0.98) + boxx = round(min(h * 0.7, w * 0.9)) + boxy = round(min(h * 0.7, w * 0.9)) - box_wide_w = round(w * 0.98) - boxx = round(min(h * 0.7, w * 0.9)) - boxy = round(min(h * 0.7, w * 0.9)) + bx = (x + w // 2) - (boxx // 2) + bx_wide = (x + w // 2) - (box_wide_w // 2) + by = round(h * 0.1) - bx = (x + w // 2) - (boxx // 2) - bx_wide = (x + w // 2) - (box_wide_w // 2) - by = round(h * 0.1) + bby = by + boxy - bby = by + boxy + # We want the text in the center, but slightly raised when area is large + text_y = y + by + boxy + ((h - bby) // 2) - 44 * gui.scale - round( + (h - bby - 94 * gui.scale) * 0.08) - # We want the text in the center, but slightly raised when area is large - text_y = y + by + boxy + ((h - bby) // 2) - 44 * gui.scale - round( - (h - bby - 94 * gui.scale) * 0.08) + small_mode = False + if window_size[1] < 550 * gui.scale: + small_mode = True + text_y = y + by + boxy + ((h - bby) // 2) - 38 * gui.scale - small_mode = False - if window_size[1] < 550 * gui.scale: - small_mode = True - text_y = y + by + boxy + ((h - bby) // 2) - 38 * gui.scale + text_x = x + w // 2 - text_x = x + w // 2 + if prefs.show_side_art: + gui.art_drawn_rect = None + default_border = (bx, by, boxx, boxy) + coll_border = default_border - if prefs.show_side_art: - gui.art_drawn_rect = None - default_border = (bx, by, boxx, boxy) - coll_border = default_border + art_box.draw( + bx_wide, by, box_wide_w, boxy, target_track=target_track, + tight_border=True, default_border=default_border) - art_box.draw( - bx_wide, by, box_wide_w, boxy, target_track=target_track, - tight_border=True, default_border=default_border) + if gui.art_drawn_rect: + coll_border = gui.art_drawn_rect - if gui.art_drawn_rect: - coll_border = gui.art_drawn_rect + if right_click and coll((x, y, w, h)) and not coll(coll_border): + if is_level_zero(include_menus=False) and target_track: + center_info_menu.activate(target_track) - if right_click and coll((x, y, w, h)) and not coll(coll_border): - if is_level_zero(include_menus=False) and target_track: + else: + text_y = y + round(h * 0.40) + if right_click and coll((x, y, w, h)) and target_track: center_info_menu.activate(target_track) - else: - text_y = y + round(h * 0.40) - if right_click and coll((x, y, w, h)) and target_track: - center_info_menu.activate(target_track) + ww = w - 25 * gui.scale - ww = w - 25 * gui.scale + gui.showed_title = True - gui.showed_title = True + if target_track: + ddt.text_background_colour = colours.side_panel_background - if target_track: - ddt.text_background_colour = colours.side_panel_background + if pctl.playing_state == 3 and not radiobox.dummy_track.title: + title = pctl.tag_meta + else: + title = target_track.title + if not title: + title = clean_string(target_track.filename) - if pctl.playing_state == 3 and not radiobox.dummy_track.title: - title = pctl.tag_meta - else: - title = target_track.title - if not title: - title = clean_string(target_track.filename) + if small_mode: + ddt.text( + (text_x, text_y - 15 * gui.scale, 2), target_track.artist, + colours.side_bar_line1, 315, max_w=ww) - if small_mode: - ddt.text( - (text_x, text_y - 15 * gui.scale, 2), target_track.artist, - colours.side_bar_line1, 315, max_w=ww) + ddt.text( + (text_x, text_y + 12 * gui.scale, 2), title, colours.side_bar_line1, 216, max_w=ww) - ddt.text( - (text_x, text_y + 12 * gui.scale, 2), title, colours.side_bar_line1, 216, max_w=ww) + line = " | ".join( + filter(None, (target_track.album, target_track.date, target_track.genre))) + ddt.text((text_x, text_y + 35 * gui.scale, 2), line, colours.side_bar_line2, 313, max_w=ww) - line = " | ".join( - filter(None, (target_track.album, target_track.date, target_track.genre))) - ddt.text((text_x, text_y + 35 * gui.scale, 2), line, colours.side_bar_line2, 313, max_w=ww) + else: + ddt.text((text_x, text_y - 15 * gui.scale, 2), target_track.artist, colours.side_bar_line1, 317, max_w=ww) - else: - ddt.text((text_x, text_y - 15 * gui.scale, 2), target_track.artist, colours.side_bar_line1, 317, max_w=ww) + ddt.text((text_x, text_y + 17 * gui.scale, 2), title, colours.side_bar_line1, 218, max_w=ww) - ddt.text((text_x, text_y + 17 * gui.scale, 2), title, colours.side_bar_line1, 218, max_w=ww) + line = " | ".join( + filter(None, (target_track.album, target_track.date, target_track.genre))) + ddt.text((text_x, text_y + 45 * gui.scale, 2), line, colours.side_bar_line2, 314, max_w=ww) - line = " | ".join( - filter(None, (target_track.album, target_track.date, target_track.genre))) - ddt.text((text_x, text_y + 45 * gui.scale, 2), line, colours.side_bar_line2, 314, max_w=ww) + # Seperation Line Drawing + if gui.rsp: - # Seperation Line Drawing - if gui.rsp: + # Draw Highlight when mouse over + if draw_sep_hl: + ddt.line( + window_size[0] - gui.rspw + 1 * gui.scale, gui.panelY + 1 * gui.scale, + window_size[0] - gui.rspw + 1 * gui.scale, + window_size[1] - 50 * gui.scale, [100, 100, 100, 70]) + draw_sep_hl = False - # Draw Highlight when mouse over - if draw_sep_hl: - ddt.line( - window_size[0] - gui.rspw + 1 * gui.scale, gui.panelY + 1 * gui.scale, - window_size[0] - gui.rspw + 1 * gui.scale, - window_size[1] - 50 * gui.scale, [100, 100, 100, 70]) - draw_sep_hl = False + if (gui.artist_info_panel and not gui.combo_mode) and not (window_size[0] < 750 * gui.scale and album_mode): + artist_info_box.draw(gui.playlist_left, gui.panelY, gui.plw, gui.artist_panel_height) - if (gui.artist_info_panel and not gui.combo_mode) and not (window_size[0] < 750 * gui.scale and album_mode): - artist_info_box.draw(gui.playlist_left, gui.panelY, gui.plw, gui.artist_panel_height) + if gui.lsp and not gui.combo_mode: - if gui.lsp and not gui.combo_mode: + # left side panel - # left side panel + h_estimate = ((playlist_box.tab_h + playlist_box.gap) * gui.scale * len( + pctl.multi_playlist)) + 13 * gui.scale - h_estimate = ((playlist_box.tab_h + playlist_box.gap) * gui.scale * len( - pctl.multi_playlist)) + 13 * gui.scale + full = (window_size[1] - (gui.panelY + gui.panelBY)) + half = int(round(full / 2)) - full = (window_size[1] - (gui.panelY + gui.panelBY)) - half = int(round(full / 2)) + pl_box_h = full - pl_box_h = full + panel_rect = (0, gui.panelY, gui.lspw, pl_box_h) + fields.add(panel_rect) - panel_rect = (0, gui.panelY, gui.lspw, pl_box_h) - fields.add(panel_rect) + if gui.force_side_on_drag and not quick_drag and not coll(panel_rect): + gui.force_side_on_drag = False + update_layout_do() - if gui.force_side_on_drag and not quick_drag and not coll(panel_rect): - gui.force_side_on_drag = False - update_layout_do() + if quick_drag and not coll_point(gui.drag_source_position_persist, panel_rect) and \ + not point_proximity_test( + gui.drag_source_position, + mouse_position, + 10 * gui.scale): + gui.force_side_on_drag = True + if mouse_up: + update_layout_do() - if quick_drag and not coll_point(gui.drag_source_position_persist, panel_rect) and \ - not point_proximity_test( - gui.drag_source_position, - mouse_position, - 10 * gui.scale): - gui.force_side_on_drag = True - if mouse_up: - update_layout_do() + if prefs.left_panel_mode == "folder view" and not gui.force_side_on_drag: + tree_view_box.render(0, gui.panelY, gui.lspw, pl_box_h) + elif prefs.left_panel_mode == "artist list" and not gui.force_side_on_drag: + artist_list_box.render(*panel_rect) + else: - if prefs.left_panel_mode == "folder view" and not gui.force_side_on_drag: - tree_view_box.render(0, gui.panelY, gui.lspw, pl_box_h) - elif prefs.left_panel_mode == "artist list" and not gui.force_side_on_drag: - artist_list_box.render(*panel_rect) - else: + preview_queue = False + if quick_drag and coll( + panel_rect) and not pctl.force_queue and prefs.show_playlist_list and prefs.hide_queue: + preview_queue = True + + if pctl.force_queue or preview_queue or not prefs.hide_queue: + + if h_estimate < half: + pl_box_h = h_estimate + else: + pl_box_h = half - preview_queue = False - if quick_drag and coll( - panel_rect) and not pctl.force_queue and prefs.show_playlist_list and prefs.hide_queue: - preview_queue = True + if preview_queue: + pl_box_h = int(round(full * 5 / 6)) - if pctl.force_queue or preview_queue or not prefs.hide_queue: + if prefs.left_panel_mode != "queue": - if h_estimate < half: - pl_box_h = h_estimate + playlist_box.draw(0, gui.panelY, gui.lspw, pl_box_h) else: - pl_box_h = half + pl_box_h = 0 - if preview_queue: - pl_box_h = int(round(full * 5 / 6)) + if pctl.force_queue or preview_queue or not prefs.show_playlist_list or not prefs.hide_queue: - if prefs.left_panel_mode != "queue": + queue_box.draw(0, gui.panelY + pl_box_h, gui.lspw, full - pl_box_h) + elif prefs.left_panel_mode == "queue": + text = _("Queue is Empty") + rect = (0, gui.panelY + pl_box_h, gui.lspw, full - pl_box_h) + ddt.rect(rect, colours.queue_background) + ddt.text_background_colour = colours.queue_background + ddt.text( + (0 + (gui.lspw // 2), gui.panelY + pl_box_h + 15 * gui.scale, 2), + text, alpha_mod(colours.index_text, 200), 212) - playlist_box.draw(0, gui.panelY, gui.lspw, pl_box_h) - else: - pl_box_h = 0 + # ------------------------------------------------ + # Scroll Bar - if pctl.force_queue or preview_queue or not prefs.show_playlist_list or not prefs.hide_queue: + # if not scroll_enable: + top = gui.panelY + if gui.artist_info_panel: + top += gui.artist_panel_height - queue_box.draw(0, gui.panelY + pl_box_h, gui.lspw, full - pl_box_h) - elif prefs.left_panel_mode == "queue": - text = _("Queue is Empty") - rect = (0, gui.panelY + pl_box_h, gui.lspw, full - pl_box_h) - ddt.rect(rect, colours.queue_background) - ddt.text_background_colour = colours.queue_background - ddt.text( - (0 + (gui.lspw // 2), gui.panelY + pl_box_h + 15 * gui.scale, 2), - text, alpha_mod(colours.index_text, 200), 212) + edge_top = top + if gui.set_bar and gui.set_mode: + edge_top += gui.set_height + edge_playlist2.render(gui.playlist_left, edge_top, gui.plw, 25 * gui.scale) - # ------------------------------------------------ - # Scroll Bar + width = 15 * gui.scale - # if not scroll_enable: - top = gui.panelY - if gui.artist_info_panel: - top += gui.artist_panel_height + x = 0 + if gui.lsp: # Move left so it sits over panel divide - edge_top = top - if gui.set_bar and gui.set_mode: - edge_top += gui.set_height - edge_playlist2.render(gui.playlist_left, edge_top, gui.plw, 25 * gui.scale) + x = gui.lspw - 1 * gui.scale + if not gui.set_mode: + width = 11 * gui.scale + if gui.set_mode and prefs.left_align_album_artist_title: + width = 11 * gui.scale - width = 15 * gui.scale + # x = gui.plw + # width = round(14 * gui.scale) + # if gui.lsp: + # x += gui.lspw + # x -= width - x = 0 - if gui.lsp: # Move left so it sits over panel divide + gui.scroll_hide_box = ( + x + 1 if not gui.maximized else x, top, 28 * gui.scale, window_size[1] - gui.panelBY - top) - x = gui.lspw - 1 * gui.scale - if not gui.set_mode: - width = 11 * gui.scale - if gui.set_mode and prefs.left_align_album_artist_title: - width = 11 * gui.scale - - # x = gui.plw - # width = round(14 * gui.scale) - # if gui.lsp: - # x += gui.lspw - # x -= width - - gui.scroll_hide_box = ( - x + 1 if not gui.maximized else x, top, 28 * gui.scale, window_size[1] - gui.panelBY - top) - - fields.add(gui.scroll_hide_box) - if scroll_hide_timer.get() < 0.9 or ((coll( - gui.scroll_hide_box) or scroll_hold or quick_search_mode) and \ - not menu_is_open() and \ - not pref_box.enabled and \ - not gui.rename_playlist_box \ - and gui.layer_focus == 0 and gui.show_playlist and not search_over.active): - - scroll_opacity = 255 - - if not gui.combo_mode: - sy = 31 * gui.scale - ey = window_size[1] - (30 + 22) * gui.scale - - if len(default_playlist) < 50: - sbl = 85 * gui.scale - if len(default_playlist) == 0: - sbp = top - else: - sbl = 105 * gui.scale + fields.add(gui.scroll_hide_box) + if scroll_hide_timer.get() < 0.9 or ((coll( + gui.scroll_hide_box) or scroll_hold or quick_search_mode) and \ + not menu_is_open() and \ + not pref_box.enabled and \ + not gui.rename_playlist_box \ + and gui.layer_focus == 0 and gui.show_playlist and not search_over.active): - fields.add((x + 2 * gui.scale, sbp, 20 * gui.scale, sbl)) - if coll((x, top, 28 * gui.scale, ey - top)) and ( - mouse_down or right_click) \ - and coll_point(click_location, (x, top, 28 * gui.scale, ey - top)): + scroll_opacity = 255 - gui.pl_update = 1 - if right_click: + if not gui.combo_mode: + sy = 31 * gui.scale + ey = window_size[1] - (30 + 22) * gui.scale - sbp = mouse_position[1] - int(sbl / 2) - if sbp + sbl > ey: - sbp = ey - sbl - elif sbp < top: + if len(default_playlist) < 50: + sbl = 85 * gui.scale + if len(default_playlist) == 0: sbp = top - per = (sbp - top) / (ey - top - sbl) - pctl.playlist_view_position = int(len(default_playlist) * per) - logging.debug("Position set by scroll bar (right click)") - pctl.playlist_view_position = max(pctl.playlist_view_position, 0) - - # if playlist_position == len(default_playlist): - # logging.info("END") - - # elif mouse_position[1] < sbp: - # pctl.playlist_view_position -= 2 - # elif mouse_position[1] > sbp + sbl: - # pctl.playlist_view_position += 2 - elif inp.mouse_click: - - if mouse_position[1] < sbp: - gui.scroll_direction = -1 - elif mouse_position[1] > sbp + sbl: - gui.scroll_direction = 1 - else: - # p_y = pointer(c_int(0)) - # p_x = pointer(c_int(0)) - # SDL_GetGlobalMouseState(p_x, p_y) - get_sdl_input.mouse_capture_want = True - - scroll_hold = True - # scroll_point = p_y.contents.value # mouse_position[1] - scroll_point = mouse_position[1] - scroll_bpoint = sbp else: - # gui.update += 1 - if sbp < mouse_position[1] < sbp + sbl: - gui.scroll_direction = 0 - pctl.playlist_view_position += gui.scroll_direction * 2 - logging.debug("Position set by scroll bar (slide)") - pctl.playlist_view_position = max(pctl.playlist_view_position, 0) - pctl.playlist_view_position = min(pctl.playlist_view_position, len(default_playlist)) + sbl = 105 * gui.scale + + fields.add((x + 2 * gui.scale, sbp, 20 * gui.scale, sbl)) + if coll((x, top, 28 * gui.scale, ey - top)) and ( + mouse_down or right_click) \ + and coll_point(click_location, (x, top, 28 * gui.scale, ey - top)): + + gui.pl_update = 1 + if right_click: + + sbp = mouse_position[1] - int(sbl / 2) + if sbp + sbl > ey: + sbp = ey - sbl + elif sbp < top: + sbp = top + per = (sbp - top) / (ey - top - sbl) + pctl.playlist_view_position = int(len(default_playlist) * per) + logging.debug("Position set by scroll bar (right click)") + pctl.playlist_view_position = max(pctl.playlist_view_position, 0) + + # if playlist_position == len(default_playlist): + # logging.info("END") + + # elif mouse_position[1] < sbp: + # pctl.playlist_view_position -= 2 + # elif mouse_position[1] > sbp + sbl: + # pctl.playlist_view_position += 2 + elif inp.mouse_click: + + if mouse_position[1] < sbp: + gui.scroll_direction = -1 + elif mouse_position[1] > sbp + sbl: + gui.scroll_direction = 1 + else: + # p_y = pointer(c_int(0)) + # p_x = pointer(c_int(0)) + # SDL_GetGlobalMouseState(p_x, p_y) + get_sdl_input.mouse_capture_want = True + + scroll_hold = True + # scroll_point = p_y.contents.value # mouse_position[1] + scroll_point = mouse_position[1] + scroll_bpoint = sbp + else: + # gui.update += 1 + if sbp < mouse_position[1] < sbp + sbl: + gui.scroll_direction = 0 + pctl.playlist_view_position += gui.scroll_direction * 2 + logging.debug("Position set by scroll bar (slide)") + pctl.playlist_view_position = max(pctl.playlist_view_position, 0) + pctl.playlist_view_position = min(pctl.playlist_view_position, len(default_playlist)) + + if sbp + sbl > ey: + sbp = ey - sbl + elif sbp < top: + sbp = top + + if not mouse_down: + scroll_hold = False + + if scroll_hold and not inp.mouse_click: + gui.pl_update = 1 + # p_y = pointer(c_int(0)) + # p_x = pointer(c_int(0)) + # SDL_GetGlobalMouseState(p_x, p_y) + get_sdl_input.mouse_capture_want = True + sbp = mouse_position[1] - (scroll_point - scroll_bpoint) if sbp + sbl > ey: sbp = ey - sbl elif sbp < top: sbp = top + per = (sbp - top) / (ey - top - sbl) + pctl.playlist_view_position = int(len(default_playlist) * per) + logging.debug("Position set by scroll bar (drag)") - if not mouse_down: - scroll_hold = False - if scroll_hold and not inp.mouse_click: - gui.pl_update = 1 - # p_y = pointer(c_int(0)) - # p_x = pointer(c_int(0)) - # SDL_GetGlobalMouseState(p_x, p_y) - get_sdl_input.mouse_capture_want = True + elif len(default_playlist) > 0: + per = pctl.playlist_view_position / len(default_playlist) + sbp = int((ey - top - sbl) * per) + top + 1 - sbp = mouse_position[1] - (scroll_point - scroll_bpoint) - if sbp + sbl > ey: - sbp = ey - sbl - elif sbp < top: - sbp = top - per = (sbp - top) / (ey - top - sbl) - pctl.playlist_view_position = int(len(default_playlist) * per) - logging.debug("Position set by scroll bar (drag)") + bg = [255, 255, 255, 6] + fg = colours.scroll_colour + if colours.lm: + bg = [200, 200, 200, 100] + fg = [100, 100, 100, 200] - elif len(default_playlist) > 0: - per = pctl.playlist_view_position / len(default_playlist) - sbp = int((ey - top - sbl) * per) + top + 1 + ddt.rect_a((x, top), (width + 1 * gui.scale, window_size[1] - top - gui.panelBY), bg) + ddt.rect_a((x + 1, sbp), (width, sbl), alpha_mod(fg, scroll_opacity)) - bg = [255, 255, 255, 6] - fg = colours.scroll_colour + if (coll((x + 2 * gui.scale, sbp, 20 * gui.scale, sbl)) and mouse_position[ + 0] != 0) or scroll_hold: + ddt.rect_a((x + 1 * gui.scale, sbp), (width, sbl), [255, 255, 255, 19]) - if colours.lm: - bg = [200, 200, 200, 100] - fg = [100, 100, 100, 200] + # NEW TOP BAR + # C-TBR - ddt.rect_a((x, top), (width + 1 * gui.scale, window_size[1] - top - gui.panelBY), bg) - ddt.rect_a((x + 1, sbp), (width, sbl), alpha_mod(fg, scroll_opacity)) + if gui.mode == 1: + top_panel.render() - if (coll((x + 2 * gui.scale, sbp, 20 * gui.scale, sbl)) and mouse_position[ - 0] != 0) or scroll_hold: - ddt.rect_a((x + 1 * gui.scale, sbp), (width, sbl), [255, 255, 255, 19]) + # RENDER EXTRA FRAME DOUBLE + if colours.lm: + if gui.lsp and not gui.combo_mode and not gui.compact_artist_list: + ddt.rect( + (0 + gui.lspw - 6 * gui.scale, gui.panelY, 6 * gui.scale, + int(round(window_size[1] - gui.panelY - gui.panelBY))), colours.grey(200)) + ddt.rect( + (0 + gui.lspw - 5 * gui.scale, gui.panelY - 1, 4 * gui.scale, + int(round(window_size[1] - gui.panelY - gui.panelBY)) + 1), colours.grey(245)) + if gui.rsp and gui.show_playlist: + w = window_size[0] - gui.rspw + ddt.rect( + (w - round(3 * gui.scale), gui.panelY, 6 * gui.scale, + int(round(window_size[1] - gui.panelY - gui.panelBY))), colours.grey(200)) + ddt.rect( + (w - round(2 * gui.scale), gui.panelY - 1, 4 * gui.scale, + int(round(window_size[1] - gui.panelY - gui.panelBY)) + 1), colours.grey(245)) + if gui.queue_frame_draw is not None: + if gui.lsp: + ddt.rect((0, gui.queue_frame_draw, gui.lspw - 6 * gui.scale, 6 * gui.scale), colours.grey(200)) + ddt.rect( + (0, gui.queue_frame_draw + 1 * gui.scale, gui.lspw - 5 * gui.scale, 4 * gui.scale), colours.grey(250)) - # NEW TOP BAR - # C-TBR + gui.queue_frame_draw = None - if gui.mode == 1: - top_panel.render() + # BOTTOM BAR! + # C-BB - # RENDER EXTRA FRAME DOUBLE - if colours.lm: - if gui.lsp and not gui.combo_mode and not gui.compact_artist_list: - ddt.rect( - (0 + gui.lspw - 6 * gui.scale, gui.panelY, 6 * gui.scale, - int(round(window_size[1] - gui.panelY - gui.panelBY))), colours.grey(200)) - ddt.rect( - (0 + gui.lspw - 5 * gui.scale, gui.panelY - 1, 4 * gui.scale, - int(round(window_size[1] - gui.panelY - gui.panelBY)) + 1), colours.grey(245)) - if gui.rsp and gui.show_playlist: - w = window_size[0] - gui.rspw - ddt.rect( - (w - round(3 * gui.scale), gui.panelY, 6 * gui.scale, - int(round(window_size[1] - gui.panelY - gui.panelBY))), colours.grey(200)) - ddt.rect( - (w - round(2 * gui.scale), gui.panelY - 1, 4 * gui.scale, - int(round(window_size[1] - gui.panelY - gui.panelBY)) + 1), colours.grey(245)) - if gui.queue_frame_draw is not None: - if gui.lsp: - ddt.rect((0, gui.queue_frame_draw, gui.lspw - 6 * gui.scale, 6 * gui.scale), colours.grey(200)) - ddt.rect( - (0, gui.queue_frame_draw + 1 * gui.scale, gui.lspw - 5 * gui.scale, 4 * gui.scale), colours.grey(250)) + ddt.text_background_colour = colours.bottom_panel_colour - gui.queue_frame_draw = None + if prefs.shuffle_lock: + bottom_bar_ao1.render() + else: + bottom_bar1.render() + + if prefs.art_bg and not prefs.bg_showcase_only: + style_overlay.display() + # if key_shift_down: + # ddt.rect_r(gui.seek_bar_rect, + # alpha_mod([150, 150, 150 ,255], 20), True) + # ddt.rect_r(gui.volume_bar_rect, + # alpha_mod(colours.volume_bar_fill, 100), True) + + style_overlay.hole_punches.clear() + + if gui.set_mode: + if rename_track_box.active is False \ + and radiobox.active is False \ + and gui.rename_playlist_box is False \ + and gui.message_box is False \ + and pref_box.enabled is False \ + and track_box is False \ + and not gui.rename_folder_box \ + and not Menu.active \ + and not artist_info_scroll.held: + + columns_tool_tip.render() + else: + columns_tool_tip.show = False - # BOTTOM BAR! - # C-BB + # Overlay GUI ---------------------- - ddt.text_background_colour = colours.bottom_panel_colour + if gui.rename_playlist_box: + rename_playlist_box.render() - if prefs.shuffle_lock: - bottom_bar_ao1.render() - else: - bottom_bar1.render() + if gui.preview_artist: - if prefs.art_bg and not prefs.bg_showcase_only: - style_overlay.display() - # if key_shift_down: - # ddt.rect_r(gui.seek_bar_rect, - # alpha_mod([150, 150, 150 ,255], 20), True) - # ddt.rect_r(gui.volume_bar_rect, - # alpha_mod(colours.volume_bar_fill, 100), True) + border = round(4 * gui.scale) + ddt.rect( + (gui.preview_artist_location[0] - border, + gui.preview_artist_location[1] - border, + artist_preview_render.size[0] + border * 2, + artist_preview_render.size[0] + border * 2), (20, 20, 20, 255)) - style_overlay.hole_punches.clear() + artist_preview_render.draw(gui.preview_artist_location[0], gui.preview_artist_location[1]) + if inp.mouse_click or right_click or mouse_wheel: + gui.preview_artist = "" - if gui.set_mode: - if rename_track_box.active is False \ - and radiobox.active is False \ - and gui.rename_playlist_box is False \ - and gui.message_box is False \ - and pref_box.enabled is False \ - and track_box is False \ - and not gui.rename_folder_box \ - and not Menu.active \ - and not artist_info_scroll.held: - - columns_tool_tip.render() - else: - columns_tool_tip.show = False + if track_box: + if inp.key_return_press or right_click or key_esc_press or inp.backspace_press or keymaps.test( + "quick-find"): + track_box = False - # Overlay GUI ---------------------- + inp.key_return_press = False - if gui.rename_playlist_box: - rename_playlist_box.render() + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False - if gui.preview_artist: + tc = pctl.master_library[r_menu_index] - border = round(4 * gui.scale) - ddt.rect( - (gui.preview_artist_location[0] - border, - gui.preview_artist_location[1] - border, - artist_preview_render.size[0] + border * 2, - artist_preview_render.size[0] + border * 2), (20, 20, 20, 255)) + w = round(540 * gui.scale) + h = round(240 * gui.scale) + comment_mode = 0 - artist_preview_render.draw(gui.preview_artist_location[0], gui.preview_artist_location[1]) - if inp.mouse_click or right_click or mouse_wheel: - gui.preview_artist = "" + if len(tc.comment) > 0: + h += 22 * gui.scale + if window_size[0] > 599: + w += 25 * gui.scale + if ddt.get_text_w(tc.comment, 12) > 330 * gui.scale or "\n" in tc.comment: + h += 80 * gui.scale + if window_size[0] > 599: + w += 30 * gui.scale + comment_mode = 1 - if track_box: - if inp.key_return_press or right_click or key_esc_press or inp.backspace_press or keymaps.test( - "quick-find"): - track_box = False + x = round((window_size[0] / 2) - (w / 2)) + y = round((window_size[1] / 2) - (h / 2)) - inp.key_return_press = False + x1 = int(x + 18 * gui.scale) + x2 = int(x + 98 * gui.scale) - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False + value_font_a = 312 + value_font = 12 - tc = pctl.master_library[r_menu_index] + # if key_shift_down: + # value_font = 12 + key_colour_off = colours.box_text_label # colours.grey_blend_bg(90) + key_colour_on = colours.box_title_text + value_colour = colours.box_sub_text + path_colour = alpha_mod(value_colour, 240) - w = round(540 * gui.scale) - h = round(240 * gui.scale) - comment_mode = 0 + # if colours.lm: + # key_colour_off = colours.grey(80) + # key_colour_on = colours.grey(120) + # value_colour = colours.grey(50) + # path_colour = colours.grey(70) - if len(tc.comment) > 0: - h += 22 * gui.scale - if window_size[0] > 599: - w += 25 * gui.scale - if ddt.get_text_w(tc.comment, 12) > 330 * gui.scale or "\n" in tc.comment: - h += 80 * gui.scale - if window_size[0] > 599: - w += 30 * gui.scale - comment_mode = 1 + ddt.rect_a( + (x - 3 * gui.scale, y - 3 * gui.scale), (w + 6 * gui.scale, h + 6 * gui.scale), + colours.box_border) + ddt.rect_a((x, y), (w, h), colours.box_background) + ddt.text_background_colour = colours.box_background - x = round((window_size[0] / 2) - (w / 2)) - y = round((window_size[1] / 2) - (h / 2)) + if inp.mouse_click and not coll([x, y, w, h]): + track_box = False - x1 = int(x + 18 * gui.scale) - x2 = int(x + 98 * gui.scale) + else: + art_size = int(115 * gui.scale) - value_font_a = 312 - value_font = 12 + # if not tc.is_network: # Don't draw album art if from network location for better performance + if comment_mode == 1: + album_art_gen.display( + tc, (int(x + w - 135 * gui.scale), int(y + 105 * gui.scale)), + (art_size, art_size)) # Mirror this size in auto theme #mark2233 + else: + album_art_gen.display( + tc, (int(x + w - 135 * gui.scale), int(y + h - 135 * gui.scale)), + (art_size, art_size)) - # if key_shift_down: - # value_font = 12 - key_colour_off = colours.box_text_label # colours.grey_blend_bg(90) - key_colour_on = colours.box_title_text - value_colour = colours.box_sub_text - path_colour = alpha_mod(value_colour, 240) + y -= int(24 * gui.scale) + y1 = int(y + (40 * gui.scale)) - # if colours.lm: - # key_colour_off = colours.grey(80) - # key_colour_on = colours.grey(120) - # value_colour = colours.grey(50) - # path_colour = colours.grey(70) + ext_rect = [x + w - round(38 * gui.scale), y + round(44 * gui.scale), round(38 * gui.scale), + round(12 * gui.scale)] - ddt.rect_a( - (x - 3 * gui.scale, y - 3 * gui.scale), (w + 6 * gui.scale, h + 6 * gui.scale), - colours.box_border) - ddt.rect_a((x, y), (w, h), colours.box_background) - ddt.text_background_colour = colours.box_background + line = tc.file_ext + ex_colour = [130, 130, 130, 255] + if line in format_colours: + ex_colour = format_colours[line] - if inp.mouse_click and not coll([x, y, w, h]): - track_box = False + # Spotify icon rendering + if line == "SPTY": + colour = [30, 215, 96, 255] + h, l, s = rgb_to_hls(colour[0], colour[1], colour[2]) - else: - art_size = int(115 * gui.scale) + rect = (x + w - round(35 * gui.scale), y + round(30 * gui.scale), round(30 * gui.scale), + round(30 * gui.scale)) + fields.add(rect) + if coll(rect): + l += 0.1 + gui.cursor_want = 3 - # if not tc.is_network: # Don't draw album art if from network location for better performance - if comment_mode == 1: - album_art_gen.display( - tc, (int(x + w - 135 * gui.scale), int(y + 105 * gui.scale)), - (art_size, art_size)) # Mirror this size in auto theme #mark2233 - else: - album_art_gen.display( - tc, (int(x + w - 135 * gui.scale), int(y + h - 135 * gui.scale)), - (art_size, art_size)) + if inp.mouse_click: + url = tc.misc.get("spotify-album-url") + if url is None: + url = tc.misc.get("spotify-track-url") + if url: + webbrowser.open(url, new=2, autoraise=True) - y -= int(24 * gui.scale) - y1 = int(y + (40 * gui.scale)) + colour = hls_to_rgb(h, l, s) - ext_rect = [x + w - round(38 * gui.scale), y + round(44 * gui.scale), round(38 * gui.scale), - round(12 * gui.scale)] + gui.spot_info_icon.render(x + w - round(33 * gui.scale), y + round(35 * gui.scale), colour) - line = tc.file_ext - ex_colour = [130, 130, 130, 255] - if line in format_colours: - ex_colour = format_colours[line] + # Codec tag rendering + else: + if tc.file_ext in ("JELY", "TIDAL"): + e_colour = [130, 130, 130, 255] + if "container" in tc.misc: + line = tc.misc["container"].upper() + if line in format_colours: + e_colour = format_colours[line] + + ddt.rect(ext_rect, e_colour) + colour = alpha_blend([10, 10, 10, 235], e_colour) + if colour_value(e_colour) < 180: + colour = alpha_blend([200, 200, 200, 235], e_colour) + ddt.text( + (int(x + w - 35 * gui.scale), round(y + (41) * gui.scale)), line, colour, 211, bg=e_colour) + ext_rect[1] += 16 * gui.scale + y += 16 * gui.scale + + ddt.rect(ext_rect, ex_colour) + colour = alpha_blend([10, 10, 10, 235], ex_colour) + if colour_value(ex_colour) < 180: + colour = alpha_blend([200, 200, 200, 235], ex_colour) + ddt.text( + (int(x + w - 35 * gui.scale), round(y + 41 * gui.scale)), tc.file_ext, colour, 211, bg=ex_colour) + + if tc.is_cue: + ext_rect[1] += 16 * gui.scale + colour = [218, 222, 73, 255] + if tc.is_embed_cue: + colour = [252, 199, 55, 255] + ddt.rect(ext_rect, colour) + ddt.text( + (int(x + w - 35 * gui.scale), int(y + (41 + 16) * gui.scale)), "CUE", + alpha_blend([10, 10, 10, 235], colour), 211, bg=colour) - # Spotify icon rendering - if line == "SPTY": - colour = [30, 215, 96, 255] - h, l, s = rgb_to_hls(colour[0], colour[1], colour[2]) - rect = (x + w - round(35 * gui.scale), y + round(30 * gui.scale), round(30 * gui.scale), - round(30 * gui.scale)) + rect = [x1, y1 + int(2 * gui.scale), 450 * gui.scale, 14 * gui.scale] fields.add(rect) if coll(rect): - l += 0.1 - gui.cursor_want = 3 + ddt.text((x1, y1), _("Title"), key_colour_on, 212) + if inp.mouse_click: + show_message(_("Copied text to clipboard")) + copy_to_clipboard(tc.title) + inp.mouse_click = False + else: + ddt.text((x1, y1), _("Title"), key_colour_off, 212) + q = ddt.text( + (x2, y1 - int(2 * gui.scale)), tc.title, + value_colour, 314, max_w=w - 170 * gui.scale) + + if coll(rect): + ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.title, 314) + + y1 += int(16 * gui.scale) + rect = [x1, y1 + (2 * gui.scale), 450 * gui.scale, 14 * gui.scale] + fields.add(rect) + if coll(rect): + ddt.text((x1, y1), _("Artist"), key_colour_on, 212) if inp.mouse_click: - url = tc.misc.get("spotify-album-url") - if url is None: - url = tc.misc.get("spotify-track-url") - if url: - webbrowser.open(url, new=2, autoraise=True) + show_message(_("Copied text to clipboard")) + copy_to_clipboard(tc.artist) + inp.mouse_click = False + else: + ddt.text((x1, y1), _("Artist"), key_colour_off, 212) - colour = hls_to_rgb(h, l, s) + q = ddt.text( + (x2, y1 - (1 * gui.scale)), tc.artist, + value_colour, value_font_a, max_w=390 * gui.scale) - gui.spot_info_icon.render(x + w - round(33 * gui.scale), y + round(35 * gui.scale), colour) + if coll(rect): + ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.artist, value_font_a) - # Codec tag rendering - else: - if tc.file_ext in ("JELY", "TIDAL"): - e_colour = [130, 130, 130, 255] - if "container" in tc.misc: - line = tc.misc["container"].upper() - if line in format_colours: - e_colour = format_colours[line] - - ddt.rect(ext_rect, e_colour) - colour = alpha_blend([10, 10, 10, 235], e_colour) - if colour_value(e_colour) < 180: - colour = alpha_blend([200, 200, 200, 235], e_colour) - ddt.text( - (int(x + w - 35 * gui.scale), round(y + (41) * gui.scale)), line, colour, 211, bg=e_colour) - ext_rect[1] += 16 * gui.scale - y += 16 * gui.scale - - ddt.rect(ext_rect, ex_colour) - colour = alpha_blend([10, 10, 10, 235], ex_colour) - if colour_value(ex_colour) < 180: - colour = alpha_blend([200, 200, 200, 235], ex_colour) - ddt.text( - (int(x + w - 35 * gui.scale), round(y + 41 * gui.scale)), tc.file_ext, colour, 211, bg=ex_colour) - - if tc.is_cue: - ext_rect[1] += 16 * gui.scale - colour = [218, 222, 73, 255] - if tc.is_embed_cue: - colour = [252, 199, 55, 255] - ddt.rect(ext_rect, colour) - ddt.text( - (int(x + w - 35 * gui.scale), int(y + (41 + 16) * gui.scale)), "CUE", - alpha_blend([10, 10, 10, 235], colour), 211, bg=colour) + y1 += int(16 * gui.scale) + rect = [x1, y1 + (2 * gui.scale), 450 * gui.scale, 14 * gui.scale] + fields.add(rect) + if coll(rect): + ddt.text((x1, y1), _("Album"), key_colour_on, 212) + if inp.mouse_click: + show_message(_("Copied text to clipboard")) + copy_to_clipboard(tc.album) + inp.mouse_click = False + else: + ddt.text((x1, y1), _("Album"), key_colour_off, 212) - rect = [x1, y1 + int(2 * gui.scale), 450 * gui.scale, 14 * gui.scale] - fields.add(rect) - if coll(rect): - ddt.text((x1, y1), _("Title"), key_colour_on, 212) - if inp.mouse_click: - show_message(_("Copied text to clipboard")) - copy_to_clipboard(tc.title) - inp.mouse_click = False - else: - ddt.text((x1, y1), _("Title"), key_colour_off, 212) - q = ddt.text( - (x2, y1 - int(2 * gui.scale)), tc.title, - value_colour, 314, max_w=w - 170 * gui.scale) - - if coll(rect): - ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.title, 314) - - y1 += int(16 * gui.scale) - - rect = [x1, y1 + (2 * gui.scale), 450 * gui.scale, 14 * gui.scale] - fields.add(rect) - if coll(rect): - ddt.text((x1, y1), _("Artist"), key_colour_on, 212) - if inp.mouse_click: - show_message(_("Copied text to clipboard")) - copy_to_clipboard(tc.artist) - inp.mouse_click = False - else: - ddt.text((x1, y1), _("Artist"), key_colour_off, 212) + q = ddt.text( + (x2, y1 - 1 * gui.scale), tc.album, + value_colour, + value_font_a, max_w=390 * gui.scale) - q = ddt.text( - (x2, y1 - (1 * gui.scale)), tc.artist, - value_colour, value_font_a, max_w=390 * gui.scale) + if coll(rect): + ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.album, value_font_a) - if coll(rect): - ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.artist, value_font_a) + y1 += int(26 * gui.scale) - y1 += int(16 * gui.scale) + rect = [x1, y1, 450 * gui.scale, 16 * gui.scale] + fields.add(rect) + path = tc.fullpath + if msys: + path = path.replace("/", "\\") + if coll(rect): + ddt.text((x1, y1), _("Path"), key_colour_on, 212) + if inp.mouse_click: + show_message(_("Copied text to clipboard")) + copy_to_clipboard(path) + inp.mouse_click = False + else: + ddt.text((x1, y1), _("Path"), key_colour_off, 212) - rect = [x1, y1 + (2 * gui.scale), 450 * gui.scale, 14 * gui.scale] - fields.add(rect) - if coll(rect): - ddt.text((x1, y1), _("Album"), key_colour_on, 212) - if inp.mouse_click: - show_message(_("Copied text to clipboard")) - copy_to_clipboard(tc.album) - inp.mouse_click = False - else: - ddt.text((x1, y1), _("Album"), key_colour_off, 212) - - q = ddt.text( - (x2, y1 - 1 * gui.scale), tc.album, - value_colour, - value_font_a, max_w=390 * gui.scale) - - if coll(rect): - ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.album, value_font_a) - - y1 += int(26 * gui.scale) - - rect = [x1, y1, 450 * gui.scale, 16 * gui.scale] - fields.add(rect) - path = tc.fullpath - if msys: - path = path.replace("/", "\\") - if coll(rect): - ddt.text((x1, y1), _("Path"), key_colour_on, 212) - if inp.mouse_click: - show_message(_("Copied text to clipboard")) - copy_to_clipboard(path) - inp.mouse_click = False - else: - ddt.text((x1, y1), _("Path"), key_colour_off, 212) + q = ddt.text( + (x2, y1 - int(3 * gui.scale)), clean_string(path), + path_colour, 210, max_w=425 * gui.scale) - q = ddt.text( - (x2, y1 - int(3 * gui.scale)), clean_string(path), - path_colour, 210, max_w=425 * gui.scale) + if coll(rect): + gui.frame_callback_list.append(TestTimer(0.71)) + if track_box_path_tool_timer.get() > 0.7: + ex_tool_tip(x2 + 185 * gui.scale, y1, q, clean_string(tc.fullpath), 210) + else: + track_box_path_tool_timer.set() - if coll(rect): - gui.frame_callback_list.append(TestTimer(0.71)) - if track_box_path_tool_timer.get() > 0.7: - ex_tool_tip(x2 + 185 * gui.scale, y1, q, clean_string(tc.fullpath), 210) - else: - track_box_path_tool_timer.set() + y1 += int(15 * gui.scale) + + if tc.samplerate != 0: + ddt.text((x1, y1), _("Samplerate"), key_colour_off, 212, max_w=70 * gui.scale) - y1 += int(15 * gui.scale) + line = str(tc.samplerate) + " Hz" - if tc.samplerate != 0: - ddt.text((x1, y1), _("Samplerate"), key_colour_off, 212, max_w=70 * gui.scale) + off = ddt.text((x2, y1), line, value_colour, value_font) - line = str(tc.samplerate) + " Hz" + if tc.bit_depth > 0: + line = str(tc.bit_depth) + " bit" + ddt.text((x2 + off + 9 * gui.scale, y1), line, value_colour, 311) - off = ddt.text((x2, y1), line, value_colour, value_font) + y1 += int(15 * gui.scale) - if tc.bit_depth > 0: - line = str(tc.bit_depth) + " bit" - ddt.text((x2 + off + 9 * gui.scale, y1), line, value_colour, 311) + if tc.bitrate not in (0, "", "0"): + ddt.text((x1, y1), _("Bitrate"), key_colour_off, 212, max_w=70 * gui.scale) + line = str(tc.bitrate) + if tc.file_ext in ("FLAC", "OPUS", "APE", "WV"): + line = "≈" + line + line += _(" kbps") + ddt.text((x2, y1), line, value_colour, 312) - y1 += int(15 * gui.scale) + # ----------- + if tc.artist != tc.album_artist != "": + x += int(170 * gui.scale) + rect = [x + 7 * gui.scale, y1 + (2 * gui.scale), 220 * gui.scale, 14 * gui.scale] + fields.add(rect) + if coll(rect): + ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Album Artist"), key_colour_on, 212) + if inp.mouse_click: + show_message(_("Copied text to clipboard")) + copy_to_clipboard(tc.album_artist) + inp.mouse_click = False + else: + ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Album Artist"), key_colour_off, 212) + + q = ddt.text( + (x + (8 + 88) * gui.scale, y1), tc.album_artist, + value_colour, value_font, max_w=120 * gui.scale) + if coll(rect): + ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.album_artist, value_font) - if tc.bitrate not in (0, "", "0"): - ddt.text((x1, y1), _("Bitrate"), key_colour_off, 212, max_w=70 * gui.scale) - line = str(tc.bitrate) - if tc.file_ext in ("FLAC", "OPUS", "APE", "WV"): - line = "≈" + line - line += _(" kbps") - ddt.text((x2, y1), line, value_colour, 312) + x -= int(170 * gui.scale) - # ----------- - if tc.artist != tc.album_artist != "": - x += int(170 * gui.scale) - rect = [x + 7 * gui.scale, y1 + (2 * gui.scale), 220 * gui.scale, 14 * gui.scale] + y1 += int(15 * gui.scale) + + rect = [x1, y1, 150 * gui.scale, 16 * gui.scale] fields.add(rect) if coll(rect): - ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Album Artist"), key_colour_on, 212) + ddt.text((x1, y1), _("Duration"), key_colour_on, 212) if inp.mouse_click: + copy_to_clipboard(time.strftime("%M:%S", time.gmtime(tc.length)).lstrip("0")) show_message(_("Copied text to clipboard")) - copy_to_clipboard(tc.album_artist) inp.mouse_click = False else: - ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Album Artist"), key_colour_off, 212) + ddt.text((x1, y1), _("Duration"), key_colour_off, 212) + line = time.strftime("%M:%S", time.gmtime(tc.length)) + ddt.text((x2, y1), line, value_colour, value_font) - q = ddt.text( - (x + (8 + 88) * gui.scale, y1), tc.album_artist, - value_colour, value_font, max_w=120 * gui.scale) + # ----------- + if tc.track_total not in ("", "0"): + x += int(170 * gui.scale) + line = str(tc.track_number) + _(" of ") + str( + tc.track_total) + ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Track"), key_colour_off, 212) + ddt.text((x + (8 + 88) * gui.scale, y1), line, value_colour, value_font) + x -= int(170 * gui.scale) + + y1 += int(15 * gui.scale) + #logging.info(tc.size) + if tc.is_cue and tc.misc.get("parent-length", 0) > 0 and tc.misc.get("parent-size", 0) > 0: + ddt.text((x1, y1), _("File size"), key_colour_off, 212, max_w=70 * gui.scale) + estimate = (tc.length / tc.misc.get("parent-length")) * tc.misc.get("parent-size") + line = f"≈{get_filesize_string(estimate, rounding=0)} / {get_filesize_string(tc.misc.get('parent-size'))}" + ddt.text((x2, y1), line, value_colour, value_font) + + elif tc.size != 0: + ddt.text((x1, y1), _("File size"), key_colour_off, 212, max_w=70 * gui.scale) + ddt.text((x2, y1), get_filesize_string(tc.size), value_colour, value_font) + + # ----------- + if tc.disc_total not in ("", "0", 0): + x += int(170 * gui.scale) + line = str(tc.disc_number) + _(" of ") + str( + tc.disc_total) + ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Disc"), key_colour_off, 212) + ddt.text((x + (8 + 88) * gui.scale, y1), line, value_colour, value_font) + x -= int(170 * gui.scale) + + y1 += int(23 * gui.scale) + + rect = [x1, y1 + (2 * gui.scale), 150 * gui.scale, 14 * gui.scale] + fields.add(rect) if coll(rect): - ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.album_artist, value_font) - - x -= int(170 * gui.scale) - - y1 += int(15 * gui.scale) - - rect = [x1, y1, 150 * gui.scale, 16 * gui.scale] - fields.add(rect) - if coll(rect): - ddt.text((x1, y1), _("Duration"), key_colour_on, 212) - if inp.mouse_click: - copy_to_clipboard(time.strftime("%M:%S", time.gmtime(tc.length)).lstrip("0")) - show_message(_("Copied text to clipboard")) - inp.mouse_click = False - else: - ddt.text((x1, y1), _("Duration"), key_colour_off, 212) - line = time.strftime("%M:%S", time.gmtime(tc.length)) - ddt.text((x2, y1), line, value_colour, value_font) - - # ----------- - if tc.track_total not in ("", "0"): - x += int(170 * gui.scale) - line = str(tc.track_number) + _(" of ") + str( - tc.track_total) - ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Track"), key_colour_off, 212) - ddt.text((x + (8 + 88) * gui.scale, y1), line, value_colour, value_font) - x -= int(170 * gui.scale) - - y1 += int(15 * gui.scale) - #logging.info(tc.size) - if tc.is_cue and tc.misc.get("parent-length", 0) > 0 and tc.misc.get("parent-size", 0) > 0: - ddt.text((x1, y1), _("File size"), key_colour_off, 212, max_w=70 * gui.scale) - estimate = (tc.length / tc.misc.get("parent-length")) * tc.misc.get("parent-size") - line = f"≈{get_filesize_string(estimate, rounding=0)} / {get_filesize_string(tc.misc.get('parent-size'))}" - ddt.text((x2, y1), line, value_colour, value_font) + ddt.text((x1, y1), _("Genre"), key_colour_on, 212) + if inp.mouse_click: + show_message(_("Copied text to clipboard")) + copy_to_clipboard(tc.genre) + inp.mouse_click = False + else: + ddt.text((x1, y1), _("Genre"), key_colour_off, 212) + ddt.text( + (x2, y1), tc.genre, value_colour, + value_font, max_w=290 * gui.scale) - elif tc.size != 0: - ddt.text((x1, y1), _("File size"), key_colour_off, 212, max_w=70 * gui.scale) - ddt.text((x2, y1), get_filesize_string(tc.size), value_colour, value_font) - - # ----------- - if tc.disc_total not in ("", "0", 0): - x += int(170 * gui.scale) - line = str(tc.disc_number) + _(" of ") + str( - tc.disc_total) - ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Disc"), key_colour_off, 212) - ddt.text((x + (8 + 88) * gui.scale, y1), line, value_colour, value_font) - x -= int(170 * gui.scale) - - y1 += int(23 * gui.scale) - - rect = [x1, y1 + (2 * gui.scale), 150 * gui.scale, 14 * gui.scale] - fields.add(rect) - if coll(rect): - ddt.text((x1, y1), _("Genre"), key_colour_on, 212) - if inp.mouse_click: - show_message(_("Copied text to clipboard")) - copy_to_clipboard(tc.genre) - inp.mouse_click = False - else: - ddt.text((x1, y1), _("Genre"), key_colour_off, 212) - ddt.text( - (x2, y1), tc.genre, value_colour, - value_font, max_w=290 * gui.scale) - - y1 += int(15 * gui.scale) - - rect = [x1, y1 + (2 * gui.scale), 150 * gui.scale, 14 * gui.scale] - fields.add(rect) - if coll(rect): - ddt.text((x1, y1), _("Date"), key_colour_on, 212) - if inp.mouse_click: - show_message(_("Copied text to clipboard")) - copy_to_clipboard(tc.date) - inp.mouse_click = False - else: - ddt.text((x1, y1), _("Date"), key_colour_off, 212) - ddt.text((x2, y1), d_date_display(tc), value_colour, value_font) + y1 += int(15 * gui.scale) - if tc.composer and tc.composer != tc.artist: - x += int(170 * gui.scale) - rect = [x + 7 * gui.scale, y1 + (2 * gui.scale), 220 * gui.scale, 14 * gui.scale] + rect = [x1, y1 + (2 * gui.scale), 150 * gui.scale, 14 * gui.scale] fields.add(rect) if coll(rect): - ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Composer"), key_colour_on, 212) + ddt.text((x1, y1), _("Date"), key_colour_on, 212) if inp.mouse_click: show_message(_("Copied text to clipboard")) - copy_to_clipboard(tc.album_artist) + copy_to_clipboard(tc.date) inp.mouse_click = False else: - ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Composer"), key_colour_off, 212) - q = ddt.text( - (x + (8 + 88) * gui.scale, y1), tc.composer, - value_colour, value_font, max_w=120 * gui.scale) - if coll(rect): - ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.composer, value_font_a) + ddt.text((x1, y1), _("Date"), key_colour_off, 212) + ddt.text((x2, y1), d_date_display(tc), value_colour, value_font) - x -= int(170 * gui.scale) + if tc.composer and tc.composer != tc.artist: + x += int(170 * gui.scale) + rect = [x + 7 * gui.scale, y1 + (2 * gui.scale), 220 * gui.scale, 14 * gui.scale] + fields.add(rect) + if coll(rect): + ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Composer"), key_colour_on, 212) + if inp.mouse_click: + show_message(_("Copied text to clipboard")) + copy_to_clipboard(tc.album_artist) + inp.mouse_click = False + else: + ddt.text((x + (8 + 75) * gui.scale, y1, 1), _("Composer"), key_colour_off, 212) + q = ddt.text( + (x + (8 + 88) * gui.scale, y1), tc.composer, + value_colour, value_font, max_w=120 * gui.scale) + if coll(rect): + ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.composer, value_font_a) - y1 += int(23 * gui.scale) + x -= int(170 * gui.scale) - total = star_store.get(r_menu_index) + y1 += int(23 * gui.scale) - ratio = 0 + total = star_store.get(r_menu_index) - if total > 0 and pctl.master_library[ - r_menu_index].length > 1: - ratio = total / (tc.length - 1) + ratio = 0 - ddt.text((x1, y1), _("Play count"), key_colour_off, 212, max_w=70 * gui.scale) - ddt.text((x2, y1), str(int(ratio)), value_colour, value_font) + if total > 0 and pctl.master_library[ + r_menu_index].length > 1: + ratio = total / (tc.length - 1) - y1 += int(15 * gui.scale) + ddt.text((x1, y1), _("Play count"), key_colour_off, 212, max_w=70 * gui.scale) + ddt.text((x2, y1), str(int(ratio)), value_colour, value_font) - rect = [x1, y1, 150, 14] + y1 += int(15 * gui.scale) - if coll(rect) and key_shift_down and mouse_wheel != 0: - star_store.add(r_menu_index, 60 * mouse_wheel) + rect = [x1, y1, 150, 14] - line = time.strftime("%H:%M:%S", time.gmtime(total)) + if coll(rect) and key_shift_down and mouse_wheel != 0: + star_store.add(r_menu_index, 60 * mouse_wheel) - ddt.text((x1, y1), _("Play time"), key_colour_off, 212, max_w=70 * gui.scale) - ddt.text((x2, y1), str(line), value_colour, value_font) + line = time.strftime("%H:%M:%S", time.gmtime(total)) - # ------- - if tc.lyrics != "": + ddt.text((x1, y1), _("Play time"), key_colour_off, 212, max_w=70 * gui.scale) + ddt.text((x2, y1), str(line), value_colour, value_font) - if draw.button(_("Lyrics"), x1 + 200 * gui.scale, y1 - 10 * gui.scale): - prefs.show_lyrics_showcase = True - track_box = False - enter_showcase_view(track_id=r_menu_index) - inp.mouse_click = False + # ------- + if tc.lyrics != "": - if len(tc.comment) > 0: - y1 += 20 * gui.scale - rect = [x1, y1 + (2 * gui.scale), 60 * gui.scale, 14 * gui.scale] - # ddt.rect_r((x2, y1, 335, 10), [255, 20, 20, 255]) - fields.add(rect) - if coll(rect): - ddt.text((x1, y1), _("Comment"), key_colour_on, 212) - if inp.mouse_click: - show_message(_("Copied text to clipboard")) - copy_to_clipboard(tc.comment) + if draw.button(_("Lyrics"), x1 + 200 * gui.scale, y1 - 10 * gui.scale): + prefs.show_lyrics_showcase = True + track_box = False + enter_showcase_view(track_id=r_menu_index) inp.mouse_click = False - else: - ddt.text((x1, y1), _("Comment"), key_colour_off, 212) - # ddt.draw_text((x1, y1), "Comment", key_colour_off, 12) - - if "\n" not in tc.comment and ( - "http://" in tc.comment or "www." in tc.comment or "https://" in tc.comment) and ddt.get_text_w( - tc.comment, 12) < 335 * gui.scale: - link_pa = draw_linked_text((x2, y1), tc.comment, value_colour, 12) - link_rect = [x + 98 * gui.scale + link_pa[0], y1 - 2 * gui.scale, link_pa[1], 20 * gui.scale] - - fields.add(link_rect) - if coll(link_rect): - if not inp.mouse_click: - gui.cursor_want = 3 + if len(tc.comment) > 0: + y1 += 20 * gui.scale + rect = [x1, y1 + (2 * gui.scale), 60 * gui.scale, 14 * gui.scale] + # ddt.rect_r((x2, y1, 335, 10), [255, 20, 20, 255]) + fields.add(rect) + if coll(rect): + ddt.text((x1, y1), _("Comment"), key_colour_on, 212) if inp.mouse_click: - webbrowser.open(link_pa[2], new=2, autoraise=True) - track_box = True + show_message(_("Copied text to clipboard")) + copy_to_clipboard(tc.comment) + inp.mouse_click = False + else: + ddt.text((x1, y1), _("Comment"), key_colour_off, 212) + # ddt.draw_text((x1, y1), "Comment", key_colour_off, 12) - elif comment_mode == 1: - ddt.text( - (x + 18 * gui.scale, y1 + 18 * gui.scale, 4, w - 36 * gui.scale, 90 * gui.scale), - tc.comment, value_colour, 12) - else: - ddt.text((x2, y1), tc.comment, value_colour, 12) + if "\n" not in tc.comment and ( + "http://" in tc.comment or "www." in tc.comment or "https://" in tc.comment) and ddt.get_text_w( + tc.comment, 12) < 335 * gui.scale: - if draw_border and gui.mode != 3: + link_pa = draw_linked_text((x2, y1), tc.comment, value_colour, 12) + link_rect = [x + 98 * gui.scale + link_pa[0], y1 - 2 * gui.scale, link_pa[1], 20 * gui.scale] - tool_rect = [window_size[0] - 110 * gui.scale, 2, 95 * gui.scale, 45 * gui.scale] - if prefs.left_window_control: - tool_rect[0] = 0 - fields.add(tool_rect) - if not gui.top_bar_mode2 or coll(tool_rect): - draw_window_tools() + fields.add(link_rect) + if coll(link_rect): + if not inp.mouse_click: + gui.cursor_want = 3 + if inp.mouse_click: + webbrowser.open(link_pa[2], new=2, autoraise=True) + track_box = True - if not gui.fullscreen and not gui.maximized: - draw_window_border() + elif comment_mode == 1: + ddt.text( + (x + 18 * gui.scale, y1 + 18 * gui.scale, 4, w - 36 * gui.scale, 90 * gui.scale), + tc.comment, value_colour, 12) + else: + ddt.text((x2, y1), tc.comment, value_colour, 12) - fader.render() - if pref_box.enabled: - # rect = [0, 0, window_size[0], window_size[1]] - # ddt.rect_r(rect, [0, 0, 0, 90], True) - pref_box.render() + if draw_border and gui.mode != 3: - if gui.rename_folder_box: + tool_rect = [window_size[0] - 110 * gui.scale, 2, 95 * gui.scale, 45 * gui.scale] + if prefs.left_window_control: + tool_rect[0] = 0 + fields.add(tool_rect) + if not gui.top_bar_mode2 or coll(tool_rect): + draw_window_tools() - if gui.level_2_click: - inp.mouse_click = True + if not gui.fullscreen and not gui.maximized: + draw_window_border() - gui.level_2_click = False + fader.render() + if pref_box.enabled: + # rect = [0, 0, window_size[0], window_size[1]] + # ddt.rect_r(rect, [0, 0, 0, 90], True) + pref_box.render() - w = 500 * gui.scale - h = 127 * gui.scale - x = int(window_size[0] / 2) - int(w / 2) - y = int(window_size[1] / 2) - int(h / 2) + if gui.rename_folder_box: - ddt.rect_a( - (x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) - ddt.rect_a((x, y), (w, h), colours.box_background) + if gui.level_2_click: + inp.mouse_click = True - ddt.text_background_colour = colours.box_background + gui.level_2_click = False - if key_esc_press or ( - (inp.mouse_click or right_click or level_2_right_click) and not coll((x, y, w, h))): - gui.rename_folder_box = False + w = 500 * gui.scale + h = 127 * gui.scale + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) - p = ddt.text( - (x + 10 * gui.scale, y + 9 * gui.scale), _("Folder Modification"), colours.box_title_text, 213) + ddt.rect_a( + (x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) + ddt.rect_a((x, y), (w, h), colours.box_background) - if rename_folder.text != prefs.rename_folder_template and draw.button( - _("Default"), - x + (300 - 63) * gui.scale, - y + 11 * gui.scale, - 70 * gui.scale): - rename_folder.text = prefs.rename_folder_template + ddt.text_background_colour = colours.box_background - rename_folder.draw(x + 14 * gui.scale, y + 41 * gui.scale, colours.box_input_text, width=300) + if key_esc_press or ( + (inp.mouse_click or right_click or level_2_right_click) and not coll((x, y, w, h))): + gui.rename_folder_box = False - ddt.rect_s( - (x + 8 * gui.scale, y + 38 * gui.scale, 300 * gui.scale, 22 * gui.scale), - colours.box_text_border, 1 * gui.scale) + p = ddt.text( + (x + 10 * gui.scale, y + 9 * gui.scale), _("Folder Modification"), colours.box_title_text, 213) - if draw.button( - _("Rename"), x + (8 + 300 + 10) * gui.scale, y + 38 * gui.scale, 80 * gui.scale, - tooltip=_("Renames the physical folder based on the template")) or inp.level_2_enter: - rename_parent(rename_index, rename_folder.text) - gui.rename_folder_box = False - inp.mouse_click = False + if rename_folder.text != prefs.rename_folder_template and draw.button( + _("Default"), + x + (300 - 63) * gui.scale, + y + 11 * gui.scale, + 70 * gui.scale): + rename_folder.text = prefs.rename_folder_template - text = _("Trash") - tt = _("Moves folder to system trash") - if key_shift_down: - text = _("Delete") - tt = _("Physically deletes folder from disk") - if draw.button( - text, x + (8 + 300 + 10) * gui.scale, y + 11 * gui.scale, 80 * gui.scale, - text_highlight_colour=colours.grey(255), background_highlight_colour=[180, 60, 60, 255], - press=mouse_up, tooltip=tt): - if key_shift_down: - delete_folder(rename_index, True) - else: - delete_folder(rename_index) - gui.rename_folder_box = False - inp.mouse_click = False + rename_folder.draw(x + 14 * gui.scale, y + 41 * gui.scale, colours.box_input_text, width=300) + + ddt.rect_s( + (x + 8 * gui.scale, y + 38 * gui.scale, 300 * gui.scale, 22 * gui.scale), + colours.box_text_border, 1 * gui.scale) - if move_folder_up(rename_index): if draw.button( - _("Raise"), x + 408 * gui.scale, y + 38 * gui.scale, 80 * gui.scale, - tooltip=_("Moves folder up 2 levels and deletes the old container folder")): - move_folder_up(rename_index, True) + _("Rename"), x + (8 + 300 + 10) * gui.scale, y + 38 * gui.scale, 80 * gui.scale, + tooltip=_("Renames the physical folder based on the template")) or inp.level_2_enter: + rename_parent(rename_index, rename_folder.text) + gui.rename_folder_box = False inp.mouse_click = False - to_clean = clean_folder(rename_index) - if to_clean > 0: + text = _("Trash") + tt = _("Moves folder to system trash") + if key_shift_down: + text = _("Delete") + tt = _("Physically deletes folder from disk") if draw.button( - "Clean (" + str(to_clean) + ")", x + 408 * gui.scale, y + 11 * gui.scale, - 80 * gui.scale, tooltip=_("Deletes some unnecessary files from folder")): - clean_folder(rename_index, True) + text, x + (8 + 300 + 10) * gui.scale, y + 11 * gui.scale, 80 * gui.scale, + text_highlight_colour=colours.grey(255), background_highlight_colour=[180, 60, 60, 255], + press=mouse_up, tooltip=tt): + if key_shift_down: + delete_folder(rename_index, True) + else: + delete_folder(rename_index) + gui.rename_folder_box = False inp.mouse_click = False - ddt.text((x + 10 * gui.scale, y + 65 * gui.scale), _("PATH"), colours.box_text_label, 212) - line = os.path.dirname( - pctl.master_library[rename_index].parent_folder_path.rstrip("\\/")).replace("\\","/") + "/" - line = right_trunc(line, 12, 420 * gui.scale) - line = clean_string(line) - ddt.text((x + 60 * gui.scale, y + 65 * gui.scale), line, colours.grey(220), 211) + if move_folder_up(rename_index): + if draw.button( + _("Raise"), x + 408 * gui.scale, y + 38 * gui.scale, 80 * gui.scale, + tooltip=_("Moves folder up 2 levels and deletes the old container folder")): + move_folder_up(rename_index, True) + inp.mouse_click = False + + to_clean = clean_folder(rename_index) + if to_clean > 0: + if draw.button( + "Clean (" + str(to_clean) + ")", x + 408 * gui.scale, y + 11 * gui.scale, + 80 * gui.scale, tooltip=_("Deletes some unnecessary files from folder")): + clean_folder(rename_index, True) + inp.mouse_click = False - ddt.text((x + 10 * gui.scale, y + 83 * gui.scale), _("OLD"), colours.box_text_label, 212) - line = pctl.master_library[rename_index].parent_folder_name - line = clean_string(line) - ddt.text((x + 60 * gui.scale, y + 83 * gui.scale), line, colours.grey(220), 211, max_w=420 * gui.scale) + ddt.text((x + 10 * gui.scale, y + 65 * gui.scale), _("PATH"), colours.box_text_label, 212) + line = os.path.dirname( + pctl.master_library[rename_index].parent_folder_path.rstrip("\\/")).replace("\\","/") + "/" + line = right_trunc(line, 12, 420 * gui.scale) + line = clean_string(line) + ddt.text((x + 60 * gui.scale, y + 65 * gui.scale), line, colours.grey(220), 211) - ddt.text((x + 10 * gui.scale, y + 101 * gui.scale), _("NEW"), colours.box_text_label, 212) - line = parse_template2(rename_folder.text, pctl.master_library[rename_index]) - ddt.text((x + 60 * gui.scale, y + 101 * gui.scale), line, colours.grey(220), 211, max_w=420 * gui.scale) + ddt.text((x + 10 * gui.scale, y + 83 * gui.scale), _("OLD"), colours.box_text_label, 212) + line = pctl.master_library[rename_index].parent_folder_name + line = clean_string(line) + ddt.text((x + 60 * gui.scale, y + 83 * gui.scale), line, colours.grey(220), 211, max_w=420 * gui.scale) - if rename_track_box.active: - rename_track_box.render() + ddt.text((x + 10 * gui.scale, y + 101 * gui.scale), _("NEW"), colours.box_text_label, 212) + line = parse_template2(rename_folder.text, pctl.master_library[rename_index]) + ddt.text((x + 60 * gui.scale, y + 101 * gui.scale), line, colours.grey(220), 211, max_w=420 * gui.scale) - if sub_lyrics_box.active: - sub_lyrics_box.render() + if rename_track_box.active: + rename_track_box.render() - if export_playlist_box.active: - export_playlist_box.render() + if sub_lyrics_box.active: + sub_lyrics_box.render() - if trans_edit_box.active: - trans_edit_box.render() + if export_playlist_box.active: + export_playlist_box.render() - if radiobox.active: - radiobox.render() + if trans_edit_box.active: + trans_edit_box.render() - if gui.message_box: - message_box.render() + if radiobox.active: + radiobox.render() - if prefs.show_nag: - nagbox.draw() + if gui.message_box: + message_box.render() - # SEARCH - # if key_ctrl_down and key_v_press: + if prefs.show_nag: + nagbox.draw() - # search_over.active = True + # SEARCH + # if key_ctrl_down and key_v_press: - search_over.render() + # search_over.active = True - if keymaps.test("quick-find") and quick_search_mode is False: - if not search_over.active and not gui.box_over: - quick_search_mode = True - if search_clear_timer.get() > 3: - search_text.text = "" - input_text = "" - elif (keymaps.test("quick-find") or ( - key_esc_press and len(editline) == 0)) or (inp.mouse_click and quick_search_mode is True): - quick_search_mode = False - search_text.text = "" - - # if (key_backslash_press or (key_ctrl_down and key_f_press)) and quick_search_mode is False: - # if not search_over.active: - # quick_search_mode = True - # if search_clear_timer.get() > 3: - # search_text.text = "" - # input_text = "" - # elif ((key_backslash_press or (key_ctrl_down and key_f_press)) or ( - # key_esc_press and len(editline) == 0)) or input.mouse_click and quick_search_mode is True: - # quick_search_mode = False - # search_text.text = "" - - if quick_search_mode is True: - - rect2 = [0, window_size[1] - 85 * gui.scale, 420 * gui.scale, 25 * gui.scale] - rect = [0, window_size[1] - 125 * gui.scale, 420 * gui.scale, 65 * gui.scale] - rect[0] = int(window_size[0] / 2) - int(rect[2] / 2) - rect2[0] = rect[0] - - ddt.rect((rect[0] - 2, rect[1] - 2, rect[2] + 4, rect[3] + 4), colours.box_border) # [220, 100, 5, 255] - # ddt.rect_r((rect[0], rect[1], rect[2], rect[3]), [255,120,5,255], True) - - ddt.text_background_colour = colours.box_background - # ddt.text_background_colour = [255,120,5,255] - # ddt.text_background_colour = [220,100,5,255] - ddt.rect(rect, colours.box_background) - - if len(input_text) > 0: - search_index = -1 - - if inp.backspace_press and search_text.text == "": + search_over.render() + + if keymaps.test("quick-find") and quick_search_mode is False: + if not search_over.active and not gui.box_over: + quick_search_mode = True + if search_clear_timer.get() > 3: + search_text.text = "" + input_text = "" + elif (keymaps.test("quick-find") or ( + key_esc_press and len(editline) == 0)) or (inp.mouse_click and quick_search_mode is True): quick_search_mode = False + search_text.text = "" - if len(search_text.text) == 0: - gui.search_error = False + # if (key_backslash_press or (key_ctrl_down and key_f_press)) and quick_search_mode is False: + # if not search_over.active: + # quick_search_mode = True + # if search_clear_timer.get() > 3: + # search_text.text = "" + # input_text = "" + # elif ((key_backslash_press or (key_ctrl_down and key_f_press)) or ( + # key_esc_press and len(editline) == 0)) or input.mouse_click and quick_search_mode is True: + # quick_search_mode = False + # search_text.text = "" - if len(search_text.text) != 0 and search_text.text[0] == "/": - # if "/love" in search_text.text: - # line = "last.fm loved tracks from user. Format: /love <username>" - # else: - line = _("Folder filter mode. Enter path segment.") - ddt.text((rect[0] + 23 * gui.scale, window_size[1] - 87 * gui.scale), line, (220, 220, 220, 100), 312) - else: - line = _("UP / DOWN to navigate. SHIFT + RETURN for new playlist.") - if len(search_text.text) == 0: - line = _("Quick find") - ddt.text((rect[0] + int(rect[2] / 2), window_size[1] - 87 * gui.scale, 2), line, colours.box_text_label, 312) + if quick_search_mode is True: - # ddt.draw_text((rect[0] + int(rect[2] / 2), window_size[1] - 118 * gui.scale, 2), "Find", - # colours.grey(90), 214) + rect2 = [0, window_size[1] - 85 * gui.scale, 420 * gui.scale, 25 * gui.scale] + rect = [0, window_size[1] - 125 * gui.scale, 420 * gui.scale, 65 * gui.scale] + rect[0] = int(window_size[0] / 2) - int(rect[2] / 2) + rect2[0] = rect[0] - # if len(pctl.track_queue) > 0: + ddt.rect((rect[0] - 2, rect[1] - 2, rect[2] + 4, rect[3] + 4), colours.box_border) # [220, 100, 5, 255] + # ddt.rect_r((rect[0], rect[1], rect[2], rect[3]), [255,120,5,255], True) - # if input_text == 'A': - # search_text.text = pctl.playing_object().artist - # input_text = "" + ddt.text_background_colour = colours.box_background + # ddt.text_background_colour = [255,120,5,255] + # ddt.text_background_colour = [220,100,5,255] + ddt.rect(rect, colours.box_background) + + if len(input_text) > 0: + search_index = -1 + + if inp.backspace_press and search_text.text == "": + quick_search_mode = False + + if len(search_text.text) == 0: + gui.search_error = False + + if len(search_text.text) != 0 and search_text.text[0] == "/": + # if "/love" in search_text.text: + # line = "last.fm loved tracks from user. Format: /love <username>" + # else: + line = _("Folder filter mode. Enter path segment.") + ddt.text((rect[0] + 23 * gui.scale, window_size[1] - 87 * gui.scale), line, (220, 220, 220, 100), 312) + else: + line = _("UP / DOWN to navigate. SHIFT + RETURN for new playlist.") + if len(search_text.text) == 0: + line = _("Quick find") + ddt.text((rect[0] + int(rect[2] / 2), window_size[1] - 87 * gui.scale, 2), line, colours.box_text_label, 312) + + # ddt.draw_text((rect[0] + int(rect[2] / 2), window_size[1] - 118 * gui.scale, 2), "Find", + # colours.grey(90), 214) + + # if len(pctl.track_queue) > 0: + + # if input_text == 'A': + # search_text.text = pctl.playing_object().artist + # input_text = "" + + if gui.search_error: + ddt.rect([rect[0], rect[1], rect[2], 30 * gui.scale], [180, 40, 40, 255]) + ddt.text_background_colour = [180, 40, 40, 255] # alpha_blend([255,0,0,25], ddt.text_background_colour) + # if input.backspace_press: + # gui.search_error = False + + search_text.draw(rect[0] + 8 * gui.scale, rect[1] + 6 * gui.scale, colours.grey(250), font=213) + + if (key_shift_down or ( + len(search_text.text) > 0 and search_text.text[0] == "/")) and inp.key_return_press: + inp.key_return_press = False + playlist = [] + if len(search_text.text) > 0: + if search_text.text[0] == "/": + + if search_text.text.lower() == "/random" or search_text.text.lower() == "/shuffle": + gen_500_random(pctl.active_playlist_viewing) + elif search_text.text.lower() == "/top" or search_text.text.lower() == "/most": + gen_top_100(pctl.active_playlist_viewing) + elif search_text.text.lower() == "/length" or search_text.text.lower() == "/duration" \ + or search_text.text.lower() == "/len": + gen_sort_len(pctl.active_playlist_viewing) + else: + + if search_text.text[-1] == "/": + tt_title = search_text.text.replace("/", "") + else: + search_text.text = search_text.text.replace("/", "") + tt_title = search_text.text + search_text.text = search_text.text.lower() + for item in default_playlist: + if search_text.text in pctl.master_library[item].parent_folder_path.lower(): + playlist.append(item) + if len(playlist) > 0: + pctl.multi_playlist.append(pl_gen(title=tt_title, playlist_ids=copy.deepcopy(playlist))) + switch_playlist(len(pctl.multi_playlist) - 1) - if gui.search_error: - ddt.rect([rect[0], rect[1], rect[2], 30 * gui.scale], [180, 40, 40, 255]) - ddt.text_background_colour = [180, 40, 40, 255] # alpha_blend([255,0,0,25], ddt.text_background_colour) - # if input.backspace_press: - # gui.search_error = False - - search_text.draw(rect[0] + 8 * gui.scale, rect[1] + 6 * gui.scale, colours.grey(250), font=213) - - if (key_shift_down or ( - len(search_text.text) > 0 and search_text.text[0] == "/")) and inp.key_return_press: - inp.key_return_press = False - playlist = [] - if len(search_text.text) > 0: - if search_text.text[0] == "/": - - if search_text.text.lower() == "/random" or search_text.text.lower() == "/shuffle": - gen_500_random(pctl.active_playlist_viewing) - elif search_text.text.lower() == "/top" or search_text.text.lower() == "/most": - gen_top_100(pctl.active_playlist_viewing) - elif search_text.text.lower() == "/length" or search_text.text.lower() == "/duration" \ - or search_text.text.lower() == "/len": - gen_sort_len(pctl.active_playlist_viewing) else: - - if search_text.text[-1] == "/": - tt_title = search_text.text.replace("/", "") - else: - search_text.text = search_text.text.replace("/", "") - tt_title = search_text.text - search_text.text = search_text.text.lower() + search_terms = search_text.text.lower().split() for item in default_playlist: - if search_text.text in pctl.master_library[item].parent_folder_path.lower(): + tr = pctl.get_track(item) + line = " ".join( + [ + tr.title, tr.artist, tr.album, tr.fullpath, + tr.composer, tr.comment, tr.album_artist, tr.misc.get("artist_sort", "")]).lower() + + # if prefs.diacritic_search and all([ord(c) < 128 for c in search_text.text]): + # line = str(unidecode(line)) + + if all(word in line for word in search_terms): playlist.append(item) if len(playlist) > 0: - pctl.multi_playlist.append(pl_gen(title=tt_title, playlist_ids=copy.deepcopy(playlist))) + pctl.multi_playlist.append(pl_gen( + title=_("Search Results"), + playlist_ids=copy.deepcopy(playlist))) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[ + pctl.active_playlist_viewing].title + "\" f\"" + search_text.text + "\"" switch_playlist(len(pctl.multi_playlist) - 1) + search_text.text = "" + quick_search_mode = False - else: - search_terms = search_text.text.lower().split() - for item in default_playlist: - tr = pctl.get_track(item) + if (len(input_text) > 0 and not gui.search_error) or key_down_press is True or inp.backspace_press \ + or gui.force_search: + + gui.pl_update = 1 + + if gui.force_search: + search_index = 0 + + if inp.backspace_press: + search_index = 0 + + if len(search_text.text) > 0 and search_text.text[0] != "/": + oi = search_index + + while search_index < len(default_playlist) - 1: + search_index += 1 + if search_index > len(default_playlist) - 1: + search_index = 0 + + search_terms = search_text.text.lower().split() + tr = pctl.get_track(default_playlist[search_index]) line = " ".join( - [ - tr.title, tr.artist, tr.album, tr.fullpath, - tr.composer, tr.comment, tr.album_artist, tr.misc.get("artist_sort", "")]).lower() + [tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, + tr.album_artist, tr.misc.get("artist_sort", "")]).lower() # if prefs.diacritic_search and all([ord(c) < 128 for c in search_text.text]): # line = str(unidecode(line)) if all(word in line for word in search_terms): - playlist.append(item) - if len(playlist) > 0: - pctl.multi_playlist.append(pl_gen( - title=_("Search Results"), - playlist_ids=copy.deepcopy(playlist))) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[ - pctl.active_playlist_viewing].title + "\" f\"" + search_text.text + "\"" - switch_playlist(len(pctl.multi_playlist) - 1) - search_text.text = "" - quick_search_mode = False - if (len(input_text) > 0 and not gui.search_error) or key_down_press is True or inp.backspace_press \ - or gui.force_search: + pctl.selected_in_playlist = search_index + if len(default_playlist) > 10 and search_index > 10: + pctl.playlist_view_position = search_index - 7 + logging.debug("Position changed by search") + else: + pctl.playlist_view_position = 0 - gui.pl_update = 1 + if gui.combo_mode: + pctl.show_selected() + gui.search_error = False - if gui.force_search: - search_index = 0 + break - if inp.backspace_press: - search_index = 0 + else: + search_index = oi + if len(input_text) > 0 or gui.force_search: + gui.search_error = True + if key_down_press: + bottom_playlist2.pulse() + + gui.force_search = False + + if key_up_press is True \ + and not key_shiftr_down \ + and not key_shift_down \ + and not key_ctrl_down \ + and not key_rctrl_down \ + and not key_meta \ + and not key_lalt \ + and not key_ralt: - if len(search_text.text) > 0 and search_text.text[0] != "/": + gui.pl_update = 1 oi = search_index - while search_index < len(default_playlist) - 1: - search_index += 1 - if search_index > len(default_playlist) - 1: - search_index = 0 - + while search_index > 1: + search_index -= 1 + search_index = min(search_index, len(default_playlist) - 1) search_terms = search_text.text.lower().split() - tr = pctl.get_track(default_playlist[search_index]) - line = " ".join( - [tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, - tr.album_artist, tr.misc.get("artist_sort", "")]).lower() + line = pctl.master_library[default_playlist[search_index]].title.lower() + \ + pctl.master_library[default_playlist[search_index]].artist.lower() \ + + pctl.master_library[default_playlist[search_index]].album.lower() + \ + pctl.master_library[default_playlist[search_index]].filename.lower() - # if prefs.diacritic_search and all([ord(c) < 128 for c in search_text.text]): - # line = str(unidecode(line)) + if prefs.diacritic_search and all([ord(c) < 128 for c in search_text.text]): + line = str(unidecode(line)) if all(word in line for word in search_terms): @@ -46994,973 +23351,926 @@ def drop_file(target): logging.debug("Position changed by search") else: pctl.playlist_view_position = 0 - if gui.combo_mode: pctl.show_selected() - gui.search_error = False - break - else: search_index = oi - if len(input_text) > 0 or gui.force_search: - gui.search_error = True - if key_down_press: - bottom_playlist2.pulse() - gui.force_search = False + edge_playlist2.pulse() - if key_up_press is True \ - and not key_shiftr_down \ + if inp.key_return_press is True and search_index > -1: + gui.pl_update = 1 + pctl.jump(default_playlist[search_index], search_index) + if album_mode: + goto_album(pctl.playlist_playing_position) + quick_search_mode = False + search_clear_timer.set() + + elif not search_over.active: + + if key_up_press and (( + not key_shiftr_down \ and not key_shift_down \ and not key_ctrl_down \ and not key_rctrl_down \ and not key_meta \ and not key_lalt \ - and not key_ralt: - - gui.pl_update = 1 - oi = search_index - - while search_index > 1: - search_index -= 1 - search_index = min(search_index, len(default_playlist) - 1) - search_terms = search_text.text.lower().split() - line = pctl.master_library[default_playlist[search_index]].title.lower() + \ - pctl.master_library[default_playlist[search_index]].artist.lower() \ - + pctl.master_library[default_playlist[search_index]].album.lower() + \ - pctl.master_library[default_playlist[search_index]].filename.lower() - - if prefs.diacritic_search and all([ord(c) < 128 for c in search_text.text]): - line = str(unidecode(line)) - - if all(word in line for word in search_terms): - - pctl.selected_in_playlist = search_index - if len(default_playlist) > 10 and search_index > 10: - pctl.playlist_view_position = search_index - 7 - logging.debug("Position changed by search") - else: - pctl.playlist_view_position = 0 - if gui.combo_mode: - pctl.show_selected() - break - else: - search_index = oi + and not key_ralt) or (keymaps.test("shift-up"))): - edge_playlist2.pulse() + pctl.show_selected() + gui.pl_update = 1 - if inp.key_return_press is True and search_index > -1: - gui.pl_update = 1 - pctl.jump(default_playlist[search_index], search_index) - if album_mode: - goto_album(pctl.playlist_playing_position) - quick_search_mode = False - search_clear_timer.set() + if not keymaps.test("shift-up"): + if pctl.selected_in_playlist > 0: + pctl.selected_in_playlist -= 1 + r_menu_index = default_playlist[pctl.selected_in_playlist] + shift_selection = [] - elif not search_over.active: + if pctl.playlist_view_position > 0 and pctl.selected_in_playlist < pctl.playlist_view_position + 2: + pctl.playlist_view_position -= 1 + logging.debug("Position changed by key up") - if key_up_press and (( - not key_shiftr_down \ - and not key_shift_down \ - and not key_ctrl_down \ - and not key_rctrl_down \ - and not key_meta \ - and not key_lalt \ - and not key_ralt) or (keymaps.test("shift-up"))): + scroll_hide_timer.set() + gui.frame_callback_list.append(TestTimer(0.9)) - pctl.show_selected() - gui.pl_update = 1 + pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(default_playlist)) - if not keymaps.test("shift-up"): - if pctl.selected_in_playlist > 0: - pctl.selected_in_playlist -= 1 - r_menu_index = default_playlist[pctl.selected_in_playlist] - shift_selection = [] + if pctl.selected_in_playlist < len(default_playlist) and ( + (key_down_press and \ + not key_shiftr_down \ + and not key_shift_down \ + and not key_ctrl_down \ + and not key_rctrl_down \ + and not key_meta \ + and not key_lalt \ + and not key_ralt) or keymaps.test("shift-down")): - if pctl.playlist_view_position > 0 and pctl.selected_in_playlist < pctl.playlist_view_position + 2: - pctl.playlist_view_position -= 1 - logging.debug("Position changed by key up") + pctl.show_selected() + gui.pl_update = 1 - scroll_hide_timer.set() - gui.frame_callback_list.append(TestTimer(0.9)) + if not keymaps.test("shift-down"): + if pctl.selected_in_playlist < len(default_playlist) - 1: + pctl.selected_in_playlist += 1 + r_menu_index = default_playlist[pctl.selected_in_playlist] + shift_selection = [] - pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(default_playlist)) + if pctl.playlist_view_position < len( + default_playlist) and pctl.selected_in_playlist > pctl.playlist_view_position + gui.playlist_view_length - 3 - gui.row_extra: + pctl.playlist_view_position += 1 + logging.debug("Position changed by key down") - if pctl.selected_in_playlist < len(default_playlist) and ( - (key_down_press and \ - not key_shiftr_down \ - and not key_shift_down \ - and not key_ctrl_down \ - and not key_rctrl_down \ - and not key_meta \ - and not key_lalt \ - and not key_ralt) or keymaps.test("shift-down")): + scroll_hide_timer.set() + gui.frame_callback_list.append(TestTimer(0.9)) - pctl.show_selected() - gui.pl_update = 1 + pctl.selected_in_playlist = max(pctl.selected_in_playlist, 0) - if not keymaps.test("shift-down"): - if pctl.selected_in_playlist < len(default_playlist) - 1: - pctl.selected_in_playlist += 1 - r_menu_index = default_playlist[pctl.selected_in_playlist] - shift_selection = [] + if inp.key_return_press and not pref_box.enabled and not radiobox.active and not trans_edit_box.active: + gui.pl_update = 1 + if pctl.selected_in_playlist > len(default_playlist) - 1: + pctl.selected_in_playlist = 0 + shift_selection = [] + if default_playlist: + pctl.jump(default_playlist[pctl.selected_in_playlist], pctl.selected_in_playlist) + if album_mode: + goto_album(pctl.playlist_playing_position) - if pctl.playlist_view_position < len( - default_playlist) and pctl.selected_in_playlist > pctl.playlist_view_position + gui.playlist_view_length - 3 - gui.row_extra: - pctl.playlist_view_position += 1 - logging.debug("Position changed by key down") - scroll_hide_timer.set() - gui.frame_callback_list.append(TestTimer(0.9)) + elif gui.mode == 3: - pctl.selected_in_playlist = max(pctl.selected_in_playlist, 0) + if (key_shift_down and inp.mouse_click) or middle_click: + if prefs.mini_mode_mode == 4: + prefs.mini_mode_mode = 1 + window_size[0] = int(330 * gui.scale) + window_size[1] = int(330 * gui.scale) + SDL_SetWindowMinimumSize(t_window, window_size[0], window_size[1]) + SDL_SetWindowSize(t_window, window_size[0], window_size[1]) + else: + prefs.mini_mode_mode = 4 + window_size[0] = int(320 * gui.scale) + window_size[1] = int(90 * gui.scale) + SDL_SetWindowMinimumSize(t_window, window_size[0], window_size[1]) + SDL_SetWindowSize(t_window, window_size[0], window_size[1]) + + if prefs.mini_mode_mode == 5: + mini_mode3.render() + elif prefs.mini_mode_mode == 4: + mini_mode2.render() + else: + mini_mode.render() - if inp.key_return_press and not pref_box.enabled and not radiobox.active and not trans_edit_box.active: - gui.pl_update = 1 - if pctl.selected_in_playlist > len(default_playlist) - 1: - pctl.selected_in_playlist = 0 - shift_selection = [] - if default_playlist: - pctl.jump(default_playlist[pctl.selected_in_playlist], pctl.selected_in_playlist) - if album_mode: - goto_album(pctl.playlist_playing_position) + t = toast_love_timer.get() + if t < 1.8 and gui.toast_love_object is not None: + track = gui.toast_love_object + ww = 0 + if gui.lsp: + ww = gui.lspw - elif gui.mode == 3: + rect = (ww + 5 * gui.scale, gui.panelY + 5 * gui.scale, 235 * gui.scale, 39 * gui.scale) + fields.add(rect) - if (key_shift_down and inp.mouse_click) or middle_click: - if prefs.mini_mode_mode == 4: - prefs.mini_mode_mode = 1 - window_size[0] = int(330 * gui.scale) - window_size[1] = int(330 * gui.scale) - SDL_SetWindowMinimumSize(t_window, window_size[0], window_size[1]) - SDL_SetWindowSize(t_window, window_size[0], window_size[1]) + if coll(rect): + toast_love_timer.force_set(10) else: - prefs.mini_mode_mode = 4 - window_size[0] = int(320 * gui.scale) - window_size[1] = int(90 * gui.scale) - SDL_SetWindowMinimumSize(t_window, window_size[0], window_size[1]) - SDL_SetWindowSize(t_window, window_size[0], window_size[1]) - - if prefs.mini_mode_mode == 5: - mini_mode3.render() - elif prefs.mini_mode_mode == 4: - mini_mode2.render() - else: - mini_mode.render() - - t = toast_love_timer.get() - if t < 1.8 and gui.toast_love_object is not None: - track = gui.toast_love_object - - ww = 0 - if gui.lsp: - ww = gui.lspw + ddt.rect(grow_rect(rect, 2 * gui.scale), colours.box_border) + ddt.rect(rect, colours.queue_card_background) - rect = (ww + 5 * gui.scale, gui.panelY + 5 * gui.scale, 235 * gui.scale, 39 * gui.scale) - fields.add(rect) + # fqo = copy.copy(pctl.force_queue[-1]) - if coll(rect): - toast_love_timer.force_set(10) - else: - ddt.rect(grow_rect(rect, 2 * gui.scale), colours.box_border) - ddt.rect(rect, colours.queue_card_background) + ddt.text_background_colour = colours.queue_card_background - # fqo = copy.copy(pctl.force_queue[-1]) + if gui.toast_love_added: + text = _("Loved track") + heart_notify_icon.render(rect[0] + 9 * gui.scale, rect[1] + 8 * gui.scale, [250, 100, 100, 255]) + else: + text = _("Un-Loved track") + heart_notify_break_icon.render( + rect[0] + 9 * gui.scale, rect[1] + 7 * gui.scale, + [150, 150, 150, 255]) - ddt.text_background_colour = colours.queue_card_background + ddt.text_background_colour = colours.queue_card_background + ddt.text((rect[0] + 42 * gui.scale, rect[1] + 3 * gui.scale), text, colours.box_text, 313) + ddt.text( + (rect[0] + 42 * gui.scale, rect[1] + 20 * gui.scale), + f"{track.track_number}. {track.artist} - {track.title}".strip(".- "), colours.box_text_label, + 13, max_w=rect[2] - 50 * gui.scale) - if gui.toast_love_added: - text = _("Loved track") - heart_notify_icon.render(rect[0] + 9 * gui.scale, rect[1] + 8 * gui.scale, [250, 100, 100, 255]) - else: - text = _("Un-Loved track") - heart_notify_break_icon.render( - rect[0] + 9 * gui.scale, rect[1] + 7 * gui.scale, - [150, 150, 150, 255]) - - ddt.text_background_colour = colours.queue_card_background - ddt.text((rect[0] + 42 * gui.scale, rect[1] + 3 * gui.scale), text, colours.box_text, 313) - ddt.text( - (rect[0] + 42 * gui.scale, rect[1] + 20 * gui.scale), - f"{track.track_number}. {track.artist} - {track.title}".strip(".- "), colours.box_text_label, - 13, max_w=rect[2] - 50 * gui.scale) - - t = queue_add_timer.get() - if t < 2.5 and gui.toast_queue_object: - track = pctl.get_track(gui.toast_queue_object.track_id) - - ww = 0 - if gui.lsp: - ww = gui.lspw - if search_over.active: - ww = window_size[0] // 2 - (215 * gui.scale // 2) + t = queue_add_timer.get() + if t < 2.5 and gui.toast_queue_object: + track = pctl.get_track(gui.toast_queue_object.track_id) - rect = (ww + 5 * gui.scale, gui.panelY + 5 * gui.scale, 215 * gui.scale, 39 * gui.scale) - fields.add(rect) + ww = 0 + if gui.lsp: + ww = gui.lspw + if search_over.active: + ww = window_size[0] // 2 - (215 * gui.scale // 2) - if coll(rect): - queue_add_timer.force_set(10) - elif len(pctl.force_queue) > 0: + rect = (ww + 5 * gui.scale, gui.panelY + 5 * gui.scale, 215 * gui.scale, 39 * gui.scale) + fields.add(rect) - fqo = copy.copy(pctl.force_queue[-1]) + if coll(rect): + queue_add_timer.force_set(10) + elif len(pctl.force_queue) > 0: - ddt.rect(grow_rect(rect, 2 * gui.scale), colours.box_border) - ddt.rect(rect, colours.queue_card_background) + fqo = copy.copy(pctl.force_queue[-1]) - ddt.text_background_colour = colours.queue_card_background - top_text = _("Track") - if gui.queue_toast_plural: - top_text = "Album" - fqo.type = 1 - if pctl.force_queue[-1].type == 1: - top_text = "Album" + ddt.rect(grow_rect(rect, 2 * gui.scale), colours.box_border) + ddt.rect(rect, colours.queue_card_background) - queue_box.draw_card( - rect[0] - 8 * gui.scale, 0, 160 * gui.scale, 210 * gui.scale, - rect[1] + 1 * gui.scale, track, fqo, True, False) + ddt.text_background_colour = colours.queue_card_background + top_text = _("Track") + if gui.queue_toast_plural: + top_text = "Album" + fqo.type = 1 + if pctl.force_queue[-1].type == 1: + top_text = "Album" - ddt.text_background_colour = colours.queue_card_background - ddt.text( - (rect[0] + rect[2] - 50 * gui.scale, rect[1] + 3 * gui.scale, 2), f"{top_text} added", - colours.box_text_label, 11) - ddt.text( - (rect[0] + rect[2] - 50 * gui.scale, rect[1] + 15 * gui.scale, 2), "to queue", - colours.box_text_label, 11) + queue_box.draw_card( + rect[0] - 8 * gui.scale, 0, 160 * gui.scale, 210 * gui.scale, + rect[1] + 1 * gui.scale, track, fqo, True, False) - t = toast_mode_timer.get() - if t < 0.98: + ddt.text_background_colour = colours.queue_card_background + ddt.text( + (rect[0] + rect[2] - 50 * gui.scale, rect[1] + 3 * gui.scale, 2), f"{top_text} added", + colours.box_text_label, 11) + ddt.text( + (rect[0] + rect[2] - 50 * gui.scale, rect[1] + 15 * gui.scale, 2), "to queue", + colours.box_text_label, 11) - wid = ddt.get_text_w(gui.mode_toast_text, 313) - wid = max(round(68 * gui.scale), wid) + t = toast_mode_timer.get() + if t < 0.98: - ww = round(7 * gui.scale) - if gui.lsp and not gui.combo_mode: - ww += gui.lspw + wid = ddt.get_text_w(gui.mode_toast_text, 313) + wid = max(round(68 * gui.scale), wid) - rect = (ww + 8 * gui.scale, gui.panelY + 15 * gui.scale, wid + 20 * gui.scale, 25 * gui.scale) - fields.add(rect) + ww = round(7 * gui.scale) + if gui.lsp and not gui.combo_mode: + ww += gui.lspw - if coll(rect): - toast_mode_timer.force_set(10) - else: - ddt.rect(grow_rect(rect, round(2 * gui.scale)), colours.grey(60)) - ddt.rect(rect, colours.queue_card_background) + rect = (ww + 8 * gui.scale, gui.panelY + 15 * gui.scale, wid + 20 * gui.scale, 25 * gui.scale) + fields.add(rect) - ddt.text_background_colour = colours.queue_card_background - ddt.text((rect[0] + (rect[2] // 2), rect[1] + 4 * gui.scale, 2), gui.mode_toast_text, colours.grey(230), 313) + if coll(rect): + toast_mode_timer.force_set(10) + else: + ddt.rect(grow_rect(rect, round(2 * gui.scale)), colours.grey(60)) + ddt.rect(rect, colours.queue_card_background) - # Render Menus------------------------------- - for instance in Menu.instances: - instance.render() + ddt.text_background_colour = colours.queue_card_background + ddt.text((rect[0] + (rect[2] // 2), rect[1] + 4 * gui.scale, 2), gui.mode_toast_text, colours.grey(230), 313) - if view_box.active: - view_box.render() + # Render Menus------------------------------- + for instance in Menu.instances: + instance.render() - tool_tip.render() - tool_tip2.render() + if view_box.active: + view_box.render() - if console.show: - rect = (20 * gui.scale, 40 * gui.scale, 580 * gui.scale, 200 * gui.scale) - ddt.rect(rect, [0, 0, 0, 245]) + tool_tip.render() + tool_tip2.render() - yy = rect[3] + 15 * gui.scale - u = False - for record in reversed(log.log_history): + if console.show: + rect = (20 * gui.scale, 40 * gui.scale, 580 * gui.scale, 200 * gui.scale) + ddt.rect(rect, [0, 0, 0, 245]) - if yy < rect[1] + 5 * gui.scale: - break + yy = rect[3] + 15 * gui.scale + u = False + for record in reversed(log.log_history): - text_colour = [60, 255, 60, 255] - message = log.format(record) + if yy < rect[1] + 5 * gui.scale: + break - t = record.created - d = time.time() - t - dt = time.localtime(t) + text_colour = [60, 255, 60, 255] + message = log.format(record) - fade = 255 - if d > 2: - fade = 200 + t = record.created + d = time.time() - t + dt = time.localtime(t) - text_colour = [120, 120, 120, fade] - if record.levelno == 10: - text_colour = [80, 80, 80, fade] - if record.levelno == 30: - text_colour = [230, 190, 90, fade] - if record.levelno == 40: - text_colour = [255, 120, 90, fade] - if record.levelno == 50: - text_colour = [255, 90, 90, fade] + fade = 255 + if d > 2: + fade = 200 + + text_colour = [120, 120, 120, fade] + if record.levelno == 10: + text_colour = [80, 80, 80, fade] + if record.levelno == 30: + text_colour = [230, 190, 90, fade] + if record.levelno == 40: + text_colour = [255, 120, 90, fade] + if record.levelno == 50: + text_colour = [255, 90, 90, fade] + + time_colour = [255, 80, 160, fade] + + w = ddt.text( + (rect[0] + 10 * gui.scale, yy), time.strftime("%H:%M:%S", dt), time_colour, 311, + rect[2] - 60 * gui.scale, bg=[5,5,5,255]) + + ddt.text((w + rect[0] + 17 * gui.scale, yy), message, text_colour, 311, rect[2] - 60 * gui.scale, bg=[5,5,5,255]) + yy -= 14 * gui.scale + if u: + gui.delay_frame(5) + + if draw.button("Copy", rect[0] + rect[2] - 55 * gui.scale, rect[1] + rect[3] - 30 * gui.scale): + + text = "" + for record in log.log_history[-50:]: + t = record.created + dt = time.localtime(t) + text += time.strftime("%H:%M:%S", dt) + " " + log.format(record) + "\n" + copy_to_clipboard(text) + show_message(_("Lines copied to clipboard"), mode="done") + + if gui.cursor_is != gui.cursor_want: + + gui.cursor_is = gui.cursor_want + + if gui.cursor_is == 0: + SDL_SetCursor(cursor_standard) + elif gui.cursor_is == 1: + SDL_SetCursor(cursor_shift) + elif gui.cursor_is == 2: + SDL_SetCursor(cursor_text) + elif gui.cursor_is == 3: + SDL_SetCursor(cursor_hand) + elif gui.cursor_is == 4: + SDL_SetCursor(cursor_br_corner) + elif gui.cursor_is == 8: + SDL_SetCursor(cursor_right_side) + elif gui.cursor_is == 9: + SDL_SetCursor(cursor_top_side) + elif gui.cursor_is == 10: + SDL_SetCursor(cursor_left_side) + elif gui.cursor_is == 11: + SDL_SetCursor(cursor_bottom_side) + + get_sdl_input.test_capture_mouse() + get_sdl_input.mouse_capture_want = False + + # # Quick view + # quick_view_box.render() + + # Drag icon next to cursor + if quick_drag and mouse_down and not point_proximity_test( + gui.drag_source_position, mouse_position, 15 * gui.scale): + i_x, i_y = get_sdl_input.mouse() + gui.drag_source_position = (0, 0) + + block_size = round(10 * gui.scale) + x_offset = round(20 * gui.scale) + y_offset = round(1 * gui.scale) + + if len(shift_selection) == 1: # Single track + ddt.rect((i_x + x_offset, i_y + y_offset, block_size, block_size), [160, 140, 235, 240]) + elif key_ctrl_down: # Add to queue undrouped + small_block = round(6 * gui.scale) + spacing = round(2 * gui.scale) + ddt.rect((i_x + x_offset, i_y + y_offset, small_block, small_block), [160, 140, 235, 240]) + ddt.rect( + (i_x + x_offset + spacing + small_block, i_y + y_offset, small_block, small_block), [160, 140, 235, 240]) + ddt.rect( + (i_x + x_offset, i_y + y_offset + spacing + small_block, small_block, small_block), [160, 140, 235, 240]) + ddt.rect( + (i_x + x_offset + spacing + small_block, i_y + y_offset + spacing + small_block, small_block, small_block), + [160, 140, 235, 240]) + ddt.rect( + (i_x + x_offset, i_y + y_offset + spacing + small_block + spacing + small_block, small_block, small_block), + [160, 140, 235, 240]) + ddt.rect( + (i_x + x_offset + spacing + small_block, + i_y + y_offset + spacing + small_block + spacing + small_block, + small_block, small_block), [160, 140, 235, 240]) - time_colour = [255, 80, 160, fade] + else: # Multiple tracks + long_block = round(25 * gui.scale) + ddt.rect((i_x + x_offset, i_y + y_offset, block_size, long_block), [160, 140, 235, 240]) - w = ddt.text( - (rect[0] + 10 * gui.scale, yy), time.strftime("%H:%M:%S", dt), time_colour, 311, - rect[2] - 60 * gui.scale, bg=[5,5,5,255]) + # gui.update += 1 + gui.update_on_drag = True - ddt.text((w + rect[0] + 17 * gui.scale, yy), message, text_colour, 311, rect[2] - 60 * gui.scale, bg=[5,5,5,255]) - yy -= 14 * gui.scale - if u: - gui.delay_frame(5) + # Drag pl tab next to cursor + if (playlist_box.drag) and mouse_down and not point_proximity_test( + gui.drag_source_position, mouse_position, 10 * gui.scale): + i_x, i_y = get_sdl_input.mouse() + gui.drag_source_position = (0, 0) + ddt.rect( + (i_x + 20 * gui.scale, i_y + 3 * gui.scale, int(50 * gui.scale), int(15 * gui.scale)), [50, 50, 50, 225]) + # ddt.rect_r((i_x + 20 * gui.scale, i_y + 1 * gui.scale, int(60 * gui.scale), int(15 * gui.scale)), [240, 240, 240, 255], True) + # ddt.draw_text((i_x + 75 * gui.scale, i_y - 0 * gui.scale, 1), pctl.multi_playlist[playlist_box.drag_on].title, [30, 30, 30, 255], 212, bg=[240, 240, 240, 255]) + if radio_view.drag and not point_proximity_test(radio_view.click_point, mouse_position, round(4 * gui.scale)): + ddt.rect(( + mouse_position[0] + round(8 * gui.scale), mouse_position[1] - round(8 * gui.scale), 48 * gui.scale, + 14 * gui.scale), colours.grey(70)) + if (gui.set_label_hold != -1) and mouse_down: - if draw.button("Copy", rect[0] + rect[2] - 55 * gui.scale, rect[1] + rect[3] - 30 * gui.scale): + gui.update_on_drag = True - text = "" - for record in log.log_history[-50:]: - t = record.created - dt = time.localtime(t) - text += time.strftime("%H:%M:%S", dt) + " " + log.format(record) + "\n" - copy_to_clipboard(text) - show_message(_("Lines copied to clipboard"), mode="done") + if not point_proximity_test(gui.set_label_point, mouse_position, 3): + i_x, i_y = get_sdl_input.mouse() + gui.set_label_point = (0, 0) - if gui.cursor_is != gui.cursor_want: + w = ddt.get_text_w(gui.pl_st[gui.set_label_hold][0], 212) + w = max(w, 45 * gui.scale) + ddt.rect( + (i_x + 25 * gui.scale, i_y + 1 * gui.scale, w + int(20 * gui.scale), int(15 * gui.scale)), + [240, 240, 240, 255]) + ddt.text( + (i_x + 25 * gui.scale + w + int(20 * gui.scale) - 4 * gui.scale, i_y - 0 * gui.scale, 1), + gui.pl_st[gui.set_label_hold][0], [30, 30, 30, 255], 212, bg=[240, 240, 240, 255]) - gui.cursor_is = gui.cursor_want + input_text = "" + gui.update -= 1 - if gui.cursor_is == 0: - SDL_SetCursor(cursor_standard) - elif gui.cursor_is == 1: - SDL_SetCursor(cursor_shift) - elif gui.cursor_is == 2: - SDL_SetCursor(cursor_text) - elif gui.cursor_is == 3: - SDL_SetCursor(cursor_hand) - elif gui.cursor_is == 4: - SDL_SetCursor(cursor_br_corner) - elif gui.cursor_is == 8: - SDL_SetCursor(cursor_right_side) - elif gui.cursor_is == 9: - SDL_SetCursor(cursor_top_side) - elif gui.cursor_is == 10: - SDL_SetCursor(cursor_left_side) - elif gui.cursor_is == 11: - SDL_SetCursor(cursor_bottom_side) - - get_sdl_input.test_capture_mouse() - get_sdl_input.mouse_capture_want = False - - # # Quick view - # quick_view_box.render() - - # Drag icon next to cursor - if quick_drag and mouse_down and not point_proximity_test( - gui.drag_source_position, mouse_position, 15 * gui.scale): - i_x, i_y = get_sdl_input.mouse() - gui.drag_source_position = (0, 0) - - block_size = round(10 * gui.scale) - x_offset = round(20 * gui.scale) - y_offset = round(1 * gui.scale) - - if len(shift_selection) == 1: # Single track - ddt.rect((i_x + x_offset, i_y + y_offset, block_size, block_size), [160, 140, 235, 240]) - elif key_ctrl_down: # Add to queue undrouped - small_block = round(6 * gui.scale) - spacing = round(2 * gui.scale) - ddt.rect((i_x + x_offset, i_y + y_offset, small_block, small_block), [160, 140, 235, 240]) - ddt.rect( - (i_x + x_offset + spacing + small_block, i_y + y_offset, small_block, small_block), [160, 140, 235, 240]) - ddt.rect( - (i_x + x_offset, i_y + y_offset + spacing + small_block, small_block, small_block), [160, 140, 235, 240]) - ddt.rect( - (i_x + x_offset + spacing + small_block, i_y + y_offset + spacing + small_block, small_block, small_block), - [160, 140, 235, 240]) - ddt.rect( - (i_x + x_offset, i_y + y_offset + spacing + small_block + spacing + small_block, small_block, small_block), - [160, 140, 235, 240]) - ddt.rect( - (i_x + x_offset + spacing + small_block, - i_y + y_offset + spacing + small_block + spacing + small_block, - small_block, small_block), [160, 140, 235, 240]) - - else: # Multiple tracks - long_block = round(25 * gui.scale) - ddt.rect((i_x + x_offset, i_y + y_offset, block_size, long_block), [160, 140, 235, 240]) - - # gui.update += 1 - gui.update_on_drag = True - - # Drag pl tab next to cursor - if (playlist_box.drag) and mouse_down and not point_proximity_test( - gui.drag_source_position, mouse_position, 10 * gui.scale): - i_x, i_y = get_sdl_input.mouse() - gui.drag_source_position = (0, 0) - ddt.rect( - (i_x + 20 * gui.scale, i_y + 3 * gui.scale, int(50 * gui.scale), int(15 * gui.scale)), [50, 50, 50, 225]) - # ddt.rect_r((i_x + 20 * gui.scale, i_y + 1 * gui.scale, int(60 * gui.scale), int(15 * gui.scale)), [240, 240, 240, 255], True) - # ddt.draw_text((i_x + 75 * gui.scale, i_y - 0 * gui.scale, 1), pctl.multi_playlist[playlist_box.drag_on].title, [30, 30, 30, 255], 212, bg=[240, 240, 240, 255]) - if radio_view.drag and not point_proximity_test(radio_view.click_point, mouse_position, round(4 * gui.scale)): - ddt.rect(( - mouse_position[0] + round(8 * gui.scale), mouse_position[1] - round(8 * gui.scale), 48 * gui.scale, - 14 * gui.scale), colours.grey(70)) - if (gui.set_label_hold != -1) and mouse_down: - - gui.update_on_drag = True - - if not point_proximity_test(gui.set_label_point, mouse_position, 3): - i_x, i_y = get_sdl_input.mouse() - gui.set_label_point = (0, 0) + # logging.info("FRAME " + str(core_timer.get())) + gui.update = min(gui.update, 1) + gui.present = True - w = ddt.get_text_w(gui.pl_st[gui.set_label_hold][0], 212) - w = max(w, 45 * gui.scale) - ddt.rect( - (i_x + 25 * gui.scale, i_y + 1 * gui.scale, w + int(20 * gui.scale), int(15 * gui.scale)), - [240, 240, 240, 255]) - ddt.text( - (i_x + 25 * gui.scale + w + int(20 * gui.scale) - 4 * gui.scale, i_y - 0 * gui.scale, 1), - gui.pl_st[gui.set_label_hold][0], [30, 30, 30, 255], 212, bg=[240, 240, 240, 255]) + SDL_SetRenderTarget(renderer, None) + SDL_RenderCopy(renderer, gui.main_texture, None, gui.tracklist_texture_rect) - input_text = "" - gui.update -= 1 + if gui.turbo: + gui.level_update = True - # logging.info("FRAME " + str(core_timer.get())) - gui.update = min(gui.update, 1) - gui.present = True + # if gui.vis == 1 and pctl.playing_state != 1 and gui.level_peak != [0, 0] and gui.turbo: + # + # # logging.info(gui.level_peak) + # gui.time_passed = gui.level_time.hit() + # if gui.time_passed > 1: + # gui.time_passed = 0 + # while gui.time_passed > 0.01: + # gui.level_peak[1] -= 0.5 + # if gui.level_peak[1] < 0: + # gui.level_peak[1] = 0 + # gui.level_peak[0] -= 0.5 + # if gui.level_peak[0] < 0: + # gui.level_peak[0] = 0 + # gui.time_passed -= 0.020 + # + # gui.level_update = True - SDL_SetRenderTarget(renderer, None) - SDL_RenderCopy(renderer, gui.main_texture, None, gui.tracklist_texture_rect) + if gui.level_update is True and not resize_mode and gui.mode != 3: + gui.level_update = False - if gui.turbo: - gui.level_update = True + SDL_SetRenderTarget(renderer, None) + if not gui.present: + SDL_RenderCopy(renderer, gui.main_texture, None, gui.tracklist_texture_rect) + gui.present = True - # if gui.vis == 1 and pctl.playing_state != 1 and gui.level_peak != [0, 0] and gui.turbo: - # - # # logging.info(gui.level_peak) - # gui.time_passed = gui.level_time.hit() - # if gui.time_passed > 1: - # gui.time_passed = 0 - # while gui.time_passed > 0.01: - # gui.level_peak[1] -= 0.5 - # if gui.level_peak[1] < 0: - # gui.level_peak[1] = 0 - # gui.level_peak[0] -= 0.5 - # if gui.level_peak[0] < 0: - # gui.level_peak[0] = 0 - # gui.time_passed -= 0.020 - # - # gui.level_update = True + if gui.vis == 3: + # Scrolling spectrogram - if gui.level_update is True and not resize_mode and gui.mode != 3: - gui.level_update = False + # if not vis_update: + # logging.info("No UPDATE " + str(random.randint(1,50))) + if len(gui.spec2_buffers) > 0 and gui.spec2_timer.get() > 0.04: + # gui.spec2_timer.force_set(gui.spec2_timer.get() - 0.04) + gui.spec2_timer.set() + vis_update = True - SDL_SetRenderTarget(renderer, None) - if not gui.present: - SDL_RenderCopy(renderer, gui.main_texture, None, gui.tracklist_texture_rect) - gui.present = True + if len(gui.spec2_buffers) > 0 and vis_update: + vis_update = False - if gui.vis == 3: - # Scrolling spectrogram + SDL_SetRenderTarget(renderer, gui.spec2_tex) + for i, value in enumerate(gui.spec2_buffers[0]): + ddt.rect( + [gui.spec2_position, i, 1, 1], + [ + min(255, prefs.spec2_base[0] + int(value * prefs.spec2_multiply[0])), + min(255, prefs.spec2_base[1] + int(value * prefs.spec2_multiply[1])), + min(255, prefs.spec2_base[2] + int(value * prefs.spec2_multiply[2])), + 255]) - # if not vis_update: - # logging.info("No UPDATE " + str(random.randint(1,50))) - if len(gui.spec2_buffers) > 0 and gui.spec2_timer.get() > 0.04: - # gui.spec2_timer.force_set(gui.spec2_timer.get() - 0.04) - gui.spec2_timer.set() - vis_update = True + del gui.spec2_buffers[0] - if len(gui.spec2_buffers) > 0 and vis_update: - vis_update = False + gui.spec2_position += 1 - SDL_SetRenderTarget(renderer, gui.spec2_tex) - for i, value in enumerate(gui.spec2_buffers[0]): - ddt.rect( - [gui.spec2_position, i, 1, 1], - [ - min(255, prefs.spec2_base[0] + int(value * prefs.spec2_multiply[0])), - min(255, prefs.spec2_base[1] + int(value * prefs.spec2_multiply[1])), - min(255, prefs.spec2_base[2] + int(value * prefs.spec2_multiply[2])), - 255]) + if gui.spec2_position > gui.spec2_w - 1: + gui.spec2_position = 0 - del gui.spec2_buffers[0] + SDL_SetRenderTarget(renderer, None) - gui.spec2_position += 1 + # + # else: + # logging.info("animation stall" + str(random.randint(1, 10))) - if gui.spec2_position > gui.spec2_w - 1: - gui.spec2_position = 0 + if prefs.spec2_scroll: - SDL_SetRenderTarget(renderer, None) + gui.spec2_source.x = 0 + gui.spec2_source.y = 0 + gui.spec2_source.w = gui.spec2_position + gui.spec2_dest.x = gui.spec2_rec.x + gui.spec2_rec.w - gui.spec2_position + gui.spec2_dest.w = gui.spec2_position + SDL_RenderCopy(renderer, gui.spec2_tex, gui.spec2_source, gui.spec2_dest) - # - # else: - # logging.info("animation stall" + str(random.randint(1, 10))) + gui.spec2_source.x = gui.spec2_position + gui.spec2_source.y = 0 + gui.spec2_source.w = gui.spec2_rec.w - gui.spec2_position + gui.spec2_dest.x = gui.spec2_rec.x + gui.spec2_dest.w = gui.spec2_rec.w - gui.spec2_position + SDL_RenderCopy(renderer, gui.spec2_tex, gui.spec2_source, gui.spec2_dest) - if prefs.spec2_scroll: + else: - gui.spec2_source.x = 0 - gui.spec2_source.y = 0 - gui.spec2_source.w = gui.spec2_position - gui.spec2_dest.x = gui.spec2_rec.x + gui.spec2_rec.w - gui.spec2_position - gui.spec2_dest.w = gui.spec2_position - SDL_RenderCopy(renderer, gui.spec2_tex, gui.spec2_source, gui.spec2_dest) + SDL_RenderCopy(renderer, gui.spec2_tex, None, gui.spec2_rec) - gui.spec2_source.x = gui.spec2_position - gui.spec2_source.y = 0 - gui.spec2_source.w = gui.spec2_rec.w - gui.spec2_position - gui.spec2_dest.x = gui.spec2_rec.x - gui.spec2_dest.w = gui.spec2_rec.w - gui.spec2_position - SDL_RenderCopy(renderer, gui.spec2_tex, gui.spec2_source, gui.spec2_dest) + if pref_box.enabled: + ddt.rect((gui.spec2_rec.x, gui.spec2_rec.y, gui.spec2_rec.w, gui.spec2_rec.h), [0, 0, 0, 90]) - else: + if gui.vis == 4 and gui.draw_vis4_top: + showcase.render_vis(True) + # gui.level_update = False - SDL_RenderCopy(renderer, gui.spec2_tex, None, gui.spec2_rec) + if gui.vis == 2 and gui.spec is not None: - if pref_box.enabled: - ddt.rect((gui.spec2_rec.x, gui.spec2_rec.y, gui.spec2_rec.w, gui.spec2_rec.h), [0, 0, 0, 90]) + # Standard spectrum visualiser - if gui.vis == 4 and gui.draw_vis4_top: - showcase.render_vis(True) - # gui.level_update = False + if gui.update_spec == 0 and pctl.playing_state != 2: + if vis_decay_timer.get() > 0.007: # Controls speed of decay after stop + vis_decay_timer.set() + for i in range(len(gui.spec)): + if gui.s_spec[i] > 0: + if gui.spec[i] > 0: + gui.spec[i] -= 1 + gui.level_update = True + else: + gui.level_update = True - if gui.vis == 2 and gui.spec is not None: + if vis_rate_timer.get() > 0.027: # Limit the change rate #to 60 fps + vis_rate_timer.set() - # Standard spectrum visualiser + if spec_smoothing and pctl.playing_state > 0: - if gui.update_spec == 0 and pctl.playing_state != 2: - if vis_decay_timer.get() > 0.007: # Controls speed of decay after stop - vis_decay_timer.set() - for i in range(len(gui.spec)): - if gui.s_spec[i] > 0: - if gui.spec[i] > 0: - gui.spec[i] -= 1 + for i in range(len(gui.spec)): + if gui.spec[i] > gui.s_spec[i]: + gui.s_spec[i] += 1 + if abs(gui.spec[i] - gui.s_spec[i]) > 4: + gui.s_spec[i] += 1 + if abs(gui.spec[i] - gui.s_spec[i]) > 6: + gui.s_spec[i] += 1 + if abs(gui.spec[i] - gui.s_spec[i]) > 8: + gui.s_spec[i] += 1 + + elif gui.spec[i] == gui.s_spec[i]: + pass + elif gui.spec[i] < gui.s_spec[i] > 0: + gui.s_spec[i] -= 1 + if abs(gui.spec[i] - gui.s_spec[i]) > 4: + gui.s_spec[i] -= 1 + if abs(gui.spec[i] - gui.s_spec[i]) > 6: + gui.s_spec[i] -= 1 + if abs(gui.spec[i] - gui.s_spec[i]) > 8: + gui.s_spec[i] -= 1 + + if pctl.playing_state == 0 and check_equal(gui.s_spec): gui.level_update = True + time.sleep(0.008) + else: + gui.s_spec = gui.spec else: - gui.level_update = True + pass - if vis_rate_timer.get() > 0.027: # Limit the change rate #to 60 fps - vis_rate_timer.set() + if not gui.test: - if spec_smoothing and pctl.playing_state > 0: + SDL_SetRenderTarget(renderer, gui.spec1_tex) - for i in range(len(gui.spec)): - if gui.spec[i] > gui.s_spec[i]: - gui.s_spec[i] += 1 - if abs(gui.spec[i] - gui.s_spec[i]) > 4: - gui.s_spec[i] += 1 - if abs(gui.spec[i] - gui.s_spec[i]) > 6: - gui.s_spec[i] += 1 - if abs(gui.spec[i] - gui.s_spec[i]) > 8: - gui.s_spec[i] += 1 + # ddt.rect_r(gui.spec_rect, colours.top_panel_background, True) + ddt.rect((0, 0, gui.spec_w, gui.spec_h), colours.vis_bg) - elif gui.spec[i] == gui.s_spec[i]: - pass - elif gui.spec[i] < gui.s_spec[i] > 0: - gui.s_spec[i] -= 1 - if abs(gui.spec[i] - gui.s_spec[i]) > 4: - gui.s_spec[i] -= 1 - if abs(gui.spec[i] - gui.s_spec[i]) > 6: - gui.s_spec[i] -= 1 - if abs(gui.spec[i] - gui.s_spec[i]) > 8: - gui.s_spec[i] -= 1 + # xx = 0 + gui.bar.x = 0 + on = 0 - if pctl.playing_state == 0 and check_equal(gui.s_spec): - gui.level_update = True - time.sleep(0.008) - else: - gui.s_spec = gui.spec - else: - pass + SDL_SetRenderDrawColor( + renderer, colours.vis_colour[0], + colours.vis_colour[1], colours.vis_colour[2], + colours.vis_colour[3]) - if not gui.test: + for item in gui.s_spec: - SDL_SetRenderTarget(renderer, gui.spec1_tex) + if on > 19: + break + on += 1 - # ddt.rect_r(gui.spec_rect, colours.top_panel_background, True) - ddt.rect((0, 0, gui.spec_w, gui.spec_h), colours.vis_bg) + item -= 1 - # xx = 0 - gui.bar.x = 0 - on = 0 + if item < 1: + gui.bar.x += round(4 * gui.scale) + continue - SDL_SetRenderDrawColor( - renderer, colours.vis_colour[0], - colours.vis_colour[1], colours.vis_colour[2], - colours.vis_colour[3]) + item = min(item, 20) - for item in gui.s_spec: + if gui.scale >= 2: + item = round(item * gui.scale) - if on > 19: - break - on += 1 + gui.bar.y = 0 + gui.spec_h - item + gui.bar.h = item - item -= 1 + SDL_RenderFillRect(renderer, gui.bar) - if item < 1: gui.bar.x += round(4 * gui.scale) - continue - - item = min(item, 20) - - if gui.scale >= 2: - item = round(item * gui.scale) - gui.bar.y = 0 + gui.spec_h - item - gui.bar.h = item + if pref_box.enabled: + ddt.rect((0, 0, gui.spec_w, gui.spec_h), [0, 0, 0, 90]) - SDL_RenderFillRect(renderer, gui.bar) + SDL_SetRenderTarget(renderer, None) + SDL_RenderCopy(renderer, gui.spec1_tex, None, gui.spec1_rec) - gui.bar.x += round(4 * gui.scale) + if gui.vis == 1: - if pref_box.enabled: - ddt.rect((0, 0, gui.spec_w, gui.spec_h), [0, 0, 0, 90]) + if prefs.backend == 2 or True: + if pctl.playing_state == 1 or pctl.playing_state == 3: + # gui.level_update = True + while tauon.level_train and tauon.level_train[0][0] < time.time(): - SDL_SetRenderTarget(renderer, None) - SDL_RenderCopy(renderer, gui.spec1_tex, None, gui.spec1_rec) + l = tauon.level_train[0][1] + r = tauon.level_train[0][2] - if gui.vis == 1: + gui.level_peak[0] = max(r, gui.level_peak[0]) + gui.level_peak[1] = max(l, gui.level_peak[1]) - if prefs.backend == 2 or True: - if pctl.playing_state == 1 or pctl.playing_state == 3: - # gui.level_update = True - while tauon.level_train and tauon.level_train[0][0] < time.time(): + del tauon.level_train[0] - l = tauon.level_train[0][1] - r = tauon.level_train[0][2] + else: + tauon.level_train.clear() - gui.level_peak[0] = max(r, gui.level_peak[0]) - gui.level_peak[1] = max(l, gui.level_peak[1]) + SDL_SetRenderTarget(renderer, gui.spec_level_tex) - del tauon.level_train[0] + x = window_size[0] - 20 * gui.scale - gui.offset_extra + y = gui.level_y + w = gui.level_w + s = gui.level_s - else: - tauon.level_train.clear() + y = 0 - SDL_SetRenderTarget(renderer, gui.spec_level_tex) + gui.spec_level_rec.x = round(x - 70 * gui.scale) + ddt.rect_a((0, 0), (79 * gui.scale, 18 * gui.scale), colours.grey(10)) - x = window_size[0] - 20 * gui.scale - gui.offset_extra - y = gui.level_y - w = gui.level_w - s = gui.level_s + x = round(gui.level_ww - 9 * gui.scale) + y = 10 * gui.scale - y = 0 + if prefs.backend == 2 or True: + if (gui.level_peak[0] > 0 or gui.level_peak[1] > 0): + # gui.level_update = True + if pctl.playing_time < 1: + gui.delay_frame(0.032) - gui.spec_level_rec.x = round(x - 70 * gui.scale) - ddt.rect_a((0, 0), (79 * gui.scale, 18 * gui.scale), colours.grey(10)) + if pctl.playing_state == 1 or pctl.playing_state == 3: + t = gui.level_decay_timer.hit() + decay = 14 * t + gui.level_peak[1] -= decay + gui.level_peak[0] -= decay + elif pctl.playing_state == 0 or pctl.playing_state == 2: + gui.level_update = True + time.sleep(0.016) + t = gui.level_decay_timer.hit() + decay = 16 * t + gui.level_peak[1] -= decay + gui.level_peak[0] -= decay - x = round(gui.level_ww - 9 * gui.scale) - y = 10 * gui.scale + for t in range(12): - if prefs.backend == 2 or True: - if (gui.level_peak[0] > 0 or gui.level_peak[1] > 0): - # gui.level_update = True - if pctl.playing_time < 1: - gui.delay_frame(0.032) + if gui.level_peak[0] < t: + met = False + else: + met = True + if gui.level_peak[0] < 0.2: + met = False - if pctl.playing_state == 1 or pctl.playing_state == 3: - t = gui.level_decay_timer.hit() - decay = 14 * t - gui.level_peak[1] -= decay - gui.level_peak[0] -= decay - elif pctl.playing_state == 0 or pctl.playing_state == 2: - gui.level_update = True - time.sleep(0.016) - t = gui.level_decay_timer.hit() - decay = 16 * t - gui.level_peak[1] -= decay - gui.level_peak[0] -= decay + if gui.level_meter_colour_mode == 1: - for t in range(12): + if not met: + cc = [15, 10, 20, 255] + else: + cc = colorsys.hls_to_rgb(0.68 + (t * 0.015), 0.4, 0.7) + cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) - if gui.level_peak[0] < t: - met = False - else: - met = True - if gui.level_peak[0] < 0.2: - met = False + elif gui.level_meter_colour_mode == 2: - if gui.level_meter_colour_mode == 1: + if not met: + cc = [11, 11, 13, 255] + else: + cc = colorsys.hls_to_rgb(0.63 - (t * 0.015), 0.4, 0.7) + cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) - if not met: - cc = [15, 10, 20, 255] - else: - cc = colorsys.hls_to_rgb(0.68 + (t * 0.015), 0.4, 0.7) - cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) + elif gui.level_meter_colour_mode == 3: - elif gui.level_meter_colour_mode == 2: + if not met: + cc = [12, 6, 0, 255] + else: + cc = colorsys.hls_to_rgb(0.11 - (t * 0.010), 0.4, 0.7 + (t * 0.02)) + cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) - if not met: - cc = [11, 11, 13, 255] - else: - cc = colorsys.hls_to_rgb(0.63 - (t * 0.015), 0.4, 0.7) - cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) + elif gui.level_meter_colour_mode == 4: - elif gui.level_meter_colour_mode == 3: + if not met: + cc = [10, 10, 10, 255] + else: + cc = colorsys.hls_to_rgb(0.3 - (t * 0.03), 0.4, 0.7 + (t * 0.02)) + cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) - if not met: - cc = [12, 6, 0, 255] else: - cc = colorsys.hls_to_rgb(0.11 - (t * 0.010), 0.4, 0.7 + (t * 0.02)) - cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) - elif gui.level_meter_colour_mode == 4: - - if not met: - cc = [10, 10, 10, 255] - else: - cc = colorsys.hls_to_rgb(0.3 - (t * 0.03), 0.4, 0.7 + (t * 0.02)) - cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) + if t < 7: + cc = colours.level_green + if met is False: + cc = colours.level_1_bg + elif t < 10: + cc = colours.level_yellow + if met is False: + cc = colours.level_2_bg + else: + cc = colours.level_red + if met is False: + cc = colours.level_3_bg + if gui.level > 0 and pctl.playing_state > 0: + pass + ddt.rect_a(((x - (w * t) - (s * t)), y), (w, w), cc) - else: + y -= 7 * gui.scale + for t in range(12): - if t < 7: - cc = colours.level_green - if met is False: - cc = colours.level_1_bg - elif t < 10: - cc = colours.level_yellow - if met is False: - cc = colours.level_2_bg + if gui.level_peak[1] < t: + met = False else: - cc = colours.level_red - if met is False: - cc = colours.level_3_bg - if gui.level > 0 and pctl.playing_state > 0: - pass - ddt.rect_a(((x - (w * t) - (s * t)), y), (w, w), cc) + met = True + if gui.level_peak[1] < 0.2: + met = False - y -= 7 * gui.scale - for t in range(12): - - if gui.level_peak[1] < t: - met = False - else: - met = True - if gui.level_peak[1] < 0.2: - met = False + if gui.level_meter_colour_mode == 1: - if gui.level_meter_colour_mode == 1: + if not met: + cc = [15, 10, 20, 255] + else: + cc = colorsys.hls_to_rgb(0.68 + (t * 0.015), 0.4, 0.7) + cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) - if not met: - cc = [15, 10, 20, 255] - else: - cc = colorsys.hls_to_rgb(0.68 + (t * 0.015), 0.4, 0.7) - cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) + elif gui.level_meter_colour_mode == 2: - elif gui.level_meter_colour_mode == 2: + if not met: + cc = [11, 11, 13, 255] + else: + cc = colorsys.hls_to_rgb(0.63 - (t * 0.015), 0.4, 0.7) + cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) - if not met: - cc = [11, 11, 13, 255] - else: - cc = colorsys.hls_to_rgb(0.63 - (t * 0.015), 0.4, 0.7) - cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) + elif gui.level_meter_colour_mode == 3: - elif gui.level_meter_colour_mode == 3: + if not met: + cc = [12, 6, 0, 255] + else: + cc = colorsys.hls_to_rgb(0.11 - (t * 0.010), 0.4, 0.7 + (t * 0.02)) + cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) - if not met: - cc = [12, 6, 0, 255] - else: - cc = colorsys.hls_to_rgb(0.11 - (t * 0.010), 0.4, 0.7 + (t * 0.02)) - cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) + elif gui.level_meter_colour_mode == 4: - elif gui.level_meter_colour_mode == 4: + if not met: + cc = [10, 10, 10, 255] + else: + cc = colorsys.hls_to_rgb(0.3 - (t * 0.03), 0.4, 0.7 + (t * 0.02)) + cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) - if not met: - cc = [10, 10, 10, 255] else: - cc = colorsys.hls_to_rgb(0.3 - (t * 0.03), 0.4, 0.7 + (t * 0.02)) - cc = (int(cc[0] * 255), int(cc[1] * 255), int(cc[2] * 255), 255) - else: + if t < 7: + cc = colours.level_green + if met is False: + cc = colours.level_1_bg + elif t < 10: + cc = colours.level_yellow + if met is False: + cc = colours.level_2_bg + else: + cc = colours.level_red + if met is False: + cc = colours.level_3_bg - if t < 7: - cc = colours.level_green - if met is False: - cc = colours.level_1_bg - elif t < 10: - cc = colours.level_yellow - if met is False: - cc = colours.level_2_bg - else: - cc = colours.level_red - if met is False: - cc = colours.level_3_bg + if gui.level > 0 and pctl.playing_state > 0: + pass + ddt.rect_a(((x - (w * t) - (s * t)), y), (w, w), cc) - if gui.level > 0 and pctl.playing_state > 0: - pass - ddt.rect_a(((x - (w * t) - (s * t)), y), (w, w), cc) + SDL_SetRenderTarget(renderer, None) + SDL_RenderCopy(renderer, gui.spec_level_tex, None, gui.spec_level_rec) - SDL_SetRenderTarget(renderer, None) - SDL_RenderCopy(renderer, gui.spec_level_tex, None, gui.spec_level_rec) + if gui.present: + # Possible bug older version of SDL (2.0.16) Wayland, setting render target to None causer last copy + # to fail when resizing? Not a big deal as it doesn't matter what the target is when presenting, just + # set to something else + # SDL_SetRenderTarget(renderer, None) + SDL_SetRenderTarget(renderer, gui.main_texture) + SDL_RenderPresent(renderer) - if gui.present: - # Possible bug older version of SDL (2.0.16) Wayland, setting render target to None causer last copy - # to fail when resizing? Not a big deal as it doesn't matter what the target is when presenting, just - # set to something else - # SDL_SetRenderTarget(renderer, None) - SDL_SetRenderTarget(renderer, gui.main_texture) - SDL_RenderPresent(renderer) + gui.present = False - gui.present = False + # ------------------------------------------------------------------------------------------- + # Misc things to update every tick - # ------------------------------------------------------------------------------------------- - # Misc things to update every tick - - # Update d-bus metadata on Linux - if (pctl.playing_state == 1 or pctl.playing_state == 3) and pctl.mpris is not None: - pctl.mpris.update_progress() - - # GUI time ticker update - if (pctl.playing_state == 1 or pctl.playing_state == 3) and gui.lowered is False: - if int(pctl.playing_time) != int(pctl.last_playing_time): - pctl.last_playing_time = pctl.playing_time - bottom_bar1.seek_time = pctl.playing_time - if not prefs.power_save or window_is_focused(): - gui.update = 1 + # Update d-bus metadata on Linux + if (pctl.playing_state == 1 or pctl.playing_state == 3) and pctl.mpris is not None: + pctl.mpris.update_progress() - # Auto save play times to disk - if pctl.total_playtime - time_last_save > 600: - try: - if should_save_state: - logging.info("Auto save playtime") - with (user_directory / "star.p").open("wb") as file: - pickle.dump(star_store.db, file, protocol=pickle.HIGHEST_PROTOCOL) - else: - logging.info("Dev mode, skip auto saving playtime") - except PermissionError: - logging.exception("Permission error encountered while writing database") - show_message(_("Permission error encountered while writing database"), "error") - except Exception: - logging.exception("Unknown error encountered while writing database") - time_last_save = pctl.total_playtime + # GUI time ticker update + if (pctl.playing_state == 1 or pctl.playing_state == 3) and gui.lowered is False: + if int(pctl.playing_time) != int(pctl.last_playing_time): + pctl.last_playing_time = pctl.playing_time + bottom_bar1.seek_time = pctl.playing_time + if not prefs.power_save or window_is_focused(): + gui.update = 1 - # Always render at least one frame per minute (to avoid SDL bugs I guess) - if min_render_timer.get() > 60: - min_render_timer.set() - gui.pl_update = 1 - gui.update += 1 + # Auto save play times to disk + if pctl.total_playtime - time_last_save > 600: + try: + if should_save_state: + logging.info("Auto save playtime") + with (user_directory / "star.p").open("wb") as file: + pickle.dump(star_store.db, file, protocol=pickle.HIGHEST_PROTOCOL) + else: + logging.info("Dev mode, skip auto saving playtime") + except PermissionError: + logging.exception("Permission error encountered while writing database") + show_message(_("Permission error encountered while writing database"), "error") + except Exception: + logging.exception("Unknown error encountered while writing database") + time_last_save = pctl.total_playtime - # Save power if the window is minimized - if gui.lowered: - time.sleep(0.2) + # Always render at least one frame per minute (to avoid SDL bugs I guess) + if min_render_timer.get() > 60: + min_render_timer.set() + gui.pl_update = 1 + gui.update += 1 -if tauon.spot_ctl.playing: - tauon.spot_ctl.control("stop") + # Save power if the window is minimized + if gui.lowered: + time.sleep(0.2) -# Send scrobble if pending -if lfm_scrobbler.queue and not lfm_scrobbler.running: - lfm_scrobbler.start_queue() - logging.info("Sending scrobble before close...") + if tauon.spot_ctl.playing: + tauon.spot_ctl.control("stop") -if gui.mode < 3: - old_window_position = get_window_position() + # Send scrobble if pending + if lfm_scrobbler.queue and not lfm_scrobbler.running: + lfm_scrobbler.start_queue() + logging.info("Sending scrobble before close...") + if gui.mode < 3: + old_window_position = get_window_position() -SDL_DestroyTexture(gui.main_texture) -SDL_DestroyTexture(gui.tracklist_texture) -SDL_DestroyTexture(gui.spec2_tex) -SDL_DestroyTexture(gui.spec1_tex) -SDL_DestroyTexture(gui.spec_level_tex) -ddt.clear_text_cache() -clear_img_cache(False) -SDL_DestroyWindow(t_window) + SDL_DestroyTexture(gui.main_texture) + SDL_DestroyTexture(gui.tracklist_texture) + SDL_DestroyTexture(gui.spec2_tex) + SDL_DestroyTexture(gui.spec1_tex) + SDL_DestroyTexture(gui.spec_level_tex) + ddt.clear_text_cache() + clear_img_cache(False) -pctl.playerCommand = "unload" -pctl.playerCommandReady = True + SDL_DestroyWindow(t_window) -if prefs.reload_play_state and pctl.playing_state in (1, 2): - logging.info("Saving play state...") - prefs.reload_state = (pctl.playing_state, pctl.playing_time) + pctl.playerCommand = "unload" + pctl.playerCommandReady = True -if should_save_state: - with (user_directory / "star.p").open("wb") as file: - pickle.dump(star_store.db, file, protocol=pickle.HIGHEST_PROTOCOL) - with (user_directory / "album-star.p").open("wb") as file: - pickle.dump(album_star_store.db, file, protocol=pickle.HIGHEST_PROTOCOL) + if prefs.reload_play_state and pctl.playing_state in (1, 2): + logging.info("Saving play state...") + prefs.reload_state = (pctl.playing_state, pctl.playing_time) -gui.gallery_positions[pl_to_id(pctl.active_playlist_viewing)] = gui.album_scroll_px -save_state() + if should_save_state: + with (user_directory / "star.p").open("wb") as file: + pickle.dump(star_store.db, file, protocol=pickle.HIGHEST_PROTOCOL) + with (user_directory / "album-star.p").open("wb") as file: + pickle.dump(album_star_store.db, file, protocol=pickle.HIGHEST_PROTOCOL) -date = datetime.date.today() -if should_save_state: - with (user_directory / "star.p.backup").open("wb") as file: - pickle.dump(star_store.db, file, protocol=pickle.HIGHEST_PROTOCOL) - with (user_directory / f"star.p.backup{str(date.month)}").open("wb") as file: - pickle.dump(star_store.db, file, protocol=pickle.HIGHEST_PROTOCOL) + gui.gallery_positions[pl_to_id(pctl.active_playlist_viewing)] = gui.album_scroll_px + save_state() -if tauon.stream_proxy and tauon.stream_proxy.download_running: - logging.info("Stopping stream...") - tauon.stream_proxy.stop() - time.sleep(2) + date = datetime.date.today() + if should_save_state: + with (user_directory / "star.p.backup").open("wb") as file: + pickle.dump(star_store.db, file, protocol=pickle.HIGHEST_PROTOCOL) + with (user_directory / f"star.p.backup{str(date.month)}").open("wb") as file: + pickle.dump(star_store.db, file, protocol=pickle.HIGHEST_PROTOCOL) -try: - if tauon.thread_manager.player_lock.locked(): - tauon.thread_manager.player_lock.release() -except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked player_lock") - else: - logging.exception("Unknown RuntimeError trying to release player_lock") -except Exception: - logging.exception("Unknown error trying to release player_lock") + if tauon.stream_proxy and tauon.stream_proxy.download_running: + logging.info("Stopping stream...") + tauon.stream_proxy.stop() + time.sleep(2) -if tauon.radio_server is not None: try: - tauon.radio_server.server_close() + if tauon.thread_manager.player_lock.locked(): + tauon.thread_manager.player_lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked player_lock") + else: + logging.exception("Unknown RuntimeError trying to release player_lock") except Exception: - logging.exception("Failed to close radio server") + logging.exception("Unknown error trying to release player_lock") + + if tauon.radio_server is not None: + try: + tauon.radio_server.server_close() + except Exception: + logging.exception("Failed to close radio server") + + if system == "Windows" or msys: + tray.stop() + if smtc: + sm.unload() + elif de_notify_support: + try: + song_notification.close() + g_tc_notify.close() + Notify.uninit() + except Exception: + logging.exception("uninit notification error") -if system == "Windows" or msys: - tray.stop() - if smtc: - sm.unload() -elif de_notify_support: try: - song_notification.close() - g_tc_notify.close() - Notify.uninit() + instance_lock.close() except Exception: - logging.exception("uninit notification error") + logging.exception("No lock object to close") -try: - instance_lock.close() -except Exception: - logging.exception("No lock object to close") + #28REWORK + IMG_Quit() + SDL_QuitSubSystem(SDL_INIT_EVERYTHING) + SDL_Quit() + #logging.info("SDL unloaded") -IMG_Quit() -SDL_QuitSubSystem(SDL_INIT_EVERYTHING) -SDL_Quit() -#logging.info("SDL unloaded") + exit_timer = Timer() + exit_timer.set() -exit_timer = Timer() -exit_timer.set() + if not tauon.quick_close: + while tauon.thread_manager.check_playback_running(): + time.sleep(0.2) + if exit_timer.get() > 2: + logging.warning("Phazor unload timeout") + break -if not tauon.quick_close: - while tauon.thread_manager.check_playback_running(): - time.sleep(0.2) - if exit_timer.get() > 2: - logging.warning("Phazor unload timeout") - break + while lfm_scrobbler.running: + time.sleep(0.2) + lfm_scrobbler.running = False + if exit_timer.get() > 15: + logging.warning("Scrobble wait timeout") + break - while lfm_scrobbler.running: - time.sleep(0.2) - lfm_scrobbler.running = False - if exit_timer.get() > 15: - logging.warning("Scrobble wait timeout") - break + if tauon.sleep_lock is not None: + del tauon.sleep_lock + if tauon.shutdown_lock is not None: + del tauon.shutdown_lock + if tauon.play_lock is not None: + del tauon.play_lock -if tauon.sleep_lock is not None: - del tauon.sleep_lock -if tauon.shutdown_lock is not None: - del tauon.shutdown_lock -if tauon.play_lock is not None: - del tauon.play_lock - -if tauon.librespot_p: - time.sleep(1) - logging.info("Killing librespot") - tauon.librespot_p.kill() - #tauon.librespot_p.communicate() - -cache_dir = tmp_cache_dir() -if os.path.isdir(cache_dir): - # This check can be Windows only, lazy deletes are fine on Linux/macOS - if sys.platform == "win32": - while tauon.cachement.running: - logging.warning("Waiting for caching to stop before deleting cache directory…") - time.sleep(0.2) - logging.info("Clearing tmp cache") - shutil.rmtree(cache_dir) + if tauon.librespot_p: + time.sleep(1) + logging.info("Killing librespot") + tauon.librespot_p.kill() + #tauon.librespot_p.communicate() + + cache_dir = tmp_cache_dir() + if os.path.isdir(cache_dir): + # This check can be Windows only, lazy deletes are fine on Linux/macOS + if sys.platform == "win32": + while tauon.cachement.running: + logging.warning("Waiting for caching to stop before deleting cache directory…") + time.sleep(0.2) + logging.info("Clearing tmp cache") + shutil.rmtree(cache_dir) -logging.info("Bye!") + logging.info("Bye!") diff --git a/src/tauon/t_modules/t_main_rework.py b/src/tauon/t_modules/t_main_rework.py new file mode 100644 index 000000000..711952d73 --- /dev/null +++ b/src/tauon/t_modules/t_main_rework.py @@ -0,0 +1,23031 @@ +from __future__ import annotations + +import base64 +import builtins +import certifi +import colorsys +import copy +import ctypes +import ctypes.util +import datetime +import gc as gbc +import gettext +import glob +import hashlib +import io +import json +import locale as py_locale +import logging +#import magic +import math +#import mimetypes +import os +import pickle +import platform +import random +import re +import secrets +import shlex +import shutil +import signal +import ssl +import socket +import subprocess +import sys +import threading +import time +import urllib.parse +import urllib.request +import webbrowser +import xml.etree.ElementTree as ET +import zipfile +from collections import OrderedDict +from ctypes import Structure, byref, c_char_p, c_double, c_int, c_uint32, c_void_p, pointer +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from tauon.t_modules.t_bootstrap import Holder + +import musicbrainzngs +import mutagen +import mutagen.flac +import mutagen.id3 +import mutagen.mp4 +import mutagen.oggvorbis +import requests +from bs4 import BeautifulSoup +from PIL import Image, ImageDraw, ImageEnhance, ImageFilter +from sdl2 import ( + SDL_BLENDMODE_BLEND, + SDL_BLENDMODE_NONE, + SDL_BUTTON_LEFT, + SDL_BUTTON_MIDDLE, + SDL_BUTTON_RIGHT, + SDL_BUTTON_X1, + SDL_BUTTON_X2, + SDL_CONTROLLER_AXIS_LEFTY, + SDL_CONTROLLER_AXIS_RIGHTX, + SDL_CONTROLLER_AXIS_RIGHTY, + SDL_CONTROLLER_AXIS_TRIGGERLEFT, + SDL_CONTROLLER_BUTTON_A, + SDL_CONTROLLER_BUTTON_B, + SDL_CONTROLLER_BUTTON_DPAD_DOWN, + SDL_CONTROLLER_BUTTON_DPAD_LEFT, + SDL_CONTROLLER_BUTTON_DPAD_RIGHT, + SDL_CONTROLLER_BUTTON_DPAD_UP, + SDL_CONTROLLER_BUTTON_LEFTSHOULDER, + SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, + SDL_CONTROLLER_BUTTON_X, + SDL_CONTROLLER_BUTTON_Y, + SDL_CONTROLLERAXISMOTION, + SDL_CONTROLLERBUTTONDOWN, + SDL_CONTROLLERDEVICEADDED, + SDL_DROPFILE, + SDL_DROPTEXT, + SDL_FALSE, + SDL_HITTEST_DRAGGABLE, + SDL_HITTEST_NORMAL, + SDL_HITTEST_RESIZE_BOTTOM, + SDL_HITTEST_RESIZE_BOTTOMLEFT, + SDL_HITTEST_RESIZE_BOTTOMRIGHT, + SDL_HITTEST_RESIZE_LEFT, + SDL_HITTEST_RESIZE_RIGHT, + SDL_HITTEST_RESIZE_TOPLEFT, + SDL_HITTEST_RESIZE_TOPRIGHT, + SDL_INIT_EVERYTHING, + SDL_INIT_GAMECONTROLLER, + SDL_KEYDOWN, + SDL_KEYUP, + SDL_MOUSEBUTTONDOWN, + SDL_MOUSEBUTTONUP, + SDL_MOUSEMOTION, + SDL_MOUSEWHEEL, + SDL_PIXELFORMAT_ARGB8888, + SDL_QUIT, + SDL_RENDER_TARGETS_RESET, + SDL_SCANCODE_A, + SDL_SCANCODE_C, + SDL_SCANCODE_V, + SDL_SCANCODE_X, + SDL_SCANCODE_Z, + SDL_SYSTEM_CURSOR_ARROW, + SDL_SYSTEM_CURSOR_HAND, + SDL_SYSTEM_CURSOR_IBEAM, + SDL_SYSTEM_CURSOR_SIZENS, + SDL_SYSTEM_CURSOR_SIZENWSE, + SDL_SYSTEM_CURSOR_SIZEWE, + SDL_SYSWM_COCOA, + SDL_SYSWM_UNKNOWN, + SDL_SYSWM_WAYLAND, + SDL_SYSWM_X11, + SDL_TEXTEDITING, + SDL_TEXTINPUT, + SDL_TEXTUREACCESS_TARGET, + SDL_TRUE, + SDL_WINDOW_FULLSCREEN_DESKTOP, + SDL_WINDOW_INPUT_FOCUS, + SDL_WINDOWEVENT, + SDL_WINDOWEVENT_DISPLAY_CHANGED, + SDL_WINDOWEVENT_ENTER, + SDL_WINDOWEVENT_EXPOSED, + SDL_WINDOWEVENT_FOCUS_GAINED, + SDL_WINDOWEVENT_FOCUS_LOST, + SDL_WINDOWEVENT_LEAVE, + SDL_WINDOWEVENT_MAXIMIZED, + SDL_WINDOWEVENT_MINIMIZED, + SDL_WINDOWEVENT_RESIZED, + SDL_WINDOWEVENT_RESTORED, + SDL_WINDOWEVENT_SHOWN, + SDLK_BACKSPACE, + SDLK_DELETE, + SDLK_DOWN, + SDLK_END, + SDLK_HOME, + SDLK_KP_ENTER, + SDLK_LALT, + SDLK_LCTRL, + SDLK_LEFT, + SDLK_LGUI, + SDLK_LSHIFT, + SDLK_RALT, + SDLK_RCTRL, + SDLK_RETURN, + SDLK_RETURN2, + SDLK_RIGHT, + SDLK_RSHIFT, + SDLK_TAB, + SDLK_UP, + SDL_CaptureMouse, + SDL_CreateColorCursor, + SDL_CreateRGBSurfaceWithFormatFrom, + SDL_CreateSystemCursor, + SDL_CreateTexture, + SDL_CreateTextureFromSurface, + SDL_Delay, + SDL_DestroyTexture, + SDL_DestroyWindow, + SDL_Event, + SDL_FreeSurface, + SDL_GameControllerNameForIndex, + SDL_GameControllerOpen, + SDL_GetClipboardText, + SDL_GetCurrentVideoDriver, + SDL_GetGlobalMouseState, + SDL_GetKeyFromName, + SDL_GetMouseState, + SDL_GetScancodeFromName, + SDL_GetVersion, + SDL_GetWindowFlags, + SDL_GetWindowPosition, + SDL_GetWindowSize, + SDL_GetWindowWMInfo, + SDL_GL_GetDrawableSize, + SDL_HasClipboardText, + SDL_HideWindow, + SDL_HitTest, + SDL_InitSubSystem, + SDL_IsGameController, + SDL_MaximizeWindow, + SDL_MinimizeWindow, + SDL_PollEvent, + SDL_PumpEvents, + SDL_PushEvent, + SDL_QueryTexture, + SDL_Quit, + SDL_QuitSubSystem, + SDL_RaiseWindow, + SDL_Rect, + SDL_RenderClear, + SDL_RenderCopy, + SDL_RenderFillRect, + SDL_RenderPresent, + SDL_RestoreWindow, + SDL_SetClipboardText, + SDL_SetCursor, + SDL_SetRenderDrawBlendMode, + SDL_SetRenderDrawColor, + SDL_SetRenderTarget, + SDL_SetTextInputRect, + SDL_SetTextureAlphaMod, + SDL_SetTextureBlendMode, + SDL_SetTextureColorMod, + SDL_SetWindowAlwaysOnTop, + SDL_SetWindowBordered, + SDL_SetWindowFullscreen, + SDL_SetWindowHitTest, + SDL_SetWindowIcon, + SDL_SetWindowMinimumSize, + SDL_SetWindowOpacity, + SDL_SetWindowPosition, + SDL_SetWindowResizable, + SDL_SetWindowSize, + SDL_SetWindowTitle, + SDL_ShowWindow, + SDL_StartTextInput, + SDL_SysWMinfo, + SDL_version, + SDL_WaitEventTimeout, + SDLK_a, + SDLK_c, + SDLK_v, + SDLK_x, + SDLK_z, + rw_from_object, +) +from sdl2.sdlimage import IMG_Load, IMG_Load_RW, IMG_Quit +from send2trash import send2trash +from unidecode import unidecode + +builtins._ = lambda x: x + +from tauon.t_modules import t_bootstrap +from tauon.t_modules.t_config import Config +from tauon.t_modules.t_db_migrate import database_migrate +from tauon.t_modules.t_dbus import Gnome +from tauon.t_modules.t_draw import QuickThumbnail, TDraw +from tauon.t_modules.t_extra import ( + ColourGenCache, + FunctionStore, + TauonPlaylist, + TauonQueueItem, + TestTimer, + Timer, + alpha_blend, + alpha_mod, + archive_file_scan, + check_equal, + clean_string, + colour_slide, + colour_value, + commonprefix, + contrast_ratio, + d_date_display, + d_date_display2, + filename_safe, + filename_to_metadata, + fit_box, + folder_file_scan, + genre_correct, + get_artist_safe, + get_artist_strip_feat, + get_display_time, + get_filesize_string, + get_filesize_string_rounded, + get_folder_size, + get_hms_time, + get_split_artists, + get_year_from_string, + grow_rect, + hls_to_rgb, + hms_to_seconds, + hsl_to_rgb, + is_grey, + is_light, + j_chars, + mac_styles, + point_distance, + point_proximity_test, + process_odat, + reduce_paths, + rgb_add_hls, + rgb_to_hls, + search_magic, + search_magic_any, + seconds_to_day_hms, + shooter, + sleep_timeout, + star_count, + star_count3, + subtract_rect, + test_lumi, + tmp_cache_dir, + tryint, + uri_parse, + year_search, +) +from tauon.t_modules.t_jellyfin import Jellyfin +from tauon.t_modules.t_launch import Launch +from tauon.t_modules.t_lyrics import genius, lyric_sources, uses_scraping +from tauon.t_modules.t_phazor import phazor_exists, player4 +from tauon.t_modules.t_prefs import Prefs +from tauon.t_modules.t_search import bandcamp_search +from tauon.t_modules.t_spot import SpotCtl +from tauon.t_modules.t_stream import StreamEnc +from tauon.t_modules.t_tagscan import Ape, Flac, M4a, Opus, Wav, parse_picture_block +from tauon.t_modules.t_themeload import Deco, load_theme +from tauon.t_modules.t_tidal import Tidal +from tauon.t_modules.t_webserve import authserve, controller, stream_proxy, webserve, webserve2 +#from tauon.t_modules.guitar_chords import GuitarChords + +class LoadImageAsset: + assets: list[LoadImageAsset] = [] + + def __init__(self, *, scaled_asset_directory: Path, path: str, is_full_path: bool = False, reload: bool = False, scale_name: str = "") -> None: + if not reload: + self.assets.append(self) + + self.path = path + self.scale_name = scale_name + self.scaled_asset_directory: Path = scaled_asset_directory + + raw_image = IMG_Load(self.path.encode()) + self.sdl_texture = SDL_CreateTextureFromSurface(renderer, raw_image) + + p_w = pointer(c_int(0)) + p_h = pointer(c_int(0)) + SDL_QueryTexture(self.sdl_texture, None, None, p_w, p_h) + + if is_full_path: + SDL_SetTextureAlphaMod(self.sdl_texture, prefs.custom_bg_opacity) + + self.rect = SDL_Rect(0, 0, p_w.contents.value, p_h.contents.value) + SDL_FreeSurface(raw_image) + self.w = p_w.contents.value + self.h = p_h.contents.value + + def reload(self) -> None: + SDL_DestroyTexture(self.sdl_texture) + if self.scale_name: + self.path = str(self.scaled_asset_directory / self.scale_name) + self.__init__(scaled_asset_directory=scaled_asset_directory, path=self.path, reload=True, scale_name=self.scale_name) + + def render(self, x: int, y: int, colour=None) -> None: + self.rect.x = round(x) + self.rect.y = round(y) + SDL_RenderCopy(renderer, self.sdl_texture, None, self.rect) + +class WhiteModImageAsset: + assets: list[WhiteModImageAsset] = [] + + def __init__(self, *, scaled_asset_directory: Path, path: str, reload: bool = False, scale_name: str = ""): + if not reload: + self.assets.append(self) + self.path = path + self.scale_name = scale_name + self.scaled_asset_directory: Path = scaled_asset_directory + + raw_image = IMG_Load(path.encode()) + self.sdl_texture = SDL_CreateTextureFromSurface(renderer, raw_image) + self.colour = [255, 255, 255, 255] + p_w = pointer(c_int(0)) + p_h = pointer(c_int(0)) + SDL_QueryTexture(self.sdl_texture, None, None, p_w, p_h) + self.rect = SDL_Rect(0, 0, p_w.contents.value, p_h.contents.value) + SDL_FreeSurface(raw_image) + self.w = p_w.contents.value + self.h = p_h.contents.value + + def reload(self) -> None: + SDL_DestroyTexture(self.sdl_texture) + if self.scale_name: + self.path = str(self.scaled_asset_directory / self.scale_name) + self.__init__(scaled_asset_directory=scaled_asset_directory, path=self.path, reload=True, scale_name=self.scale_name) + + def render(self, x: int, y: int, colour) -> None: + if colour != self.colour: + SDL_SetTextureColorMod(self.sdl_texture, colour[0], colour[1], colour[2]) + SDL_SetTextureAlphaMod(self.sdl_texture, colour[3]) + self.colour = colour + self.rect.x = round(x) + self.rect.y = round(y) + SDL_RenderCopy(renderer, self.sdl_texture, None, self.rect) + +class DConsole: + """GUI console with logs""" + + def __init__(self) -> None: + self.show: bool = False + + def toggle(self) -> None: + """Toggle the GUI console with logs on and off""" + self.show ^= True + +class GuiVar: + """Use to hold any variables for use in relation to UI""" + + def update_layout(self) -> None: + global update_layout + update_layout = True + + def show_message(self, line1: str, line2: str = "", line3: str = "", mode: str = "info") -> None: + show_message(line1, line2, line3, mode=mode) + + def delay_frame(self, t): + self.frame_callback_list.append(TestTimer(t)) + + def destroy_textures(self): + SDL_DestroyTexture(self.spec4_tex) + SDL_DestroyTexture(self.spec1_tex) + SDL_DestroyTexture(self.spec2_tex) + SDL_DestroyTexture(self.spec_level_tex) + + # def test_text_input(self): + # if self.text_input_request and not self.text_input_active: + # SDL_StartTextInput() + # self.update += 1 + # if not self.text_input_request and self.text_input_active: + # SDL_StopTextInput() + # self.text_input_request = False + + def rescale(self): + self.spec_y = int(round(5 * self.scale)) + self.spec_w = int(round(80 * self.scale)) + self.spec_h = int(round(20 * self.scale)) + self.spec1_rec = SDL_Rect(0, self.spec_y, self.spec_w, self.spec_h) + + self.spec4_y = int(round(200 * self.scale)) + self.spec4_w = int(round(322 * self.scale)) + self.spec4_h = int(round(100 * self.scale)) + self.spec4_rec = SDL_Rect(0, self.spec4_y, self.spec4_w, self.spec4_h) + + self.bar = SDL_Rect(10, 10, round(3 * self.scale), 10) # spec bar bin + self.bar4 = SDL_Rect(10, 10, round(3 * self.scale), 10) # spec bar bin + self.set_height = round(25 * self.scale) + self.panelBY = round(51 * self.scale) + self.panelY = round(30 * self.scale) + self.panelY2 = round(30 * self.scale) + self.playlist_top = self.panelY + (8 * self.scale) + self.playlist_top_bk = self.playlist_top + self.scroll_hide_box = (0, self.panelY, 28, window_size[1] - self.panelBY - self.panelY) + + self.spec2_y = int(round(22 * self.scale)) + self.spec2_w = int(round(140 * self.scale)) + self.spec2 = [0] * self.spec2_y + self.spec2_phase = 0 + self.spec2_buffers = [] + self.spec2_rec = SDL_Rect(1230, round(4 * self.scale), self.spec2_w, self.spec2_y) + self.spec2_source = SDL_Rect(900, round(4 * self.scale), self.spec2_w, self.spec2_y) + self.spec2_dest = SDL_Rect(900, round(4 * self.scale), self.spec2_w, self.spec2_y) + self.spec2_position = 0 + self.spec2_timer = Timer() + self.spec2_timer.set() + + self.level_w = 5 * self.scale + self.level_y = 16 * self.scale + self.level_s = 1 * self.scale + self.level_ww = round(79 * self.scale) + self.level_hh = round(18 * self.scale) + self.spec_level_rec = SDL_Rect( + 0, round(self.level_y - 10 * self.scale), round(self.level_ww),round(self.level_hh)) + + self.spec2_tex = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, self.spec2_w, self.spec2_y) + self.spec4_tex = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, self.spec4_w, self.spec4_y) + self.spec1_tex = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, self.spec_w, self.spec_h) + self.spec_level_tex = SDL_CreateTexture( + renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, self.level_ww, self.level_hh) + SDL_SetTextureBlendMode(self.spec4_tex, SDL_BLENDMODE_BLEND) + self.artist_panel_height = 320 * self.scale + self.last_artist_panel_height = self.artist_panel_height + + self.window_control_hit_area_w = 100 * self.scale + self.window_control_hit_area_h = 30 * self.scale + + def __init__(self, prefs: Prefs): + + self.scale = prefs.ui_scale + + self.window_id = 0 + self.update = 2 # UPDATE + self.turbo = True + self.turbo_next = 0 + self.pl_update = 1 + self.lowered = False + self.request_raise = False + self.maximized = False + + self.message_box = False + self.message_text = "" + self.message_mode = "info" + self.message_subtext = "" + self.message_subtext2 = "" + self.message_box_confirm_reference = None + self.message_box_use_reference = True + self.message_box_confirm_callback = None + + self.save_size = [450, 310] + self.show_playlist = True + self.show_bottom_title = False + # self.show_top_title = True + self.search_error = False + + self.level_update = False + self.level_time = Timer() + self.level_peak: list[float] = [0, 0] + self.level = 0 + self.time_passed = 0 + self.level_meter_colour_mode = 3 + + self.vis = 0 # visualiser mode actual + self.vis_want = 2 # visualiser mode setting + self.spec = None + self.s_spec = [0] * 24 + self.s4_spec = [0] * 45 + self.update_spec = 0 + + # self.spec_rect = [0, 5, 80, 20] # x = 72 + 24 - 6 - 10 + + self.spec4_array = [] + + self.draw_spec4 = False + + self.combo_mode = False + self.showcase_mode = False + self.display_time_mode = 0 + + self.pl_text_real_height = 12 + self.pl_title_real_height = 11 + + self.row_extra = 0 + self.test = False + self.light_mode = False + + self.level_2_click = False + self.universal_y_text_offset = 0 + + self.star_text_y_offset = 0 + if system == "Windows": + self.star_text_y_offset = -2 + + self.set_bar = True + self.set_mode = False + self.set_hold = -1 + self.set_label_hold = -1 + self.set_label_point = (0, 0) + self.set_point = 0 + self.set_old = 0 + self.pl_st = [ + ["Artist", 156, False], ["Title", 188, False], ["T", 40, True], ["Album", 153, False], + ["P", 28, True], ["Starline", 86, True], ["Date", 48, True], ["Codec", 55, True], + ["Time", 53, True]] + + for item in self.pl_st: + item[1] = item[1] * self.scale + + self.offset_extra: int = 0 + + self.playlist_row_height: int = 16 + self.playlist_text_offset: int = 0 + self.row_font_size: int = 13 + self.compact_bar = False + self.tracklist_texture_rect = tracklist_texture_rect + self.tracklist_texture = tracklist_texture + + self.trunk_end = "..." # "…" + self.temp_themes = {} + self.theme_temp_current = -1 + + self.pl_title_y_offset = 0 + self.pl_title_font_offset = -1 + + self.playlist_box_d_click = -1 + + self.gallery_show_text = True + self.bb_show_art = False + + self.rename_folder_box = False + + self.present = False + self.drag_source_position = (0, 0) + self.drag_source_position_persist = (0, 0) + self.album_tab_mode = False + self.main_art_box = (0, 0, 10, 10) + self.gall_tab_enter = False + + self.lightning_copy = False + + self.gallery_animate_highlight_on = 0 + + self.seek_cur_show = False + self.cur_time = "0" + self.force_showcase_index = -1 + + self.frame_callback_list = [] + + self.playlist_left = None + self.image_downloading = False + self.tc_cancel = False + self.im_cancel = False + self.force_search = False + + self.pl_pulse = False + + self.view_name = "S" + self.restart_album_mode = False + + self.dtm3_index = -1 + self.dtm3_cum = 0 + self.dtm3_total = 0 + self.previous_playlist_id = "" + + self.star_mode = "line" + self.heart_fields = [] + self.show_ratings = False + + self.web_running = False + + self.rsp = True + if phone: + self.rsp = False + self.rspw = round(300 * self.scale) + self.lsp = False + self.lspw = round(220 * self.scale) + self.plw = None + + self.pref_rspw = 300 + + self.pref_gallery_w = 600 + + self.artist_info_panel = False + + self.show_hearts = True + + self.cursor_is = 0 + self.cursor_want = 0 + # 0 standard + # 1 drag horizontal + # 2 text + # 3 hand + + self.power_bar = None + self.gallery_scroll_field_left = 1 + self.combo_was_album = False + + self.gallery_positions = {} + + self.remember_library_mode = False + + self.first_in_grid = None + + self.art_aspect_ratio = 1 + self.art_drawn_rect = None + self.art_unlock_ratio = False + self.art_max_ratio_lock = 1 + self.side_bar_drag_source = 0 + self.side_bar_drag_original = 0 + + self.scroll_direction = 0 + self.add_music_folder_ready = False + + self.playlist_current_visible_tracks = 0 + self.playlist_current_visible_tracks_id = 0 + + self.theme_name = "" + self.rename_playlist_box = False + self.queue_frame_draw = None # Set when need draw frame later + + self.mode = 1 + + self.save_position = [0, 0] + + self.draw_vis4_top = False + # self.vis_4_colour = [0,0,0,255] + self.vis_4_colour = None + + self.layer_focus = 0 + self.tab_menu_pl = 0 + + self.tool_tip_lock_off_f = False + self.tool_tip_lock_off_b = False + + self.auto_play_import = False + + self.transcoding_batch_total = 0 + self.transcoding_bach_done = 0 + + self.seek_bar_rect = (0, 0, 0, 0) + self.volume_bar_rect = (0, 0, 0, 0) + + self.mini_mode_return_maximized = False + + self.opened_config_file = False + + self.notify_main_id = None + + self.halt_image_rendering = False + self.generating_chart = False + + self.top_bar_mode2 = False + self.mode_toast_text = "" + + self.rescale() + # self.smooth_scrolling = False + + self.compact_artist_list = False + + self.rsp_full_lock = False + + self.album_scroll_px = album_v_slide_value + self.queue_toast_plural = False + self.reload_theme = False + self.theme_number = 0 + self.toast_queue_object: TauonQueueItem | None = None + self.toast_love_object = None + self.toast_love_added = True + + self.force_side_on_drag = False + self.last_left_panel_mode = "playlist" + self.showing_l_panel = False + + self.downloading_bass = False + self.d_click_ref = -1 + + self.max_window_tex = max_window_tex + self.main_texture = main_texture + self.main_texture_overlay_temp = main_texture_overlay_temp + + self.preview_artist = "" + self.preview_artist_location = (0, 0) + self.preview_artist_loading = "" + self.mouse_left_window = False + + self.rendered_playlist_position = 0 + + self.console = console + self.show_album_ratings = False + self.gen_code_errors = False + + self.regen_single = -1 + self.regen_single_id = None + + self.tracklist_bg_is_light = False + self.clear_image_cache_next = 0 + + self.column_d_click_timer = Timer(10) + self.column_d_click_on = -1 + self.column_sort_ani_timer = Timer(10) + self.column_sort_down_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "sort-down.png", True) + self.column_sort_up_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "sort-up.png", True) + self.column_sort_ani_direction = 1 + self.column_sort_ani_x = 0 + + self.restore_showcase_view = False + self.restore_radio_view = False + + self.tracklist_center_mode = False + self.tracklist_inset_left = 0 + self.tracklist_inset_width = 0 + self.tracklist_highlight_width = 0 + self.tracklist_highlight_left = 0 + + self.hide_tracklist_in_gallery = False + + self.saved_prime_tab = 0 + self.saved_prime_direction = 0 + + self.stop_sync = False + self.sync_progress = "" + self.sync_speed = "" + + self.bar_hover_timer = Timer() + + self.level_decay_timer = Timer() + + self.showed_title = False + + self.to_get = 0 + self.to_got = 0 + self.switch_showcase_off = False + + self.backend_reloading = False + + self.spot_info_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "spot-info.png", True) + self.tray_active = False + self.buffering = False + self.buffering_text = "" + + self.update_on_drag = False + self.pl_update_on_drag = False + self.drop_playlist_target = 0 + self.discord_status = "Standby" + self.mouse_unknown = False + self.macstyle = prefs.macstyle + if macos or detect_macstyle: + self.macstyle = True + self.radio_view = False + self.window_size = window_size + self.box_over = False + self.suggest_clean_db = False + self.style_worker_timer = Timer() + + self.shuffle_was_showcase = False + self.shuffle_was_random = True + self.shuffle_was_repeat = False + + self.was_radio = False + self.fullscreen = False + self.mouse_in_window = True + + self.write_tag_in_progress = False + self.tag_write_count = 0 + # self.text_input_request = False + # self.text_input_active = False + self.center_blur_pixel = (0, 0, 0) + +class StarStore: + def __init__(self) -> None: + self.db = {} + + def key(self, track_id: int) -> tuple[str, str, str]: + track_object = pctl.master_library[track_id] + return track_object.artist, track_object.title, track_object.filename + + def object_key(self, track: TrackClass) -> tuple[str, str, str]: + return track.artist, track.title, track.filename + + def add(self, index: int, value): + """Increments the play time""" + track_object = pctl.master_library[index] + + if after_scan: + if track_object in after_scan: + return + + key = track_object.artist, track_object.title, track_object.filename + + if key in self.db: + self.db[key][0] += value + if value < 0 and self.db[key][0] < 0: + self.db[key][0] = 0 + else: + self.db[key] = [value, "", 0, 0] # Playtime in s, flags, rating, love timestamp + + def get(self, index: int): + """Returns the track play time""" + if index < 0: + return 0 + return self.db.get(self.key(index), (0,))[0] + + def get_rating(self, index: int): + """Returns the track user rating""" + key = self.key(index) + if key in self.db: + # self.db[key] + return self.db[key][2] + return 0 + + def set_rating(self, index: int, value: int, write: bool = False) -> None: + """Sets the track user rating""" + key = self.key(index) + if key not in self.db: + self.db[key] = self.new_object() + self.db[key][2] = value + + tr = pctl.get_track(index) + if tr.file_ext == "SUB": + self.db[key][2] = math.ceil(value / 2) * 2 + shooter(subsonic.set_rating, (tr, value)) + + if prefs.write_ratings and write: + logging.info("Writing rating..") + assert value <= 10 + assert value >= 0 + + if tr.file_ext == "OGG" or tr.file_ext == "OPUS": + tag = mutagen.oggvorbis.OggVorbis(tr.fullpath) + if value == 0: + if "FMPS_RATING" in tag: + del tag["FMPS_RATING"] + tag.save() + else: + tag["FMPS_RATING"] = [f"{value / 10:.2f}"] + tag.save() + + elif tr.file_ext == "MP3": + tag = mutagen.id3.ID3(tr.fullpath) + + # if True: + # if value == 0: + # tag.delall("POPM") + # else: + # p_rating = 0 + # + # tag.add(mutagen.id3.POPM(email="Windows Media Player 9 Series", rating=int)) + + if value == 0: + changed = False + frames = tag.getall("TXXX") + for i in reversed(range(len(frames))): + if frames[i].desc.lower() == "fmps_rating": + changed = True + if changed: + tag.delall("TXXX:FMPS_RATING") + tag.save() + else: + changed = False + frames = tag.getall("TXXX") + for i in reversed(range(len(frames))): + if frames[i].desc.lower() == "fmps_rating": + frames[i].text = f"{value / 10:.2f}" + changed = True + if not changed: + tag.add( + mutagen.id3.TXXX( + encoding=mutagen.id3.Encoding.UTF8, text=f"{value / 10:.2f}", + desc="FMPS_RATING")) + tag.save() + + elif tr.file_ext == "FLAC": + audio = mutagen.flac.FLAC(tr.fullpath) + tags = audio.tags + if value == 0: + if "FMPS_Rating" in tags: + del tags["FMPS_Rating"] + audio.save() + else: + tags["FMPS_Rating"] = f"{value / 10:.2f}" + audio.save() + + tr.misc["FMPS_Rating"] = float(value / 10) + if value == 0: + del tr.misc["FMPS_Rating"] + + def new_object(self): + return [0, "", 0, 0] + + def get_by_object(self, track: TrackClass): + + return self.db.get(self.object_key(track), (0,))[0] + + def get_total(self): + + return sum(item[0] for item in self.db.values()) + + def full_get(self, index: int): + return self.db.get(self.key(index)) + + def remove(self, index: int): + key = self.key(index) + if key in self.db: + del self.db[key] + + def insert(self, index: int, object): + key = self.key(index) + self.db[key] = object + + def merge(self, index: int, object): + if object is None or object == self.new_object(): + return + key = self.key(index) + if key not in self.db: + self.db[key] = object + else: + self.db[key][0] += object[0] + self.db[key][2] = object[2] + for cha in object[1]: + if cha not in self.db[key][1]: + self.db[key][1] += cha + +class AlbumStarStore: + + def __init__(self) -> None: + self.db = {} + + def get_key(self, track_object: TrackClass) -> str: + artist = track_object.album_artist + if not artist: + artist = track_object.artist + return artist + ":" + track_object.album + + def get_rating(self, track_object: TrackClass): + return self.db.get(self.get_key(track_object), 0) + + def set_rating(self, track_object: TrackClass, rating): + self.db[self.get_key(track_object)] = rating + if track_object.file_ext == "SUB": + self.db[self.get_key(track_object)] = math.ceil(rating / 2) * 2 + subsonic.set_album_rating(track_object, rating) + + def set_rating_artist_title(self, artist: str, album: str, rating): + self.db[artist + ":" + album] = rating + + def get_rating_artist_title(self, artist: str, album: str): + return self.db.get(artist + ":" + album, 0) + +class Fonts: + """Used to hold font sizes (I forget to use this)""" + + def __init__(self): + self.tabs = 211 + self.panel_title = 213 + + self.side_panel_line1 = 214 + self.side_panel_line2 = 13 + + self.bottom_panel_time = 212 + + # if system == 'Windows': + # self.bottom_panel_time = 12 # The Arial bold font is too big so just leaving this as normal. (lazy) + +class Input: + """Used to keep track of button states (or should be)""" + + def __init__(self) -> None: + self.mouse_click = False + # self.right_click = False + self.level_2_enter = False + self.key_return_press = False + self.key_tab_press = False + self.backspace_press = 0 + + self.media_key = "" + + def m_key_play(self) -> None: + self.media_key = "Play" + gui.update += 1 + + def m_key_pause(self) -> None: + self.media_key = "Pause" + gui.update += 1 + + def m_key_stop(self) -> None: + self.media_key = "Stop" + gui.update += 1 + + def m_key_next(self) -> None: + self.media_key = "Next" + gui.update += 1 + + def m_key_previous(self) -> None: + self.media_key = "Previous" + gui.update += 1 + +class KeyMap: + + def __init__(self): + + self.hits = [] # The keys hit this frame + self.maps = {} # Loaded from input.txt + + def load(self): + + path = config_directory / "input.txt" + with path.open(encoding="utf_8") as f: + content = f.read().splitlines() + for p in content: + if len(p) == 0 or len(p) > 100: + continue + if p[0] == " " or p[0] == "#": + continue + + items = p.split() + if 1 < len(items) < 5: + function = items[0] + + if items[1] in ("MB4", "MB5"): + key = items[1] + else: + if prefs.use_scancodes: + key = SDL_GetScancodeFromName(items[1].encode()) + else: + key = SDL_GetKeyFromName(items[1].encode()) + if key == 0: + continue + + mod = [] + + if len(items) > 2: + mod.append(items[2].lower()) + if len(items) > 3: + mod.append(items[3].lower()) + + if function in self.maps: + self.maps[function].append((key, mod)) + else: + self.maps[function] = [(key, mod)] + + def test(self, function): + + if not self.hits: + return False + if function not in self.maps: + return False + + for code, mod in self.maps[function]: + + if code in self.hits: + + ctrl = (key_ctrl_down or key_rctrl_down) * 1 + shift = (key_shift_down or key_shiftr_down) * 10 + alt = (key_lalt or key_ralt) * 100 + + if ctrl + shift + alt == ("ctrl" in mod) * 1 + ("shift" in mod) * 10 + ("alt" in mod) * 100: + return True + + return False + +class ColoursClass: + """Used to store colour values for UI elements + + These are changed for themes + """ + + def grey(self, value: int) -> list[int]: + return [value, value, value, 255] + + def alpha_grey(self, value: int) -> list[int]: + return [255, 255, 255, value] + + def grey_blend_bg(self, value: int) -> list[int]: + return alpha_blend((255, 255, 255, value), self.box_background) + + def __init__(self) -> None: + + self.deco = None + self.column_colours = {} + self.column_colours_playing = {} + + self.last_album = "" + self.link_text = [100, 200, 252, 255] + + self.tb_line = self.grey(21) # not currently used + self.art_box = self.grey(24) + + self.volume_bar_background = self.grey(30) + self.volume_bar_fill = self.grey(125) + self.seek_bar_background = self.grey(30) + self.seek_bar_fill = self.grey(80) + + self.tab_text_active = self.grey(230) + self.tab_text = self.grey(215) + self.tab_background = self.grey(25) + self.tab_highlight = self.grey(40) + self.tab_background_active = self.grey(45) + + self.title_text = [190, 190, 190, 255] + self.index_text = self.grey(70) + self.time_text = self.grey(180) + self.artist_text = [195, 255, 104, 255] + self.album_text = [245, 240, 90, 255] + + self.index_playing = self.grey(190) + self.artist_playing = [195, 255, 104, 255] + self.album_playing = [245, 240, 90, 255] + self.title_playing = self.grey(230) + + self.time_playing = [180, 194, 107, 255] + + self.playlist_text_missing = self.grey(85) + self.bar_time = self.grey(70) + + self.top_panel_background = self.grey(15) + self.status_text_over = rgb_add_hls(self.top_panel_background, 0, 0.83, 0) + self.status_text_normal = rgb_add_hls(self.top_panel_background, 0, 0.30, -0.15) + + self.side_panel_background = self.grey(18) + self.gallery_background = self.side_panel_background + self.playlist_panel_background = self.grey(21) + self.bottom_panel_colour = self.grey(15) + + self.row_playing_highlight = [255, 255, 255, 4] + self.row_select_highlight = [255, 255, 255, 5] + + self.side_bar_line1 = self.grey(230) + self.side_bar_line2 = self.grey(210) + + self.mode_button_off = self.grey(50) + self.mode_button_over = self.grey(200) + self.mode_button_active = self.grey(190) + + self.media_buttons_over = self.grey(220) + self.media_buttons_active = self.grey(220) + self.media_buttons_off = self.grey(55) + + self.star_line = [100, 100, 100, 255] + self.star_line_playing = None + self.folder_title = [130, 130, 130, 255] + self.folder_line = [40, 40, 40, 255] + + self.scroll_colour = [45, 45, 45, 255] + + self.level_1_bg = [0, 30, 0, 255] + self.level_2_bg = [30, 30, 0, 255] + self.level_3_bg = [30, 0, 0, 255] + self.level_green = [20, 120, 20, 255] + self.level_red = [190, 30, 30, 255] + self.level_yellow = [135, 135, 30, 255] + + self.vis_colour = self.grey(200) + self.vis_bg = [0, 0, 0, 255] + + self.menu_background = None # self.grey(12) + self.menu_highlight_background = None + self.menu_text = [230, 230, 230, 255] + self.menu_text_disabled = self.grey(50) + self.menu_icons = [255, 255, 255, 25] + self.menu_tab = self.grey(30) + + self.gallery_highlight = self.artist_playing + + self.status_info_text = [245, 205, 0, 255] + self.streaming_text = [220, 75, 60, 255] + self.lyrics = self.grey(245) + + self.corner_button = [255, 255, 255, 50] # [60, 60, 60, 255] + self.corner_button_active = [255, 255, 255, 230] # [230, 230, 230, 255] + + self.window_buttons_bg = [0, 0, 0, 50] + self.window_buttons_bg_over = [255, 255, 255, 10] # [80, 80, 80, 120] + self.window_buttons_icon_over = (255, 255, 255, 60) + self.window_button_icon_off = (255, 255, 255, 40) + self.window_button_x_on = None + self.window_button_x_off = self.window_button_icon_off + + self.message_box_bg = self.grey(0) + self.message_box_text = self.grey(230) + + self.sys_title = self.grey(220) + self.sys_title_strong = self.grey(230) + self.lm = False + + self.pluse_colour = [244, 212, 66, 255] + + self.mini_mode_background = [19, 19, 19, 255] + self.mini_mode_border = [45, 45, 45, 255] + self.mini_mode_text_1 = [255, 255, 255, 240] + self.mini_mode_text_2 = [255, 255, 255, 77] + + self.queue_drag_indicator_colour = [200, 50, 240, 255] + + self.playlist_box_background: list[int] = self.side_panel_background + + self.bar_title_text = None + + self.corner_icon = [40, 40, 40, 255] + self.queue_background = None # self.side_panel_background #self.grey(18) # 18 + self.queue_card_background = self.grey(23) + + self.column_bar_background = [30, 30, 30, 255] + self.column_grip = [255, 255, 255, 14] + self.column_bar_text = [240, 240, 240, 255] + + self.window_frame = [30, 30, 30, 255] + + self.box_background: list[int] = [16, 16, 16, 255] + self.box_border = rgb_add_hls(self.box_background, 0, 0.17, 0) + self.box_text_border = rgb_add_hls(self.box_background, 0, 0.1, 0) + self.box_text_label = rgb_add_hls(self.box_background, 0, 0.32, -0.1) + self.box_sub_highlight = rgb_add_hls(self.box_background, 0, 0.07, -0.05) # 58, 47, 85 + self.box_check_border = [255, 255, 255, 18] + + self.box_title_text = self.grey(245) + self.box_text = self.grey(240) + self.box_sub_text = self.grey_blend_bg(225) + self.box_input_text = self.grey(225) + self.box_button_text_highlight = self.grey(250) + self.box_button_text = self.grey(225) + self.box_button_background = alpha_blend([255, 255, 255, 11], self.box_background) + self.box_thumb_background = None + self.box_button_background_highlight = alpha_blend([255, 255, 255, 20], self.box_background) + + self.artist_bio_background = [27, 27, 27, 255] + self.artist_bio_text = [230, 230, 230, 255] + + def post_config(self): + + if self.box_thumb_background is None: + self.box_thumb_background = alpha_mod(self.box_button_background, 175) + + # Pre calculate alpha blend for spec background + self.vis_bg[0] = int(0.05 * 255 + (1 - 0.05) * self.top_panel_background[0]) + self.vis_bg[1] = int(0.05 * 255 + (1 - 0.05) * self.top_panel_background[1]) + self.vis_bg[2] = int(0.05 * 255 + (1 - 0.05) * self.top_panel_background[2]) + + self.message_box_bg = self.box_background + self.sys_tab_bg = self.tab_background + self.sys_tab_hl = self.tab_background_active + self.toggle_box_on = self.folder_title + self.toggle_box_on = [255, 150, 100, 255] + self.toggle_box_on = self.artist_playing + if colour_value(self.toggle_box_on) < 150: + self.toggle_box_on = [160, 160, 160, 255] + # self.time_sub = [255, 255, 255, 80]#alpha_blend([255, 255, 255, 80], self.bottom_panel_colour) + + self.time_sub = rgb_add_hls(self.bottom_panel_colour, 0, 0.29, 0) + + if test_lumi(self.bottom_panel_colour) < 0.2: + # self.time_sub = [0, 0, 0, 80] + self.time_sub = rgb_add_hls(self.bottom_panel_colour, 0, -0.15, -0.3) + elif test_lumi(self.bottom_panel_colour) < 0.8: + self.time_sub = [255, 255, 255, 135] + # self.time_sub = self.mode_button_off + + if self.bar_title_text is None: + self.bar_title_text = self.side_bar_line1 + + self.gallery_artist_line = alpha_mod(self.side_bar_line2, 120) + + if self.menu_highlight_background is None: + self.menu_highlight_background = [40, 40, 40, 255] + + if not self.queue_background: + self.queue_background = self.side_panel_background + + if test_lumi(self.queue_background) > 0.8: + self.queue_card_background = alpha_blend([255, 255, 255, 10], self.queue_background) + + if self.menu_background is None and not self.lm: + self.menu_background = self.bottom_panel_colour + + self.message_box_text = self.box_text + self.message_box_border = self.box_border + + if self.window_button_x_on is None: + self.window_button_x_on = self.artist_playing + + if test_lumi(self.column_bar_background) < 0.4: + self.column_bar_text = [40, 40, 40, 200] + self.column_grip = [255, 255, 255, 20] + + def light_mode(self): + + self.lm = True + self.star_line_playing = [255, 255, 255, 255] + self.sys_tab_bg = self.grey(25) + self.sys_tab_hl = self.grey(45) + # self.box_background = self.grey(30) + self.toggle_box_on = self.tab_background_active + # if colour_value(self.tab_background_active) < 250: + # self.toggle_box_on = [255, 255, 255, 200] + + # self.time_sub = [0, 0, 0, 200] + self.gallery_artist_line = self.grey(40) + # self.bar_title_text = self.grey(30) + self.status_text_normal = self.grey(70) + self.status_text_over = self.grey(40) + self.status_info_text = [40, 40, 40, 255] + + # self.bar_title_text = self.grey(255) + self.vis_bg = [235, 235, 235, 255] + # self.menu_background = [240, 240, 240, 250] + # self.menu_text = self.grey(40) + # self.menu_text_disabled = self.grey(180) + # self.menu_highlight_background = [200, 200, 200, 250] + if self.menu_background is None: + self.menu_background = [15, 15, 15, 250] + if not self.menu_icons: + self.menu_icons = [0, 0, 0, 40] + + # self.menu_background = [40, 40, 40, 250] + # self.menu_text = self.grey(220) + # self.menu_text_disabled = self.grey(120) + # self.menu_highlight_background = [120, 80, 220, 250] + + self.corner_button = self.grey(160) + self.corner_button_active = self.grey(35) + # self.window_buttons_bg = [0, 0, 0, 5] + self.message_box_bg = [245, 245, 245, 255] + self.message_box_text = self.grey(20) + self.message_box_border = self.grey(40) + self.gallery_background = self.grey(230) + self.gallery_artist_line = self.grey(40) + self.pluse_colour = [212, 66, 244, 255] + + # view_box.off_colour = self.grey(200) + +class TrackClass: + """This is the fundamental object/data structure of a track""" + + def __init__(self) -> None: + self.index: int = 0 + self.subtrack: int = 0 + self.fullpath: str = "" + self.filename: str = "" + self.parent_folder_path: str = "" + self.parent_folder_name: str = "" + self.file_ext: str = "" + self.size: int = 0 + self.modified_time: float = 0 + + self.is_network: bool = False + self.url_key: str = "" + self.art_url_key: str = "" + + self.artist: str = "" + self.album_artist: str = "" + self.title: str = "" + self.composer: str = "" + self.length: float = 0 + self.bitrate: int = 0 + self.samplerate: int = 0 + self.bit_depth: int = 0 + self.album: str = "" + self.date: str = "" + self.track_number: str = "" + self.track_total: str = "" + self.start_time: int = 0 + self.is_cue: bool = False + self.is_embed_cue: bool = False + self.cue_sheet: str = "" + self.genre: str = "" + self.found: bool = True + self.skips: int = 0 + self.comment: str = "" + self.disc_number: str = "" + self.disc_total: str = "" + self.lyrics: str = "" + + self.lfm_friend_likes = set() + self.lfm_scrobbles: int = 0 + self.misc: list = {} + +class LoadClass: + """Object for import track jobs (passed to worker thread)""" + + def __init__(self) -> None: + self.target: str = "" + self.playlist: int = 0 # Playlist UID + self.tracks: list[TrackClass] = [] + self.stage: int = 0 + self.playlist_position: int | None = None + self.replace_stem: bool = False + self.notify: bool = False + self.play: bool = False + self.force_scan: bool = False + +class GetSDLInput: + + def __init__(self): + self.i_y = pointer(c_int(0)) + self.i_x = pointer(c_int(0)) + + self.mouse_capture_want = False + self.mouse_capture = False + + def mouse(self): + SDL_PumpEvents() + SDL_GetMouseState(self.i_x, self.i_y) + return int(self.i_x.contents.value / logical_size[0] * window_size[0]), int( + self.i_y.contents.value / logical_size[0] * window_size[0]) + + def test_capture_mouse(self): + if not self.mouse_capture and self.mouse_capture_want: + SDL_CaptureMouse(SDL_TRUE) + self.mouse_capture = True + elif self.mouse_capture and not self.mouse_capture_want: + SDL_CaptureMouse(SDL_FALSE) + self.mouse_capture = False + +class MOD(Structure): + """Access functions from libopenmpt for scanning tracker files""" + _fields_ = [("ctl", c_char_p), ("value", c_char_p)] + +class GMETrackInfo(Structure): + _fields_ = [ + ("length", c_int), + ("intro_length", c_int), + ("loop_length", c_int), + ("play_length", c_int), + ("fade_length", c_int), + ("i5", c_int), + ("i6", c_int), + ("i7", c_int), + ("i8", c_int), + ("i9", c_int), + ("i10", c_int), + ("i11", c_int), + ("i12", c_int), + ("i13", c_int), + ("i14", c_int), + ("i15", c_int), + ("system", c_char_p), + ("game", c_char_p), + ("song", c_char_p), + ("author", c_char_p), + ("copyright", c_char_p), + ("comment", c_char_p), + ("dumper", c_char_p), + ("s7", c_char_p), + ("s8", c_char_p), + ("s9", c_char_p), + ("s10", c_char_p), + ("s11", c_char_p), + ("s12", c_char_p), + ("s13", c_char_p), + ("s14", c_char_p), + ("s15", c_char_p), + ] + +class PlayerCtl: + """Main class that controls playback (play, pause, stepping, playlists, queue etc). Sends commands to backend.""" + + # C-PC + def __init__(self, prefs: Prefs): + #self.tauon = + self.running: bool = True + self.prefs: Prefs = prefs + self.install_directory: Path = install_directory + + # Database + + self.master_count = master_count + self.total_playtime: float = 0 + self.master_library = master_library + # Lets clients know when to invalidate cache + self.db_inc = random.randint(0, 10000) + # self.star_library = star_library + self.LoadClass = LoadClass + + self.gen_codes = gen_codes + + self.shuffle_pools = {} + self.after_import_flag = False + self.quick_add_target = None + + self.album_mbid_release_cache = {} + self.album_mbid_release_group_cache = {} + self.mbid_image_url_cache = {} + + # Misc player control + + self.url: str = "" + # self.save_urls = url_saves + self.tag_meta: str = "" + self.found_tags = {} + self.encoder_pause = 0 + + # Playback + + self.track_queue = track_queue + self.queue_step = playing_in_queue + self.playing_time = 0 + self.playlist_playing_position = playlist_playing # track in playlist that is playing + if self.playlist_playing_position is None: + self.playlist_playing_position = -1 + self.playlist_view_position = playlist_view_position + self.selected_in_playlist = selected_in_playlist + self.target_open = "" + self.target_object = None + self.start_time = 0 + self.b_start_time = 0 + self.playerCommand = "" + self.playerSubCommand = "" + self.playerCommandReady = False + self.playing_state: int = 0 + self.playing_length: float = 0 + self.jump_time = 0 + self.random_mode = prefs.random_mode + self.repeat_mode = prefs.repeat_mode + self.album_repeat_mode = prefs.album_repeat_mode + self.album_shuffle_mode = prefs.album_shuffle_mode + # self.album_shuffle_pool = [] + # self.album_shuffle_id = "" + self.last_playing_time = 0 + self.multi_playlist = multi_playlist + self.active_playlist_viewing: int = playlist_active # the playlist index that is being viewed + self.active_playlist_playing: int = playlist_active # the playlist index that is playing from + self.force_queue: list[TauonQueueItem] = p_force_queue + self.pause_queue: bool = False + self.left_time = 0 + self.left_index = 0 + self.player_volume: float = volume + self.new_time = 0 + self.time_to_get = [] + self.a_time = 0 + self.b_time = 0 + # self.playlist_backup = [] + self.active_replaygain = 0 + self.auto_stop = False + + self.record_stream = False + self.record_title = "" + + # Bass + + self.bass_devices = [] + self.set_device = 0 + + self.gst_devices = [] # Display names + self.gst_outputs = {} # Display name : (sink, device) + #TODO(Martin) : Fix this by moving the class to root of the module + self.mpris: Gnome.main.MPRIS | None = None + self.tray_update = None + self.eq = [0] * 2 # not used + self.enable_eq = True # not used + + self.playing_time_int = 0 # playing time but with no decimel + + self.windows_progress = None + + self.finish_transition = False + # self.queue_target = 0 + self.start_time_target = 0 + + self.decode_time = 0 + self.download_time = 0 + + self.radio_meta_on = "" + + self.radio_scrobble_trip = True + self.radio_scrobble_timer = Timer() + + self.radio_image_bin = None + self.radio_rate_timer = Timer(2) + self.radio_poll_timer = Timer(2) + + self.volume_update_timer = Timer() + self.wake_past_time = 0 + + self.regen_in_progress = False + self.notify_in_progress = False + + self.radio_playlists = radio_playlists + self.radio_playlist_viewing = radio_playlist_viewing + self.tag_history = {} + + self.commit: int | None = None + self.spot_playing = False + + self.buffering_percent = 0 + + + + def notify_change(self) -> None: + self.db_inc += 1 + tauon.bg_save() + + def update_tag_history(self) -> None: + if prefs.auto_rec: + self.tag_history[radiobox.song_key] = { + "title": radiobox.dummy_track.title, + "artist": radiobox.dummy_track.artist, + "album": radiobox.dummy_track.album, + # "image": self.radio_image_bin + } + + def radio_progress(self) -> None: + if radiobox.loaded_url and "radio.plaza.one" in radiobox.loaded_url and self.radio_poll_timer.get() > 0: + self.radio_poll_timer.force_set(-10) + response = requests.get("https://api.plaza.one/status", timeout=10) + + if response.status_code == 200: + d = json.loads(response.text) + if "song" in d and "artist" in d["song"] and "title" in d["song"]: + self.tag_meta = d["song"]["artist"] + " - " + d["song"]["title"] + + if self.tag_meta: + if self.radio_rate_timer.get() > 7 and self.radio_meta_on != self.tag_meta: + self.radio_rate_timer.set() + self.radio_scrobble_trip = False + self.radio_meta_on = self.tag_meta + + radiobox.dummy_track.art_url_key = "" + radiobox.dummy_track.title = "" + radiobox.dummy_track.date = "" + radiobox.dummy_track.artist = "" + radiobox.dummy_track.album = "" + radiobox.dummy_track.lyrics = "" + radiobox.dummy_track.date = "" + + tags = self.found_tags + if "title" in tags: + radiobox.dummy_track.title = tags["title"] + if "artist" in tags: + radiobox.dummy_track.artist = tags["artist"] + if "year" in tags: + radiobox.dummy_track.date = tags["year"] + if "album" in tags: + radiobox.dummy_track.album = tags["album"] + + elif self.tag_meta.count( + "-") == 1 and ":" not in self.tag_meta and "advert" not in self.tag_meta.lower(): + artist, title = self.tag_meta.split("-") + radiobox.dummy_track.title = title.strip() + radiobox.dummy_track.artist = artist.strip() + + if self.tag_meta: + radiobox.song_key = self.tag_meta + else: + radiobox.song_key = radiobox.dummy_track.artist + " - " + radiobox.dummy_track.title + + self.update_tag_history() + if radiobox.loaded_url not in radiobox.websocket_source_urls: + self.radio_image_bin = None + logging.info("NEXT RADIO TRACK") + + try: + get_radio_art() + except Exception: + logging.exception("Get art error") + + self.notify_update(mpris=False) + if self.mpris: + self.mpris.update(force=True) + + lfm_scrobbler.listen_track(radiobox.dummy_track) + lfm_scrobbler.start_queue() + + if self.radio_scrobble_trip is False and self.radio_scrobble_timer.get() > 45: + self.radio_scrobble_trip = True + lfm_scrobbler.scrob_full_track(copy.deepcopy(radiobox.dummy_track)) + + def update_shuffle_pool(self, pl_id: int) -> None: + new_pool = copy.deepcopy(self.multi_playlist[id_to_pl(pl_id)].playlist_ids) + random.shuffle(new_pool) + self.shuffle_pools[pl_id] = new_pool + logging.info("Refill shuffle pool") + + def notify_update_fire(self) -> None: + if self.mpris is not None: + self.mpris.update() + if tauon.update_play_lock is not None: + tauon.update_play_lock() + # if self.tray_update is not None: + # self.tray_update() + self.notify_in_progress = False + + def notify_update(self, mpris: bool = True) -> None: + tauon.tray_releases += 1 + if tauon.tray_lock.locked(): + try: + tauon.tray_lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked tray_lock") + else: + logging.exception("Unknown RuntimeError trying to release tray_lock") + except Exception: + logging.exception("Failed to release tray_lock") + + if mpris and smtc: + tr = self.playing_object() + if tr: + state = 0 + if self.playing_state == 1: + state = 1 + if self.playing_state == 2: + state = 2 + image_path = "" + try: + image_path = tauon.thumb_tracks.path(tr) + except Exception: + logging.exception("Failed to set image_path from thumb_tracks.path") + + if image_path is None: + image_path = "" + + image_path = image_path.replace("/", "\\") + #logging.info(image_path) + + sm.update( + state, tr.title.encode("utf-16"), len(tr.title), tr.artist.encode("utf-16"), len(tr.artist), + image_path.encode("utf-16"), len(image_path)) + + + if self.mpris is not None and mpris is True: + while self.notify_in_progress: + time.sleep(0.01) + self.notify_in_progress = True + shoot = threading.Thread(target=self.notify_update_fire) + shoot.daemon = True + shoot.start() + if prefs.art_bg or (gui.mode == 3 and prefs.mini_mode_mode == 5): + tauon.thread_manager.ready("style") + + def get_url(self, track_object: TrackClass) -> tuple[str | None, dict | None] | None: + if track_object.file_ext == "TIDAL": + return tauon.tidal.resolve_stream(track_object), None + if track_object.file_ext == "PLEX": + return plex.resolve_stream(track_object.url_key), None + + if track_object.file_ext == "JELY": + return jellyfin.resolve_stream(track_object.url_key) + + if track_object.file_ext == "KOEL": + return koel.resolve_stream(track_object.url_key) + + if track_object.file_ext == "SUB": + return subsonic.resolve_stream(track_object.url_key) + + if track_object.file_ext == "TAU": + return tau.resolve_stream(track_object.url_key), None + + return None, None + + def playing_playlist(self) -> list[int] | None: + return self.multi_playlist[self.active_playlist_playing].playlist_ids + + def playing_ready(self) -> bool: + return len(self.track_queue) > 0 + + def selected_ready(self) -> bool: + return default_playlist and self.selected_in_playlist < len(default_playlist) + + def render_playlist(self) -> None: + if taskbar_progress and msys and self.windows_progress: + self.windows_progress.update(True) + gui.pl_update = 1 + + def show_selected(self) -> int: + if gui.playlist_view_length < 1: + return 0 + + global shift_selection + + for i in range(len(self.multi_playlist[self.active_playlist_viewing].playlist_ids)): + + if i == self.selected_in_playlist: + + if i < self.playlist_view_position: + self.playlist_view_position = i - random.randint(2, int((gui.playlist_view_length / 3) * 2) + int(gui.playlist_view_length / 6)) + logging.debug("Position changed show selected (a)") + elif abs(self.playlist_view_position - i) > gui.playlist_view_length: + self.playlist_view_position = i + logging.debug("Position changed show selected (b)") + if i > 6: + self.playlist_view_position -= 5 + logging.debug("Position changed show selected (c)") + if i > gui.playlist_view_length * 1 and i + (gui.playlist_view_length * 2) < len( + self.multi_playlist[self.active_playlist_viewing].playlist_ids) and i > 10: + self.playlist_view_position = i - random.randint(2, int(gui.playlist_view_length / 3) * 2) + logging.debug("Position changed show selected (d)") + break + + self.render_playlist() + + return 0 + + def get_track(self, track_index: int) -> TrackClass: + """Get track object by track_index""" + return self.master_library[track_index] + + def get_track_in_playlist(self, track_index: int, playlist_index: int) -> TrackClass: + """Get track object by playlist_index and track_index""" + if playlist_index == -1: + playlist_index = self.active_playlist_viewing + try: + playlist = self.multi_playlist[playlist_index].playlist + return self.get_track(playlist[track_index]) + except IndexError: + logging.exception("Failed getting track object by playlist_index and track_index!") + except Exception: + logging.exception("Unknown error getting track object by playlist_index and track_index!") + return None + + def show_object(self) -> None: + """The track to show in the metadata side panel""" + target_track = None + + if self.playing_state == 3: + return radiobox.dummy_track + + if 3 > self.playing_state > 0: + target_track = self.playing_object() + + elif self.playing_state == 0 and prefs.meta_shows_selected: + if -1 < self.selected_in_playlist < len(self.multi_playlist[self.active_playlist_viewing].playlist_ids): + target_track = self.get_track(self.multi_playlist[self.active_playlist_viewing].playlist_ids[self.selected_in_playlist]) + + elif self.playing_state == 0 and prefs.meta_persists_stop: + target_track = self.master_library[self.track_queue[self.queue_step]] + + if prefs.meta_shows_selected_always: + if -1 < self.selected_in_playlist < len(self.multi_playlist[self.active_playlist_viewing].playlist_ids): + target_track = self.get_track(self.multi_playlist[self.active_playlist_viewing].playlist_ids[self.selected_in_playlist]) + + return target_track + + def playing_object(self) -> TrackClass | None: + + if self.playing_state == 3: + return radiobox.dummy_track + + if len(self.track_queue) > 0: + return self.master_library[self.track_queue[self.queue_step]] + return None + + def title_text(self) -> str: + line = "" + track = self.playing_object() + if track: + title = track.title + artist = track.artist + + if not title: + line = clean_string(track.filename) + else: + if artist != "": + line += artist + if title != "": + if line != "": + line += " - " + line += title + + if self.playing_state == 3 and not title and not artist: + return self.tag_meta + + return line + + def show(self) -> int | None: + global shift_selection + + if not self.track_queue: + return 0 + return None + + def show_current( + self, select: bool = True, playing: bool = True, quiet: bool = False, this_only: bool = False, highlight: bool = False, + index: int | None = None, no_switch: bool = False, folder_list: bool = True, + ) -> int | None: + + # logging.info("show------") + # logging.info(select) + # logging.info(playing) + # logging.info(quiet) + # logging.info(this_only) + # logging.info(highlight) + # logging.info("--------") + logging.debug("Position set by show playing") + + global shift_selection + + if tauon.spot_ctl.coasting: + sptr = tauon.dummy_track.misc.get("spotify-track-url") + if sptr: + + for p in default_playlist: + tr = self.get_track(p) + if tr.misc.get("spotify-track-url") == sptr: + index = tr.index + break + else: + for i, pl in enumerate(self.multi_playlist): + for p in pl.playlist_ids: + tr = self.get_track(p) + if tr.misc.get("spotify-track-url") == sptr: + index = tr.index + switch_playlist(i) + break + else: + continue + break + else: + return None + + if not self.track_queue: + return 0 + + track_index = self.track_queue[self.queue_step] + if index is not None: + track_index = index + + # Switch to source playlist + if not no_switch: + if self.active_playlist_viewing != self.active_playlist_playing and ( + track_index not in self.multi_playlist[self.active_playlist_viewing].playlist_ids): + switch_playlist(self.active_playlist_playing) + + if gui.playlist_view_length < 1: + return 0 + + for i in range(len(self.multi_playlist[self.active_playlist_viewing].playlist_ids)): + if self.multi_playlist[self.active_playlist_viewing].playlist_ids[i] == track_index: + + if self.playlist_playing_position < len(self.multi_playlist[self.active_playlist_viewing].playlist_ids) and \ + self.active_playlist_viewing == self.active_playlist_playing and track_index == \ + self.multi_playlist[self.active_playlist_viewing].playlist_ids[self.playlist_playing_position] and \ + i != self.playlist_playing_position: + # continue + i = self.playlist_playing_position + + if select: + self.selected_in_playlist = i + + if playing: + # Make the found track the playing track + self.playlist_playing_position = i + self.active_playlist_playing = self.active_playlist_viewing + + vl = gui.playlist_view_length + if self.multi_playlist[self.active_playlist_viewing].uuid_int == gui.playlist_current_visible_tracks_id: + vl = gui.playlist_current_visible_tracks + + if not ( + quiet and self.playing_object().length < 15): # or (abs(self.playlist_view_position - i) < vl - 1)): + + # Align to album if in view range (and folder titles are active) + ap = get_album_info(i)[1][0] + + if not (quiet and self.playlist_view_position <= i <= self.playlist_view_position + vl) and ( + not abs(i - ap) > vl - 2) and not self.multi_playlist[self.active_playlist_viewing].hide_title: + self.playlist_view_position = ap + + # Move to a random offset --- + + elif i == self.playlist_view_position - 1 and self.playlist_view_position > 1: + self.playlist_view_position -= 1 + + # Move a bit if its just out of range + elif self.playlist_view_position + vl - 2 == i and i < len( + self.multi_playlist[self.active_playlist_viewing].playlist_ids) - 5: + self.playlist_view_position += 3 + + # We know its out of range if above view postion + elif i < self.playlist_view_position: + self.playlist_view_position = i - random.randint(2, int(( + gui.playlist_view_length / 3) * 2) + int(gui.playlist_view_length / 6)) + + # If its below we need to test if its in view. If playing track in view, don't jump + elif abs(self.playlist_view_position - i) >= vl: + self.playlist_view_position = i + if i > 6: + self.playlist_view_position -= 5 + if i > gui.playlist_view_length and i + (gui.playlist_view_length * 2) < len( + self.multi_playlist[self.active_playlist_viewing].playlist_ids) and i > 10: + self.playlist_view_position = i - random.randint(2, + int(gui.playlist_view_length / 3) * 2) + + break + + else: # Search other all other playlists + if not this_only: + for i, playlist in enumerate(self.multi_playlist): + if track_index in playlist.playlist_ids: + switch_playlist(i, quiet=True) + self.show_current(select, playing, quiet, this_only=True, index=track_index) + break + + self.playlist_view_position = max(self.playlist_view_position, 0) + + # if self.playlist_view_position > len(self.multi_playlist[self.active_playlist_viewing].playlist_ids) - 1: + # logging.info("Run Over") + + if select: + shift_selection = [] + + self.render_playlist() + + if album_mode and not quiet: + if highlight: + gui.gallery_animate_highlight_on = goto_album(self.selected_in_playlist) + gallery_select_animate_timer.set() + else: + goto_album(self.selected_in_playlist) + + if prefs.left_panel_mode == "artist list" and gui.lsp and not quiet: + artist_list_box.locate_artist(self.playing_object()) + + if folder_list and prefs.left_panel_mode == "folder view" and gui.lsp and not quiet and not tree_view_box.lock_pl: + tree_view_box.show_track(self.playing_object()) + + return 0 + + def toggle_mute(self) -> None: + global volume_store + if self.player_volume > 0: + volume_store = self.player_volume + self.player_volume = 0 + else: + self.player_volume = volume_store + + self.set_volume() + + def set_volume(self, notify: bool = True) -> None: + + if (tauon.spot_ctl.coasting or tauon.spot_ctl.playing) and not tauon.spot_ctl.local and mouse_down: + # Rate limit network volume change + t = self.volume_update_timer.get() + if t < 0.3: + return + + self.volume_update_timer.set() + self.playerCommand = "volume" + self.playerCommandReady = True + if notify: + self.notify_update() + + def revert(self) -> None: + + if self.queue_step == 0: + return + + prev = 0 + while len(self.track_queue) > prev + 1 and prev < 5: + if self.track_queue[len(self.track_queue) - 1 - prev] == self.left_index: + self.queue_step = len(self.track_queue) - 1 - prev + self.jump_time = self.left_time + self.playing_time = self.left_time + self.decode_time = self.left_time + break + prev += 1 + else: + self.queue_step -= 1 + self.jump_time = 0 + self.playing_time = 0 + self.decode_time = 0 + + if not len(self.track_queue) > self.queue_step >= 0: + logging.error("There is no previous track?") + return + + self.target_open = self.master_library[self.track_queue[self.queue_step]].fullpath + self.target_object = self.master_library[self.track_queue[self.queue_step]] + self.start_time = self.master_library[self.track_queue[self.queue_step]].start_time + self.start_time_target = self.start_time + self.playing_length = self.master_library[self.track_queue[self.queue_step]].length + self.playerCommand = "open" + self.playerCommandReady = True + self.playing_state = 1 + + if tauon.stream_proxy.download_running: + tauon.stream_proxy.stop() + + self.show_current() + self.render_playlist() + + def deduct_shuffle(self, track_id: int) -> None: + if self.multi_playlist and self.random_mode: + pl = self.multi_playlist[self.active_playlist_playing] + id = pl.uuid_int + + if id not in self.shuffle_pools: + self.update_shuffle_pool(pl.uuid_int) + + pool = self.shuffle_pools[id] + if not pool: + del self.shuffle_pools[id] + self.update_shuffle_pool(pl.uuid_int) + pool = self.shuffle_pools[id] + + if track_id in pool: + pool.remove(track_id) + + + def play_target_rr(self) -> None: + tauon.thread_manager.ready_playback() + self.playing_length = self.master_library[self.track_queue[self.queue_step]].length + + if self.playing_length > 2: + random_start = random.randrange(1, int(self.playing_length) - 45 if self.playing_length > 50 else int( + self.playing_length)) + else: + random_start = 0 + + self.playing_time = random_start + self.target_open = self.master_library[self.track_queue[self.queue_step]].fullpath + self.target_object = self.master_library[self.track_queue[self.queue_step]] + self.start_time = self.master_library[self.track_queue[self.queue_step]].start_time + self.start_time_target = self.start_time + self.jump_time = random_start + self.playerCommand = "open" + if not prefs.use_jump_crossfade: + self.playerSubCommand = "now" + self.playerCommandReady = True + self.playing_state = 1 + radiobox.loaded_station = None + + if tauon.stream_proxy.download_running: + tauon.stream_proxy.stop() + + if update_title: + update_title_do() + + self.deduct_shuffle(self.target_object.index) + + def play_target(self, gapless: bool = False, jump: bool = False) -> None: + + tauon.thread_manager.ready_playback() + + #logging.info(self.track_queue) + self.playing_time = 0 + self.decode_time = 0 + target = self.master_library[self.track_queue[self.queue_step]] + self.target_open = target.fullpath + self.target_object = target + self.start_time = target.start_time + self.start_time_target = self.start_time + self.playing_length = target.length + self.last_playing_time = 0 + self.commit = None + radiobox.loaded_station = None + + if tauon.stream_proxy and tauon.stream_proxy.download_running: + tauon.stream_proxy.stop() + + if self.multi_playlist[self.active_playlist_playing].persist_time_positioning: + t = target.misc.get("position", 0) + if t: + self.playing_time = 0 + self.decode_time = 0 + self.jump_time = t + + self.playerCommand = "open" + if jump: # and not prefs.use_jump_crossfade: + self.playerSubCommand = "now" + + self.playerCommandReady = True + + self.playing_state = 1 + self.update_change() + self.deduct_shuffle(target.index) + + def update_change(self) -> None: + if update_title: + update_title_do() + self.notify_update() + hit_discord() + self.render_playlist() + + if lfm_scrobbler.a_sc: + lfm_scrobbler.a_sc = False + self.a_time = 0 + + lfm_scrobbler.start_queue() + + if (album_mode or not gui.rsp) and (gui.theme_name == "Carbon" or prefs.colour_from_image): + target = self.playing_object() + if target and prefs.colour_from_image and target.parent_folder_path == colours.last_album: + return + + album_art_gen.display(target, (0, 0), (50, 50), theme_only=True) + + def jump(self, index: int, pl_position: int = None, jump: bool = True) -> None: + lfm_scrobbler.start_queue() + self.auto_stop = False + + if self.force_queue and not self.pause_queue: + if self.force_queue[0].uuid_int == 1: # TODO(Martin): How can the UUID be 1 when we're doing a random on 1-1m except for massive chance? Is that the point? + if self.get_track(self.force_queue[0].track_id).parent_folder_path != self.get_track(index).parent_folder_path: + del self.force_queue[0] + + if len(self.track_queue) > 0: + self.left_time = self.playing_time + self.left_index = self.track_queue[self.queue_step] + + if self.playing_state == 1 and self.left_time > 5 and self.playing_length - self.left_time > 15: + self.master_library[self.left_index].skips += 1 + + global playlist_hold + gui.update_spec = 0 + self.active_playlist_playing = self.active_playlist_viewing + self.track_queue.append(index) + self.queue_step = len(self.track_queue) - 1 + playlist_hold = False + self.play_target(jump=jump) + + if pl_position is not None: + self.playlist_playing_position = pl_position + + gui.pl_update = 1 + + def back(self) -> None: + if self.playing_state < 3 and prefs.back_restarts and self.playing_time > 6: + self.seek_time(0) + self.render_playlist() + return + + if tauon.spot_ctl.coasting: + tauon.spot_ctl.control("previous") + tauon.spot_ctl.update_timer.set() + self.playing_time = -2 + self.decode_time = -2 + return + + if len(self.track_queue) > 0: + self.left_time = self.playing_time + self.left_index = self.track_queue[self.queue_step] + + gui.update_spec = 0 + # Move up + if self.random_mode is False and len(self.playing_playlist()) > self.playlist_playing_position > 0: + + if len(self.track_queue) > 0 and self.playing_playlist()[self.playlist_playing_position] != \ + self.track_queue[ + self.queue_step]: + + try: + p = self.playing_playlist().index(self.track_queue[self.queue_step]) + except Exception: + logging.exception("Failed to change playing_playlist") + p = random.randrange(len(self.playing_playlist())) + if p is not None: + self.playlist_playing_position = p + + self.playlist_playing_position -= 1 + self.track_queue.append(self.playing_playlist()[self.playlist_playing_position]) + self.queue_step = len(self.track_queue) - 1 + self.play_target(jump=True) + + elif self.random_mode is True and self.queue_step > 0: + self.queue_step -= 1 + self.play_target(jump=True) + else: + logging.info("BACK: NO CASE!") + self.show_current() + + if self.active_playlist_viewing == self.active_playlist_playing: + self.show_current(False, True) + + if album_mode: + goto_album(self.playlist_playing_position) + if gui.combo_mode and self.active_playlist_viewing == self.active_playlist_playing: + self.show_current() + + self.render_playlist() + self.notify_update() + notify_song() + lfm_scrobbler.start_queue() + gui.pl_update += 1 + + def stop(self, block: bool = False, run : bool = False) -> None: + + self.playerCommand = "stop" + if run: + self.playerCommand = "runstop" + if block: + self.playerSubCommand = "return" + + self.playerCommandReady = True + + if tauon.thread_manager.player_lock.locked(): + try: + tauon.thread_manager.player_lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked player_lock") + else: + logging.exception("Unknown RuntimeError trying to release player_lock") + except Exception: + logging.exception("Unknown exception trying to release player_lock") + + self.record_stream = False + if len(self.track_queue) > 0: + self.left_time = self.playing_time + self.left_index = self.track_queue[self.queue_step] + previous_state = self.playing_state + self.playing_time = 0 + self.decode_time = 0 + self.playing_state = 0 + self.render_playlist() + + gui.update_spec = 0 + # gui.update_level = True # Allows visualiser to enter decay sequence + gui.update = True + if update_title: + update_title_do() # Update title bar text + + if tauon.stream_proxy and tauon.stream_proxy.download_running: + tauon.stream_proxy.stop() + + if block: + loop = 0 + sleep_timeout(lambda: self.playerSubCommand != "stopped", 2) + if tauon.stream_proxy.download_running: + sleep_timeout(lambda: tauon.stream_proxy.download_running, 2) + + if tauon.spot_ctl.playing or tauon.spot_ctl.coasting: + logging.info("Spotify stop") + tauon.spot_ctl.control("stop") + + self.notify_update() + lfm_scrobbler.start_queue() + return previous_state + + def pause(self) -> None: + + if tauon.spotc and tauon.spotc.running and tauon.spot_ctl.playing: + if self.playing_state == 1: + self.playerCommand = "pauseon" + self.playerCommandReady = True + elif self.playing_state == 2: + self.playerCommand = "pauseoff" + self.playerCommandReady = True + + if self.playing_state == 3: + if tauon.spot_ctl.coasting: + if tauon.spot_ctl.paused: + tauon.spot_ctl.control("resume") + else: + tauon.spot_ctl.control("pause") + return + + if tauon.spot_ctl.playing: + if self.playing_state == 2: + tauon.spot_ctl.control("resume") + self.playing_state = 1 + elif self.playing_state == 1: + tauon.spot_ctl.control("pause") + self.playing_state = 2 + self.render_playlist() + return + + if self.playing_state == 1: + self.playerCommand = "pauseon" + self.playing_state = 2 + elif self.playing_state == 2: + self.playerCommand = "pauseoff" + self.playing_state = 1 + notify_song() + + self.playerCommandReady = True + + self.render_playlist() + self.notify_update() + + def pause_only(self) -> None: + if self.playing_state == 1: + self.playerCommand = "pauseon" + self.playing_state = 2 + + self.playerCommandReady = True + self.render_playlist() + self.notify_update() + + def play_pause(self) -> None: + if self.playing_state == 3: + self.stop() + elif self.playing_state > 0: + self.pause() + else: + self.play() + + def seek_decimal(self, decimal: int) -> None: + # if self.commit: + # return + if self.playing_state in (1, 2) or (self.playing_state == 3 and tauon.spot_ctl.coasting): + if decimal > 1: + decimal = 1 + elif decimal < 0: + decimal = 0 + self.new_time = self.playing_length * decimal + #logging.info('seek to:' + str(self.new_time)) + self.playerCommand = "seek" + self.playerCommandReady = True + self.playing_time = self.new_time + + if msys and taskbar_progress and self.windows_progress: + self.windows_progress.update(True) + + if self.mpris is not None: + self.mpris.seek_do(self.playing_time) + + def seek_time(self, new: float) -> None: + # if self.commit: + # return + if self.playing_state in (1, 2) or (self.playing_state == 3 and tauon.spot_ctl.coasting): + + if new > self.playing_length - 0.5: + self.advance() + return + + if new < 0.4: + new = 0 + + self.new_time = new + self.playing_time = new + + self.playerCommand = "seek" + self.playerCommandReady = True + + if self.mpris is not None: + self.mpris.seek_do(self.playing_time) + + def play(self) -> None: + + if tauon.spot_ctl.playing: + if self.playing_state == 2: + self.play_pause() + return + + # Unpause if paused + if self.playing_state == 2: + self.playerCommand = "pauseoff" + self.playerCommandReady = True + self.playing_state = 1 + self.notify_update() + + # If stopped + elif self.playing_state == 0: + + if radiobox.loaded_station: + radiobox.start(radiobox.loaded_station) + return + + # If the queue is empty + if self.track_queue == [] and len(self.multi_playlist[self.active_playlist_playing].playlist_ids) > 0: + self.track_queue.append(self.multi_playlist[self.active_playlist_playing].playlist_ids[0]) + self.queue_step = 0 + self.playlist_playing_position = 0 + self.active_playlist_playing = 0 + + self.play_target() + + # If the queue is not empty, play? + elif len(self.track_queue) > 0: + self.play_target() + + self.render_playlist() + + def spot_test_progress(self) -> None: + if self.playing_state in (1, 2) and tauon.spot_ctl.playing: + th = 5 # the rate to poll the spotify API + if self.playing_time > self.playing_length: + th = 1 + if not tauon.spot_ctl.paused: + if tauon.spot_ctl.start_timer.get() < 0.5: + tauon.spot_ctl.progress_timer.set() + return + add_time = tauon.spot_ctl.progress_timer.get() + if add_time > 5: + add_time = 0 + self.playing_time += add_time + self.decode_time = self.playing_time + # self.test_progress() + tauon.spot_ctl.progress_timer.set() + if len(self.track_queue) > 0 and 2 > add_time > 0: + star_store.add(self.track_queue[self.queue_step], add_time) + if tauon.spot_ctl.update_timer.get() > th: + tauon.spot_ctl.update_timer.set() + shooter(tauon.spot_ctl.monitor) + else: + self.test_progress() + + elif self.playing_state == 3 and tauon.spot_ctl.coasting: + th = 7 + if self.playing_time > self.playing_length or self.playing_time < 2.5: + th = 1 + if tauon.spot_ctl.update_timer.get() < th: + if not tauon.spot_ctl.paused: + self.playing_time += tauon.spot_ctl.progress_timer.get() + self.decode_time = self.playing_time + tauon.spot_ctl.progress_timer.set() + + else: + tauon.spot_ctl.update_timer.set() + tauon.spot_ctl.update() + + def purge_track(self, track_id: int, fast: bool = False) -> None: + """Remove a track from the database""" + # Remove from all playlists + if not fast: + for playlist in self.multi_playlist: + while track_id in playlist.playlist: + album_dex.clear() + playlist.playlist.remove(track_id) + # Stop if track is playing track + if self.track_queue and self.track_queue[self.queue_step] == track_id and self.playing_state != 0: + self.stop(block=True) + # Remove from playback history + while track_id in self.track_queue: + self.track_queue.remove(track_id) + self.queue_step -= 1 + # Remove track from force queue + for i in reversed(range(len(self.force_queue))): + if self.force_queue[i].track_id == track_id: + del self.force_queue[i] + del self.master_library[track_id] + + def test_progress(self) -> None: + # Fuzzy reload lastfm for rescrobble + if lfm_scrobbler.a_sc and self.playing_time < 1: + lfm_scrobbler.a_sc = False + self.a_time = 0 + + # Update the UI if playing time changes a whole number + # next_round = int(self.playing_time) + # if self.playing_time_int != next_round: + # #if not prefs.power_save: + # #gui.update += 1 + # self.playing_time_int = next_round + + gap_extra = 2 # 2 + + if tauon.spot_ctl.playing or tauon.chrome_mode: + gap_extra = 3 + + if msys and taskbar_progress and self.windows_progress: + self.windows_progress.update(True) + + if self.commit is not None: + return + + if self.playing_state == 1 and self.multi_playlist[self.active_playlist_playing].persist_time_positioning: + tr = self.playing_object() + if tr: + tr.misc["position"] = self.decode_time + + if self.playing_state == 1 and self.decode_time + gap_extra >= self.playing_length and self.decode_time > 0.2: + + # Allow some time for spotify playing time to update? + if tauon.spot_ctl.playing and tauon.spot_ctl.start_timer.get() < 3: + return + + # Allow some time for backend to provide a length + if self.playing_time < 6 and self.playing_length == 0: + return + if not tauon.spot_ctl.playing and self.a_time < 2: + return + + self.decode_time = 0 + + pp = self.playing_playlist() + + if self.auto_stop: # and not self.force_queue and not (self.force_queue and self.pause_queue): + self.stop(run=True) + if self.force_queue or (not self.force_queue and not self.random_mode and not self.repeat_mode): + self.advance(play=False) + gui.update += 2 + self.auto_stop = False + + elif self.force_queue and not self.pause_queue: + id = self.advance(end=True, quiet=True, dry=True) + if id is not None: + self.start_commit(id) + return + self.advance(end=True, quiet=True) + + + + elif self.repeat_mode is True: + + if self.album_repeat_mode: + + if self.playlist_playing_position > len(pp) - 1: + self.playlist_playing_position = 0 # Hack fix, race condition bug? + + ti = self.get_track(pp[self.playlist_playing_position]) + + i = self.playlist_playing_position + + # Test if next track is in same folder + if i + 1 < len(pp): + nt = self.get_track(pp[i + 1]) + if ti.parent_folder_path == nt.parent_folder_path: + # The next track is in the same folder + # so advance normally + self.advance(quiet=True, end=True) + return + + # We need to backtrack to see where the folder begins + i -= 1 + while i >= 0: + nt = self.get_track(pp[i]) + if ti.parent_folder_path != nt.parent_folder_path: + i += 1 + break + i -= 1 + i = max(i, 0) + + self.selected_in_playlist = i + shift_selection = [i] + + self.jump(pp[i], i, jump=False) + + elif prefs.playback_follow_cursor and self.playing_ready() \ + and self.multi_playlist[self.active_playlist_viewing].playlist[ + self.selected_in_playlist] != self.playing_object().index \ + and -1 < self.selected_in_playlist < len(default_playlist): + + logging.info("Repeat follow cursor") + + self.playing_time = 0 + self.decode_time = 0 + self.active_playlist_playing = self.active_playlist_viewing + self.playlist_playing_position = self.selected_in_playlist + + self.track_queue.append(default_playlist[self.selected_in_playlist]) + self.queue_step = len(self.track_queue) - 1 + self.play_target(jump=False) + self.render_playlist() + lfm_scrobbler.start_queue() + + else: + id = self.track_queue[self.queue_step] + self.commit = id + target = self.get_track(id) + self.target_open = target.fullpath + self.target_object = target + self.start_time = target.start_time + self.start_time_target = self.start_time + self.playerCommand = "open" + self.playerSubCommand = "repeat" + self.playerCommandReady = True + + #self.render_playlist() + lfm_scrobbler.start_queue() + + # Reload lastfm for rescrobble + if lfm_scrobbler.a_sc: + lfm_scrobbler.a_sc = False + self.a_time = 0 + + elif self.random_mode is False and len(pp) > self.playlist_playing_position + 1 and \ + self.master_library[pp[self.playlist_playing_position]].is_cue is True \ + and self.master_library[pp[self.playlist_playing_position + 1]].filename == \ + self.master_library[pp[self.playlist_playing_position]].filename and int( + self.master_library[pp[self.playlist_playing_position]].track_number) == int( + self.master_library[pp[self.playlist_playing_position + 1]].track_number) - 1: + + # not (self.force_queue and not self.pause_queue) and \ + + # We can shave it closer + if not self.playing_time + 0.1 >= self.playing_length: + return + + logging.info("Do transition CUE") + self.playlist_playing_position += 1 + self.queue_step += 1 + self.track_queue.append(pp[self.playlist_playing_position]) + self.playing_state = 1 + self.playing_time = 0 + self.decode_time = 0 + self.playing_length = self.master_library[self.track_queue[self.queue_step]].length + self.start_time = self.master_library[self.track_queue[self.queue_step]].start_time + self.start_time_target = self.start_time + lfm_scrobbler.start_queue() + + gui.update += 1 + gui.pl_update = 1 + + if update_title: + update_title_do() + self.notify_update() + else: + # self.advance(quiet=True, end=True) + + id = self.advance(quiet=True, end=True, dry=True) + if id is not None and not tauon.spot_ctl.playing: + #logging.info("Commit") + self.start_commit(id) + return + + self.advance(quiet=True, end=True) + self.playing_time = 0 + self.decode_time = 0 + + def start_commit(self, commit_id: int, repeat: bool = False) -> None: + self.commit = commit_id + target = self.get_track(commit_id) + self.target_open = target.fullpath + self.target_object = target + self.start_time = target.start_time + self.start_time_target = self.start_time + self.playerCommand = "open" + if repeat: + self.playerSubCommand = "repeat" + self.playerCommandReady = True + + def advance( + self, rr: bool = False, quiet: bool = False, inplace: bool = False, end: bool = False, + force: bool = False, play: bool = True, dry: bool = False, + ) -> int | None: + # Spotify remote control mode + if not dry and tauon.spot_ctl.coasting: + tauon.spot_ctl.control("next") + tauon.spot_ctl.update_timer.set() + self.playing_time = -2 + self.decode_time = -2 + return None + + # Temporary Workaround for UI block causing unwanted dragging + if not dry: + quick_d_timer.set() + + if prefs.show_current_on_transition: + quiet = False + + # Trim the history if it gets too long + while len(self.track_queue) > 250: + self.queue_step -= 1 + del self.track_queue[0] + + # Save info about the track we are leaving + if not dry and len(self.track_queue) > 0: + self.left_time = self.playing_time + self.left_index = self.track_queue[self.queue_step] + + # Test to register skip (not currently used for anything) + if not dry and self.playing_state == 1 and 1 < self.left_time < 45: + self.master_library[self.left_index].skips += 1 + #logging.info('skip registered') + + if not dry: + self.playing_time = 0 + self.decode_time = 0 + self.playing_length = 100 + gui.update_spec = 0 + + old = self.queue_step + end_of_playlist = False + + # Force queue (middle click on track) + if len(self.force_queue) > 0 and not self.pause_queue: + + q = self.force_queue[0] + target_index = q.track_id + + if q.type == 1: + # This is an album type + + if q.album_stage == 0: + # We have not started playing the album yet + # So we go to that track + # (This is a copy of the track code, but we don't delete the item) + + if not dry: + + pl = id_to_pl(q.playlist_id) + if pl is not None: + self.active_playlist_playing = pl + + if target_index not in self.playing_playlist(): + del self.force_queue[0] + self.advance() + return None + + if dry: + return target_index + + self.playlist_playing_position = q.position + self.track_queue.append(target_index) + self.queue_step = len(self.track_queue) - 1 + # self.queue_target = len(self.track_queue) - 1 + if play: + self.play_target(jump=not end) + + # Set the flag that we have entered the album + self.force_queue[0].album_stage = 1 + + # This code is mirrored below ------- + ok_continue = True + + # Check if we are at end of playlist + pl = self.multi_playlist[self.active_playlist_playing].playlist_ids + if self.playlist_playing_position > len(pl) - 3: + ok_continue = False + + # Check next song is in album + if ok_continue and self.get_track(pl[self.playlist_playing_position + 1]).parent_folder_path != self.get_track(target_index).parent_folder_path: + ok_continue = False + + # ----------- + + + elif q.album_stage == 1: + # We have previously started playing this album + + # Check to see if we still are: + ok_continue = True + + if self.get_track(target_index).parent_folder_path != self.playing_object().parent_folder_path: + # Remember to set jumper check this too (leave album if we jump to some other track, i.e. double click)) + ok_continue = False + + pl = self.multi_playlist[self.active_playlist_playing].playlist_ids + + # Check next song is in album + if ok_continue: + + # Check if we are at end of playlist, or already at end of album + if self.playlist_playing_position >= len(pl) - 1 or (self.playlist_playing_position < len( + pl) - 1 and \ + self.get_track(pl[self.playlist_playing_position + 1]).parent_folder_path != self.get_track( + target_index).parent_folder_path): + + if dry: + return None + + del self.force_queue[0] + self.advance() + return None + + + # Check if 2 songs down is in album, remove entry in queue if not + if self.playlist_playing_position < len(pl) - 2 and \ + self.get_track(pl[self.playlist_playing_position + 2]).parent_folder_path != self.get_track( + target_index).parent_folder_path: + ok_continue = False + + # if ok_continue: + # We seem to be still in the album. Step down one and play + if not dry: + self.playlist_playing_position += 1 + + if len(pl) <= self.playlist_playing_position: + if dry: + return None + logging.info("END OF PLAYLIST!") + del self.force_queue[0] + self.advance() + return None + + if dry: + return pl[self.playlist_playing_position + 1] + self.track_queue.append(pl[self.playlist_playing_position]) + self.queue_step = len(self.track_queue) - 1 + # self.queue_target = len(self.track_queue) - 1 + if play: + self.play_target(jump=not end) + + if not ok_continue: + # It seems this item has expired, remove it and call advance again + + if dry: + return None + + logging.info("Remove expired album from queue") + del self.force_queue[0] + + if q.auto_stop: + self.auto_stop = True + if prefs.stop_end_queue and not self.force_queue: + self.auto_stop = True + + if queue_box.scroll_position > 0: + queue_box.scroll_position -= 1 + + # self.advance() + # return + + else: + # This is track type + pl = id_to_pl(q.playlist_id) + if not dry and pl is not None: + self.active_playlist_playing = pl + + if target_index not in self.playing_playlist(): + if dry: + return None + del self.force_queue[0] + self.advance() + return None + + if dry: + return target_index + + self.playlist_playing_position = q.position + self.track_queue.append(target_index) + self.queue_step = len(self.track_queue) - 1 + # self.queue_target = len(self.track_queue) - 1 + if play: + self.play_target(jump=not end) + del self.force_queue[0] + if q.auto_stop: + self.auto_stop = True + if prefs.stop_end_queue and not self.force_queue: + self.auto_stop = True + if queue_box.scroll_position > 0: + queue_box.scroll_position -= 1 + + # Stop if playlist is empty + elif len(self.playing_playlist()) == 0: + if dry: + return None + self.stop() + return 0 + + # Playback follow cursor + elif prefs.playback_follow_cursor and self.playing_ready() \ + and self.multi_playlist[self.active_playlist_viewing].playlist_ids[ + self.selected_in_playlist] != self.playing_object().index \ + and -1 < self.selected_in_playlist < len(default_playlist): + + if dry: + return default_playlist[self.selected_in_playlist] + + self.active_playlist_playing = self.active_playlist_viewing + self.playlist_playing_position = self.selected_in_playlist + + self.track_queue.append(default_playlist[self.selected_in_playlist]) + self.queue_step = len(self.track_queue) - 1 + if play: + self.play_target(jump=not end) + + # If random, jump to random track + elif (self.random_mode or rr) and len(self.playing_playlist()) > 0 and not ( + self.album_shuffle_mode or prefs.album_shuffle_lock_mode): + # self.queue_step += 1 + new_step = self.queue_step + 1 + + if new_step == len(self.track_queue): + + if self.album_repeat_mode and self.repeat_mode: + # Album shuffle mode + pp = self.playing_playlist() + k = self.playlist_playing_position + # ti = self.get_track(pp[k]) + ti = self.master_library[self.track_queue[self.queue_step]] + + if ti.index not in pp: + if dry: + return None + logging.info("No tracks to repeat!") + return 0 + + matches = [] + for i, p in enumerate(pp): + + if self.get_track(p).parent_folder_path == ti.parent_folder_path: + matches.append((i, p)) + + if matches: + # Avoid a repeat of same track + if len(matches) > 1 and (k, ti.index) in matches: + matches.remove((k, ti.index)) + + i, p = random.choice(matches) # not used + + if prefs.true_shuffle: + + id = ti.parent_folder_path + + while True: + if id in self.shuffle_pools: + + pool = self.shuffle_pools[id] + + if not pool: + del self.shuffle_pools[id] # Trigger a refill + continue + + ref = pool.pop() + if dry: + pool.append(ref) + return ref[1] + # ref = random.choice(pool) + # pool.remove(ref) + + if ref[1] not in pp: # Check track still in the live playlist + logging.info("Track not in pool") + continue + + i, p = ref # Find position of reference in playlist + break + + # Refill the pool + random.shuffle(matches) + self.shuffle_pools[id] = matches + logging.info("Refill folder shuffle pool") + + self.playlist_playing_position = i + self.track_queue.append(p) + + else: + # Normal select from playlist + + if prefs.true_shuffle: + # True shuffle avoids repeats by using a pool + + pl = self.multi_playlist[self.active_playlist_playing] + id = pl.uuid_int + + while True: + + if id in self.shuffle_pools: + + pool = self.shuffle_pools[id] + + if not pool: + del self.shuffle_pools[id] # Trigger a refill + continue + + ref = pool.pop() + if dry: + pool.append(ref) + return ref + # ref = random.choice(pool) + # pool.remove(ref) + + if ref not in pl.playlist_ids: # Check track still in the live playlist + continue + + random_jump = pl.playlist_ids.index(ref) # Find position of reference in playlist + break + + # Refill the pool + self.update_shuffle_pool(pl.uuid_int) + + else: + random_jump = random.randrange(len(self.playing_playlist())) # not used + + self.playlist_playing_position = random_jump + self.track_queue.append(self.playing_playlist()[random_jump]) + + if inplace and self.queue_step > 1: + del self.track_queue[self.queue_step] + else: + if dry: + return self.track_queue[new_step] + self.queue_step = new_step + + if rr: + if dry: + return None + self.play_target_rr() + elif play: + self.play_target(jump=not end) + + + # If not random mode, Step down 1 on the playlist + elif self.random_mode is False and len(self.playing_playlist()) > 0: + + # Stop at end of playlist + if self.playlist_playing_position == len(self.playing_playlist()) - 1: + if dry: + return None + if prefs.end_setting == "stop": + self.playing_state = 0 + self.playerCommand = "runstop" + self.playerCommandReady = True + end_of_playlist = True + + elif prefs.end_setting in ("advance", "cycle"): + + # If at end playlist and not cycle mode, stop playback + if self.active_playlist_playing == len( + self.multi_playlist) - 1 and prefs.end_setting != "cycle": + self.playing_state = 0 + self.playerCommand = "runstop" + self.playerCommandReady = True + end_of_playlist = True + + else: + + p = self.active_playlist_playing + for i in range(len(self.multi_playlist)): + + k = (p + i + 1) % len(self.multi_playlist) + + # Skip a playlist if empty + if not (self.multi_playlist[k].playlist_ids): + continue + + # Skip a playlist if hidden + if self.multi_playlist[k].hidden and prefs.tabs_on_top: + continue + + # Set found playlist as playing the first track + self.active_playlist_playing = k + self.playlist_playing_position = -1 + self.advance(end=end, force=True, play=play) + break + + else: + # Restart current if no other eligible playlist found + self.playlist_playing_position = -1 + self.advance(end=end, force=True, play=play) + + return None + + elif prefs.end_setting == "repeat": + self.playlist_playing_position = -1 + self.advance(end=end, force=True, play=play) + return None + + gui.update += 3 + + else: + if self.playlist_playing_position > len(self.playing_playlist()) - 1: + if dry: + return None + self.playlist_playing_position = 0 + + elif not force and len(self.track_queue) > 0 and self.playing_playlist()[ + self.playlist_playing_position] != self.track_queue[ + self.queue_step]: + try: + if dry: + return None + self.playlist_playing_position = self.playing_playlist().index( + self.track_queue[self.queue_step]) + except Exception: + logging.exception("Failed to set playlist_playing_position") + + if len(self.playing_playlist()) == self.playlist_playing_position + 1: + return None + + if dry: + return self.playing_playlist()[self.playlist_playing_position + 1] + self.playlist_playing_position += 1 + self.track_queue.append(self.playing_playlist()[self.playlist_playing_position]) + + # logging.info("standand advance") + # self.queue_target = len(self.track_queue) - 1 + # if end: + # self.play_target_gapless(jump= not end) + # else: + self.queue_step = len(self.track_queue) - 1 + if play: + self.play_target(jump=not end) + + elif self.random_mode and (self.album_shuffle_mode or prefs.album_shuffle_lock_mode): + + # Album shuffle mode + logging.info("Album shuffle mode") + + po = self.playing_object() + + redraw = False + + # Checks + if po is not None and len(self.playing_playlist()) > 0: + + # If we at end of playlist, we'll go to a new album + if len(self.playing_playlist()) == self.playlist_playing_position + 1: + redraw = True + # If the next track is a new album, go to a new album + elif po.parent_folder_path != self.get_track( + self.playing_playlist()[self.playlist_playing_position + 1]).parent_folder_path: + redraw = True + # Always redraw on press in album shuffle lockdown + if prefs.album_shuffle_lock_mode and not end: + redraw = True + + if not redraw: + if dry: + return self.playing_playlist()[self.playlist_playing_position + 1] + self.playlist_playing_position += 1 + self.track_queue.append(self.playing_playlist()[self.playlist_playing_position]) + self.queue_step = len(self.track_queue) - 1 + # self.queue_target = len(self.track_queue) - 1 + if play: + self.play_target(jump=not end) + + else: + + if dry: + return None + albums = [] + current_folder = "" + for i in range(len(self.playing_playlist())): + if i == 0: + albums.append(i) + current_folder = self.master_library[self.playing_playlist()[i]].parent_folder_path + elif self.master_library[self.playing_playlist()[i]].parent_folder_path != current_folder: + current_folder = self.master_library[self.playing_playlist()[i]].parent_folder_path + albums.append(i) + + random.shuffle(albums) + + for a in albums: + if self.get_track(self.playing_playlist()[a]).parent_folder_path != self.playing_object().parent_folder_path: + self.playlist_playing_position = a + self.track_queue.append(self.playing_playlist()[a]) + self.queue_step = len(self.track_queue) - 1 + # self.queue_target = len(self.track_queue) - 1 + if play: + self.play_target(jump=not end) + break + a = 0 + self.playlist_playing_position = a + self.track_queue.append(self.playing_playlist()[a]) + self.queue_step = len(self.track_queue) - 1 + if play: + self.play_target(jump=not end) + # logging.info("THERE IS ONLY ONE ALBUM IN THE PLAYLIST") + # self.stop() + + else: + logging.error("ADVANCE ERROR - NO CASE!") + + if dry: + return None + + if self.active_playlist_viewing == self.active_playlist_playing: + self.show_current(quiet=quiet) + elif prefs.auto_goto_playing: + self.show_current(quiet=quiet, this_only=True, playing=False, highlight=True, no_switch=True) + + # if album_mode: + # goto_album(self.playlist_playing) + + self.render_playlist() + + if tauon.spot_ctl.playing and end_of_playlist: + tauon.spot_ctl.control("stop") + + self.notify_update() + lfm_scrobbler.start_queue() + if play: + notify_song(end_of_playlist, delay=1.3) + return None + + def reset_missing_flags(self) -> None: + for value in self.master_library.values(): + value.found = True + gui.pl_update += 1 + +class LastFMapi: + API_SECRET = "6e433964d3ff5e817b7724d16a9cf0cc" + connected = False + API_KEY = "bfdaf6357f1dddd494e5bee1afe38254" + scanning_username = "" + + network = None + lastfm_network = None + tries = 0 + + scanning_friends = False + scanning_loves = False + scanning_scrobbles = False + + def __init__(self) -> None: + self.sg = None + self.url = None + + def get_network(self) -> LibreFMNetwork: + if prefs.use_libre_fm: + return pylast.LibreFMNetwork + return pylast.LastFMNetwork + + def auth1(self) -> None: + if not last_fm_enable: + show_message(_("Optional module python-pylast not installed"), mode="warning") + return + # This is step one where the user clicks "login" + + if self.network is None: + self.no_user_connect() + + self.sg = pylast.SessionKeyGenerator(self.network) + self.url = self.sg.get_web_auth_url() + show_message(_("Web auth page opened"), _("Once authorised click the 'done' button."), mode="arrow") + webbrowser.open(self.url, new=2, autoraise=True) + + def auth2(self) -> None: + + # This is step 2 where the user clicks "Done" + + if self.sg is None: + show_message(_("You need to log in first")) + return + + try: + # session_key = self.sg.get_web_auth_session_key(self.url) + session_key, username = self.sg.get_web_auth_session_key_username(self.url) + prefs.last_fm_token = session_key + self.network = self.get_network()(api_key=self.API_KEY, api_secret= + self.API_SECRET, session_key=prefs.last_fm_token) + # user = self.network.get_authenticated_user() + # username = user.get_name() + prefs.last_fm_username = username + + except Exception as e: + if "Unauthorized Token" in str(e): + logging.exception("Not authorized") + show_message(_("Error - Not authorized"), mode="error") + else: + logging.exception("Unknown error") + show_message(_("Error"), _("Unknown error."), mode="error") + + if not toggle_lfm_auto(mode=1): + toggle_lfm_auto() + + def auth3(self) -> None: + """This is used for 'logout'""" + prefs.last_fm_token = None + prefs.last_fm_username = "" + show_message(_("Logout will complete on app restart.")) + + def connect(self, m_notify: bool = True) -> bool | None: + + if not last_fm_enable: + return False + + if self.connected is True: + if m_notify: + show_message(_("Already connected to Last.fm")) + return True + + if prefs.last_fm_token is None: + show_message(_("No Last.Fm account registered"), _("Authorise an account in settings"), mode="info") + return None + + logging.info("Attempting to connect to Last.fm network") + + try: + + self.network = self.get_network()( + api_key=self.API_KEY, api_secret=self.API_SECRET, session_key=prefs.last_fm_token) # , username=lfm_username, password_hash=lfm_hash) + + self.connected = True + if m_notify: + show_message(_("Connection to Last.fm was successful."), mode="done") + + logging.info("Connection to lastfm appears successful") + return True + + except Exception as e: + logging.exception("Error connecting to Last.fm network") + show_message(_("Error connecting to Last.fm network"), str(e), mode="warning") + return False + + def toggle(self) -> None: + prefs.scrobble_hold ^= True + + def details_ready(self) -> bool: + if prefs.last_fm_token: + return True + return False + + def last_fm_only_connect(self) -> bool: + if not last_fm_enable: + return False + try: + self.lastfm_network = pylast.LastFMNetwork(api_key=self.API_KEY, api_secret=self.API_SECRET) + logging.info("Connection appears successful") + return True + + except Exception as e: + logging.exception("Error communicating with Last.fm network") + show_message(_("Error communicating with Last.fm network"), str(e), mode="warning") + return False + + def no_user_connect(self) -> bool: + if not last_fm_enable: + return False + try: + self.network = self.get_network()(api_key=self.API_KEY, api_secret=self.API_SECRET) + logging.info("Connection appears successful") + return True + + except Exception as e: + logging.exception("Error communicating with Last.fm network") + show_message(_("Error communicating with Last.fm network"), str(e), mode="warning") + return False + + def get_all_scrobbles_estimate_time(self) -> float | None: + + if not self.connected: + self.connect(False) + if not self.connected or not prefs.last_fm_username: + return None + + user = pylast.User(prefs.last_fm_username, self.network) + total = user.get_playcount() + + if total: + return 0.04364 * total + return 0 + + def get_all_scrobbles(self) -> None: + + if not self.connected: + self.connect(False) + if not self.connected or not prefs.last_fm_username: + return + + try: + self.scanning_scrobbles = True + self.network.enable_rate_limit() + user = pylast.User(prefs.last_fm_username, self.network) + # username = user.get_name() + perf_timer.set() + tracks = user.get_recent_tracks(None) + + counts = {} + + # Count up the unique pairs + for track in tracks: + key = (str(track.track.artist), str(track.track.title)) + c = counts.get(key, 0) + counts[key] = c + 1 + + touched = [] + + # Add counts to matching tracks + for key, value in counts.items(): + artist, title = key + artist = artist.lower() + title = title.lower() + + for track in pctl.master_library.values(): + t_artist = track.artist.lower() + artists = [x.lower() for x in get_split_artists(track)] + if t_artist == artist or artist in artists or ( + track.album_artist and track.album_artist.lower() == artist): + if track.title.lower() == title: + if track.index in touched: + track.lfm_scrobbles += value + else: + track.lfm_scrobbles = value + touched.append(track.index) + except Exception: + logging.exception("Scanning failed. Try again?") + gui.pl_update += 1 + self.scanning_scrobbles = False + show_message(_("Scanning failed. Try again?"), mode="error") + return + + logging.info(perf_timer.get()) + gui.pl_update += 1 + self.scanning_scrobbles = False + tauon.bg_save() + show_message(_("Scanning scrobbles complete"), mode="done") + + def artist_info(self, artist: str): + + if self.lastfm_network is None: + if self.last_fm_only_connect() is False: + return False, "", "" + + try: + if artist != "": + l_artist = pylast.Artist( + artist.replace("/", "").replace("\\", "").replace(" & ", " and ").replace("&", " "), + self.lastfm_network) + bio = l_artist.get_bio_content() + # cover_link = l_artist.get_cover_image() + mbid = l_artist.get_mbid() + url = l_artist.get_url() + + return True, bio, "", mbid, url + except Exception: + logging.exception("last.fm get artist info failed") + + return False, "", "", "", "" + + def artist_mbid(self, artist: str): + + if self.lastfm_network is None: + if self.last_fm_only_connect() is False: + return "" + + try: + if artist != "": + l_artist = pylast.Artist( + artist.replace("/", "").replace("\\", "").replace(" & ", " and ").replace("&", " "), + self.lastfm_network) + mbid = l_artist.get_mbid() + return mbid + except Exception: + logging.exception("last.fm get artist mbid info failed") + + return "" + + def sync_pull_love(self, track_object: TrackClass) -> None: + if not prefs.lastfm_pull_love or not (track_object.artist and track_object.title): + return + if not last_fm_enable: + return + if prefs.auto_lfm: + self.connect(False) + if not self.connected: + return + + try: + track = self.network.get_track(track_object.artist, track_object.title) + if not track: + logging.error("Get love: track not found") + return + track.username = prefs.last_fm_username + + remote_loved = track.get_userloved() + + if track_object.title != track.get_correction() or track_object.artist != track.get_artist().get_correction(): + logging.warning(f"Pylast/lastfm bug workaround. API thought {track_object.artist} - {track_object.title} loved status was: {remote_loved}") + return + + if remote_loved is None: + logging.error("Error getting loved status") + return + + local_loved = love(set=False, track_id=track_object.index, notify=False, sync=False) + + if remote_loved != local_loved: + love(set=True, track_id=track_object.index, notify=False, sync=False) + except Exception: + logging.exception("Failed to pull love") + + def scrobble(self, track_object: TrackClass, timestamp: float | None = None) -> bool: + if not last_fm_enable: + return True + if prefs.scrobble_hold: + return True + if prefs.auto_lfm: + self.connect(False) + + if timestamp is None: + timestamp = int(time.time()) + + # lastfm_user = self.network.get_user(self.username) + + title = track_object.title + album = track_object.album + artist = get_artist_strip_feat(track_object) + album_artist = track_object.album_artist + + logging.info("Submitting scrobble...") + + # Act + try: + if title != "" and artist != "": + if album != "": + if album_artist and album_artist != artist: + self.network.scrobble( + artist=artist, title=title, album=album, album_artist=album_artist, timestamp=timestamp) + else: + self.network.scrobble(artist=artist, title=title, album=album, timestamp=timestamp) + else: + self.network.scrobble(artist=artist, title=title, timestamp=timestamp) + # logging.info('Scrobbled') + + # Pull loved status + + self.sync_pull_love(track_object) + + + else: + logging.warning("Not sent, incomplete metadata") + + except Exception as e: + logging.exception("Failed to Scrobble!") + if "retry" in str(e): + logging.warning("Retrying in a couple seconds...") + time.sleep(7) + + try: + self.network.scrobble(artist=artist, title=title, timestamp=timestamp) + # logging.info('Scrobbled') + return True + except Exception: + logging.exception("Failed to retry!") + + # show_message(_("Error: Could not scrobble. ", str(e), mode='warning') + logging.error("Error connecting to last.fm") + scrobble_warning_timer.set() + gui.update += 1 + gui.delay_frame(5) + + return False + return True + + def get_bio(self, artist: str) -> str: + + if self.lastfm_network is None: + if self.last_fm_only_connect() is False: + return "" + + artist_object = pylast.Artist(artist, self.lastfm_network) + bio = artist_object.get_bio_summary(language="en") + # logging.info(artist_object.get_cover_image()) + # logging.info("\n\n") + # logging.info(bio) + # logging.info("\n\n") + # logging.info(artist_object.get_bio_content()) + return bio + # else: + # return "" + + def love(self, artist: str, title: str): + + if not self.connected and prefs.auto_lfm: + self.connect(False) + prefs.scrobble_hold = True + if self.connected and artist != "" and title != "": + track = self.network.get_track(artist, title) + track.love() + + def unlove(self, artist: str, title: str): + if not last_fm_enable: + return + if not self.connected and prefs.auto_lfm: + self.connect(False) + prefs.scrobble_hold = True + if self.connected and artist != "" and title != "": + track = self.network.get_track(artist, title) + track.love() + track.unlove() + + def clear_friends_love(self) -> None: + + count = 0 + for index, tr in pctl.master_library.items(): + count += len(tr.lfm_friend_likes) + tr.lfm_friend_likes.clear() + + show_message(_("Removed {N} loves.").format(N=count)) + + def get_friends_love(self): + if not last_fm_enable: + return + self.scanning_friends = True + + try: + username = prefs.last_fm_username + logging.info(f"Username is {username}") + + if not username: + self.scanning_friends = False + show_message(_("There was an error, try re-log in")) + return + + if self.network is None: + self.no_user_connect() + + self.network.enable_rate_limit() + lastfm_user = self.network.get_user(username) + friends = lastfm_user.get_friends(limit=None) + show_message(_("Getting friend data..."), _("This may take a very long time."), mode="info") + for friend in friends: + self.scanning_username = friend.name + logging.info("Getting friend loves: " + friend.name) + + try: + loves = friend.get_loved_tracks(limit=None) + except Exception: + logging.exception("Failed to get_loved_tracks!") + + for track in loves: + title = track.track.title.casefold() + artist = track.track.artist.name.casefold() + for index, tr in pctl.master_library.items(): + + if tr.title.casefold() == title and tr.artist.casefold() == artist: + tr.lfm_friend_likes.add(friend.name) + logging.info("MATCH") + logging.info(" " + artist + " - " + title) + logging.info(" ----- " + friend.name) + + except Exception: + logging.exception("There was an error getting friends loves") + show_message(_("There was an error getting friends loves"), "", mode="warning") + + self.scanning_friends = False + + def dl_love(self) -> None: + if not last_fm_enable: + return + username = prefs.last_fm_username + show_message(_("Scanning loved tracks for: {username}").format(username=username), mode="info") + self.scanning_username = username + + if not username: + show_message(_("No username found"), mode="error") + return + + if len(username) > 25: + logging.error("Aborted due to long username") + return + + self.scanning_loves = True + + logging.info("Connect for friend scan") + + try: + if self.network is None: + self.no_user_connect() + + self.network.enable_rate_limit() + logging.info("Get user...") + lastfm_user = self.network.get_user(username) + tracks = lastfm_user.get_loved_tracks(limit=None) + + matches = 0 + updated = 0 + + for track in tracks: + title = track.track.title.casefold() + artist = track.track.artist.name.casefold() + + for index, tr in pctl.master_library.items(): + if tr.title.casefold() == title and tr.artist.casefold() == artist: + matches += 1 + logging.info("MATCH:") + logging.info(" " + artist + " - " + title) + star = star_store.full_get(index) + if star is None: + star = star_store.new_object() + if "L" not in star[1]: + updated += 1 + logging.info(" NEW LOVE") + star[1] += "L" + + star_store.insert(index, star) + + self.scanning_loves = False + if len(tracks) == 0: + show_message(_("User has no loved tracks.")) + return + if matches > 0 and updated == 0: + show_message(_("{N} matched tracks are up to date.").format(N=str(matches))) + return + if matches > 0 and updated > 0: + show_message(_("{N} tracks matched. {T} were updated.").format(N=str(matches), T=str(updated))) + return + show_message(_("Of {N} loved tracks, no matches were found in local db").format(N=str(len(tracks)))) + return + except Exception: + logging.exception("This doesn't seem to be working :(") + show_message(_("This doesn't seem to be working :("), mode="error") + self.scanning_loves = False + + def update(self, track_object: TrackClass) -> int | None: + if not last_fm_enable: + return None + if prefs.scrobble_hold: + return 0 + if prefs.auto_lfm: + if self.connect(False) is False: + prefs.auto_lfm = False + else: + return 0 + + # logging.info('Updating Now Playing') + + title = track_object.title + album = track_object.album + artist = get_artist_strip_feat(track_object) + + try: + if title != "" and artist != "": + self.network.update_now_playing( + artist=artist, title=title, album=album) + return 0 + logging.error("Not sent, incomplete metadata") + return 0 + except Exception as e: + logging.exception("Error connecting to last.fm.") + if "retry" in str(e): + return 2 + # show_message(_("Could not update Last.fm. ", str(e), mode='warning') + pctl.b_time -= 5000 + return 1 + +class ListenBrainz: + + def __init__(self, prefs: Prefs): + + self.enable = prefs.enable_lb + # self.url = "https://api.listenbrainz.org/1/submit-listens" + + def url(self): + url = prefs.listenbrainz_url + if not url: + url = "https://api.listenbrainz.org/" + if not url.endswith("/"): + url += "/" + return url + "1/submit-listens" + + def listen_full(self, track_object: TrackClass, time) -> bool: + + if self.enable is False: + return True + if prefs.scrobble_hold is True: + return True + if prefs.lb_token is None: + show_message(_("ListenBrainz is enabled but there is no token."), _("How did this even happen."), mode="error") + + title = track_object.title + album = track_object.album + artist = get_artist_strip_feat(track_object) + + if title == "" or artist == "": + return True + + data = {"listen_type": "single", "payload": []} + metadata = {"track_name": title, "artist_name": artist} + + additional = {} + + # MusicBrainz Artist IDs + if "musicbrainz_artistids" in track_object.misc: + additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] + + # MusicBrainz Release ID + if "musicbrainz_albumid" in track_object.misc: + additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] + + # MusicBrainz Recording ID + if "musicbrainz_recordingid" in track_object.misc: + additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] + + # MusicBrainz Track ID + if "musicbrainz_trackid" in track_object.misc: + additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] + + if additional: + metadata["additional_info"] = additional + + # logging.info(additional) + data["payload"].append({"track_metadata": metadata}) + data["payload"][0]["listened_at"] = time + + r = requests.post(self.url(), headers={"Authorization": "Token " + prefs.lb_token}, data=json.dumps(data), timeout=10) + if r.status_code != 200: + show_message(_("There was an error submitting data to ListenBrainz"), r.text, mode="warning") + return False + return True + + def listen_playing(self, track_object: TrackClass) -> None: + if self.enable is False: + return + if prefs.scrobble_hold is True: + return + if prefs.lb_token is None: + show_message(_("ListenBrainz is enabled but there is no token."), _("How did this even happen."), mode="error") + title = track_object.title + album = track_object.album + artist = get_artist_strip_feat(track_object) + + if title == "" or artist == "": + return + + data = {"listen_type": "playing_now", "payload": []} + metadata = {"track_name": title, "artist_name": artist} + + additional = {} + + # MusicBrainz Artist IDs + if "musicbrainz_artistids" in track_object.misc: + additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] + + # MusicBrainz Release ID + if "musicbrainz_albumid" in track_object.misc: + additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] + + # MusicBrainz Recording ID + if "musicbrainz_recordingid" in track_object.misc: + additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] + + # MusicBrainz Track ID + if "musicbrainz_trackid" in track_object.misc: + additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] + + if track_object.track_number: + try: + additional["tracknumber"] = str(int(track_object.track_number)) + except Exception: + logging.exception("Error trying to get track_number") + + if track_object.length: + additional["duration"] = str(int(track_object.length)) + + additional["media_player"] = t_title + additional["submission_client"] = t_title + additional["media_player_version"] = str(n_version) + + metadata["additional_info"] = additional + data["payload"].append({"track_metadata": metadata}) + # data["payload"][0]["listened_at"] = int(time.time()) + + r = requests.post(self.url(), headers={"Authorization": "Token " + prefs.lb_token}, data=json.dumps(data), timeout=10) + if r.status_code != 200: + show_message(_("There was an error submitting data to ListenBrainz"), r.text, mode="warning") + logging.error("There was an error submitting data to ListenBrainz") + logging.error(r.status_code) + logging.error(r.json()) + + def paste_key(self): + + text = copy_from_clipboard() + if text == "": + show_message(_("There is no text in the clipboard"), mode="error") + return + + if prefs.listenbrainz_url: + prefs.lb_token = text + return + + if len(text) == 36 and text[8] == "-": + prefs.lb_token = text + else: + show_message(_("That is not a valid token."), mode="error") + + def clear_key(self): + + prefs.lb_token = "" + save_prefs() + self.enable = False + +class LastScrob: + + def __init__(self): + + self.a_index = -1 + self.a_sc = False + self.a_pt = False + self.queue = [] + self.running = False + + def start_queue(self): + + self.running = True + mini_t = threading.Thread(target=self.process_queue) + mini_t.daemon = True + mini_t.start() + + def process_queue(self): + + time.sleep(0.4) + + while self.queue: + + try: + tr = self.queue.pop() + + gui.pl_update = 1 + logging.info("Submit Scrobble " + tr[0].artist + " - " + tr[0].title) + + success = True + + if tr[2] == "lfm" and prefs.auto_lfm and (lastfm.connected or lastfm.details_ready()): + success = lastfm.scrobble(tr[0], tr[1]) + elif tr[2] == "lb" and lb.enable: + success = lb.listen_full(tr[0], tr[1]) + elif tr[2] == "maloja": + success = maloja_scrobble(tr[0], tr[1]) + elif tr[2] == "air": + success = subsonic.listen(tr[0], submit=True) + elif tr[2] == "koel": + success = koel.listen(tr[0], submit=True) + + if not success: + logging.info("Re-queue scrobble") + self.queue.append(tr) + time.sleep(10) + break + + except Exception: + logging.exception("SCROBBLE QUEUE ERROR") + + if not self.queue: + scrobble_warning_timer.force_set(1000) + + self.running = False + + def update(self, add_time): + + if pctl.queue_step > len(pctl.track_queue) - 1: + logging.info("Queue step error 1") + return + + if self.a_index != pctl.track_queue[pctl.queue_step]: + pctl.a_time = 0 + pctl.b_time = 0 + self.a_index = pctl.track_queue[pctl.queue_step] + self.a_pt = False + self.a_sc = False + if pctl.playing_time == 0 and self.a_sc is True: + logging.info("Reset scrobble timer") + pctl.a_time = 0 + pctl.b_time = 0 + self.a_pt = False + self.a_sc = False + + if pctl.a_time > 6 and self.a_pt is False and pctl.master_library[self.a_index].length > 30: + self.a_pt = True + self.listen_track(pctl.master_library[self.a_index]) + # if prefs.auto_lfm and (lastfm.connected or lastfm.details_ready()) and not prefs.scrobble_hold: + # mini_t = threading.Thread(target=lastfm.update, args=([pctl.master_library[self.a_index]])) + # mini_t.daemon = True + # mini_t.start() + # + # if lb.enable and not prefs.scrobble_hold: + # mini_t = threading.Thread(target=lb.listen_playing, args=([pctl.master_library[self.a_index]])) + # mini_t.daemon = True + # mini_t.start() + + if pctl.a_time > 6 and self.a_pt: + pctl.b_time += add_time + if pctl.b_time > 20: + pctl.b_time = 0 + self.listen_track(pctl.master_library[self.a_index]) + + send_full = False + if pctl.master_library[self.a_index].length > 30 and pctl.a_time > pctl.master_library[self.a_index].length \ + * 0.50 and self.a_sc is False: + self.a_sc = True + send_full = True + + if self.a_sc is False and pctl.master_library[self.a_index].length > 30 and pctl.a_time > 240: + self.a_sc = True + send_full = True + + if send_full: + self.scrob_full_track(pctl.master_library[self.a_index]) + + def listen_track(self, track_object: TrackClass): + # logging.info("LISTEN") + + if track_object.is_network: + if track_object.file_ext == "SUB": + subsonic.listen(track_object, submit=False) + + if not prefs.scrobble_hold: + if prefs.auto_lfm and (lastfm.connected or lastfm.details_ready()): + mini_t = threading.Thread(target=lastfm.update, args=([track_object])) + mini_t.daemon = True + mini_t.start() + + if lb.enable: + mini_t = threading.Thread(target=lb.listen_playing, args=([track_object])) + mini_t.daemon = True + mini_t.start() + + def scrob_full_track(self, track_object: TrackClass): + # logging.info("SCROBBLE") + track_object.lfm_scrobbles += 1 + gui.pl_update += 1 + + if track_object.is_network: + if track_object.file_ext == "SUB": + self.queue.append((track_object, int(time.time()), "air")) + if track_object.file_ext == "KOEL": + self.queue.append((track_object, int(time.time()), "koel")) + + if not prefs.scrobble_hold: + if prefs.auto_lfm and (lastfm.connected or lastfm.details_ready()): + self.queue.append((track_object, int(time.time()), "lfm")) + if lb.enable: + self.queue.append((track_object, int(time.time()), "lb")) + if prefs.maloja_url and prefs.maloja_enable: + self.queue.append((track_object, int(time.time()), "maloja")) + +class Strings: + + def __init__(self): + self.spotify_likes = _("Spotify Likes") + self.spotify_albums = _("Spotify Albums") + self.spotify_un_liked = _("Track removed from liked tracks") + self.spotify_already_un_liked = _("Track was already un-liked") + self.spotify_already_liked = _("Track is already liked") + self.spotify_like_added = _("Track added to liked tracks") + self.spotify_account_connected = _("Spotify account connected") + self.spotify_not_playing = _("This Spotify account isn't currently playing anything") + self.spotify_error_starting = _("Error starting Spotify") + self.spotify_request_auth = _("Please authorise Spotify in settings!") + self.spotify_need_enable = _("Please authorise and click the enable toggle first!") + self.spotify_import_complete = _("Spotify import complete") + + self.day = _("day") + self.days = _("days") + + self.scan_chrome = _("Scanning for Chromecasts...") + self.cast_to = _("Cast to: %s") + self.no_chromecasts = _("No Chromecast devices found") + self.stop_cast = _("End Cast") + + self.web_server_stopped = _("Web server stopped.") + + self.menu_open_tauon = _("Open Tauon Music Box") + self.menu_play_pause = _("Play/Pause") + self.menu_next = _("Next Track") + self.menu_previous = _("Previous Track") + self.menu_quit = _("Quit") + +class Chunker: + + def __init__(self): + self.master_count = 0 + self.chunks = {} + self.header = None + self.headers = [] + self.h2 = None + + self.clients = {} + +class MenuIcon: + + def __init__(self, asset): + self.asset = asset + self.colour = [170, 170, 170, 255] + self.base_asset = None + self.base_asset_mod = None + self.colour_callback = None + self.mode_callback = None + self.xoff = 0 + self.yoff = 0 + +class MenuItem: + __slots__ = [ + "title", # 0 + "is_sub_menu", # 1 + "func", # 2 + "render_func", # 3 + "no_exit", # 4 + "pass_ref", # 5 + "hint", # 6 + "icon", # 7 + "show_test", # 8 + "pass_ref_deco", # 9 + "disable_test", # 10 + "set_ref", # 11 + "args", # 12 + "sub_menu_number", # 13 + "sub_menu_width", # 14 + ] + def __init__( + self, title, func, render_func=None, no_exit=False, pass_ref=False, hint=None, icon=None, show_test=None, + pass_ref_deco=False, disable_test=None, set_ref=None, is_sub_menu=False, args=None, sub_menu_number=None, sub_menu_width=0, + ): + self.title = title + self.is_sub_menu = is_sub_menu + self.func = func + self.render_func = render_func + self.no_exit = no_exit + self.pass_ref = pass_ref + self.hint = hint + self.icon = icon + self.show_test = show_test + self.pass_ref_deco = pass_ref_deco + self.disable_test = disable_test + self.set_ref = set_ref + self.args = args + self.sub_menu_number = sub_menu_number + self.sub_menu_width = sub_menu_width + +class ThreadManager: + + def __init__(self): + + self.worker1: Thread | None = None # Artist list, download monitor, folder move, importing, db cleaning, transcoding + self.worker2: Thread | None = None # Art bg, search + self.worker3: Thread | None = None # Gallery rendering + self.playback: Thread | None = None + self.player_lock: Lock = threading.Lock() + + self.d: dict = {} + + def ready(self, type): + if self.d[type][2] is None or not self.d[type][2].is_alive(): + shoot = threading.Thread(target=self.d[type][0], args=self.d[type][1]) + shoot.daemon = True + shoot.start() + self.d[type][2] = shoot + + def ready_playback(self) -> None: + if self.playback is None or not self.playback.is_alive(): + if prefs.backend == 4: + self.playback = threading.Thread(target=player4, args=[tauon]) + # elif prefs.backend == 2: + # from tauon.t_modules.t_gstreamer import player3 + # self.playback = threading.Thread(target=player3, args=[tauon]) + self.playback.daemon = True + self.playback.start() + + def check_playback_running(self) -> bool: + if self.playback is None: + return False + return self.playback.is_alive() + +class Menu: + """Right click context menu generator""" + + switch = 0 + count = switch + 1 + instances: list[Menu] = [] + active = False + + def rescale(self): + self.vertical_size = round(self.base_v_size * gui.scale) + self.h = self.vertical_size + self.w = self.request_width * gui.scale + if gui.scale == 2: + self.w += 15 + + def __init__(self, tauon: Tauon, width: int, show_icons: bool = False) -> None: + self.tauon = tauon + self.base_v_size = 22 + self.active = False + self.request_width: int = width + self.close_next_frame = False + self.clicked = False + self.pos = [0, 0] + self.rescale() + + self.reference = 0 + self.items: list[MenuItem] = [] + self.subs: list[list[MenuItem]] = [] + self.selected = -1 + self.up = False + self.down = False + self.font = 412 + self.show_icons: bool = show_icons + self.sub_arrow = MenuIcon(asset_loader(scaled_asset_directory, loaded_asset_dc, "sub.png", True)) + + self.id = Menu.count + self.break_height = round(4 * gui.scale) + + Menu.count += 1 + + self.sub_number = 0 + self.sub_active = -1 + self.sub_y_postion = 0 + Menu.instances.append(self) + + @staticmethod + def deco(_=_): + return [colours.menu_text, colours.menu_background, None] + + def click(self) -> None: + self.clicked = True + # cheap hack to prevent scroll bar from being activated when closing menu + global click_location + click_location = [0, 0] + + def add(self, menu_item: MenuItem) -> None: + if menu_item.render_func is None: + menu_item.render_func = self.deco + self.items.append(menu_item) + + def br(self) -> None: + self.items.append(None) + + def add_sub(self, title: str, width: int, show_test=None) -> None: + self.items.append(MenuItem(title, self.deco, sub_menu_width=width, show_test=show_test, is_sub_menu=True, sub_menu_number=self.sub_number)) + self.sub_number += 1 + self.subs.append([]) + + def add_to_sub(self, sub_menu_index: int, menu_item: MenuItem) -> None: + if menu_item.render_func is None: + menu_item.render_func = self.deco + self.subs[sub_menu_index].append(menu_item) + + def test_item_active(self, item): + if item.show_test is not None: + if item.show_test(1) is False: + return False + return True + + def is_item_disabled(self, item): + if item.disable_test is not None: + if item.pass_ref_deco: + return item.disable_test(self.reference) + return item.disable_test() + + def render_icon(self, x, y, icon, selected, fx): + + if colours.lm: + selected = True + + if icon is not None: + + x += icon.xoff * gui.scale + y += icon.yoff * gui.scale + + colour = None + + if icon.base_asset is None: + # Colourise mode + + if icon.colour_callback is not None: # and icon.colour_callback() is not None: + colour = icon.colour_callback() + + elif selected and fx[0] != colours.menu_text_disabled: + colour = icon.colour + + if colour is None and icon.base_asset_mod: + colour = colours.menu_icons + # if colours.lm: + # colour = [160, 160, 160, 255] + icon.base_asset_mod.render(x, y, colour) + return + + if colour is None: + # colour = [145, 145, 145, 70] + colour = colours.menu_icons # [255, 255, 255, 35] + # colour = [50, 50, 50, 255] + + icon.asset.render(x, y, colour) + + else: + if not is_grey(colours.menu_background): + return # Since these are currently pre-rendered greyscale, they are + # Incompatible with coloured backgrounds. Fix TODO + if selected and fx[0] == colours.menu_text_disabled: + icon.base_asset.render(x, y) + return + + # Pre-rendered mode + if icon.mode_callback is not None: + if icon.mode_callback(): + icon.asset.render(x, y) + else: + icon.base_asset.render(x, y) + elif selected: + icon.asset.render(x, y) + else: + icon.base_asset.render(x, y) + + def render(self): + if self.active: + + if Menu.switch != self.id: + self.active = False + + for menu in Menu.instances: + if menu.active: + break + else: + Menu.active = False + + return + + # ytoff = 3 + y_run = round(self.pos[1]) + to_call = None + + # if window_size[1] < 250 * gui.scale: + # self.h = round(14 * gui.scale) + # ytoff = -1 * gui.scale + # else: + self.h = self.vertical_size + ytoff = round(self.h * 0.71 - 13 * gui.scale) + + x_run = self.pos[0] + + for i in range(len(self.items)): + #logging.info(self.items[i]) + + # Draw menu break + if self.items[i] is None: + + if is_light(colours.menu_background): + break_colour = rgb_add_hls(colours.menu_background, 0, -0.1, -0.1) + else: + break_colour = rgb_add_hls(colours.menu_background, 0, 0.06, 0) + + rect = (x_run, y_run, self.w, self.break_height - 1) + if coll(rect): + self.clicked = False + + ddt.rect_a((x_run, y_run), (self.w, self.break_height), colours.menu_background) + + ddt.rect_a((x_run, y_run + 2 * gui.scale), (self.w, 2 * gui.scale), break_colour) + + # Draw tab + ddt.rect_a((x_run, y_run), (4 * gui.scale, self.break_height), colours.menu_tab) + y_run += self.break_height + + continue + + if self.test_item_active(self.items[i]) is False: + continue + # if self.items[i][1] is False and self.items[i][8] is not None: + # if self.items[i][8](1) == False: + # continue + + # Get properties for menu item + if self.items[i].render_func is not None: + if self.items[i].pass_ref_deco: + fx = self.items[i].render_func(self.reference) + else: + fx = self.items[i].render_func() + else: + fx = self.deco() + + if fx[2] is not None: + label = fx[2] + else: + label = self.items[i].title + + # Show text as disabled if disable_test() passes + if self.is_item_disabled(self.items[i]): + fx[0] = colours.menu_text_disabled + + # Draw item background, black by default + ddt.rect_a((x_run, y_run), (self.w, self.h), fx[1]) + bg = fx[1] + + # Detect if mouse is over this item + selected = False + rect = (x_run, y_run, self.w, self.h - 1) + fields.add(rect) + + if coll_point(mouse_position, (x_run, y_run, self.w, self.h - 1)): + ddt.rect_a((x_run, y_run), (self.w, self.h), colours.menu_highlight_background) # [15, 15, 15, 255] + selected = True + bg = alpha_blend(colours.menu_highlight_background, bg) + + # Call menu items callback if clicked + if self.clicked: + + if self.items[i].is_sub_menu is False: + to_call = i + if self.items[i].set_ref is not None: + self.reference = self.items[i].set_ref + global mouse_down + mouse_down = False + + else: + self.clicked = False + self.sub_active = self.items[i].sub_menu_number + self.sub_y_postion = y_run + + # Draw tab + ddt.rect_a((x_run, y_run), (4 * gui.scale, self.h), colours.menu_tab) + + # Draw Icon + x = 12 * gui.scale + if self.items[i].is_sub_menu is False and self.show_icons: + icon = self.items[i].icon + self.render_icon(x_run + x, y_run + 5 * gui.scale, icon, selected, fx) + + if self.show_icons: + x += 25 * gui.scale + + # Draw arrow icon for sub menu + if self.items[i].is_sub_menu is True: + + if is_light(bg) or colours.lm: + colour = rgb_add_hls(bg, 0, -0.6, -0.1) + else: + colour = rgb_add_hls(bg, 0, 0.1, 0) + + if self.sub_active == self.items[i].func: + if is_light(bg) or colours.lm: + colour = rgb_add_hls(bg, 0, -0.8, -0.1) + else: + colour = rgb_add_hls(bg, 0, 0.40, 0) + + # colour = [50, 50, 50, 255] + # if selected: + # colour = [150, 150, 150, 255] + # if self.sub_active == self.items[i][2]: + # colour = [150, 150, 150, 255] + self.sub_arrow.asset.render(x_run + self.w - 13 * gui.scale, y_run + 7 * gui.scale, colour) + + # Render the items label + ddt.text((x_run + x, y_run + ytoff), label, fx[0], self.font, max_w=self.w - (x + 9 * gui.scale), bg=bg) + + # Render the items hint + if self.items[i].hint != None: + + if is_light(bg) or colours.lm: + hint_colour = rgb_add_hls(bg, 0, -0.30, -0.3) + else: + hint_colour = rgb_add_hls(bg, 0, 0.15, 0) + + # colo = alpha_blend([255, 255, 255, 50], bg) + ddt.text((x_run + self.w - 5, y_run + ytoff, 1), self.items[i].hint, hint_colour, self.font, bg=bg) + + y_run += self.h + + if y_run > window_size[1] - self.h: + direc = 1 + if self.pos[0] > window_size[0] // 2: + direc = -1 + x_run += self.w * direc + y_run = self.pos[1] + + # Render sub menu if active + if self.sub_active > -1 and self.items[i].is_sub_menu and self.sub_active == self.items[i].sub_menu_number: + + # sub_pos = [x_run + self.w, self.pos[1] + i * self.h] + sub_pos = [x_run + self.w, self.sub_y_postion] + sub_w = self.items[i].sub_menu_width * gui.scale + + if sub_pos[0] + sub_w > window_size[0]: + sub_pos[0] = x_run - sub_w + if view_box.active: + sub_pos[0] -= view_box.w + + fx = self.deco() + + minY = window_size[1] - self.h * len(self.subs[self.sub_active]) - 15 * gui.scale + sub_pos[1] = min(sub_pos[1], minY) + + xoff = 0 + for i in self.subs[self.sub_active]: + if i.icon is not None: + xoff = 24 * gui.scale + break + + for w in range(len(self.subs[self.sub_active])): + + if self.subs[self.sub_active][w].show_test is not None: + if not self.subs[self.sub_active][w].show_test(self.reference): + continue + + # Get item colours + if self.subs[self.sub_active][w].render_func is not None: + if self.subs[self.sub_active][w].pass_ref_deco: + fx = self.subs[self.sub_active][w].render_func(self.reference) + else: + fx = self.subs[self.sub_active][w].render_func() + + # Item background + ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (sub_w, self.h), fx[1]) + + # Detect if mouse is over this item + rect = (sub_pos[0], sub_pos[1] + w * self.h, sub_w, self.h - 1) + fields.add(rect) + this_select = False + bg = colours.menu_background + if coll_point(mouse_position, (sub_pos[0], sub_pos[1] + w * self.h, sub_w, self.h - 1)): + ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (sub_w, self.h), colours.menu_highlight_background) + bg = alpha_blend(colours.menu_highlight_background, bg) + this_select = True + + # Call Callback + if self.clicked and not self.is_item_disabled(self.subs[self.sub_active][w]): + + # If callback needs args + if self.subs[self.sub_active][w].args is not None: + self.subs[self.sub_active][w].func(self.reference, self.subs[self.sub_active][w].args) + + # If callback just need ref + elif self.subs[self.sub_active][w].pass_ref: + self.subs[self.sub_active][w].func(self.reference) + + else: + self.subs[self.sub_active][w].func() + + if fx[2] is not None: + label = fx[2] + else: + label = self.subs[self.sub_active][w].title + + # Show text as disabled if disable_test() passes + if self.is_item_disabled(self.subs[self.sub_active][w]): + fx[0] = colours.menu_text_disabled + + # Render sub items icon + icon = self.subs[self.sub_active][w].icon + self.render_icon(sub_pos[0] + 11 * gui.scale, sub_pos[1] + w * self.h + 5 * gui.scale, icon, this_select, fx) + + # Render the items label + ddt.text( + (sub_pos[0] + 10 * gui.scale + xoff, sub_pos[1] + ytoff + w * self.h), label, fx[0], self.font, bg=bg) + + # Draw tab + ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (4 * gui.scale, self.h), colours.menu_tab) + + # Render the menu outline + # ddt.rect_a(sub_pos, (sub_w, self.h * len(self.subs[self.sub_active])), colours.grey(40)) + + # Process Click Actions + if to_call is not None: + + if not self.is_item_disabled(self.items[to_call]): + if self.items[to_call].pass_ref: + self.items[to_call].func(self.reference) + else: + self.items[to_call].func() + + if self.clicked or key_esc_press or self.close_next_frame: + self.close_next_frame = False + self.active = False + self.clicked = False + + last_click_location[0] = 0 + last_click_location[1] = 0 + + for menu in Menu.instances: + if menu.active: + break + else: + Menu.active = False + + # Render the menu outline + # ddt.rect_a(self.pos, (self.w, self.h * len(self.items)), colours.grey(40)) + + def activate(self, in_reference=0, position=None): + + Menu.active = True + + if position != None: + self.pos = [position[0], position[1]] + else: + self.pos = [copy.deepcopy(mouse_position[0]), copy.deepcopy(mouse_position[1])] + + self.reference = in_reference + Menu.switch = self.id + self.sub_active = -1 + + # Reposition the menu if it would otherwise intersect with far edge of window + if not position: + if self.pos[0] + self.w > window_size[0]: + self.pos[0] -= round(self.w + 3 * gui.scale) + + # Get height size of menu + full_h = 0 + shown_h = 0 + for item in self.items: + if item is None: + full_h += self.break_height + shown_h += self.break_height + else: + full_h += self.h + if self.test_item_active(item) is True: + shown_h += self.h + + # Flip menu up if would intersect with bottom of window + if self.pos[1] + full_h > window_size[1]: + self.pos[1] -= shown_h + + # Prevent moving outside top of window + if self.pos[1] < gui.panelY: + self.pos[1] = gui.panelY + self.pos[0] += 5 * gui.scale + + self.active = True + +class GallClass: + def __init__(self, size=250, save_out=True): + self.gall = {} + self.size = size + self.queue = [] + self.key_list = [] + self.save_out = save_out + self.i = 0 + self.lock = threading.Lock() + self.limit = 60 + + def get_file_source(self, track_object: TrackClass): + + global album_art_gen + + sources = album_art_gen.get_sources(track_object) + + if len(sources) == 0: + return False, 0 + + offset = album_art_gen.get_offset(track_object.fullpath, sources) + return sources[offset], offset + + def worker_render(self): + + self.lock.acquire() + # time.sleep(0.1) + + if search_over.active: + while QuickThumbnail.queue: + img = QuickThumbnail.queue.pop(0) + response = urllib.request.urlopen(img.url, context=ssl_context) + source_image = io.BytesIO(response.read()) + img.read_and_thumbnail(source_image, img.size, img.size) + source_image.close() + gui.update += 1 + + while len(self.queue) > 0: + + source_image = None + + if gui.halt_image_rendering: + self.queue.clear() + break + + self.i += 1 + + try: + # key = self.queue[0] + key = self.queue.pop(0) + except Exception: + logging.exception("thumb queue empty") + break + + if key not in self.gall: + order = [1, None, None, None] + self.gall[key] = order + else: + order = self.gall[key] + + size = key[1] + + slow_load = False + cache_load = False + + try: + + if True: + offset = 0 + parent_folder = key[0].parent_folder_path + if parent_folder in folder_image_offsets: + offset = folder_image_offsets[parent_folder] + img_name = str(key[2]) + "-" + str(size) + "-" + str(key[0].index) + "-" + str(offset) + if prefs.cache_gallery and os.path.isfile(os.path.join(g_cache_dir, img_name + ".jpg")): + source_image = open(os.path.join(g_cache_dir, img_name + ".jpg"), "rb") + # logging.info('load from cache') + cache_load = True + else: + slow_load = True + + if slow_load: + + source, c_offset = self.get_file_source(key[0]) + + if source is False: + order[0] = 0 + self.gall[key] = order + # del self.queue[0] + continue + + img_name = str(key[2]) + "-" + str(size) + "-" + str(key[0].index) + "-" + str(c_offset) + + # gall_render_last_timer.set() + + if prefs.cache_gallery and os.path.isfile(os.path.join(g_cache_dir, img_name + ".jpg")): + source_image = open(os.path.join(g_cache_dir, img_name + ".jpg"), "rb") + logging.info("slow load image") + cache_load = True + + # elif source[0] == 1: + # #logging.info('tag') + # source_image = io.BytesIO(album_art_gen.get_embed(key[0])) + # + # elif source[0] == 2: + # try: + # url = get_network_thumbnail_url(key[0]) + # response = urllib.request.urlopen(url) + # source_image = response + # except Exception: + # logging.exception("IMAGE NETWORK LOAD ERROR") + # else: + # source_image = open(source[1], 'rb') + source_image = album_art_gen.get_source_raw(0, 0, key[0], subsource=source) + + g = io.BytesIO() + g.seek(0) + + if cache_load: + g.write(source_image.read()) + + else: + error = False + try: + # Process image + im = Image.open(source_image) + if im.mode != "RGB": + im = im.convert("RGB") + im.thumbnail((size, size), Image.Resampling.LANCZOS) + except Exception: + logging.exception("Failed to work with thumbnail") + im = album_art_gen.get_error_img(size) + error = True + + im.save(g, "BMP") + + if not error and self.save_out and prefs.cache_gallery and not os.path.isfile( + os.path.join(g_cache_dir, img_name + ".jpg")): + im.save(os.path.join(g_cache_dir, img_name + ".jpg"), "JPEG", quality=95) + + g.seek(0) + + # source_image.close() + + order = [2, g, None, None] + self.gall[key] = order + + gui.update += 1 + if source_image: + source_image.close() + source_image = None + # del self.queue[0] + + time.sleep(0.001) + + except Exception: + logging.exception("Image load failed on track: " + key[0].fullpath) + order = [0, None, None, None] + self.gall[key] = order + gui.update += 1 + # del self.queue[0] + + if size < 150: + random.shuffle(self.queue) + + if self.i > 0: + self.i = 0 + return True + return False + + def render(self, track: TrackClass, location, size=None, force_offset=None) -> bool | None: + if gallery_load_delay.get() < 0.5: + return None + + x = round(location[0]) + y = round(location[1]) + + # time.sleep(0.1) + if size is None: + size = self.size + + size = round(size) + + # offset = self.get_offset(pctl.master_library[index].fullpath, self.get_sources(index)) + if track.parent_folder_path in folder_image_offsets: + offset = folder_image_offsets[track.parent_folder_path] + else: + offset = 0 + + if force_offset is not None: + offset = force_offset + + key = (track, size, offset) + + if key in self.gall: + #logging.info("old") + + order = self.gall[key] + + if order[0] == 0: + # broken + return False + + if order[0] == 1: + # not done yet + return False + + if order[0] == 2: + # finish processing + + wop = rw_from_object(order[1]) + s_image = IMG_Load_RW(wop, 0) + c = SDL_CreateTextureFromSurface(renderer, s_image) + SDL_FreeSurface(s_image) + tex_w = pointer(c_int(size)) + tex_h = pointer(c_int(size)) + SDL_QueryTexture(c, None, None, tex_w, tex_h) + dst = SDL_Rect(x, y) + dst.w = int(tex_w.contents.value) + dst.h = int(tex_h.contents.value) + + + order[0] = 3 + order[1].close() + order[1] = None + order[2] = c + order[3] = dst + self.gall[(track, size, offset)] = order + + if order[0] == 3: + # ready + + order[3].x = x + order[3].y = y + order[3].x = int((size - order[3].w) / 2) + order[3].x + order[3].y = int((size - order[3].h) / 2) + order[3].y + SDL_RenderCopy(renderer, order[2], None, order[3]) + + if (track, size, offset) in self.key_list: + self.key_list.remove((track, size, offset)) + self.key_list.append((track, size, offset)) + + # Remove old images to conserve RAM usage + if len(self.key_list) > self.limit: + gui.update += 1 + key = self.key_list[0] + # while key in self.queue: + # self.queue.remove(key) + if self.gall[key][2] is not None: + SDL_DestroyTexture(self.gall[key][2]) + del self.gall[key] + del self.key_list[0] + + return True + + else: + if key not in self.queue: + self.queue.append(key) + if self.lock.locked(): + try: + self.lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked lock") + else: + logging.exception("Unknown RuntimeError trying to release lock") + except Exception: + logging.exception("Unknown error trying to release lock") + return False + +class ThumbTracks: + def __init__(self) -> None: + pass + + def path(self, track: TrackClass) -> str: + source, offset = tauon.gall_ren.get_file_source(track) + + if source is False: # No art + return None + + image_name = track.album + track.parent_folder_path + str(offset) + image_name = hashlib.md5(image_name.encode("utf-8", "replace")).hexdigest() + + t_path = os.path.join(e_cache_dir, image_name + ".jpg") + + if os.path.isfile(t_path): + return t_path + + source_image = album_art_gen.get_source_raw(0, 0, track, subsource=source) + + with Image.open(source_image) as im: + if im.mode != "RGB": + im = im.convert("RGB") + im.thumbnail((1000, 1000), Image.Resampling.LANCZOS) + im.save(t_path, "JPEG") + source_image.close() + return t_path + +class Tauon: + """Root class for everything Tauon""" + def __init__(self, holder: Holder): + + self.t_title = holder.t_title + self.t_version = holder.t_version + self.t_agent = holder.t_agent + self.t_id = holder.t_id + self.desktop: str | None = desktop + self.device = socket.gethostname() + + #TODO(Martin) : Fix this by moving the class to root of the module + self.cachement: player4.Cachement | None = None + self.dummy_event: SDL_Event = SDL_Event() + self.translate = _ + self.strings: Strings = strings + self.pctl: PlayerCtl = pctl + self.lfm_scrobbler: LastScrob = lfm_scrobbler + self.star_store: StarStore = star_store + self.gui: GuiVar = gui + self.prefs: Prefs = prefs + self.cache_directory: Path = cache_directory + self.user_directory: Path | None = user_directory + self.music_directory: Path | None = music_directory + self.locale_directory: Path = locale_directory + self.worker_save_state: bool = False + self.launch_prefix: str = launch_prefix + self.whicher = whicher + self.load_orders: list[LoadClass] = load_orders + self.switch_playlist = None + self.open_uri = open_uri + self.love = love + self.snap_mode = snap_mode + self.console = console + self.msys = msys + self.TrackClass = TrackClass + self.pl_gen = pl_gen + self.gall_ren = GallClass(album_mode_art_size) + self.QuickThumbnail = QuickThumbnail + self.thumb_tracks = ThumbTracks() + self.pl_to_id = pl_to_id + self.id_to_pl = id_to_pl + self.chunker = Chunker() + self.thread_manager: ThreadManager = ThreadManager() + self.stream_proxy = None + self.stream_proxy = StreamEnc(self) + self.level_train: list[list[float]] = [] + self.radio_server = None + self.mod_formats = MOD_Formats + self.listen_alongers = {} + self.encode_folder_name = encode_folder_name + self.encode_track_name = encode_track_name + + self.tray_lock = threading.Lock() + self.tray_releases = 0 + + self.play_lock = None + self.update_play_lock = None + self.sleep_lock = None + self.shutdown_lock = None + self.quick_close = False + + self.copied_track = None + self.macos = macos + self.aud: CDLL | None = None + + self.recorded_songs = [] + + self.chrome_mode = False + self.web_running = False + self.web_thread = None + self.remote_limited = True + self.enable_librespot = shutil.which("librespot") + + #TODO(Martin) : Fix this by moving the class to root of the module + self.spotc: player4.LibreSpot | None = None + self.librespot_p = None + self.MenuItem = MenuItem + self.tag_scan = tag_scan + + self.gme_formats = GME_Formats + + self.spot_ctl: SpotCtl = SpotCtl(self) + self.tidal: Tidal = Tidal(self) + self.chrome: Chrome | None = None + self.chrome_menu: Menu | None = None + + self.ssl_context = ssl_context + + def start_remote(self) -> None: + + if not self.web_running: + self.web_thread = threading.Thread( + target=webserve2, args=[pctl, prefs, gui, album_art_gen, str(install_directory), strings, tauon]) + self.web_thread.daemon = True + self.web_thread.start() + self.web_running = True + + def download_ffmpeg(self, x): + def go(): + url = "https://github.com/GyanD/codexffmpeg/releases/download/5.0.1/ffmpeg-5.0.1-essentials_build.zip" + sha = "9e00da9100ae1bba22b1385705837392e8abcdfd2efc5768d447890d101451b5" + show_message(_("Starting download...")) + try: + f = io.BytesIO() + r = requests.get(url, stream=True, timeout=1800) # ffmpeg is 77MB, give it half an hour in case someone is willing to suffer it on a slow connection + + dl = 0 + for data in r.iter_content(chunk_size=4096): + dl += len(data) + f.write(data) + mb = round(dl / 1000 / 1000) + if mb > 90: + break + if mb % 5 == 0: + show_message(_("Downloading... {N}/80MB").format(N=mb)) + + except Exception as e: + logging.exception("Download failed") + show_message(_("Download failed"), str(e), mode="error") + + f.seek(0) + if hashlib.sha256(f.read()).hexdigest() != sha: + show_message(_("Download completed but checksum failed"), mode="error") + return + show_message(_("Download completed.. extracting")) + f.seek(0) + z = zipfile.ZipFile(f, mode="r") + exe = z.open("ffmpeg-5.0.1-essentials_build/bin/ffmpeg.exe") + with (user_directory / "ffmpeg.exe").open("wb") as file: + file.write(exe.read()) + + exe = z.open("ffmpeg-5.0.1-essentials_build/bin/ffprobe.exe") + with (user_directory / "ffprobe.exe").open("wb") as file: + file.write(exe.read()) + + exe.close() + show_message(_("FFMPEG fetch complete"), mode="done") + + shooter(go) + + def set_tray_icons(self, force: bool = False): + + indicator_icon_play = str(pctl.install_directory / "assets/svg/tray-indicator-play.svg") + indicator_icon_pause = str(pctl.install_directory / "assets/svg/tray-indicator-pause.svg") + indicator_icon_default = str(pctl.install_directory / "assets/svg/tray-indicator-default.svg") + + if prefs.tray_theme == "gray": + indicator_icon_play = str(pctl.install_directory / "assets/svg/tray-indicator-play-g1.svg") + indicator_icon_pause = str(pctl.install_directory / "assets/svg/tray-indicator-pause-g1.svg") + indicator_icon_default = str(pctl.install_directory / "assets/svg/tray-indicator-default-g1.svg") + + user_icon_dir = self.cache_directory / "icon-export" + def install_tray_icon(src: str, name: str) -> None: + alt = user_icon_dir / f"{name}.svg" + if not alt.is_file() or force: + shutil.copy(src, str(alt)) + + if not user_icon_dir.is_dir(): + os.makedirs(user_icon_dir) + + install_tray_icon(indicator_icon_play, "tray-indicator-play") + install_tray_icon(indicator_icon_pause, "tray-indicator-pause") + install_tray_icon(indicator_icon_default, "tray-indicator-default") + + def get_tray_icon(self, name: str) -> str: + return str(self.cache_directory / "icon-export" / f"{name}.svg") + + def test_ffmpeg(self) -> bool: + if self.get_ffmpeg(): + return True + if msys: + show_message(_("This feature requires FFMPEG. Shall I can download that for you? (80MB)"), mode="confirm") + gui.message_box_confirm_callback = self.download_ffmpeg + gui.message_box_confirm_reference = (None,) + else: + show_message(_("FFMPEG could not be found")) + return False + + def get_ffmpeg(self) -> str | None: + logging.debug(f"Looking for ffmpeg in PATH: {os.environ.get('PATH')}") + p = shutil.which("ffmpeg") + if p: + return p + p = str(user_directory / "ffmpeg.exe") + if msys and os.path.isfile(p): + return p + return None + + def get_ffprobe(self) -> str | None: + p = shutil.which("ffprobe") + if p: + return p + p = str(user_directory / "ffprobe.exe") + if msys and os.path.isfile(p): + return p + return None + + def bg_save(self) -> None: + self.worker_save_state = True + tauon.thread_manager.ready("worker") + + def exit(self, reason: str) -> None: + logging.info("Shutting down. Reason: " + reason) + pctl.running = False + self.wake() + + def min_to_tray(self) -> None: + SDL_HideWindow(t_window) + gui.mouse_unknown = True + + def raise_window(self) -> None: + SDL_ShowWindow(t_window) + SDL_RaiseWindow(t_window) + SDL_RestoreWindow(t_window) + gui.lowered = False + gui.update += 1 + + def focus_window(self) -> None: + SDL_RaiseWindow(t_window) + + def get_playing_playlist_id(self) -> int: + return pl_to_id(pctl.active_playlist_playing) + + def wake(self) -> None: + SDL_PushEvent(ctypes.byref(self.dummy_event)) + +class PlexService: + + def __init__(self): + self.connected = False + self.resource = None + self.scanning = False + + def connect(self): + + if not prefs.plex_username or not prefs.plex_password or not prefs.plex_servername: + show_message(_("Missing username, password and/or server name"), mode="warning") + self.scanning = False + return + + try: + from plexapi.myplex import MyPlexAccount + except ModuleNotFoundError: + logging.warning("Unable to import python-plexapi, plex support will be disabled.") + except Exception: + logging.exception("Unknown error to import python-plexapi, plex support will be disabled.") + show_message(_("Error importing python-plexapi"), mode="error") + self.scanning = False + return + + try: + account = MyPlexAccount(prefs.plex_username, prefs.plex_password) + self.resource = account.resource(prefs.plex_servername).connect() # returns a PlexServer instance + except Exception: + logging.exception("Error connecting to PLEX server, check login credentials and server accessibility.") + show_message( + _("Error connecting to PLEX server"), + _("Try checking login credentials and that the server is accessible."), mode="error") + self.scanning = False + return + + # from plexapi.server import PlexServer + # baseurl = 'http://localhost:32400' + # token = '' + + # self.resource = PlexServer(baseurl, token) + + self.connected = True + + def resolve_stream(self, location): + logging.info("Get plex stream") + if not self.connected: + self.connect() + + # return self.resource.url(location, True) + return self.resource.library.fetchItem(location).getStreamURL() + + def resolve_thumbnail(self, location): + + if not self.connected: + self.connect() + if self.connected: + return self.resource.url(location, True) + return None + + def get_albums(self, return_list=False): + + gui.update += 1 + self.scanning = True + + if not self.connected: + self.connect() + + if not self.connected: + self.scanning = False + return [] + + playlist = [] + + existing = {} + for track_id, track in pctl.master_library.items(): + if track.is_network and track.file_ext == "PLEX": + existing[track.url_key] = track_id + + albums = self.resource.library.section("Music").albums() + gui.to_got = 0 + + for album in albums: + year = album.year + album_artist = album.parentTitle + album_title = album.title + + parent = (album_artist + " - " + album_title).strip("- ") + + for track in album.tracks(): + + if not track.duration: + logging.warning("Skipping track with invalid duration - " + track.title + " - " + track.grandparentTitle) + continue + + id = pctl.master_count + replace_existing = False + + e = existing.get(track.key) + if e is not None: + id = e + replace_existing = True + + title = track.title + track_artist = track.grandparentTitle + duration = track.duration / 1000 + + nt = TrackClass() + nt.index = id + nt.track_number = track.index + nt.file_ext = "PLEX" + nt.parent_folder_path = parent + nt.parent_folder_name = parent + nt.album_artist = album_artist + nt.artist = track_artist + nt.title = title + nt.album = album_title + nt.length = duration + if hasattr(track, "locations") and track.locations: + nt.fullpath = track.locations[0] + + nt.is_network = True + + if track.thumb: + nt.art_url_key = track.thumb + + nt.url_key = track.key + nt.date = str(year) + + pctl.master_library[id] = nt + + if not replace_existing: + pctl.master_count += 1 + + playlist.append(nt.index) + + gui.to_got += 1 + gui.update += 1 + gui.pl_update += 1 + + self.scanning = False + + if return_list: + return playlist + + pctl.multi_playlist.append(pl_gen(title=_("PLEX Collection"), playlist_ids=playlist)) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "plex path" + switch_playlist(len(pctl.multi_playlist) - 1) + +class SubsonicService: + + def __init__(self): + self.scanning = False + self.playlists = prefs.subsonic_playlists + + def r(self, point, p=None, binary: bool = False, get_url: bool = False): + salt = secrets.token_hex(8) + server = prefs.subsonic_server.rstrip("/") + "/" + + params = { + "u": prefs.subsonic_user, + "v": "1.13.0", + "c": t_title, + "f": "json", + } + + if prefs.subsonic_password_plain: + params["p"] = prefs.subsonic_password + else: + params["t"] = hashlib.md5((prefs.subsonic_password + salt).encode()).hexdigest() + params["s"] = salt + + if p: + params.update(p) + + point = "rest/" + point + + url = server + point + + if get_url: + return url, params + + response = requests.get(url, params=params, timeout=10) + + if binary: + return response.content + + d = json.loads(response.text) + # logging.info(d) + + if d["subsonic-response"]["status"] != "ok": + show_message(_("Subsonic Error: ") + response.text, mode="warning") + logging.error("Subsonic Error: " + response.text) + + return d + + def get_cover(self, track_object: TrackClass): + response = self.r("getCoverArt", p={"id": track_object.art_url_key}, binary=True) + return io.BytesIO(response) + + def resolve_stream(self, key): + + p = {"id": key} + if prefs.network_stream_bitrate > 0: + p["maxBitRate"] = prefs.network_stream_bitrate + + return self.r("stream", p={"id": key}, get_url=True) + # logging.info(response.content) + + def listen(self, track_object: TrackClass, submit: bool = False): + + try: + a = self.r("scrobble", p={"id": track_object.url_key, "submission": submit}) + except Exception: + logging.exception("Error connecting for scrobble on airsonic") + return True + + def set_rating(self, track_object: TrackClass, rating): + + try: + a = self.r("setRating", p={"id": track_object.url_key, "rating": math.ceil(rating / 2)}) + except Exception: + logging.exception("Error connect for set rating on airsonic") + return True + + def set_album_rating(self, track_object: TrackClass, rating): + id = track_object.misc.get("subsonic-folder-id") + if id is not None: + try: + a = self.r("setRating", p={"id": id, "rating": math.ceil(rating / 2)}) + except Exception: + logging.exception("Error connect for set rating on airsonic") + return True + + def get_music3(self, return_list: bool = False): + + self.scanning = True + gui.to_got = 0 + + existing = {} + + for track_id, track in pctl.master_library.items(): + if track.is_network and track.file_ext == "SUB": + existing[track.url_key] = track_id + + try: + a = self.r("getIndexes") + except Exception: + logging.exception("Error connecting to Airsonic server") + show_message(_("Error connecting to Airsonic server"), mode="error") + self.scanning = False + return [] + + b = a["subsonic-response"]["indexes"]["index"] + + folders = [] + + for letter in b: + artists = letter["artist"] + for artist in artists: + folders.append(( + artist["id"], + artist["name"], + )) + + playlist = [] + + songsets = [] + for i in range(len(folders)): + songsets.append([]) + statuses = [0] * len(folders) + dupes = [] + + def getsongs(index, folder_id, name: str, inner: bool = False, parent=None): + + try: + d = self.r("getMusicDirectory", p={"id": folder_id}) + if "child" not in d["subsonic-response"]["directory"]: + if not inner: + statuses[index] = 2 + return + + except json.decoder.JSONDecodeError: + logging.exception("Error reading Airsonic directory") + if not inner: + statuses[index] = 2 + show_message(_("Error reading Airsonic directory!"), mode="warning") + return + except Exception: + logging.exception("Unknown Error reading Airsonic directory") + + items = d["subsonic-response"]["directory"]["child"] + + gui.update = 2 + + for item in items: + + if item["isDir"]: + + if "userRating" in item and "artist" in item: + rating = item["userRating"] + if album_star_store.get_rating_artist_title(item["artist"], item["title"]) == 0 and rating == 0: + pass + else: + album_star_store.set_rating_artist_title(item["artist"], item["title"], int(rating * 2)) + + getsongs(index, item["id"], item["title"], inner=True, parent=item) + continue + + gui.to_got += 1 + song = item + nt = TrackClass() + + if parent and "artist" in parent: + nt.album_artist = parent["artist"] + + if "title" in song: + nt.title = song["title"] + if "artist" in song: + nt.artist = song["artist"] + if "album" in song: + nt.album = song["album"] + if "track" in song: + nt.track_number = song["track"] + if "year" in song: + nt.date = str(song["year"]) + if "duration" in song: + nt.length = song["duration"] + + nt.file_ext = "SUB" + nt.parent_folder_name = name + if "path" in song: + nt.fullpath = song["path"] + nt.parent_folder_path = os.path.dirname(song["path"]) + if "coverArt" in song: + nt.art_url_key = song["id"] + nt.url_key = song["id"] + nt.misc["subsonic-folder-id"] = folder_id + nt.is_network = True + + rating = 0 + if "userRating" in song: + rating = int(song["userRating"]) + + songsets[index].append((nt, name, song["id"], rating)) + + if inner: + return + statuses[index] = 2 + + i = -1 + for id, name in folders: + i += 1 + while statuses.count(1) > 3: + time.sleep(0.1) + + statuses[i] = 1 + t = threading.Thread(target=getsongs, args=([i, id, name])) + t.daemon = True + t.start() + + while statuses.count(2) != len(statuses): + time.sleep(0.1) + + for sset in songsets: + for nt, name, song_id, rating in sset: + + id = pctl.master_count + + replace_existing = False + ex = existing.get(song_id) + if ex is not None: + id = ex + replace_existing = True + + nt.index = id + pctl.master_library[id] = nt + if not replace_existing: + pctl.master_count += 1 + + playlist.append(nt.index) + + if star_store.get_rating(nt.index) == 0 and rating == 0: + pass + else: + star_store.set_rating(nt.index, rating * 2) + + self.scanning = False + if return_list: + return playlist + + pctl.multi_playlist.append(pl_gen(title=_("Airsonic Collection"), playlist_ids=playlist)) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "air" + switch_playlist(len(pctl.multi_playlist) - 1) + + # def get_music2(self, return_list=False): + # + # self.scanning = True + # gui.to_got = 0 + # + # existing = {} + # + # for track_id, track in pctl.master_library.items(): + # if track.is_network and track.file_ext == "SUB": + # existing[track.url_key] = track_id + # + # try: + # a = self.r("getIndexes") + # except Exception: + # show_message(_("Error connecting to Airsonic server"), mode="error") + # self.scanning = False + # return [] + # + # b = a["subsonic-response"]["indexes"]["index"] + # + # folders = [] + # + # for letter in b: + # artists = letter["artist"] + # for artist in artists: + # folders.append(( + # artist["id"], + # artist["name"] + # )) + # + # playlist = [] + # + # def get(folder_id, name): + # + # try: + # d = self.r("getMusicDirectory", p={"id": folder_id}) + # if "child" not in d["subsonic-response"]["directory"]: + # return + # + # except json.decoder.JSONDecodeError: + # logging.error("Error reading Airsonic directory") + # show_message(_("Error reading Airsonic directory!)", mode="warning") + # return + # + # items = d["subsonic-response"]["directory"]["child"] + # + # gui.update = 1 + # + # for item in items: + # + # gui.to_got += 1 + # + # if item["isDir"]: + # get(item["id"], item["title"]) + # continue + # + # song = item + # id = pctl.master_count + # + # replace_existing = False + # ex = existing.get(song["id"]) + # if ex is not None: + # id = ex + # replace_existing = True + # + # nt = TrackClass() + # + # if "title" in song: + # nt.title = song["title"] + # if "artist" in song: + # nt.artist = song["artist"] + # if "album" in song: + # nt.album = song["album"] + # if "track" in song: + # nt.track_number = song["track"] + # if "year" in song: + # nt.date = str(song["year"]) + # if "duration" in song: + # nt.length = song["duration"] + # + # # if "bitRate" in song: + # # nt.bitrate = song["bitRate"] + # + # nt.file_ext = "SUB" + # + # nt.index = id + # + # nt.parent_folder_name = name + # if "path" in song: + # nt.fullpath = song["path"] + # nt.parent_folder_path = os.path.dirname(song["path"]) + # + # if "coverArt" in song: + # nt.art_url_key = song["id"] + # + # nt.url_key = song["id"] + # nt.is_network = True + # + # pctl.master_library[id] = nt + # + # if not replace_existing: + # pctl.master_count += 1 + # + # playlist.append(nt.index) + # + # for id, name in folders: + # get(id, name) + # + # self.scanning = False + # if return_list: + # return playlist + # + # pctl.multi_playlist.append(pl_gen(title="Airsonic Collection", playlist_ids=playlist)) + # pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "air" + # switch_playlist(len(pctl.multi_playlist) - 1) + +class STray: + + def __init__(self) -> None: + self.active = False + + def up(self, systray: SysTrayIcon): + SDL_ShowWindow(t_window) + SDL_RaiseWindow(t_window) + SDL_RestoreWindow(t_window) + gui.lowered = False + + def down(self) -> None: + if self.active: + SDL_HideWindow(t_window) + + def advance(self, systray: SysTrayIcon) -> None: + pctl.advance() + + def back(self, systray: SysTrayIcon) -> None: + pctl.back() + + def pause(self, systray: SysTrayIcon) -> None: + pctl.play_pause() + + def track_stop(self, systray: SysTrayIcon) -> None: + pctl.stop() + + def on_quit_callback(self, systray: SysTrayIcon) -> None: + tauon.exit("Exit called from tray.") + + def start(self) -> None: + menu_options = (("Show", None, self.up), + ("Play/Pause", None, self.pause), + ("Stop", None, self.track_stop), + ("Forward", None, self.advance), + ("Back", None, self.back)) + self.systray = SysTrayIcon( + str(install_directory / "assets" / "icon.ico"), "Tauon Music Box", + menu_options, on_quit=self.on_quit_callback) + self.systray.start() + self.active = True + gui.tray_active = True + + def stop(self) -> None: + self.systray.shutdown() + self.active = False + +class GStats: + def __init__(self): + + self.last_db = 0 + self.last_pl = 0 + self.artist_list = [] + self.album_list = [] + self.genre_list = [] + self.genre_dict = {} + + def update(self, playlist): + + pt = 0 + + if pctl.master_count != self.last_db or self.last_pl != playlist: + self.last_db = pctl.master_count + self.last_pl = playlist + + artists = {} + + for index in pctl.multi_playlist[playlist].playlist_ids: + artist = pctl.master_library[index].artist + + if artist == "": + artist = "<Artist Unspecified>" + + pt = int(star_store.get(index)) + if pt < 30: + continue + + if artist in artists: + artists[artist] += pt + else: + artists[artist] = pt + + art_list = artists.items() + + sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) + + self.artist_list = copy.deepcopy(sorted_list) + + genres = {} + genre_dict = {} + + for index in pctl.multi_playlist[playlist].playlist_ids: + genre_r = pctl.master_library[index].genre + + pt = int(star_store.get(index)) + + gn = [] + if "," in genre_r: + for g in genre_r.split(","): + g = g.rstrip(" ").lstrip(" ") + if len(g) > 0: + gn.append(g) + elif ";" in genre_r: + for g in genre_r.split(";"): + g = g.rstrip(" ").lstrip(" ") + if len(g) > 0: + gn.append(g) + elif "/" in genre_r: + for g in genre_r.split("/"): + g = g.rstrip(" ").lstrip(" ") + if len(g) > 0: + gn.append(g) + elif " & " in genre_r: + for g in genre_r.split(" & "): + g = g.rstrip(" ").lstrip(" ") + if len(g) > 0: + gn.append(g) + else: + gn = [genre_r] + + pt = int(pt / len(gn)) + + for genre in gn: + + if genre.lower() in {"", "other", "unknown", "misc"}: + genre = "<Genre Unspecified>" + if genre.lower() in {"jpop", "japanese pop"}: + genre = "J-Pop" + if genre.lower() in {"jrock", "japanese rock"}: + genre = "J-Rock" + if genre.lower() in {"alternative music", "alt-rock", "alternative", "alternrock", "alt"}: + genre = "Alternative Rock" + if genre.lower() in {"jpunk", "japanese punk"}: + genre = "J-Punk" + if genre.lower() in {"post rock", "post-rock"}: + genre = "Post-Rock" + if genre.lower() in {"video game", "game", "game music", "video game music", "game ost"}: + genre = "Video Game Soundtrack" + if genre.lower() in {"general soundtrack", "ost", "Soundtracks"}: + genre = "Soundtrack" + if genre.lower() in ("anime", "アニメ", "anime ost"): + genre = "Anime Soundtrack" + if genre.lower() in {"同人"}: + genre = "Doujin" + if genre.lower() in {"chill, chill out", "chill-out"}: + genre = "Chillout" + + genre = genre.title() + + if len(genre) == 3 and genre[2] == "m": + genre = genre.upper() + + if genre in genres: + + genres[genre] += pt + else: + genres[genre] = pt + + if genre in genre_dict: + genre_dict[genre].append(index) + else: + genre_dict[genre] = [index] + + art_list = genres.items() + sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) + + self.genre_list = copy.deepcopy(sorted_list) + self.genre_dict = genre_dict + + # logging.info('\n-----------------------\n') + + g_albums = {} + + for index in pctl.multi_playlist[playlist].playlist_ids: + album = pctl.master_library[index].album + + if album == "": + album = "<Album Unspecified>" + + pt = int(star_store.get(index)) + + if pt < 30: + continue + + if album in g_albums: + g_albums[album] += pt + else: + g_albums[album] = pt + + art_list = g_albums.items() + + sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) + + self.album_list = copy.deepcopy(sorted_list) + +class Drawing: + + def button( + self, text, x, y, w=None, h=None, font=212, text_highlight_colour=None, text_colour=None, + background_colour=None, background_highlight_colour=None, press=None, tooltip=""): + + if w is None: + w = ddt.get_text_w(text, font) + 18 * gui.scale + if h is None: + h = 22 * gui.scale + + rect = (x, y, w, h) + fields.add(rect) + + if text_highlight_colour is None: + text_highlight_colour = colours.box_button_text_highlight + if text_colour is None: + text_colour = colours.box_button_text + if background_colour is None: + background_colour = colours.box_button_background + if background_highlight_colour is None: + background_highlight_colour = colours.box_button_background_highlight + + click = False + + if press is None: + press = inp.mouse_click + + if coll(rect): + if tooltip: + tool_tip.test(x + 15 * gui.scale, y - 28 * gui.scale, tooltip) + ddt.rect(rect, background_highlight_colour) + + # if background_highlight_colour[3] != 255: + # background_highlight_colour = None + + ddt.text( + (rect[0] + int(rect[2] / 2), rect[1] + 2 * gui.scale, 2), text, text_highlight_colour, font, bg=background_highlight_colour) + if press: + click = True + else: + ddt.rect(rect, background_colour) + if background_highlight_colour[3] != 255: + background_colour = None + ddt.text( + (rect[0] + int(rect[2] / 2), rect[1] + 2 * gui.scale, 2), text, text_colour, font, bg=background_colour) + return click + +class DropShadow: + + def __init__(self, gui: GuiVar): + self.readys = {} + self.underscan = int(15 * gui.scale) + self.radius = 4 + self.grow = 2 * gui.scale + self.opacity = 90 + + def prepare(self, w, h): + fh = h + self.underscan + fw = w + self.underscan + + im = Image.new("RGBA", (round(fw), round(fh)), 0x00000000) + draw = ImageDraw.Draw(im) + draw.rectangle(((self.underscan, self.underscan), (w + 2, h + 2)), fill="black") + + im = im.filter(ImageFilter.GaussianBlur(self.radius)) + + g = io.BytesIO() + g.seek(0) + im.save(g, "PNG") + g.seek(0) + + wop = rw_from_object(g) + s_image = IMG_Load_RW(wop, 0) + c = SDL_CreateTextureFromSurface(renderer, s_image) + SDL_SetTextureAlphaMod(c, self.opacity) + + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + SDL_QueryTexture(c, None, None, tex_w, tex_h) + + dst = SDL_Rect(0, 0) + dst.w = int(tex_w.contents.value) + dst.h = int(tex_h.contents.value) + + SDL_FreeSurface(s_image) + g.close() + im.close() + + unit = (dst, c) + self.readys[(w, h)] = unit + + def render(self, x, y, w, h): + if (w, h) not in self.readys: + self.prepare(w, h) + + unit = self.readys[(w, h)] + unit[0].x = round(x) - round(self.underscan) + unit[0].y = round(y) - round(self.underscan) + SDL_RenderCopy(renderer, unit[1], None, unit[0]) + +class LyricsRenMini: + + def __init__(self): + self.index = -1 + self.text = "" + + self.lyrics_position = 0 + + def generate(self, index, w): + self.text = pctl.master_library[index].lyrics + self.lyrics_position = 0 + + def render(self, index, x, y, w, h, p): + if index != self.index or self.text != pctl.master_library[index].lyrics: + self.index = index + self.generate(index, w) + + colour = colours.side_bar_line1 + + # if key_ctrl_down: + # if mouse_wheel < 0: + # prefs.lyrics_font_size += 1 + # if mouse_wheel > 0: + # prefs.lyrics_font_size -= 1 + + ddt.text((x, y, 4, w), self.text, colour, prefs.lyrics_font_size, w - (w % 2), colours.side_panel_background) + +class LyricsRen: + + def __init__(self): + + self.index = -1 + self.text = "" + + self.lyrics_position = 0 + + def test_update(self, track_object: TrackClass): + + if track_object.index != self.index or self.text != track_object.lyrics: + self.index = track_object.index + self.text = track_object.lyrics + self.lyrics_position = 0 + + def render(self, x, y, w, h, p): + + colour = colours.lyrics + if test_lumi(colours.gallery_background) < 0.5: + colour = colours.grey(40) + + ddt.text((x, y, 4, w), self.text, colour, 17, w, colours.playlist_panel_background) + +class TimedLyricsToStatic: + + def __init__(self): + self.cache_key = None + self.cache_lyrics = "" + + def get(self, track: TrackClass): + if track.lyrics: + return track.lyrics + if track.is_network: + return "" + if track == self.cache_key: + return self.cache_lyrics + data = find_synced_lyric_data(track) + + if data is None: + self.cache_lyrics = "" + self.cache_key = track + return "" + text = "" + + for line in data: + if len(line) < 10: + continue + + if line[0] != "[" or line[9] != "]" or ":" not in line or "." not in line: + continue + + text += line.split("]")[-1].rstrip("\n") + "\n" + + self.cache_lyrics = text + self.cache_key = track + return text + +class TimedLyricsRen: + + def __init__(self): + + self.index = -1 + + self.scanned = {} + self.ready = False + self.data = [] + + self.scroll_position = 0 + + def generate(self, track: TrackClass) -> bool | None: + + if self.index == track.index: + return self.ready + + self.ready = False + self.index = track.index + self.scroll_position = 0 + self.data.clear() + + data = find_synced_lyric_data(track) + if data is None: + return None + + for line in data: + if len(line) < 10: + continue + + if line[0] != "[" or "]" not in line or ":" not in line or "." not in line: + continue + + try: + + text = line.split("]")[-1].rstrip("\n") + t = line + + while t[0] == "[" and t[9] == "]" and ":" in t and "." in t: + + a = t.lstrip("[") + t = t.split("]")[1] + "]" + + a = a.split("]")[0] + mm, b = a.split(":") + ss, ms = b.split(".") + + s = int(mm) * 60 + int(ss) + if len(ms) == 2: + s += int(ms) / 100 + elif len(ms) == 3: + s += int(ms) / 1000 + + self.data.append((s, text)) + + if len(t) < 10: + break + except Exception: + logging.exception("Failed generating timed lyrics") + continue + + self.data = sorted(self.data, key=lambda x: x[0]) + # logging.info(self.data) + + self.ready = True + return True + + def render(self, index: int, x: int, y: int, side_panel: bool = False, w: int = 0, h: int = 0) -> bool | None: + + if index != self.index: + self.ready = False + self.generate(pctl.master_library[index]) + + if right_click and x and y and coll((x, y, w, h)): + showcase_menu.activate(pctl.master_library[index]) + + if not self.ready: + return False + + if mouse_wheel and (pctl.playing_state != 1 or pctl.track_queue[pctl.queue_step] != index): + if side_panel: + if coll((x, y, w, h)): + self.scroll_position += int(mouse_wheel * 30 * gui.scale) + else: + self.scroll_position += int(mouse_wheel * 30 * gui.scale) + + line_active = -1 + last = -1 + + highlight = True + + if side_panel: + bg = colours.top_panel_background + font_size = 15 + spacing = round(17 * gui.scale) + else: + bg = colours.playlist_panel_background + font_size = 17 + spacing = round(23 * gui.scale) + + test_time = get_real_time() + + if pctl.track_queue[pctl.queue_step] == index: + + for i, line in enumerate(self.data): + if line[0] < test_time: + last = i + + if line[0] > test_time: + pctl.wake_past_time = line[0] + line_active = last + break + else: + line_active = len(self.data) - 1 + + if pctl.playing_state == 1: + self.scroll_position = (max(0, line_active)) * spacing * -1 + + yy = y + self.scroll_position + + for i, line in enumerate(self.data): + + if 0 < yy < window_size[1]: + + colour = colours.lyrics + if test_lumi(colours.gallery_background) < 0.5: + colour = colours.grey(40) + + if i == line_active and highlight: + colour = [255, 210, 50, 255] + if colours.lm: + colour = [180, 130, 210, 255] + + h = ddt.text((x, yy, 4, w - 20 * gui.scale), line[1], colour, font_size, w - 20 * gui.scale, bg) + yy += max(h - round(6 * gui.scale), spacing) + else: + yy += spacing + return None + +class TextBox2: + cursor = True + + def __init__(self) -> None: + + self.text: str = "" + self.cursor_position = 0 + self.selection = 0 + self.offset = 0 + self.down_lock = False + self.paste_text = "" + + def paste(self) -> None: + + if SDL_HasClipboardText(): + clip = SDL_GetClipboardText().decode("utf-8") + self.paste_text = clip + + def copy(self) -> None: + + text = self.get_selection() + if not text: + text = self.text + if text != "": + SDL_SetClipboardText(text.encode("utf-8")) + + def set_text(self, text: str) -> None: + + self.text = text + if self.cursor_position > len(text): + self.cursor_position = 0 + self.selection = 0 + else: + self.selection = self.cursor_position + + def clear(self) -> None: + self.text = "" + #self.cursor_position = 0 + self.selection = self.cursor_position + + def highlight_all(self) -> None: + + self.selection = len(self.text) + self.cursor_position = 0 + + def eliminate_selection(self) -> None: + if self.selection != self.cursor_position: + if self.selection > self.cursor_position: + self.text = self.text[0: len(self.text) - self.selection] + self.text[len(self.text) - self.cursor_position:] + self.selection = self.cursor_position + else: + self.text = self.text[0: len(self.text) - self.cursor_position] + self.text[len(self.text) - self.selection:] + self.cursor_position = self.selection + + def get_selection(self, p: int = 1) -> str: + if self.selection != self.cursor_position: + if p == 1: + if self.selection > self.cursor_position: + return self.text[len(self.text) - self.selection: len(self.text) - self.cursor_position] + + return self.text[len(self.text) - self.cursor_position: len(self.text) - self.selection] + if p == 0: + return self.text[0: len(self.text) - max(self.cursor_position, self.selection)] + if p == 2: + return self.text[len(self.text) - min(self.cursor_position, self.selection):] + + else: + return "" + + def draw( + self, x, y, colour, active=True, secret=False, font=13, width=0, click=False, selection_height=18, big=False): + + # A little bit messy + # For now, this is set up so where 'width' is set > 0, the cursor position becomes editable, + # otherwise it is fixed to end + + SDL_SetRenderTarget(renderer, text_box_canvas) + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + + text_box_canvas_rect.x = 0 + text_box_canvas_rect.y = 0 + SDL_RenderFillRect(renderer, text_box_canvas_rect) + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + + selection_height *= gui.scale + + if click is False: + click = inp.mouse_click + if mouse_down: + gui.update = 2 # TODO, more elegant fix + + rect = (x - 3, y - 2, width - 3, 21 * gui.scale) + select_rect = (x - 20 * gui.scale, y - 2, width + 20 * gui.scale, 21 * gui.scale) + + fields.add(rect) + + # Activate Menu + if coll(rect): + if right_click or level_2_right_click: + field_menu.activate(self) + + if width > 0 and active: + + if click and field_menu.active: + # field_menu.click() + click = False + + # Add text from input + if input_text != "": + self.eliminate_selection() + self.text = self.text[0: len(self.text) - self.cursor_position] + input_text + self.text[len( + self.text) - self.cursor_position:] + + def g(): + if len(self.text) == 0 or self.cursor_position == len(self.text): + return None + return self.text[len(self.text) - self.cursor_position - 1] + + def g2(): + if len(self.text) == 0 or self.cursor_position == 0: + return None + return self.text[len(self.text) - self.cursor_position] + + def d(): + self.text = self.text[0: len(self.text) - self.cursor_position - 1] + self.text[len( + self.text) - self.cursor_position:] + self.selection = self.cursor_position + + # Ctrl + Backspace to delete word + if inp.backspace_press and (key_ctrl_down or key_rctrl_down) and \ + self.cursor_position == self.selection and len(self.text) > 0 and self.cursor_position < len( + self.text): + while g() == " ": + d() + while g() != " " and g() != None: + d() + + # Ctrl + left to move cursor back a word + elif (key_ctrl_down or key_rctrl_down) and key_left_press: + while g() == " ": + self.cursor_position += 1 + if not key_shift_down: + self.selection = self.cursor_position + while g() != None and g() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": + self.cursor_position += 1 + if not key_shift_down: + self.selection = self.cursor_position + if g() == " ": + self.cursor_position -= 1 + if not key_shift_down: + self.selection = self.cursor_position + break + + # Ctrl + right to move cursor forward a word + elif (key_ctrl_down or key_rctrl_down) and key_right_press: + while g2() == " ": + self.cursor_position -= 1 + if not key_shift_down: + self.selection = self.cursor_position + while g2() != None and g2() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": + self.cursor_position -= 1 + if not key_shift_down: + self.selection = self.cursor_position + if g2() == " ": + self.cursor_position += 1 + if not key_shift_down: + self.selection = self.cursor_position + break + + # Handle normal backspace + elif inp.backspace_press and len(self.text) > 0 and self.cursor_position < len(self.text): + while inp.backspace_press and len(self.text) > 0 and self.cursor_position < len(self.text): + if self.selection != self.cursor_position: + self.eliminate_selection() + else: + self.text = self.text[0:len(self.text) - self.cursor_position - 1] + self.text[len( + self.text) - self.cursor_position:] + inp.backspace_press -= 1 + elif inp.backspace_press and len(self.get_selection()) > 0: + self.eliminate_selection() + + # Left and right arrow keys to move cursor + if key_right_press: + if self.cursor_position > 0: + self.cursor_position -= 1 + if not key_shift_down and not key_shiftr_down: + self.selection = self.cursor_position + + if key_left_press: + if self.cursor_position < len(self.text): + self.cursor_position += 1 + if not key_shift_down and not key_shiftr_down: + self.selection = self.cursor_position + + if self.paste_text: + if "http://" in self.text and "http://" in self.paste_text: + self.text = "" + + self.paste_text = self.paste_text.rstrip(" ").lstrip(" ") + self.paste_text = self.paste_text.replace("\n", " ").replace("\r", "") + + self.eliminate_selection() + self.text = self.text[0: len(self.text) - self.cursor_position] + self.paste_text + self.text[len( + self.text) - self.cursor_position:] + self.paste_text = "" + + # Paste via ctrl-v + if key_ctrl_down and key_v_press: + clip = SDL_GetClipboardText().decode("utf-8") + self.eliminate_selection() + self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( + self.text) - self.cursor_position:] + + if key_ctrl_down and key_c_press: + self.copy() + + if key_ctrl_down and key_x_press: + if len(self.get_selection()) > 0: + text = self.get_selection() + if text != "": + SDL_SetClipboardText(text.encode("utf-8")) + self.eliminate_selection() + + if key_ctrl_down and key_a_press: + self.cursor_position = 0 + self.selection = len(self.text) + + # ddt.rect(rect, [255, 50, 50, 80], True) + if coll(rect) and not field_menu.active: + gui.cursor_want = 2 + + # Delete key to remove text in front of cursor + if key_del: + if self.selection != self.cursor_position: + self.eliminate_selection() + else: + self.text = self.text[0:len(self.text) - self.cursor_position] + self.text[len( + self.text) - self.cursor_position + 1:] + if self.cursor_position > 0: + self.cursor_position -= 1 + self.selection = self.cursor_position + + if key_home_press: + self.cursor_position = len(self.text) + if not key_shift_down and not key_shiftr_down: + self.selection = self.cursor_position + if key_end_press: + self.cursor_position = 0 + if not key_shift_down and not key_shiftr_down: + self.selection = self.cursor_position + + width -= round(15 * gui.scale) + t_len = ddt.get_text_w(self.text, font) + if active and editline and editline != input_text: + t_len += ddt.get_text_w(editline, font) + if not click and not self.down_lock: + cursor_x = ddt.get_text_w(self.text[:len(self.text) - self.cursor_position], font) + if self.cursor_position == 0 or cursor_x < self.offset + round( + 15 * gui.scale) or cursor_x > self.offset + width: + if t_len > width: + self.offset = t_len - width + + if cursor_x < self.offset: + self.offset = cursor_x - round(15 * gui.scale) + + self.offset = max(self.offset, 0) + else: + self.offset = 0 + + x -= self.offset + + if coll(select_rect): # coll((x - 15, y, width + 16, selection_height + 1)): + # ddt.rect_r((x - 15, y, width + 16, 19), [50, 255, 50, 50], True) + if click: + pre = 0 + post = 0 + if mouse_position[0] < x + 1: + self.cursor_position = len(self.text) + else: + for i in range(len(self.text)): + post = ddt.get_text_w(self.text[0:i + 1], font) + # pre_half = int((post - pre) / 2) + + if x + pre - 0 <= mouse_position[0] <= x + post + 0: + diff = post - pre + if mouse_position[0] >= x + pre + int(diff / 2): + self.cursor_position = len(self.text) - i - 1 + else: + self.cursor_position = len(self.text) - i + break + pre = post + else: + self.cursor_position = 0 + self.selection = 0 + self.down_lock = True + + if mouse_up: + self.down_lock = False + if self.down_lock: + pre = 0 + post = 0 + text = self.text + if secret: + text = "●" * len(self.text) + if mouse_position[0] < x + 1: + self.selection = len(text) + else: + + for i in range(len(text)): + post = ddt.get_text_w(text[0:i + 1], font) + # pre_half = int((post - pre) / 2) + + if x + pre - 0 <= mouse_position[0] <= x + post + 0: + diff = post - pre + + if mouse_position[0] >= x + pre + int(diff / 2): + self.selection = len(text) - i - 1 + + else: + self.selection = len(text) - i + + break + pre = post + + else: + self.selection = 0 + + text = self.text[0: len(self.text) - self.cursor_position] + if secret: + text = "●" * len(text) + a = ddt.get_text_w(text, font) + + text = self.text[0: len(self.text) - self.selection] + if secret: + text = "●" * len(text) + b = ddt.get_text_w(text, font) + + top = y + if big: + top -= 12 * gui.scale + + ddt.rect([a, 0, b - a, selection_height], [40, 120, 180, 255]) + + if self.selection != self.cursor_position: + inf_comp = 0 + text = self.get_selection(0) + if secret: + text = "●" * len(text) + space = ddt.text((0, 0), text, colour, font) + text = self.get_selection(1) + if secret: + text = "●" * len(text) + space += ddt.text((0 + space - inf_comp, 0), text, [240, 240, 240, 255], font, bg=[40, 120, 180, 255]) + text = self.get_selection(2) + if secret: + text = "●" * len(text) + ddt.text((0 + space - (inf_comp * 2), 0), text, colour, font) + else: + text = self.text + if secret: + text = "●" * len(text) + ddt.text((0, 0), text, colour, font) + + text = self.text[0: len(self.text) - self.cursor_position] + if secret: + text = "●" * len(text) + space = ddt.get_text_w(text, font) + + if TextBox.cursor and self.selection == self.cursor_position: + # ddt.line(x + space, y + 2, x + space, y + 15, colour) + + ddt.rect((0 + space, 0 + 2, 1 * gui.scale, 14 * gui.scale), colour) + + if click: + self.selection = self.cursor_position + + else: + width -= round(15 * gui.scale) + text = self.text + if secret: + text = "●" * len(text) + t_len = ddt.get_text_w(text, font) + ddt.text((0, 0), text, colour, font) + self.offset = 0 + if coll(rect) and not field_menu.active: + gui.cursor_want = 2 + + if active and editline != "" and editline != input_text: + ex = ddt.text((space + round(4 * gui.scale), 0), editline, [240, 230, 230, 255], font) + tw, th = ddt.get_text_wh(editline, font, max_x=2000) + ddt.rect((space + round(4 * gui.scale), th + round(2 * gui.scale), ex, round(1 * gui.scale)), [245, 245, 245, 255]) + + rect = SDL_Rect(pixel_to_logical(x + space + tw + (5 * gui.scale)), pixel_to_logical(y + th + 4 * gui.scale), 1, 1) + SDL_SetTextInputRect(rect) + + animate_monitor_timer.set() + + text_box_canvas_hide_rect.x = 0 + text_box_canvas_hide_rect.y = 0 + + # if self.offset: + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) + + text_box_canvas_hide_rect.w = round(self.offset) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_RenderFillRect(renderer, text_box_canvas_hide_rect) + + text_box_canvas_hide_rect.w = round(t_len) + text_box_canvas_hide_rect.x = round(self.offset + width + round(5 * gui.scale)) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_RenderFillRect(renderer, text_box_canvas_hide_rect) + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + SDL_SetRenderTarget(renderer, gui.main_texture) + + text_box_canvas_rect.x = round(x) + text_box_canvas_rect.y = round(y) + SDL_RenderCopy(renderer, text_box_canvas, None, text_box_canvas_rect) + +class TextBox: + cursor = True + + def __init__(self) -> None: + + self.text = "" + self.cursor_position = 0 + self.selection = 0 + self.down_lock = False + + def paste(self) -> None: + + if SDL_HasClipboardText(): + clip = SDL_GetClipboardText().decode("utf-8") + + if "http://" in self.text and "http://" in clip: + self.text = "" + + clip = clip.rstrip(" ").lstrip(" ") + clip = clip.replace("\n", " ").replace("\r", "") + + self.eliminate_selection() + self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( + self.text) - self.cursor_position:] + + def copy(self) -> None: + + text = self.get_selection() + if not text: + text = self.text + if text != "": + SDL_SetClipboardText(text.encode("utf-8")) + + def set_text(self, text): + + self.text = text + self.cursor_position = 0 + self.selection = 0 + + def clear(self) -> None: + self.text = "" + + def highlight_all(self) -> None: + + self.selection = len(self.text) + self.cursor_position = 0 + + def highlight_none(self) -> None: + self.selection = 0 + self.cursor_position = 0 + + def eliminate_selection(self) -> None: + if self.selection != self.cursor_position: + if self.selection > self.cursor_position: + self.text = self.text[0: len(self.text) - self.selection] + self.text[ + len(self.text) - self.cursor_position:] + self.selection = self.cursor_position + else: + self.text = self.text[0: len(self.text) - self.cursor_position] + self.text[ + len(self.text) - self.selection:] + self.cursor_position = self.selection + + def get_selection(self, p: int = 1): + if self.selection != self.cursor_position: + if p == 1: + if self.selection > self.cursor_position: + return self.text[len(self.text) - self.selection: len(self.text) - self.cursor_position] + + return self.text[len(self.text) - self.cursor_position: len(self.text) - self.selection] + if p == 0: + return self.text[0: len(self.text) - max(self.cursor_position, self.selection)] + if p == 2: + return self.text[len(self.text) - min(self.cursor_position, self.selection):] + + else: + return "" + + def draw( + self, x: int, y: int, colour: list[int], active: bool = True, secret: bool = False, + font: int = 13, width: int = 0, click: bool = False, selection_height: int = 18, big: bool = False): + + # A little bit messy + # For now, this is set up so where 'width' is set > 0, the cursor position becomes editable, + # otherwise it is fixed to end + + selection_height *= gui.scale + + if click is False: + click = inp.mouse_click + + if width > 0 and active: + + rect = (x - 3, y - 2, width - 3, 21 * gui.scale) + select_rect = (x - 20 * gui.scale, y - 2, width + 20 * gui.scale, 21 * gui.scale) + if big: + rect = (x - 3, y - 15 * gui.scale, width - 3, 35 * gui.scale) + select_rect = (x - 50 * gui.scale, y - 15 * gui.scale, width + 50 * gui.scale, 35 * gui.scale) + + # Activate Menu + if coll(rect): + if right_click or level_2_right_click: + field_menu.activate(self) + + if click and field_menu.active: + # field_menu.click() + click = False + + # Add text from input + if input_text != "": + self.eliminate_selection() + self.text = self.text[0: len(self.text) - self.cursor_position] + input_text + self.text[ + len(self.text) - self.cursor_position:] + + def g(): + if len(self.text) == 0 or self.cursor_position == len(self.text): + return None + return self.text[len(self.text) - self.cursor_position - 1] + + def g2(): + if len(self.text) == 0 or self.cursor_position == 0: + return None + return self.text[len(self.text) - self.cursor_position] + + def d(): + self.text = self.text[0: len(self.text) - self.cursor_position - 1] + self.text[ + len(self.text) - self.cursor_position:] + self.selection = self.cursor_position + + # Ctrl + Backspace to delete word + if inp.backspace_press and (key_ctrl_down or key_rctrl_down) and \ + self.cursor_position == self.selection and len(self.text) > 0 and self.cursor_position < len( + self.text): + while g() == " ": + d() + while g() != " " and g() != None: + d() + + # Ctrl + left to move cursor back a word + elif (key_ctrl_down or key_rctrl_down) and key_left_press: + while g() == " ": + self.cursor_position += 1 + if not key_shift_down: + self.selection = self.cursor_position + while g() != None and g() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": + self.cursor_position += 1 + if not key_shift_down: + self.selection = self.cursor_position + if g() == " ": + self.cursor_position -= 1 + if not key_shift_down: + self.selection = self.cursor_position + break + + # Ctrl + right to move cursor forward a word + elif (key_ctrl_down or key_rctrl_down) and key_right_press: + while g2() == " ": + self.cursor_position -= 1 + if not key_shift_down: + self.selection = self.cursor_position + while g2() != None and g2() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": + self.cursor_position -= 1 + if not key_shift_down: + self.selection = self.cursor_position + if g2() == " ": + self.cursor_position += 1 + if not key_shift_down: + self.selection = self.cursor_position + break + + # Handle normal backspace + elif inp.backspace_press and len(self.text) > 0 and self.cursor_position < len(self.text): + while inp.backspace_press and len(self.text) > 0 and self.cursor_position < len(self.text): + if self.selection != self.cursor_position: + self.eliminate_selection() + else: + self.text = self.text[0:len(self.text) - self.cursor_position - 1] + self.text[ + len(self.text) - self.cursor_position:] + inp.backspace_press -= 1 + elif inp.backspace_press and len(self.get_selection()) > 0: + self.eliminate_selection() + + # Left and right arrow keys to move cursor + if key_right_press: + if self.cursor_position > 0: + self.cursor_position -= 1 + if not key_shift_down and not key_shiftr_down: + self.selection = self.cursor_position + + if key_left_press: + if self.cursor_position < len(self.text): + self.cursor_position += 1 + if not key_shift_down and not key_shiftr_down: + self.selection = self.cursor_position + + # Paste via ctrl-v + if key_ctrl_down and key_v_press: + clip = SDL_GetClipboardText().decode("utf-8") + self.eliminate_selection() + self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( + self.text) - self.cursor_position:] + + if key_ctrl_down and key_c_press: + self.copy() + + if key_ctrl_down and key_x_press: + if len(self.get_selection()) > 0: + text = self.get_selection() + if text != "": + SDL_SetClipboardText(text.encode("utf-8")) + self.eliminate_selection() + + if key_ctrl_down and key_a_press: + self.cursor_position = 0 + self.selection = len(self.text) + + # ddt.rect_r(rect, [255, 50, 50, 80], True) + if coll(rect) and not field_menu.active: + gui.cursor_want = 2 + + fields.add(rect) + + # Delete key to remove text in front of cursor + if key_del: + if self.selection != self.cursor_position: + self.eliminate_selection() + else: + self.text = self.text[0:len(self.text) - self.cursor_position] + self.text[len( + self.text) - self.cursor_position + 1:] + if self.cursor_position > 0: + self.cursor_position -= 1 + self.selection = self.cursor_position + + if key_home_press: + self.cursor_position = len(self.text) + if not key_shift_down and not key_shiftr_down: + self.selection = self.cursor_position + if key_end_press: + self.cursor_position = 0 + if not key_shift_down and not key_shiftr_down: + self.selection = self.cursor_position + + if coll(select_rect): + # ddt.rect_r((x - 15, y, width + 16, 19), [50, 255, 50, 50], True) + if click: + pre = 0 + post = 0 + if mouse_position[0] < x + 1: + self.cursor_position = len(self.text) + else: + for i in range(len(self.text)): + post = ddt.get_text_w(self.text[0:i + 1], font) + # pre_half = int((post - pre) / 2) + + if x + pre - 0 <= mouse_position[0] <= x + post + 0: + diff = post - pre + if mouse_position[0] >= x + pre + int(diff / 2): + self.cursor_position = len(self.text) - i - 1 + else: + self.cursor_position = len(self.text) - i + break + pre = post + else: + self.cursor_position = 0 + self.selection = 0 + self.down_lock = True + + if mouse_up: + self.down_lock = False + if self.down_lock: + pre = 0 + post = 0 + if mouse_position[0] < x + 1: + + self.selection = len(self.text) + else: + + for i in range(len(self.text)): + post = ddt.get_text_w(self.text[0:i + 1], font) + # pre_half = int((post - pre) / 2) + + if x + pre - 0 <= mouse_position[0] <= x + post + 0: + diff = post - pre + + if mouse_position[0] >= x + pre + int(diff / 2): + self.selection = len(self.text) - i - 1 + + else: + self.selection = len(self.text) - i + + break + pre = post + + else: + self.selection = 0 + + a = ddt.get_text_w(self.text[0: len(self.text) - self.cursor_position], font) + # logging.info("") + # logging.info(self.selection) + # logging.info(self.cursor_position) + + b = ddt.get_text_w(self.text[0: len(self.text) - self.selection], font) + + # rint((a, b)) + + top = y + if big: + top -= 12 * gui.scale + + ddt.rect([x + a, top, b - a, selection_height], [40, 120, 180, 255]) + + if self.selection != self.cursor_position: + inf_comp = 0 + space = ddt.text((x, y), self.get_selection(0), colour, font) + space += ddt.text( + (x + space - inf_comp, y), self.get_selection(1), [240, 240, 240, 255], font, + bg=[40, 120, 180, 255]) + ddt.text((x + space - (inf_comp * 2), y), self.get_selection(2), colour, font) + else: + ddt.text((x, y), self.text, colour, font) + + space = ddt.get_text_w(self.text[0: len(self.text) - self.cursor_position], font) + + if TextBox.cursor and self.selection == self.cursor_position: + # ddt.line(x + space, y + 2, x + space, y + 15, colour) + + if big: + # ddt.rect_r((xx + 1 , yy - 12 * gui.scale, 2 * gui.scale, 27 * gui.scale), colour, True) + ddt.rect((x + space, y - 15 * gui.scale + 2, 1 * gui.scale, 30 * gui.scale), colour) + else: + ddt.rect((x + space, y + 2, 1 * gui.scale, 14 * gui.scale), colour) + + if click: + self.selection = self.cursor_position + + else: + if active: + self.text += input_text + if input_text != "": + self.cursor = True + + while inp.backspace_press and len(self.text) > 0: + self.text = self.text[:-1] + inp.backspace_press -= 1 + + if key_ctrl_down and key_v_press: + self.paste() + + if secret: + space = ddt.text((x, y), "●" * len(self.text), colour, font) + else: + space = ddt.text((x, y), self.text, colour, font) + + if active and TextBox.cursor: + xx = x + space + 1 + yy = y + 3 + if big: + ddt.rect((xx + 1, yy - 12 * gui.scale, 2 * gui.scale, 27 * gui.scale), colour) + else: + ddt.rect((xx, yy, 1 * gui.scale, 14 * gui.scale), colour) + + if active and editline != "" and editline != input_text: + ex = ddt.text((x + space + round(4 * gui.scale), y), editline, [240, 230, 230, 255], font) + tw, th = ddt.get_text_wh(editline, font, max_x=2000) + ddt.rect((x + space + round(4 * gui.scale), (y + th) - round(4 * gui.scale), ex, round(1 * gui.scale)), + [245, 245, 245, 255]) + + rect = SDL_Rect(pixel_to_logical(x + space + tw + 5 * gui.scale), pixel_to_logical(y + th + 4 * gui.scale), 1, 1) + SDL_SetTextInputRect(rect) + + animate_monitor_timer.set() + +class ImageObject: + def __init__(self) -> None: + self.index = 0 + self.texture = None + self.rect = None + self.request_size = (0, 0) + self.original_size = (0, 0) + self.actual_size = (0, 0) + self.source = "" + self.offset = 0 + self.stats = True + self.format = "" + +class AlbumArt: + def __init__(self): + self.image_types = {"jpg", "JPG", "jpeg", "JPEG", "PNG", "png", "BMP", "bmp", "GIF", "gif", "jxl", "JXL"} + self.art_folder_names = { + "art", "scans", "scan", "booklet", "images", "image", "cover", + "covers", "coverart", "albumart", "gallery", "jacket", "artwork", + "bonus", "bk", "cover artwork", "cover art"} + self.source_cache: dict[int, list[tuple[int, str]]] = {} + self.image_cache: list[ImageObject] = [] + self.current_wu = None + + self.blur_texture = None + self.blur_rect = None + self.loaded_bg_type = 0 + + self.download_in_progress = False + self.downloaded_image = None + self.downloaded_track = None + + self.base64cache = (0, 0, "") + self.processing64on = None + + self.bin_cached = (None, None, None) # track, subsource, bin + + self.embed_cached = (None, None) + + def async_download_image(self, track: TrackClass, subsource: list[tuple[int, str]]) -> None: + + self.downloaded_image = album_art_gen.get_source_raw(0, 0, track, subsource=subsource) + self.downloaded_track = track + self.download_in_progress = False + gui.update += 1 + + def get_info(self, track_object: TrackClass) -> list[tuple[str, int, int, int, str]]: + + sources = self.get_sources(track_object) + if len(sources) == 0: + return None + + offset = self.get_offset(track_object.fullpath, sources) + + o_size = (0, 0) + format = "ERROR" + + for item in self.image_cache: + if item.index == track_object.index and item.offset == offset: + o_size = item.original_size + format = item.format + break + + else: + # Hacky fix + # A quirk is the index stays of the cached image + # This workaround can be done since (currently) cache has max size of 1 + if self.image_cache: + o_size = self.image_cache[0].original_size + format = self.image_cache[0].format + + return [sources[offset][0], len(sources), offset, o_size, format] + + def get_sources(self, tr: TrackClass) -> list[tuple[int, str]]: + + filepath = tr.fullpath + ext = tr.file_ext + + # Check if source list already exists, if not, make it + if tr.index in self.source_cache: + return self.source_cache[tr.index] + + source_list: list[tuple[int, str]] = [] # istag, + + # Source type the is first element in list + # 0 = File + # 1 = Embedded in tag + # 2 = Network location + + if tr.is_network: + # Add url if network target + if tr.art_url_key: + source_list.append([2, tr.art_url_key]) + else: + # Check for local image files + direc = os.path.dirname(filepath) + try: + items_in_dir = os.listdir(direc) + except FileNotFoundError: + logging.warning(f"Failed to find directory: {direc}") + return [] + except Exception: + logging.exception(f"Unknown error loading directory: {direc}") + return [] + + # Check for embedded image + try: + pic = self.get_embed(tr) + if pic: + source_list.append([1, filepath]) + except Exception: + logging.exception("Failed to get embedded image") + + if not tr.is_network: + + dirs_in_dir = [ + subdirec for subdirec in items_in_dir if + os.path.isdir(os.path.join(direc, subdirec)) and subdirec.lower() in self.art_folder_names] + + ins = len(source_list) + for i in range(len(items_in_dir)): + if os.path.splitext(items_in_dir[i])[1][1:] in self.image_types: + dir_path = os.path.join(direc, items_in_dir[i]).replace("\\", "/") + # The image name "Folder" is likely desired to be prioritised over other names + if os.path.splitext(os.path.basename(dir_path))[0] in ("Folder", "folder", "Cover", "cover"): + source_list.insert(ins, [0, dir_path]) + else: + source_list.append([0, dir_path]) + + for i in range(len(dirs_in_dir)): + subdirec = os.path.join(direc, dirs_in_dir[i]) + items_in_dir2 = os.listdir(subdirec) + + for y in range(len(items_in_dir2)): + if os.path.splitext(items_in_dir2[y])[1][1:] in self.image_types: + dir_path = os.path.join(subdirec, items_in_dir2[y]).replace("\\", "/") + source_list.append([0, dir_path]) + + self.source_cache[tr.index] = source_list + + return source_list + + def get_error_img(self, size: float) -> ImageFile: + im = Image.open(str(install_directory / "assets" / "load-error.png")) + im.thumbnail((size, size), Image.Resampling.LANCZOS) + return im + + def fast_display(self, index, location, box, source: list[tuple[int, str]], offset) -> int: + """Renders cached image only by given size for faster performance""" + + found_unit = None + max_h = 0 + + for unit in self.image_cache: + if unit.source == source[offset][1]: + if unit.actual_size[1] > max_h: + max_h = unit.actual_size[1] + found_unit = unit + + if found_unit == None: + return 1 + + unit = found_unit + + temp_dest.x = round(location[0]) + temp_dest.y = round(location[1]) + + temp_dest.w = unit.original_size[0] # round(box[0]) + temp_dest.h = unit.original_size[1] # round(box[1]) + + bh = round(box[1]) + bw = round(box[0]) + + if prefs.zoom_art: + temp_dest.w, temp_dest.h = fit_box((unit.original_size[0], unit.original_size[1]), box) + else: + + # Constrain image to given box + if temp_dest.w > bw: + temp_dest.w = bw + temp_dest.h = int(bw * (unit.original_size[1] / unit.original_size[0])) + + if temp_dest.h > bh: + temp_dest.h = bh + temp_dest.w = int(temp_dest.h * (unit.original_size[0] / unit.original_size[1])) + + # prevent scaling larger than original image size + if temp_dest.w > unit.original_size[0] or temp_dest.h > unit.original_size[1]: + temp_dest.w = unit.original_size[0] + temp_dest.h = unit.original_size[1] + + # center the image + temp_dest.x = int((box[0] - temp_dest.w) / 2) + temp_dest.x + temp_dest.y = int((box[1] - temp_dest.h) / 2) + temp_dest.y + + # render the image + SDL_RenderCopy(renderer, unit.texture, None, temp_dest) + style_overlay.hole_punches.append(temp_dest) + + gui.art_drawn_rect = (temp_dest.x, temp_dest.y, temp_dest.w, temp_dest.h) + + return 0 + + def open_external(self, track_object: TrackClass) -> int: + + index = track_object.index + + source = self.get_sources(track_object) + if len(source) == 0: + return 0 + + offset = self.get_offset(track_object.fullpath, source) + + if track_object.is_network: + show_message(_("Saving network images not implemented")) + return 0 + if source[offset][0] > 0: + pic = album_art_gen.get_embed(track_object) + if not pic: + show_message(_("Image save error."), _("No embedded album art."), mode="warning") + return 0 + + source_image = io.BytesIO(pic) + im = Image.open(source_image) + source_image.close() + + ext = "." + im.format.lower() + if im.format == "JPEG": + ext = ".jpg" + target = str(cache_directory / "open-image") + if not os.path.exists(target): + os.makedirs(target) + target = os.path.join(target, "embed-" + str(im.height) + "px-" + str(track_object.index) + ext) + + if len(pic) > 30: + with open(target, "wb") as w: + w.write(pic) + + else: + target = source[offset][1] + + if system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", target]) + else: + subprocess.call(["xdg-open", target]) + + return 0 + + def cycle_offset(self, track_object: TrackClass, reverse: bool = False) -> int: + + filepath = track_object.fullpath + sources = self.get_sources(track_object) + if len(sources) == 0: + return 0 + parent_folder = os.path.dirname(filepath) + # Find cached offset + if parent_folder in folder_image_offsets: + + if reverse: + folder_image_offsets[parent_folder] -= 1 + else: + folder_image_offsets[parent_folder] += 1 + + folder_image_offsets[parent_folder] %= len(sources) + return 0 + + def cycle_offset_reverse(self, track_object: TrackClass) -> None: + self.cycle_offset(track_object, True) + + def get_offset(self, filepath: str, source: list[tuple[int, str]]) -> int: + + # Check if folder offset already exsts, if not, make it + parent_folder = os.path.dirname(filepath) + + if parent_folder in folder_image_offsets: + + # Reset the offset if greater than number of images available + if folder_image_offsets[parent_folder] > len(source) - 1: + folder_image_offsets[parent_folder] = 0 + else: + folder_image_offsets[parent_folder] = 0 + + return folder_image_offsets[parent_folder] + + def get_embed(self, track: TrackClass): + + # cached = self.embed_cached + # if cached[0] == track: + # #logging.info("used cached") + # return cached[1] + + filepath = track.fullpath + + # Use cached file if present + if prefs.precache and tauon.cachement: + path = tauon.cachement.get_file_cached_only(track) + if path: + filepath = path + + pic = None + + if track.file_ext == "MP3": + try: + tag = mutagen.id3.ID3(filepath) + frame = tag.getall("APIC") + if frame: + pic = frame[0].data + except Exception: + logging.exception(f"Failed to get tags on file: {filepath}") + + if pic is not None and len(pic) < 30: + pic = None + + elif track.file_ext == "FLAC": + with Flac(filepath) as tag: + tag.read(True) + if tag.has_picture and len(tag.picture) > 30: + pic = tag.picture + + elif track.file_ext == "APE": + with Ape(filepath) as tag: + tag.read() + if tag.has_picture and len(tag.picture) > 30: + pic = tag.picture + + elif track.file_ext == "M4A": + with M4a(filepath) as tag: + tag.read(True) + if tag.has_picture and len(tag.picture) > 30: + pic = tag.picture + + elif track.file_ext == "OPUS" or track.file_ext == "OGG" or track.file_ext == "OGA": + with Opus(filepath) as tag: + tag.read() + if tag.has_picture and len(tag.picture) > 30: + with io.BytesIO(base64.b64decode(tag.picture)) as a: + a.seek(0) + image = parse_picture_block(a) + pic = image + + # self.embed_cached = (track, pic) + return pic + + def get_source_raw(self, offset: int, sources: list[tuple[int, str]] | int, track: TrackClass, subsource: list[tuple[int, str]] | None = None): + + source_image = None + + if subsource is None: + subsource = sources[offset] + + if subsource[0] == 1: + # Target is a embedded image\\\ + pic = self.get_embed(track) + assert pic + source_image = io.BytesIO(pic) + + elif subsource[0] == 2: + try: + if track.file_ext == "RADIO" or track.file_ext == "Spotify": + if pctl.radio_image_bin: + return pctl.radio_image_bin + + cached_path = os.path.join(n_cache_dir, hashlib.md5(track.art_url_key.encode()).hexdigest()[:12]) + if os.path.isfile(cached_path): + source_image = open(cached_path, "rb") + else: + if track.file_ext == "SUB": + source_image = subsonic.get_cover(track) + elif track.file_ext == "JELY": + source_image = jellyfin.get_cover(track) + else: + response = urllib.request.urlopen(get_network_thumbnail_url(track), context=ssl_context) + source_image = io.BytesIO(response.read()) + if source_image: + with Path(cached_path).open("wb") as file: + file.write(source_image.read()) + source_image.seek(0) + + except Exception: + logging.exception("Failed to get source") + + else: + source_image = open(subsource[1], "rb") + + return source_image + + def get_base64(self, track: TrackClass, size): + + # Wait if an identical track is already being processed + if self.processing64on == track: + t = 0 + while True: + if self.processing64on is None: + break + time.sleep(0.05) + t += 1 + if t > 20: + break + + cahced = self.base64cache + if track == cahced[0] and size == cahced[1]: + return cahced[2] + + self.processing64on = track + + filepath = track.fullpath + sources = self.get_sources(track) + + if len(sources) == 0: + self.processing64on = None + return False + + offset = self.get_offset(filepath, sources) + + # Get source IO + source_image = self.get_source_raw(offset, sources, track) + + if source_image is None: + self.processing64on = None + return "" + + im = Image.open(source_image) + if im.mode != "RGB": + im = im.convert("RGB") + im.thumbnail(size, Image.Resampling.LANCZOS) + buff = io.BytesIO() + im.save(buff, format="JPEG") + sss = base64.b64encode(buff.getvalue()) + + self.base64cache = (track, size, sss) + self.processing64on = None + return sss + + def get_background(self, track: TrackClass) -> BytesIO | BufferedReader | None: + #logging.info("Find background...") + # Determine artist name to use + artist = get_artist_safe(track) + if not artist: + return None + + # Check cache for existing image + path = os.path.join(b_cache_dir, artist) + if os.path.isfile(path): + logging.info("Load cached background") + return open(path, "rb") + + # Try last.fm background + path = artist_info_box.get_data(artist, get_img_path=True) + if os.path.isfile(path): + logging.info("Load cached background lfm") + return open(path, "rb") + + # Check we've not already attempted a search for this artist + if artist in prefs.failed_background_artists: + return None + + # Get artist MBID + try: + s = musicbrainzngs.search_artists(artist, limit=1) + artist_id = s["artist-list"][0]["id"] + except Exception: + logging.exception(f"Failed to find artist MBID for: {artist}") + prefs.failed_background_artists.append(artist) + return None + + # Search fanart.tv for background + try: + + r = requests.get( + "https://webservice.fanart.tv/v3/music/" \ + + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) + + artlink = r.json()["artistbackground"][0]["url"] + + response = urllib.request.urlopen(artlink, context=ssl_context) + info = response.info() + + assert info.get_content_maintype() == "image" + + t = io.BytesIO() + t.seek(0) + t.write(response.read()) + t.seek(0, 2) + l = t.tell() + t.seek(0) + + assert l > 1000 + + # Cache image for future use + path = os.path.join(a_cache_dir, artist + "-ftv-full.jpg") + with open(path, "wb") as f: + f.write(t.read()) + t.seek(0) + return t + + except Exception: + logging.exception(f"Failed to find fanart background for: {artist}") + if not gui.artist_info_panel: + artist_info_box.get_data(artist) + path = artist_info_box.get_data(artist, get_img_path=True) + if os.path.isfile(path): + logging.debug("Downloaded background lfm") + return open(path, "rb") + + + prefs.failed_background_artists.append(artist) + return None + + def get_blur_im(self, track: TrackClass) -> BytesIO | bool | None: + + source_image = None + self.loaded_bg_type = 0 + if prefs.enable_fanart_bg: + source_image = self.get_background(track) + if source_image: + self.loaded_bg_type = 1 + + if source_image is None: + filepath = track.fullpath + sources = self.get_sources(track) + + if len(sources) == 0: + return False + + offset = self.get_offset(filepath, sources) + + source_image = self.get_source_raw(offset, sources, track) + + if source_image is None: + return None + + im = Image.open(source_image) + + ox_size = im.size[0] + oy_size = im.size[1] + + format = im.format + if im.format == "JPEG": + format = "JPG" + + #logging.info(im.size) + if im.mode != "RGB": + im = im.convert("RGB") + + ratio = window_size[0] / ox_size + ratio += 0.2 + + if (oy_size * ratio) - ((oy_size * ratio) // 4) < window_size[1]: + logging.info("Adjust bg vertical") + ratio = window_size[1] / (oy_size - (oy_size // 4)) + ratio += 0.2 + + new_x = round(ox_size * ratio) + new_y = round(oy_size * ratio) + + im = im.resize((new_x, new_y)) + + if self.loaded_bg_type == 1: + artist = get_artist_safe(track) + if artist and artist in prefs.bg_flips: + im = im.transpose(Image.FLIP_LEFT_RIGHT) + + if (ox_size < 500 or prefs.art_bg_always_blur) or gui.mode == 3: + blur = prefs.art_bg_blur + if prefs.mini_mode_mode == 5 and gui.mode == 3: + blur = 160 + pix = im.getpixel((new_x // 2, new_y // 4 * 3)) + pixel_sum = sum(pix) / (255 * 3) + if pixel_sum > 0.6: + enhancer = ImageEnhance.Brightness(im) + deduct = 1 - ((pixel_sum - 0.6) * 1.5) + im = enhancer.enhance(deduct) + logging.info(deduct) + + gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 4 * 3)) + + im = im.filter(ImageFilter.GaussianBlur(blur)) + + + gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 2)) + + g = io.BytesIO() + g.seek(0) + + a_channel = Image.new("L", im.size, 255) # 'L' 8-bit pixels, black and white + im.putalpha(a_channel) + + im.save(g, "PNG") + g.seek(0) + + # source_image.close() + + return g + + def save_thumb(self, track_object: TrackClass, size: tuple[int, int], save_path: str, png=False, zoom=False): + + filepath = track_object.fullpath + sources = self.get_sources(track_object) + + if len(sources) == 0: + logging.error("Error thumbnailing; no source images found") + return False + + offset = self.get_offset(filepath, sources) + source_image = self.get_source_raw(offset, sources, track_object) + + im = Image.open(source_image) + if im.mode != "RGB": + im = im.convert("RGB") + + if not zoom: + im.thumbnail(size, Image.Resampling.LANCZOS) + else: + w, h = im.size + if w != h: + m = min(w, h) + im = im.crop(( + (w - m) / 2, + (h - m) / 2, + (w + m) / 2, + (h + m) / 2, + )) + + im = im.resize(size, Image.Resampling.LANCZOS) + + if not save_path: + g = io.BytesIO() + g.seek(0) + if png: + im.save(g, "PNG") + else: + im.save(g, "JPEG") + g.seek(0) + return g + + if png: + im.save(save_path + ".png", "PNG") + else: + im.save(save_path + ".jpg", "JPEG") + + def display(self, track: TrackClass, location, box, fast: bool = False, theme_only: bool = False) -> int | None: + index = track.index + filepath = track.fullpath + + if prefs.colour_from_image and track.album != gui.theme_temp_current and box[0] != 115: + if track.album in gui.temp_themes: + global colours + colours = gui.temp_themes[track.album] + gui.theme_temp_current = track.album + + source = self.get_sources(track) + + if len(source) == 0: + return 1 + + offset = self.get_offset(filepath, source) + + if not theme_only: + # Check if request matches previous + if self.current_wu is not None and self.current_wu.source == source[offset][1] and \ + self.current_wu.request_size == box: + self.render(self.current_wu, location) + return 0 + + if fast: + return self.fast_display(track, location, box, source, offset) + + # Check if cached + for unit in self.image_cache: + if unit.index == index and unit.request_size == box and unit.offset == offset: + self.render(unit, location) + return 0 + + close = True + # Render new + try: + # Get source IO + if source[offset][0] == 1: + # Target is a embedded image + # source_image = io.BytesIO(self.get_embed(track)) + source_image = self.get_source_raw(0, 0, track, source[offset]) + + elif source[offset][0] == 2: + idea = prefs.encoder_output / encode_folder_name(track) / "cover.jpg" + if idea.is_file(): + source_image = idea.open("rb") + else: + try: + close = False + # We want to download the image asynchronously as to not block the UI + if self.downloaded_image and self.downloaded_track == track: + source_image = self.downloaded_image + + elif self.download_in_progress: + return 0 + + else: + self.download_in_progress = True + shoot_dl = threading.Thread( + target=self.async_download_image, + args=([track, source[offset]])) + shoot_dl.daemon = True + shoot_dl.start() + + # We'll block with a small timeout to avoid unwanted flashing between frames + s = 0 + while self.download_in_progress: + s += 1 + time.sleep(0.01) + if s > 20: # 200 ms + break + + if self.downloaded_track != track: + return None + + assert self.downloaded_image + source_image = self.downloaded_image + + + except Exception: + logging.exception("IMAGE NETWORK LOAD ERROR") + raise + + else: + # source_image = open(source[offset][1], 'rb') + source_image = self.get_source_raw(0, 0, track, source[offset]) + + # Generate + g = io.BytesIO() + g.seek(0) + im = Image.open(source_image) + o_size = im.size + + format = im.format + + try: + if im.format == "JPEG": + format = "JPG" + + if im.mode != "RGB": + im = im.convert("RGB") + except Exception: + logging.exception("Failed to convert image") + if theme_only: + source_image.close() + g.close() + return None + im = Image.open(str(install_directory / "assets" / "load-error.png")) + o_size = im.size + + + if not theme_only: + + if prefs.zoom_art: + new_size = fit_box(o_size, box) + try: + im = im.resize(new_size, Image.Resampling.LANCZOS) + except Exception: + logging.exception("Failed to resize image") + im = Image.open(str(install_directory / "assets" / "load-error.png")) + o_size = im.size + new_size = fit_box(o_size, box) + im = im.resize(new_size, Image.Resampling.LANCZOS) + else: + try: + im.thumbnail((box[0], box[1]), Image.Resampling.LANCZOS) + except Exception: + logging.exception("Failed to convert image to thumbnail") + im = Image.open(str(install_directory / "assets" / "load-error.png")) + o_size = im.size + im.thumbnail((box[0], box[1]), Image.Resampling.LANCZOS) + im.save(g, "BMP") + g.seek(0) + + # Processing for "Carbon" theme + if track == pctl.playing_object() and gui.theme_name == "Carbon" and track.parent_folder_path != colours.last_album: + + # Find main image colours + try: + im.thumbnail((50, 50), Image.Resampling.LANCZOS) + except Exception: + logging.exception("theme gen error") + source_image.close() + g.close() + return None + pixels = im.getcolors(maxcolors=2500) + pixels = sorted(pixels, key=lambda x: x[0], reverse=True)[:] + colour = pixels[0][1] + + # Try and find a colour that is not grayscale + for c in pixels: + cc = c[1] + av = sum(cc) / 3 + if abs(cc[0] - av) > 10 or abs(cc[1] - av) > 10 or abs(cc[2] - av) > 10: + colour = cc + break + + h_colour = rgb_to_hls(colour[0], colour[1], colour[2]) + + l = .51 + s = .44 + + hh = h_colour[0] + if 0.14 < hh < 0.3: # Yellow and green are hard to read text on, so lower the luminance for those + l = .45 + if check_equal(colour): # Default to theme purple if source colour was grayscale + hh = 0.72 + + colours.bottom_panel_colour = hls_to_rgb(hh, l, s) + colours.last_album = track.parent_folder_path + + # Processing for "Auto-theme" setting + if prefs.colour_from_image and box[0] != 115 and track.album != gui.theme_temp_current \ + and track.album not in gui.temp_themes: # and pctl.master_library[index].parent_folder_path != colours.last_album: #mark2233 + colours.last_album = track.parent_folder_path + + colours = copy.deepcopy(colours) + + im.thumbnail((50, 50), Image.Resampling.LANCZOS) + pixels = im.getcolors(maxcolors=2500) + #logging.info(pixels) + pixels = sorted(pixels, key=lambda x: x[0], reverse=True)[:] + #logging.info(pixels) + + min_colour_varience = 75 + + x_colours = [] + for item in pixels: + colour = item[1] + for cc in x_colours: + if abs( + colour[0] - cc[0]) < min_colour_varience and abs( + colour[1] - cc[1]) < min_colour_varience and abs( + colour[2] - cc[2]) < min_colour_varience: + break + else: + x_colours.append(colour) + + #logging.info(x_colours) + colours.playlist_panel_bg = colours.side_panel_background + colours.playlist_box_background = colours.side_panel_background + + colours.playlist_panel_background = x_colours[0] + (255,) + if len(x_colours) > 1: + colours.side_panel_background = x_colours[1] + (255,) + colours.playlist_box_background = colours.side_panel_background + if len(x_colours) > 2: + colours.title_text = x_colours[2] + (255,) + colours.title_playing = x_colours[2] + (255,) + if len(x_colours) > 3: + colours.artist_text = x_colours[3] + (255,) + colours.artist_playing = x_colours[3] + (255,) + if len(x_colours) > 4: + colours.playlist_box_background = x_colours[4] + (255,) + + colours.queue_background = colours.side_panel_background + # Check artist text colour + if contrast_ratio(colours.artist_text, colours.playlist_panel_background) < 1.9: + + black = [25, 25, 25, 255] + white = [220, 220, 220, 255] + + con_b = contrast_ratio(black, colours.playlist_panel_background) + con_w = contrast_ratio(white, colours.playlist_panel_background) + + choice = black + if con_w > con_b: + choice = white + + colours.artist_text = choice + colours.artist_playing = choice + + # Check title text colour + if contrast_ratio(colours.title_text, colours.playlist_panel_background) < 1.9: + + black = [60, 60, 60, 255] + white = [180, 180, 180, 255] + + con_b = contrast_ratio(black, colours.playlist_panel_background) + con_w = contrast_ratio(white, colours.playlist_panel_background) + + choice = black + if con_w > con_b: + choice = white + + colours.title_text = choice + colours.title_playing = choice + + if test_lumi(colours.side_panel_background) < 0.50: + colours.side_bar_line1 = [25, 25, 25, 255] + colours.side_bar_line2 = [35, 35, 35, 255] + else: + colours.side_bar_line1 = [250, 250, 250, 255] + colours.side_bar_line2 = [235, 235, 235, 255] + + colours.album_text = colours.title_text + colours.album_playing = colours.title_playing + + gui.pl_update = 1 + + prcl = 100 - int(test_lumi(colours.playlist_panel_background) * 100) + + if prcl > 45: + ce = alpha_blend([0, 0, 0, 180], colours.playlist_panel_background) # [40, 40, 40, 255] + colours.index_text = ce + colours.index_playing = ce + colours.time_text = ce + colours.bar_time = ce + colours.folder_title = ce + colours.star_line = [60, 60, 60, 255] + colours.row_select_highlight = [0, 0, 0, 30] + colours.row_playing_highlight = [0, 0, 0, 20] + colours.gallery_background = rgb_add_hls(colours.playlist_panel_background, 0, -0.03, -0.03) + else: + ce = alpha_blend([255, 255, 255, 160], colours.playlist_panel_background) # [165, 165, 165, 255] + colours.index_text = ce + colours.index_playing = ce + colours.time_text = ce + colours.bar_time = ce + colours.folder_title = ce + colours.star_line = ce # [150, 150, 150, 255] + colours.row_select_highlight = [255, 255, 255, 12] + colours.row_playing_highlight = [255, 255, 255, 8] + colours.gallery_background = rgb_add_hls(colours.playlist_panel_background, 0, 0.03, 0.03) + + gui.temp_themes[track.album] = copy.deepcopy(colours) + colours = gui.temp_themes[track.album] + gui.theme_temp_current = track.album + + if theme_only: + source_image.close() + g.close() + return None + + wop = rw_from_object(g) + s_image = IMG_Load_RW(wop, 0) + #logging.error(IMG_GetError()) + + c = SDL_CreateTextureFromSurface(renderer, s_image) + + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + + SDL_QueryTexture(c, None, None, tex_w, tex_h) + + dst = SDL_Rect(round(location[0]), round(location[1])) + dst.w = int(tex_w.contents.value) + dst.h = int(tex_h.contents.value) + + # Clean uo + SDL_FreeSurface(s_image) + source_image.close() + g.close() + # if close: + # source_image.close() + + unit = ImageObject() + unit.index = index + unit.texture = c + unit.rect = dst + unit.request_size = box + unit.original_size = o_size + unit.actual_size = (dst.w, dst.h) + unit.source = source[offset][1] + unit.offset = offset + unit.format = format + + self.current_wu = unit + self.image_cache.append(unit) + + self.render(unit, location) + + if len(self.image_cache) > 5 or (prefs.colour_from_image and len(self.image_cache) > 1): + SDL_DestroyTexture(self.image_cache[0].texture) + del self.image_cache[0] + + # temp fix + global move_on_title + global playlist_hold + global quick_drag + quick_drag = False + move_on_title = False + playlist_hold = False + + except Exception: + logging.exception("Image load error") + logging.error("-- Associated track: " + track.fullpath) + + self.current_wu = None + try: + del self.source_cache[index][offset] + except Exception: + logging.exception(" -- Error, no source cache?") + + return 1 + + return 0 + + def render(self, unit, location) -> None: + + rect = unit.rect + + gui.art_aspect_ratio = unit.actual_size[0] / unit.actual_size[1] + + rect.x = round(int((unit.request_size[0] - unit.actual_size[0]) / 2) + location[0]) + rect.y = round(int((unit.request_size[1] - unit.actual_size[1]) / 2) + location[1]) + + style_overlay.hole_punches.append(rect) + + SDL_RenderCopy(renderer, unit.texture, None, rect) + + gui.art_drawn_rect = (rect.x, rect.y, rect.w, rect.h) + + def clear_cache(self) -> None: + + for unit in self.image_cache: + SDL_DestroyTexture(unit.texture) + + self.image_cache.clear() + self.source_cache.clear() + self.current_wu = None + self.downloaded_track = None + + self.base64cahce = (0, 0, "") + self.processing64on = None + self.bin_cached = (None, None, None) + self.loading_bin = (None, None) + self.embed_cached = (None, None) + + gui.temp_themes.clear() + gui.theme_temp_current = -1 + colours.last_album = "" + +class StyleOverlay: + + def __init__(self): + + self.min_on_timer = Timer() + self.fade_on_timer = Timer(0) + self.fade_off_timer = Timer() + + self.stage = 0 + + self.im = None + + self.a_texture = None + self.a_rect = None + + self.b_texture = None + self.b_rect = None + + self.a_type = 0 + self.b_type = 0 + + self.window_size = None + self.parent_path = None + + self.hole_punches = [] + self.hole_refills = [] + + self.go_to_sleep = False + + self.current_track_album = "none" + self.current_track_id = -1 + + def worker(self) -> None: + + if self.stage == 0: + + if (gui.mode == 3 and prefs.mini_mode_mode == 5): + pass + elif prefs.bg_showcase_only and not gui.combo_mode: + return + + if pctl.playing_ready() and self.min_on_timer.get() > 0: + + track = pctl.playing_object() + + self.window_size = copy.copy(window_size) + self.parent_path = track.parent_folder_path + self.current_track_id = track.index + self.current_track_album = track.album + + try: + self.im = album_art_gen.get_blur_im(track) + except Exception: + logging.exception("Blur blackground error") + raise + #logging.debug(track.fullpath) + + if self.im is None or self.im is False: + if self.a_texture: + self.stage = 2 + self.fade_off_timer.set() + self.go_to_sleep = True + return + self.flush() + self.min_on_timer.force_set(-4) + return + + self.stage = 1 + gui.update += 1 + return + + def flush(self): + + if self.a_texture is not None: + SDL_DestroyTexture(self.a_texture) + self.a_texture = None + if self.b_texture is not None: + SDL_DestroyTexture(self.b_texture) + self.b_texture = None + self.min_on_timer.force_set(-0.2) + self.parent_path = "None" + self.stage = 0 + tauon.thread_manager.ready("worker") + gui.style_worker_timer.set() + gui.delay_frame(0.25) + gui.update += 1 + + def display(self) -> None: + + if self.min_on_timer.get() < 0: + return + + if self.stage == 1: + + wop = rw_from_object(self.im) + s_image = IMG_Load_RW(wop, 0) + + c = SDL_CreateTextureFromSurface(renderer, s_image) + + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + + SDL_QueryTexture(c, None, None, tex_w, tex_h) + + dst = SDL_Rect(round(-40, 0)) + dst.w = int(tex_w.contents.value) + dst.h = int(tex_h.contents.value) + + # Clean uo + SDL_FreeSurface(s_image) + self.im.close() + + # SDL_SetTextureAlphaMod(c, 10) + self.fade_on_timer.set() + + if self.a_texture is not None: + self.b_texture = self.a_texture + self.b_rect = self.a_rect + self.b_type = self.a_type + + self.a_texture = c + self.a_rect = dst + self.a_type = album_art_gen.loaded_bg_type + + self.stage = 2 + self.radio_meta = None + + gui.update += 1 + + if self.stage == 2: + track = pctl.playing_object() + + if pctl.playing_state == 3 and not tauon.spot_ctl.coasting: + if self.radio_meta != pctl.tag_meta: + self.radio_meta = pctl.tag_meta + self.current_track_id = -1 + self.stage = 0 + + elif not self.go_to_sleep and self.b_texture is None and self.current_track_id != track.index: + self.radio_meta = None + if not track.album: + self.stage = 0 + else: + self.current_track_id = track.index + if ( + self.parent_path != pctl.playing_object().parent_folder_path or self.current_track_album != pctl.playing_object().album): + self.stage = 0 + + if gui.mode == 3 and prefs.mini_mode_mode == 5: + pass + elif prefs.bg_showcase_only: + if not gui.combo_mode: + return + + t = self.fade_on_timer.get() + SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) + SDL_RenderClear(renderer) + + if self.a_texture is not None: + if self.window_size != window_size: + self.flush() + + if self.b_texture is not None: + + self.b_rect.y = 0 - self.b_rect.h // 4 + if self.b_type == 1: + self.b_rect.y = 0 + + if t < 0.4: + + SDL_RenderCopy(renderer, self.b_texture, None, self.b_rect) + + else: + SDL_DestroyTexture(self.b_texture) + self.b_texture = None + self.b_rect = None + + if self.a_texture is not None: + + self.a_rect.y = 0 - self.a_rect.h // 4 + if self.a_type == 1: + self.a_rect.y = 0 + + if t < 0.4: + fade = round(t / 0.4 * 255) + gui.update += 1 + + else: + fade = 255 + + if self.go_to_sleep: + t = self.fade_off_timer.get() + gui.update += 1 + + if t < 1: + fade = 255 + elif t < 1.4: + fade = 255 - round((t - 1) / 0.4 * 255) + else: + self.go_to_sleep = False + self.flush() + return + + if prefs.bg_showcase_only and not (prefs.mini_mode_mode == 5 and gui.mode == 3): + tb = SDL_Rect(0, 0, window_size[0], gui.panelY) + bb = SDL_Rect(0, window_size[1] - gui.panelBY, window_size[0], gui.panelBY) + self.hole_punches.append(tb) + self.hole_punches.append(bb) + + # Center image + if window_size[0] < 900 * gui.scale: + self.a_rect.x = (window_size[0] // 2) - self.a_rect.w // 2 + else: + self.a_rect.x = -40 + + SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) + + SDL_SetTextureAlphaMod(self.a_texture, fade) + SDL_RenderCopy(renderer, self.a_texture, None, self.a_rect) + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) + + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + for rect in self.hole_punches: + SDL_RenderFillRect(renderer, rect) + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + + SDL_SetRenderTarget(renderer, gui.main_texture) + opacity = prefs.art_bg_opacity + if prefs.mini_mode_mode == 5 and gui.mode == 3: + opacity = 255 + + SDL_SetTextureAlphaMod(gui.main_texture_overlay_temp, opacity) + SDL_RenderCopy(renderer, gui.main_texture_overlay_temp, None, None) + + SDL_SetRenderTarget(renderer, gui.main_texture) + + else: + SDL_SetRenderTarget(renderer, gui.main_texture) + +class ToolTip: + + def __init__(self) -> None: + self.text = "" + self.h = 24 * gui.scale + self.w = 62 * gui.scale + self.x = 0 + self.y = 0 + self.timer = Timer() + self.trigger = 1.1 + self.font = 13 + self.called = False + self.a = False + + def test(self, x, y, text): + + if self.text != text or x != self.x or y != self.y: + self.text = text + # self.timer.set() + self.a = False + + self.x = x + self.y = y + self.w = ddt.get_text_w(text, self.font) + 20 * gui.scale + + self.called = True + + if self.a is False: + self.timer.set() + gui.frame_callback_list.append(TestTimer(self.trigger)) + self.a = True + + def render(self) -> None: + + if self.called is True: + + if self.timer.get() > self.trigger: + + ddt.rect((self.x, self.y, self.w, self.h), colours.box_button_background) + # ddt.rect((self.x, self.y, self.w, self.h), colours.grey(45)) + ddt.text( + (self.x + int(self.w / 2), self.y + 4 * gui.scale, 2), self.text, + colours.menu_text, self.font, bg=colours.box_button_background) + else: + # gui.update += 1 + pass + else: + self.timer.set() + self.a = False + + self.called = False + +class ToolTip3: + + def __init__(self) -> None: + self.x = 0 + self.y = 0 + self.text = "" + self.font = None + self.show = False + self.width = 0 + self.height = 24 * gui.scale + self.timer = Timer() + self.pl_position = 0 + self.click_exclude_point = (0, 0) + + def set(self, x, y, text, font, rect): + + y -= round(11 * gui.scale) + if self.show == False or self.y != y or x != self.x or self.pl_position != pctl.playlist_view_position: + self.timer.set() + + if point_proximity_test(self.click_exclude_point, mouse_position, 20 * gui.scale): + self.timer.set() + return + + if inp.mouse_click: + self.click_exclude_point = copy.copy(mouse_position) + self.timer.set() + return + + self.x = x + self.y = y + self.text = text + self.font = font + self.show = True + self.rect = rect + self.pl_position = pctl.playlist_view_position + + def render(self): + + if not self.show: + return + + if not point_proximity_test(self.click_exclude_point, mouse_position, 20 * gui.scale): + self.click_exclude_point = (0, 0) + + if not coll( + self.rect) or inp.mouse_click or gui.level_2_click or self.pl_position != pctl.playlist_view_position: + self.show = False + + gui.frame_callback_list.append(TestTimer(0.02)) + + if self.timer.get() < 0.6: + return + + w = ddt.get_text_w(self.text, 312) + self.height + x = self.x # - int(self.width / 2) + y = self.y + h = self.height + + border = 1 * gui.scale + + ddt.rect((x - border, y - border, w + border * 2, h + border * 2), colours.grey(60)) + ddt.rect((x, y, w, h), colours.menu_background) + p = ddt.text( + (x + int(w / 2), y + 3 * gui.scale, 2), self.text, colours.menu_text, 312, bg=colours.menu_background) + + if not coll(self.rect): + self.show = False + +class RenameTrackBox: + + def __init__(self): + + self.active = False + self.target_track_id = None + self.single_only = False + + def activate(self, track_id): + + self.active = True + self.target_track_id = track_id + if key_shift_down or key_shiftr_down: + self.single_only = True + else: + self.single_only = False + + def disable_test(self, track_id): + if key_shift_down or key_shiftr_down: + single_only = True + else: + single_only = False + + if not single_only: + for item in default_playlist: + if pctl.master_library[item].parent_folder_path == pctl.master_library[track_id].parent_folder_path: + + if pctl.master_library[item].is_network is True: + return True + return False + + def render(self): + + if not self.active: + return + + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False + + w = 420 * gui.scale + h = 155 * gui.scale + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) + + ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) + ddt.rect_a((x, y), (w, h), colours.box_background) + ddt.text_background_colour = colours.box_background + + if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not coll((x, y, w, h))): + rename_track_box.active = False + + r_todo = [] + + # Find matching folder tracks in playlist + if not self.single_only: + for item in default_playlist: + if pctl.master_library[item].parent_folder_path == pctl.master_library[ + self.target_track_id].parent_folder_path: + + # Close and display error if any tracks are not single local files + if pctl.master_library[item].is_network is True: + rename_track_box.active = False + show_message(_("Cannot rename"), _("One or more tracks is from a network location!"), mode="info") + if pctl.master_library[item].is_cue is True: + rename_track_box.active = False + show_message(_("This function does not support renaming CUE Sheet tracks.")) + else: + r_todo.append(item) + else: + r_todo = [self.target_track_id] + + ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Track Renaming"), colours.grey(230), 213) + + # if draw.button("Default", x + 230 * gui.scale, y + 8 * gui.scale, + if rename_files.text != prefs.rename_tracks_template and draw.button( + _("Default"), x + w - 85 * gui.scale, y + h - 35 * gui.scale, 70 * gui.scale): + rename_files.text = prefs.rename_tracks_template + + # ddt.draw_text((x + 14, y + 40,), NRN + cursor, colours.grey(150), 12) + rename_files.draw(x + 14 * gui.scale, y + 39 * gui.scale, colours.box_input_text, width=300) + NRN = rename_files.text + + ddt.rect_s( + (x + 8 * gui.scale, y + 36 * gui.scale, 300 * gui.scale, 22 * gui.scale), colours.box_text_border, 1 * gui.scale) + + afterline = "" + warn = False + underscore = False + + for item in r_todo: + + if pctl.master_library[item].track_number == "" or pctl.master_library[item].artist == "" or \ + pctl.master_library[item].title == "" or pctl.master_library[item].album == "": + warn = True + + if item == self.target_track_id: + afterline = parse_template2(NRN, pctl.master_library[item]) + + ddt.text((x + 10 * gui.scale, y + 68 * gui.scale), _("BEFORE"), colours.box_text_label, 212) + line = trunc_line(pctl.master_library[self.target_track_id].filename, 12, 335) + ddt.text((x + 70 * gui.scale, y + 68 * gui.scale), line, colours.grey(210), 211, max_w=340) + + ddt.text((x + 10 * gui.scale, y + 83 * gui.scale), _("AFTER"), colours.box_text_label, 212) + ddt.text((x + 70 * gui.scale, y + 83 * gui.scale), afterline, colours.grey(210), 211, max_w=340) + + if (len(NRN) > 3 and len(pctl.master_library[self.target_track_id].filename) > 3 and afterline[-3:].lower() != + pctl.master_library[self.target_track_id].filename[-3:].lower()) or len(NRN) < 4 or "." not in afterline[-5:]: + ddt.text( + (x + 10 * gui.scale, y + 108 * gui.scale), _("Warning: This may change the file extension"), + [245, 90, 90, 255], + 13) + + colour_warn = [143, 186, 65, 255] + if not unique_template(NRN): + ddt.text( + (x + 10 * gui.scale, y + 123 * gui.scale), _("Warning: The filename might not be unique"), + [245, 90, 90, 255], + 13) + if warn: + ddt.text( + (x + 10 * gui.scale, y + 135 * gui.scale), _("Warning: A track has incomplete metadata"), + [245, 90, 90, 255], + 13) + colour_warn = [180, 60, 60, 255] + + label = _("Write") + " (" + str(len(r_todo)) + ")" + + if draw.button( + label, x + (8 + 300 + 10) * gui.scale, y + 36 * gui.scale, 80 * gui.scale, + text_highlight_colour=colours.grey(255), background_highlight_colour=colour_warn, + tooltip=_("Physically renames all the tracks in the folder")) or inp.level_2_enter: + + inp.mouse_click = False + total_todo = len(r_todo) + pre_state = 0 + + for item in r_todo: + + if pctl.playing_state > 0 and item == pctl.track_queue[pctl.queue_step]: + pre_state = pctl.stop(True) + + try: + + afterline = parse_template2(NRN, pctl.master_library[item], strict=True) + + oldname = pctl.master_library[item].filename + oldpath = pctl.master_library[item].fullpath + + logging.info("Renaming...") + + star = star_store.full_get(item) + star_store.remove(item) + + oldpath = pctl.master_library[item].fullpath + + oldsplit = os.path.split(oldpath) + + if os.path.exists(os.path.join(oldsplit[0], afterline)): + logging.error("A file with that name already exists") + total_todo -= 1 + continue + + if not afterline: + logging.error("Rename Error") + total_todo -= 1 + continue + + if "." in afterline and not afterline.split(".")[0]: + logging.error("A file does not have a target filename") + total_todo -= 1 + continue + + os.rename(pctl.master_library[item].fullpath, os.path.join(oldsplit[0], afterline)) + + pctl.master_library[item].fullpath = os.path.join(oldsplit[0], afterline) + pctl.master_library[item].filename = afterline + + search_string_cache.pop(item, None) + search_dia_string_cache.pop(item, None) + + if star is not None: + star_store.insert(item, star) + + except Exception: + logging.exception("Rendering error") + total_todo -= 1 + + rename_track_box.active = False + logging.info("Done") + if pre_state == 1: + pctl.revert() + + if total_todo != len(r_todo): + show_message( + _("Rename complete."), + _("{N} / {T} filenames were written.") + .format(N=str(total_todo), T=str(len(r_todo))), mode="warning") + else: + show_message( + _("Rename complete."), + _("{N} / {T} filenames were written.") + .format(N=str(total_todo), T=str(len(r_todo))), mode="done") + pctl.notify_change() + +class TransEditBox: + + def __init__(self): + self.active = False + self.active_field = 1 + self.selected = [] + self.playlist = -1 + + def render(self): + + if not self.active: + return + + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False + + w = 500 * gui.scale + h = 255 * gui.scale + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) + + ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) + ddt.rect_a((x, y), (w, h), colours.box_background) + ddt.text_background_colour = colours.box_background + + if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not coll((x, y, w, h))): + self.active = False + + select = list(set(shift_selection)) + if not select and pctl.selected_ready(): + select = [pctl.selected_in_playlist] + + titles = [pctl.get_track(default_playlist[s]).title for s in select] + artists = [pctl.get_track(default_playlist[s]).artist for s in select] + albums = [pctl.get_track(default_playlist[s]).album for s in select] + album_artists = [pctl.get_track(default_playlist[s]).album_artist for s in select] + + #logging.info(select) + if select != self.selected or pctl.active_playlist_viewing != self.playlist: + #logging.info("reset") + self.selected = select + self.playlist = pctl.active_playlist_viewing + edit_album.clear() + edit_artist.clear() + edit_title.clear() + edit_album_artist.clear() + + if len(select) == 0: + return + + tr = pctl.get_track(default_playlist[select[0]]) + edit_title.set_text(tr.title) + + if check_equal(artists): + edit_artist.set_text(artists[0]) + + if check_equal(albums): + edit_album.set_text(albums[0]) + + if check_equal(album_artists): + edit_album_artist.set_text(album_artists[0]) + + x += round(20 * gui.scale) + y += round(18 * gui.scale) + + ddt.text((x, y), _("Simple tag editor"), colours.box_title_text, 215) + + if draw.button(_("?"), x + 440 * gui.scale, y): + show_message( + _("Press Enter in each field to apply its changes to local database."), + _("When done, press WRITE TAGS to save to tags in actual files. (Optional but recommended)"), + mode="info") + + y += round(24 * gui.scale) + ddt.text((x, y), _("Number of tracks selected: {N}").format(N=len(select)), colours.box_title_text, 313) + + y += round(24 * gui.scale) + + if inp.key_tab_press: + if key_shift_down or key_shiftr_down: + self.active_field -= 1 + else: + self.active_field += 1 + + if self.active_field < 0: + self.active_field = 3 + if self.active_field == 4: + self.active_field = 0 + if len(select) > 1: + self.active_field = 1 + + def field_edit(x, y, label, field_number, names, text_box): + changed = 0 + ddt.text((x, y), label, colours.box_text_label, 11) + y += round(16 * gui.scale) + rect1 = (x, y, round(370 * gui.scale), round(17 * gui.scale)) + fields.add(rect1) + if (coll(rect1) and inp.mouse_click) or (inp.key_tab_press and self.active_field == field_number): + self.active_field = field_number + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + tc = colours.box_input_text + if names and check_equal(names) and text_box.text == names[0]: + h, l, s = rgb_to_hls(tc[0], tc[1], tc[2]) + l *= 0.7 + tc = hls_to_rgb(h, l, s) + else: + changed = 1 + if not (names and check_equal(names)) and not text_box.text: + changed = 0 + ddt.text((x + round(2 * gui.scale), y), _("<Multiple selected>"), colours.box_text_label, 12) + text_box.draw(x + round(3 * gui.scale), y, tc, self.active_field == field_number, width=370 * gui.scale) + if changed: + ddt.text((x + 377 * gui.scale, y - 1 * gui.scale), "⮨", colours.box_title_text, 214) + return changed + + changed = 0 + if len(select) == 1: + changed = field_edit(x, y, _("Track title"), 0, titles, edit_title) + y += round(40 * gui.scale) + changed += field_edit(x, y, _("Album name"), 1, albums, edit_album) + y += round(40 * gui.scale) + changed += field_edit(x, y, _("Artist name"), 2, artists, edit_artist) + y += round(40 * gui.scale) + changed += field_edit(x, y, _("Album-artist name"), 3, album_artists, edit_album_artist) + + y += round(40 * gui.scale) + for s in select: + tr = pctl.get_track(default_playlist[s]) + if tr.is_network: + ddt.text((x, y), _("Editing network tracks is not recommended!"), [245, 90, 90, 255], 312) + + if inp.key_return_press: + + gui.pl_update += 1 + if self.active_field == 0 and len(select) == 1: + for s in select: + tr = pctl.get_track(default_playlist[s]) + star = star_store.full_get(tr.index) + star_store.remove(tr.index) + tr.title = edit_title.text + star_store.merge(tr.index, star) + + if self.active_field == 1: + for s in select: + tr = pctl.get_track(default_playlist[s]) + tr.album = edit_album.text + if self.active_field == 2: + for s in select: + tr = pctl.get_track(default_playlist[s]) + star = star_store.full_get(tr.index) + star_store.remove(tr.index) + tr.artist = edit_artist.text + star_store.merge(tr.index, star) + if self.active_field == 3: + for s in select: + tr = pctl.get_track(default_playlist[s]) + tr.album_artist = edit_album_artist.text + tauon.bg_save() + + + ww = ddt.get_text_w(_("WRITE TAGS"), 212) + round(48 * gui.scale) + if gui.write_tag_in_progress: + text = f"{gui.tag_write_count}/{len(select)}" + text = _("WRITE TAGS") + if draw.button(text, (x + w) - ww, y - round(0) * gui.scale): + if changed: + show_message(_("Press enter on fields to apply your changes first!")) + return + + if gui.write_tag_in_progress: + return + + def write_tag_go(): + + + for s in select: + tr = pctl.get_track(default_playlist[s]) + + if tr.is_network: + show_message(_("Writing to a network track is not applicable!"), mode="error") + gui.write_tag_in_progress = True + return + if tr.is_cue: + show_message(_("Cannot write CUE sheet types!"), mode="error") + gui.write_tag_in_progress = True + return + + muta = mutagen.File(tr.fullpath, easy=True) + + def write_tag(track: TrackClass, muta, field_name_tauon, field_name_muta): + item = muta.get(field_name_muta) + if item and len(item) > 1: + show_message(_("Cannot handle multi-field! Please use external tag editor"), mode="error") + return 0 + if not getattr(tr, field_name_tauon): # Want delete tag field + if item: + del muta[field_name_muta] + else: + muta[field_name_muta] = getattr(tr, field_name_tauon) + return 1 + + write_tag(tr, muta, "artist", "artist") + write_tag(tr, muta, "album", "album") + write_tag(tr, muta, "title", "title") + write_tag(tr, muta, "album_artist", "albumartist") + + muta.save() + gui.tag_write_count += 1 + gui.update += 1 + tauon.bg_save() + if not gui.message_box: + show_message(_("{N} files rewritten").format(N=gui.tag_write_count), mode="done") + gui.write_tag_in_progress = False + if not gui.write_tag_in_progress: + gui.tag_write_count = 0 + gui.write_tag_in_progress = True + shooter(write_tag_go) + +class TransEditBox: + + def __init__(self): + self.active = False + self.active_field = 1 + self.selected = [] + self.playlist = -1 + + def render(self): + + if not self.active: + return + + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False + + w = 500 * gui.scale + h = 255 * gui.scale + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) + + ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) + ddt.rect_a((x, y), (w, h), colours.box_background) + ddt.text_background_colour = colours.box_background + + if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not coll((x, y, w, h))): + self.active = False + + select = list(set(shift_selection)) + if not select and pctl.selected_ready(): + select = [pctl.selected_in_playlist] + + titles = [pctl.get_track(default_playlist[s]).title for s in select] + artists = [pctl.get_track(default_playlist[s]).artist for s in select] + albums = [pctl.get_track(default_playlist[s]).album for s in select] + album_artists = [pctl.get_track(default_playlist[s]).album_artist for s in select] + + #logging.info(select) + if select != self.selected or pctl.active_playlist_viewing != self.playlist: + #logging.info("reset") + self.selected = select + self.playlist = pctl.active_playlist_viewing + edit_album.clear() + edit_artist.clear() + edit_title.clear() + edit_album_artist.clear() + + if len(select) == 0: + return + + tr = pctl.get_track(default_playlist[select[0]]) + edit_title.set_text(tr.title) + + if check_equal(artists): + edit_artist.set_text(artists[0]) + + if check_equal(albums): + edit_album.set_text(albums[0]) + + if check_equal(album_artists): + edit_album_artist.set_text(album_artists[0]) + + x += round(20 * gui.scale) + y += round(18 * gui.scale) + + ddt.text((x, y), _("Simple tag editor"), colours.box_title_text, 215) + + if draw.button(_("?"), x + 440 * gui.scale, y): + show_message( + _("Press Enter in each field to apply its changes to local database."), + _("When done, press WRITE TAGS to save to tags in actual files. (Optional but recommended)"), + mode="info") + + y += round(24 * gui.scale) + ddt.text((x, y), _("Number of tracks selected: {N}").format(N=len(select)), colours.box_title_text, 313) + + y += round(24 * gui.scale) + + if inp.key_tab_press: + if key_shift_down or key_shiftr_down: + self.active_field -= 1 + else: + self.active_field += 1 + + if self.active_field < 0: + self.active_field = 3 + if self.active_field == 4: + self.active_field = 0 + if len(select) > 1: + self.active_field = 1 + + def field_edit(x, y, label, field_number, names, text_box): + changed = 0 + ddt.text((x, y), label, colours.box_text_label, 11) + y += round(16 * gui.scale) + rect1 = (x, y, round(370 * gui.scale), round(17 * gui.scale)) + fields.add(rect1) + if (coll(rect1) and inp.mouse_click) or (inp.key_tab_press and self.active_field == field_number): + self.active_field = field_number + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + tc = colours.box_input_text + if names and check_equal(names) and text_box.text == names[0]: + h, l, s = rgb_to_hls(tc[0], tc[1], tc[2]) + l *= 0.7 + tc = hls_to_rgb(h, l, s) + else: + changed = 1 + if not (names and check_equal(names)) and not text_box.text: + changed = 0 + ddt.text((x + round(2 * gui.scale), y), _("<Multiple selected>"), colours.box_text_label, 12) + text_box.draw(x + round(3 * gui.scale), y, tc, self.active_field == field_number, width=370 * gui.scale) + if changed: + ddt.text((x + 377 * gui.scale, y - 1 * gui.scale), "⮨", colours.box_title_text, 214) + return changed + + changed = 0 + if len(select) == 1: + changed = field_edit(x, y, _("Track title"), 0, titles, edit_title) + y += round(40 * gui.scale) + changed += field_edit(x, y, _("Album name"), 1, albums, edit_album) + y += round(40 * gui.scale) + changed += field_edit(x, y, _("Artist name"), 2, artists, edit_artist) + y += round(40 * gui.scale) + changed += field_edit(x, y, _("Album-artist name"), 3, album_artists, edit_album_artist) + + y += round(40 * gui.scale) + for s in select: + tr = pctl.get_track(default_playlist[s]) + if tr.is_network: + ddt.text((x, y), _("Editing network tracks is not recommended!"), [245, 90, 90, 255], 312) + + if inp.key_return_press: + + gui.pl_update += 1 + if self.active_field == 0 and len(select) == 1: + for s in select: + tr = pctl.get_track(default_playlist[s]) + star = star_store.full_get(tr.index) + star_store.remove(tr.index) + tr.title = edit_title.text + star_store.merge(tr.index, star) + + if self.active_field == 1: + for s in select: + tr = pctl.get_track(default_playlist[s]) + tr.album = edit_album.text + if self.active_field == 2: + for s in select: + tr = pctl.get_track(default_playlist[s]) + star = star_store.full_get(tr.index) + star_store.remove(tr.index) + tr.artist = edit_artist.text + star_store.merge(tr.index, star) + if self.active_field == 3: + for s in select: + tr = pctl.get_track(default_playlist[s]) + tr.album_artist = edit_album_artist.text + tauon.bg_save() + + + ww = ddt.get_text_w(_("WRITE TAGS"), 212) + round(48 * gui.scale) + if gui.write_tag_in_progress: + text = f"{gui.tag_write_count}/{len(select)}" + text = _("WRITE TAGS") + if draw.button(text, (x + w) - ww, y - round(0) * gui.scale): + if changed: + show_message(_("Press enter on fields to apply your changes first!")) + return + + if gui.write_tag_in_progress: + return + + def write_tag_go(): + + + for s in select: + tr = pctl.get_track(default_playlist[s]) + + if tr.is_network: + show_message(_("Writing to a network track is not applicable!"), mode="error") + gui.write_tag_in_progress = True + return + if tr.is_cue: + show_message(_("Cannot write CUE sheet types!"), mode="error") + gui.write_tag_in_progress = True + return + + muta = mutagen.File(tr.fullpath, easy=True) + + def write_tag(track: TrackClass, muta, field_name_tauon, field_name_muta): + item = muta.get(field_name_muta) + if item and len(item) > 1: + show_message(_("Cannot handle multi-field! Please use external tag editor"), mode="error") + return 0 + if not getattr(tr, field_name_tauon): # Want delete tag field + if item: + del muta[field_name_muta] + else: + muta[field_name_muta] = getattr(tr, field_name_tauon) + return 1 + + write_tag(tr, muta, "artist", "artist") + write_tag(tr, muta, "album", "album") + write_tag(tr, muta, "title", "title") + write_tag(tr, muta, "album_artist", "albumartist") + + muta.save() + gui.tag_write_count += 1 + gui.update += 1 + tauon.bg_save() + if not gui.message_box: + show_message(_("{N} files rewritten").format(N=gui.tag_write_count), mode="done") + gui.write_tag_in_progress = False + if not gui.write_tag_in_progress: + gui.tag_write_count = 0 + gui.write_tag_in_progress = True + shooter(write_tag_go) + +class SubLyricsBox: + + def __init__(self): + + self.active = False + self.target_track = None + self.active_field = 1 + + def activate(self, track: TrackClass): + + self.active = True + gui.box_over = True + self.target_track = track + + sub_lyrics_a.text = prefs.lyrics_subs.get(self.target_track.artist, "") + sub_lyrics_b.text = prefs.lyrics_subs.get(self.target_track.title, "") + + if not sub_lyrics_a.text: + sub_lyrics_a.text = self.target_track.artist + if not sub_lyrics_b.text: + sub_lyrics_b.text = self.target_track.title + + def render(self): + + if not self.active: + return + + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False + + w = 400 * gui.scale + h = 155 * gui.scale + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) + + ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) + ddt.rect_a((x, y), (w, h), colours.box_background) + ddt.text_background_colour = colours.box_background + + if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not coll((x, y, w, h))): + self.active = False + gui.box_over = False + + if sub_lyrics_a.text and sub_lyrics_a.text != self.target_track.artist: + prefs.lyrics_subs[self.target_track.artist] = sub_lyrics_a.text + elif self.target_track.artist in prefs.lyrics_subs: + del prefs.lyrics_subs[self.target_track.artist] + + if sub_lyrics_b.text and sub_lyrics_b.text != self.target_track.title: + prefs.lyrics_subs[self.target_track.title] = sub_lyrics_b.text + elif self.target_track.title in prefs.lyrics_subs: + del prefs.lyrics_subs[self.target_track.title] + + ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Substitute Lyric Search"), colours.grey(230), 213) + + y += round(35 * gui.scale) + x += round(23 * gui.scale) + + xx = x + xx += ddt.text( + (x + round(0 * gui.scale), y + round(0 * gui.scale)), _("Substitute"), colours.box_text_label, 212) + xx += round(6 * gui.scale) + ddt.text((xx, y + round(0 * gui.scale)), self.target_track.artist, colours.box_sub_text, 312) + + y += round(19 * gui.scale) + xx = x + xx += ddt.text((xx + round(0 * gui.scale), y + round(0 * gui.scale)), _("with"), colours.box_text_label, 212) + xx += round(6 * gui.scale) + rect1 = (xx, y, round(250 * gui.scale), round(17 * gui.scale)) + fields.add(rect1) + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + if (coll(rect1) and inp.mouse_click) or (inp.key_tab_press and self.active_field == 2): + self.active_field = 1 + inp.key_tab_press = False + + sub_lyrics_a.draw( + xx + round(4 * gui.scale), y, colours.box_input_text, self.active_field == 1, + width=rect1[2] - 8 * gui.scale) + + y += round(28 * gui.scale) + + xx = x + xx += ddt.text( + (x + round(0 * gui.scale), y + round(0 * gui.scale)), _("Substitute"), colours.box_text_label, 212) + xx += round(6 * gui.scale) + ddt.text((xx, y + round(0 * gui.scale)), self.target_track.title, colours.box_sub_text, 312) + + y += round(19 * gui.scale) + xx = x + xx += ddt.text((xx + round(0 * gui.scale), y + round(0 * gui.scale)), _("with"), colours.box_text_label, 212) + xx += round(6 * gui.scale) + rect1 = (xx, y, round(250 * gui.scale), round(16 * gui.scale)) + fields.add(rect1) + if (coll(rect1) and inp.mouse_click) or (inp.key_tab_press and self.active_field == 1): + self.active_field = 2 + # ddt.rect(rect1, [40, 40, 40, 255], True) + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + sub_lyrics_b.draw( + xx + round(4 * gui.scale), y, colours.box_input_text, self.active_field == 2, width=rect1[2] - 8 * gui.scale) + +class ExportPlaylistBox: + + def __init__(self): + + self.active = False + self.id = None + self.directory_text_box = TextBox2() + self.default = { + "path": str(music_directory) if music_directory else str(user_directory / "playlists"), + "type": "xspf", + "relative": False, + "auto": False, + } + + def activate(self, playlist): + + self.active = True + gui.box_over = True + self.id = pl_to_id(playlist) + + # Prune old enteries + ids = [] + for playlist in pctl.multi_playlist: + ids.append(playlist.uuid_int) + for key in list(prefs.playlist_exports.keys()): + if key not in ids: + del prefs.playlist_exports[key] + + def render(self) -> None: + if not self.active: + return + + w = 500 * gui.scale + h = 220 * gui.scale + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) + + ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) + ddt.rect_a((x, y), (w, h), colours.box_background) + ddt.text_background_colour = colours.box_background + + if key_esc_press or ((inp.mouse_click or gui.level_2_click or right_click or level_2_right_click) and not coll( + (x, y, w, h))): + self.active = False + gui.box_over = False + + current = prefs.playlist_exports.get(self.id) + if not current: + current = copy.copy(self.default) + + ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Export Playlist"), colours.grey(230), 213) + + x += round(15 * gui.scale) + y += round(25 * gui.scale) + + ddt.text((x, y + 8 * gui.scale), _("Save directory"), colours.grey(230), 11) + y += round(30 * gui.scale) + + rect1 = (x, y, round(450 * gui.scale), round(16 * gui.scale)) + fields.add(rect1) + # ddt.rect(rect1, [40, 40, 40, 255], True) + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + self.directory_text_box.text = current["path"] + self.directory_text_box.draw( + x + round(4 * gui.scale), y, colours.box_input_text, True, + width=rect1[2] - 8 * gui.scale, click=gui.level_2_click) + current["path"] = self.directory_text_box.text + + y += round(30 * gui.scale) + if pref_box.toggle_square(x, y, current["type"] == "xspf", "XSPF", gui.level_2_click): + current["type"] = "xspf" + if pref_box.toggle_square(x + round(80 * gui.scale), y, current["type"] == "m3u", "M3U", gui.level_2_click): + current["type"] = "m3u" + # pref_box.toggle_square(x + round(160 * gui.scale), y, False, "PLS", gui.level_2_click) + y += round(35 * gui.scale) + current["relative"] = pref_box.toggle_square( + x, y, current["relative"], _("Use relative paths"), + gui.level_2_click) + y += round(60 * gui.scale) + current["auto"] = pref_box.toggle_square(x, y, current["auto"], _("Auto-export"), gui.level_2_click) + + y += round(0 * gui.scale) + ww = ddt.get_text_w(_("Export"), 211) + x = ((int(window_size[0] / 2) - int(w / 2)) + w) - (ww + round(40 * gui.scale)) + + prefs.playlist_exports[self.id] = current + + if draw.button(_("Export"), x, y, press=gui.level_2_click): + self.run_export(current, self.id, warnings=True) + + def run_export(self, current, id, warnings=True) -> None: + logging.info("Export playlist") + path = current["path"] + if not os.path.isdir(path): + if warnings: + show_message(_("Directory does not exist"), mode="warning") + return + target = "" + if current["type"] == "xspf": + target = export_xspf(id_to_pl(id), direc=path, relative=current["relative"], show=False) + if current["type"] == "m3u": + target = export_m3u(id_to_pl(id), direc=path, relative=current["relative"], show=False) + + if warnings and target != 1: + show_message(_("Playlist exported"), target, mode="done") + +class KoelService: + + def __init__(self) -> None: + self.connected: bool = False + self.resource = None + self.scanning: bool = False + self.server: str = "" + + self.token: str = "" + + def connect(self) -> None: + + logging.info("Connect to koel...") + if not prefs.koel_username or not prefs.koel_password or not prefs.koel_server_url: + show_message(_("Missing username, password and/or server URL"), mode="warning") + self.scanning = False + return + + if self.token: + self.connected = True + logging.info("Already authorised") + return + + password = prefs.koel_password + username = prefs.koel_username + server = prefs.koel_server_url + self.server = server + + target = server + "/api/me" + + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + body = { + "email": username, + "password": password, + } + + try: + r = requests.post(target, json=body, headers=headers, timeout=10) + except Exception: + logging.exception("Could not establish connection") + gui.show_message(_("Could not establish connection"), mode="error") + return + + if r.status_code == 200: + # logging.info(r.json()) + self.token = r.json()["token"] + if self.token: + logging.info("GOT KOEL TOKEN") + self.connected = True + + else: + logging.info("AUTH ERROR") + + else: + error = "" + j = r.json() + if "message" in j: + error = j["message"] + + gui.show_message(_("Could not establish connection/authorisation"), error, mode="error") + + + def resolve_stream(self, id: str) -> tuple[str, dict[str, str]]: + + if not self.connected: + self.connect() + + if prefs.network_stream_bitrate > 0: + target = f"{self.server}/api/{id}/play/1/{prefs.network_stream_bitrate}" + else: + target = f"{self.server}/api/{id}/play/0/0" + params = {"jwt-token": self.token } + + # if prefs.network_stream_bitrate > 0: + # target = f"{self.server}/api/play/{id}/1/{prefs.network_stream_bitrate}" + # else: + #target = f"{self.server}/api/play/{id}/0/0" + #target = f"{self.server}/api/{id}/play" + + #params = {"token": self.token, } + + #target = f"{self.server}/api/download/songs" + #params["songs"] = [id,] + logging.info(target) + logging.info(urllib.parse.urlencode(params)) + + return target, params + + def listen(self, track_object: TrackClass, submit: bool = False) -> None: + if submit: + try: + target = self.server + "/api/interaction/play" + headers = { + "Authorization": "Bearer " + self.token, + "Accept": "application/json", + "Content-Type": "application/json", + } + + r = requests.post(target, headers=headers, json={"song": track_object.url_key}, timeout=10) + # logging.info(r.status_code) + # logging.info(r.text) + except Exception: + logging.exception("error submitting listen to koel") + + def get_albums(self, return_list: bool = False) -> list[int] | None: + + gui.update += 1 + self.scanning = True + + if not self.connected: + self.connect() + + if not self.connected: + self.scanning = False + return [] + + playlist = [] + + target = self.server + "/api/data" + headers = { + "Authorization": "Bearer " + self.token, + "Accept": "application/json", + "Content-Type": "application/json", + } + + r = requests.get(target, headers=headers, timeout=10) + data = r.json() + + artists = data["artists"] + albums = data["albums"] + songs = data["songs"] + + artist_ids = {} + for artist in artists: + id = artist["id"] + if id not in artist_ids: + artist_ids[id] = artist["name"] + + album_ids = {} + covers = {} + for album in albums: + id = album["id"] + if id not in album_ids: + album_ids[id] = album["name"] + if "cover" in album: + covers[id] = album["cover"] + + existing = {} + + for track_id, track in pctl.master_library.items(): + if track.is_network and track.file_ext == "KOEL": + existing[track.url_key] = track_id + + for song in songs: + + id = pctl.master_count + replace_existing = False + + e = existing.get(song["id"]) + if e is not None: + id = e + replace_existing = True + + nt = TrackClass() + + nt.title = song["title"] + nt.index = id + if "track" in song and song["track"] is not None: + nt.track_number = song["track"] + if "disc" in song and song["disc"] is not None: + nt.disc = song["disc"] + nt.length = float(song["length"]) + + nt.artist = artist_ids.get(song["artist_id"], "") + nt.album = album_ids.get(song["album_id"], "") + nt.parent_folder_name = (nt.artist + " - " + nt.album).strip("- ") + nt.parent_folder_path = nt.album + "/" + nt.parent_folder_name + + nt.art_url_key = covers.get(song["album_id"], "") + nt.url_key = song["id"] + + nt.is_network = True + nt.file_ext = "KOEL" + + pctl.master_library[id] = nt + + if not replace_existing: + pctl.master_count += 1 + + playlist.append(nt.index) + + self.scanning = False + + if return_list: + return playlist + + pctl.multi_playlist.append(pl_gen(title=_("Koel Collection"), playlist_ids=playlist)) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "koel path tn" + standard_sort(len(pctl.multi_playlist) - 1) + switch_playlist(len(pctl.multi_playlist) - 1) + +class TauService: + def __init__(self) -> None: + self.processing = False + + def resolve_stream(self, key: str) -> str: + return "http://" + prefs.sat_url + ":7814/api1/file/" + key + + def resolve_picture(self, key: str) -> str: + return "http://" + prefs.sat_url + ":7814/api1/pic/medium/" + key + + def get(self, point: str): + url = "http://" + prefs.sat_url + ":7814/api1/" + data = None + try: + r = requests.get(url + point, timeout=10) + data = r.json() + except Exception as e: + logging.exception("Network error") + show_message(_("Network error"), str(e), mode="error") + return data + + def get_playlist(self, playlist_name: str | None = None, return_list: bool = False) -> list[int] | None: + + p = self.get("playlists") + + if not p or not p["playlists"]: + self.processing = False + return [] + + if playlist_name is None: + playlist_name = text_sat_playlist.text.strip() + if not playlist_name: + show_message(_("No playlist name")) + return [] + + id = None + name = "" + for pp in p["playlists"]: + if pp["name"].lower() == playlist_name.lower(): + id = pp["id"] + name = pp["name"] + + if id is None: + show_message(_("Playlist not found on target"), mode="error") + self.processing = False + return [] + + try: + t = self.get("tracklist/" + id) + except Exception: + logging.exception("error getting tracklist") + return [] + at = t["tracks"] + + exist = {} + for k, v in pctl.master_library.items(): + if v.is_network and v.file_ext == "TAU": + exist[v.url_key] = k + + playlist = [] + for item in at: + replace_existing = True + + tid = item["id"] + id = exist.get(str(tid)) + if id is None: + id = pctl.master_count + replace_existing = False + + nt = TrackClass() + nt.index = id + nt.title = item.get("title", "") + nt.artist = item.get("artist", "") + nt.album = item.get("album", "") + nt.album_artist = item.get("album_artist", "") + nt.length = int(item.get("duration", 0) / 1000) + nt.track_number = item.get("track_number", 0) + + nt.fullpath = item.get("path", "") + nt.filename = os.path.basename(nt.fullpath) + nt.parent_folder_name = os.path.basename(os.path.dirname(nt.fullpath)) + nt.parent_folder_path = os.path.dirname(nt.fullpath) + + nt.url_key = str(tid) + nt.art_url_key = str(tid) + + nt.is_network = True + nt.file_ext = "TAU" + pctl.master_library[id] = nt + + if not replace_existing: + pctl.master_count += 1 + playlist.append(nt.index) + + if return_list: + self.processing = False + return playlist + + pctl.multi_playlist.append(pl_gen(title=name, playlist_ids=playlist)) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "tau path tn" + standard_sort(len(pctl.multi_playlist) - 1) + switch_playlist(len(pctl.multi_playlist) - 1) + self.processing = False + +class SearchOverlay: + + def __init__(self): + + self.active = False + self.search_text = TextBox() + + self.results = [] + self.searched_text = "" + self.on = 0 + self.force_select = -1 + self.old_mouse = [0, 0] + self.sip = False + self.delay_enter = False + self.last_animate_time = 0 + self.animate_timer = Timer(100) + self.input_timer = Timer(100) + self.all_folders = False + self.spotify_mode = False + + def clear(self): + self.search_text.text = "" + self.results.clear() + self.searched_text = "" + self.on = 0 + self.all_folders = False + + def click_artist(self, name, get_list=False, search_lists=None): + + playlist = [] + + if search_lists is None: + search_lists = [] + for pl in pctl.multi_playlist: + search_lists.append(pl.playlist_ids) + + for pl in search_lists: + for item in pl: + tr = pctl.master_library[item] + n = name.lower() + if tr.artist.lower() == n \ + or tr.album_artist.lower() == n \ + or ("artists" in tr.misc and name in tr.misc["artists"]): + if item not in playlist: + playlist.append(item) + + if get_list: + return playlist + + pctl.multi_playlist.append(pl_gen( + title=_("Artist: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) + + if gui.combo_mode: + exit_combo() + switch_playlist(len(pctl.multi_playlist) - 1) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "a\"" + name + "\"" + + inp.key_return_press = False + + def click_year(self, name, get_list: bool = False): + + playlist = [] + for pl in pctl.multi_playlist: + for item in pl.playlist_ids: + if name in pctl.master_library[item].date: + if item not in playlist: + playlist.append(item) + + if get_list: + return playlist + + pctl.multi_playlist.append(pl_gen( + title=_("Year: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) + + if gui.combo_mode: + exit_combo() + + switch_playlist(len(pctl.multi_playlist) - 1) + + inp.key_return_press = False + + def click_composer(self, name: str, get_list: bool = False): + + playlist = [] + for pl in pctl.multi_playlist: + for item in pl.playlist_ids: + if pctl.master_library[item].composer.lower() == name.lower(): + if item not in playlist: + playlist.append(item) + + if get_list: + return playlist + + pctl.multi_playlist.append(pl_gen( + title=_("Composer: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) + + if gui.combo_mode: + exit_combo() + + switch_playlist(len(pctl.multi_playlist) - 1) + + inp.key_return_press = False + + def click_meta(self, name: str, get_list: bool = False, search_lists=None): + + if search_lists is None: + search_lists = [] + for pl in pctl.multi_playlist: + search_lists.append(pl.playlist_ids) + + playlist = [] + for pl in search_lists: + for item in pl: + if name in pctl.master_library[item].parent_folder_path: + if item not in playlist: + playlist.append(item) + + if get_list: + return playlist + + pctl.multi_playlist.append(pl_gen( + title=os.path.basename(name).upper(), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) + + if gui.combo_mode: + exit_combo() + + switch_playlist(len(pctl.multi_playlist) - 1) + + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "p\"" + name + "\"" + + inp.key_return_press = False + + def click_genre(self, name: str, get_list: bool = False, search_lists=None): + + playlist = [] + + if search_lists is None: + search_lists = [] + for pl in pctl.multi_playlist: + search_lists.append(pl.playlist_ids) + + include_multi = False + if name.endswith("+") or not prefs.sep_genre_multi: + name = name.rstrip("+") + include_multi = True + + for pl in search_lists: + for item in pl: + track = pctl.master_library[item] + if track.genre.lower().replace("-", "") == name.lower().replace("-", ""): + if item not in playlist: + playlist.append(item) + elif include_multi and ("/" in track.genre or "," in track.genre or ";" in track.genre): + for split in track.genre.replace(",", "/").replace(";", "/").split("/"): + split = split.strip() + if name.lower().replace("-", "") == split.lower().replace("-", ""): + if item not in playlist: + playlist.append(item) + + if get_list: + return playlist + + pctl.multi_playlist.append(pl_gen( + title=_("Genre: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) + + if gui.combo_mode: + exit_combo() + + switch_playlist(len(pctl.multi_playlist) - 1) + + if include_multi: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "gm\"" + name + "\"" + else: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "g=\"" + name + "\"" + + inp.key_return_press = False + + def click_album(self, index): + + pctl.jump(index) + if gui.combo_mode: + exit_combo() + + pctl.show_current() + + inp.key_return_press = False + + def render(self): + global input_text + if self.active is False: + + # Activate search overlay on key presses + if prefs.search_on_letter and input_text != "" and gui.layer_focus == 0 and \ + not key_lalt and not key_ralt and \ + not key_ctrl_down and not radiobox.active and not rename_track_box.active and \ + not quick_search_mode and not pref_box.enabled and not gui.rename_playlist_box \ + and not gui.rename_folder_box and input_text.isalnum() and not gui.box_over \ + and not trans_edit_box.active: + + # Divert to artist list if mouse over + if gui.lsp and prefs.left_panel_mode == "artist list" and 2 < mouse_position[0] < gui.lspw \ + and gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY: + artist_list_box.locate_artist_letter(input_text) + return + + activate_search_overlay() + self.old_mouse = copy.deepcopy(mouse_position) + + if self.active: + + x = 0 + y = 0 + w = window_size[0] + h = window_size[1] + + if keymaps.test("add-to-queue"): + input_text = "" + + if inp.backspace_press: + # self.searched_text = "" + # self.results.clear() + + if len(self.search_text.text) - inp.backspace_press < 1: + self.active = False + self.search_text.text = "" + self.results.clear() + self.searched_text = "" + return + + if key_esc_press: + if self.delay_enter: + self.delay_enter = False + else: + self.active = False + self.search_text.text = "" + self.results.clear() + self.searched_text = "" + return + + if gui.level_2_click and mouse_position[0] > 350 * gui.scale: + self.active = False + self.search_text.text = "" + + mouse_change = False + if not point_proximity_test(self.old_mouse, mouse_position, 25): + mouse_change = True + # mouse_change = True + + ddt.rect((x, y, w, h), [3, 3, 3, 235]) + ddt.text_background_colour = [12, 12, 12, 255] + + + input_text_x = 80 * gui.scale + highlight_x = 30 * gui.scale + thumbnail_rx = 100 * gui.scale + text_lx = 120 * gui.scale + + s_font = 15 + s_b_font = 214 + b_font = 215 + + if window_size[0] < 400 * gui.scale: + input_text_x = 30 * gui.scale + highlight_x = 4 * gui.scale + thumbnail_rx = 65 * gui.scale + text_lx = 80 * gui.scale + s_font = 415 + s_b_font = 514 + d_font = 515 + + #album_art_size_s = 0 * gui.scale + + # Search active animation + if self.sip: + x = round(15 * gui.scale) + y = x + s = round(7 * gui.scale) + g = round(4 * gui.scale) + + t = self.animate_timer.get() + if abs(t - self.last_animate_time) > 0.3: + self.animate_timer.set() + t = 0 + + self.last_animate_time = t + + for item in range(4): + a = 100 + if round(t * 14) % 4 == item: + a = 255 + if self.spotify_mode: + colour = (145, 245, 78, a) + else: + colour = (140, 100, 255, a) + + ddt.rect((x, y, s, s), colour) + x += g + s + + gui.update += 1 + + # No results found message + elif not self.results and len(self.search_text.text) > 1: + if self.input_timer.get() > 0.5 and not self.sip: + ddt.text((window_size[0] // 2, 200 * gui.scale, 2), _("No results found"), [250, 250, 250, 255], 216, + bg=[12, 12, 12, 255]) + + # Spotify search text + if prefs.spot_mode and not self.spotify_mode: + text = _("Press Tab key to switch to Spotify search") + ddt.text((window_size[0] // 2, window_size[1] - 30 * gui.scale, 2), text, [250, 250, 250, 255], 212, + bg=[12, 12, 12, 255]) + + self.search_text.draw(input_text_x, 60 * gui.scale, [230, 230, 230, 255], True, False, 30, + window_size[0] - 100, big=True, click=gui.level_2_click, selection_height=30) + + if inp.key_tab_press: + search_over.spotify_mode ^= True + self.sip = True + search_over.searched_text = search_over.search_text.text + if worker2_lock.locked(): + try: + worker2_lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked worker2_lock") + else: + logging.exception("Unknown RuntimeError trying to release worker2_lock") + except Exception: + logging.exception("Unknown error trying to release worker2_lock") + + if input_text or key_backspace_press: + self.input_timer.set() + + gui.update += 1 + elif self.input_timer.get() >= 0.20 and \ + (len(search_over.search_text.text) > 1 or (len(search_over.search_text.text) == 1 and ord(search_over.search_text.text) > 128)) \ + and search_over.search_text.text != search_over.searched_text: + self.sip = True + if worker2_lock.locked(): + try: + worker2_lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked worker2_lock") + else: + logging.exception("Unknown RuntimeError trying to release worker2_lock") + except Exception: + logging.exception("Unknown error trying to release worker2_lock") + + if self.input_timer.get() < 10: + gui.frame_callback_list.append(TestTimer(0.1)) + + yy = 110 * gui.scale + + if key_down_press: + + self.force_select += 1 + if self.force_select > 4: + self.on = self.force_select - 4 + self.force_select = min(self.force_select, len(self.results) - 1) + self.old_mouse = copy.deepcopy(mouse_position) + + if key_up_press: + + if self.force_select > -1: + self.force_select -= 1 + self.force_select = max(self.force_select, 0) + + if self.force_select < self.on + 4: + self.on = self.force_select - 4 + self.on = max(self.on, 0) + + self.old_mouse = copy.deepcopy(mouse_position) + + if mouse_wheel == -1: + self.on += 1 + self.force_select += 1 + if mouse_wheel == 1 and self.on > -1: + self.on -= 1 + self.force_select -= 1 + + enter = False + + if self.delay_enter and not self.sip and self.search_text.text == self.searched_text: + enter = True + self.delay_enter = False + + elif inp.key_return_press: + if self.results: + enter = True + self.delay_enter = False + elif self.sip or self.input_timer.get() < 0.25: + self.delay_enter = True + else: + enter = True + self.delay_enter = False + + inp.key_return_press = False + + bar_colour = [140, 80, 240, 255] + track_in_bar_colour = [244, 209, 66, 255] + + self.on = max(self.on, 0) + self.on = min(len(self.results) - 1, self.on) + + full_count = 0 + + sec = False + + p = -1 + + if self.on > 4: + p += self.on - 4 + p = self.on - 1 + clear = False + + for i, item in enumerate(self.results): + + p += 1 + + if p > len(self.results) - 1: + break + + item: list[int] = self.results[p] + + fade = 1 + selected = self.on + if self.force_select > -1: + selected = self.force_select + + #logging.info(selected) + + if selected != p: + fade = 0.8 + + start = yy + + n = item[0] + + names = { + 0: "Artist", + 1: "Album", + 2: "Track", + 3: "Genre", + 5: "Folder", + 6: "Composer", + 7: "Year", + 8: "Playlist", + 10: "Artist", + 11: "Album", + 12: "Track", + } + type_colours = { + 0: [250, 140, 190, 255], # Artist + 1: [250, 140, 190, 255], # Album + 2: [250, 220, 190, 255], # Track + 3: [240, 240, 160, 255], # Genre + 5: [250, 100, 50, 255], # Folder + 6: [180, 250, 190, 255], # Composer + 7: [250, 50, 140, 255], # Year + 8: [100, 210, 250, 255], # Playlist + 10: [145, 245, 78, 255], # Spotify Artist + 11: [130, 237, 69, 255], # Spotify Album + 12: [200, 255, 150, 255], # Spotify Track + } + if n not in names: + name = "NYI" + colour = [255, 255, 255, 255] + else: + name = names[n] + colour = type_colours[n] + colour[3] = int(colour[3] * fade) + + pad = round(4 * gui.scale) + height = round(25 * gui.scale) + if n in (1, 11): + height = round(50 * gui.scale) + album_art_size = height + + + # Selection bar + s_rect = (highlight_x, yy, 600 * gui.scale, height + pad + pad - 1) + fields.add(s_rect) + if fade == 1: + ddt.rect((highlight_x, yy + pad, 4 * gui.scale, height), bar_colour) + if n in (2,): + if key_ctrl_down and item[2] in default_playlist: + ddt.rect((highlight_x + round(5 * gui.scale), yy + pad, 4 * gui.scale, height), track_in_bar_colour) + + # Type text + if n in (0, 3, 5, 6, 7, 8, 10, 12): + ddt.text((thumbnail_rx, yy + pad + round(3 * gui.scale), 1), names[n], type_colours[n], 214) + + # Thumbnail + if n in (1, 2): + thl = thumbnail_rx - album_art_size + ddt.rect((thl, yy + pad, album_art_size, album_art_size), [50, 50, 50, 150]) + tauon.gall_ren.render(pctl.get_track(item[2]), (thl, yy + pad), album_art_size) + if fade != 1: + ddt.rect((thl, yy + pad, album_art_size, album_art_size), [0, 0, 0, 70]) + if n in (11,): + thl = thumbnail_rx - album_art_size + ddt.rect((thl, yy + pad, album_art_size, album_art_size), [50, 50, 50, 150]) + # tauon.gall_ren.render(pctl.get_track(item[2]), (50 * gui.scale, yy + 5), 50 * gui.scale) + if not item[5].draw(thumbnail_rx - album_art_size, yy + pad): + if tauon.gall_ren.lock.locked(): + try: + tauon.gall_ren.lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked gall_ren_lock") + else: + logging.exception("Unknown RuntimeError trying to release gall_ren_lock") + except Exception: + logging.exception("Unknown error trying to release gall_ren_lock") + + # Result text + if n in (0, 5, 6, 7, 8, 10): # Bold + xx = ddt.text((text_lx, yy + pad + round(3 * gui.scale)), item[1], [255, 255, 255, int(255 * fade)], b_font) + if n in (3,): # Genre + xx = ddt.text((text_lx, yy + pad + round(3 * gui.scale)), item[1].rstrip("+"), [255, 255, 255, int(255 * fade)], b_font) + if item[1].endswith("+"): + ddt.text( + (xx + text_lx + 13 * gui.scale, yy + pad + round(3 * gui.scale)), _("(Include multi-tag results)"), + [255, 255, 255, int(255 * fade) // 2], 313) + if n == 11: # Spotify Album + xx = ddt.text((text_lx, yy + round(5 * gui.scale)), item[1][0], [255, 255, 255, int(255 * fade)], s_b_font) + artist = item[1][1] + ddt.text((text_lx + 5 * gui.scale, yy + 30 * gui.scale), _("BY"), [250, 240, 110, int(255 * fade)], 212) + xx += 8 * gui.scale + xx += ddt.text((text_lx + 30 * gui.scale, yy + 30 * gui.scale), artist, [250, 250, 250, int(255 * fade)], s_font) + if n in (12,): # Spotify Track + yyy = yy + yyy += round(6 * gui.scale) + xx = ddt.text((text_lx, yyy), item[1][0], [255, 255, 255, int(255 * fade)], s_font) + xx += 9 * gui.scale + ddt.text((xx + text_lx, yyy), _("BY"), [250, 160, 110, int(255 * fade)], 212) + xx += 25 * gui.scale + xx += ddt.text((xx + text_lx, yyy), item[1][1], [255, 255, 255, int(255 * fade)], s_b_font) + if n in (2, ): # Track + yyy = yy + yyy += round(6 * gui.scale) + track = pctl.master_library[item[2]] + if track.artist == track.title == "": + text = os.path.splitext(track.filename)[0] + xx = ddt.text((text_lx, yyy + pad), text, [255, 255, 255, int(255 * fade)], s_font) + else: + xx = ddt.text((text_lx, yyy), item[1], [255, 255, 255, int(255 * fade)], s_font) + xx += 9 * gui.scale + ddt.text((xx + text_lx, yyy), _("BY"), [250, 160, 110, int(255 * fade)], 212) + xx += 25 * gui.scale + artist = track.artist + xx += ddt.text((xx + text_lx, yyy), artist, [255, 255, 255, int(255 * fade)], s_b_font) + if track.album: + xx += 9 * gui.scale + xx += ddt.text((xx + text_lx, yyy), _("FROM"), [120, 120, 120, int(255 * fade)], 212) + xx += 8 * gui.scale + xx += ddt.text((xx + text_lx, yyy), track.album, [80, 80, 80, int(255 * fade)], 212) + + if n in (1,): # Two line album + track = pctl.master_library[item[2]] + artist = track.album_artist + if not artist: + artist = track.artist + + xx = ddt.text((text_lx, yy + pad + round(5 * gui.scale)), item[1], [255, 255, 255, int(255 * fade)], s_b_font) + + ddt.text((text_lx + 5 * gui.scale, yy + 30 * gui.scale), _("BY"), [250, 240, 110, int(255 * fade)], 212) + xx += 8 * gui.scale + xx += ddt.text((text_lx + 30 * gui.scale, yy + 30 * gui.scale), artist, [250, 250, 250, int(255 * fade)], s_font) + + + yy += height + pad + pad + + show = False + go = False + extend = False + if coll(s_rect) and mouse_change: + if self.force_select != p: + self.force_select = p + gui.update = 2 + + if gui.level_2_click: + if key_ctrl_down: + extend = True + else: + go = True + clear = True + + + if level_2_right_click: + show = True + clear = True + + if enter and key_shift_down and fade == 1: + show = True + clear = True + + elif enter and fade == 1: + if key_shift_down or key_shiftr_down: + show = True + clear = True + else: + go = True + clear = True + + if extend: + match n: + case 0: + default_playlist.extend(self.click_artist(item[1], get_list=True)) + case 1: + for k, pl in enumerate(pctl.multi_playlist): + if item[2] in pl.playlist_ids: + default_playlist.extend( + get_album_from_first_track(pl.playlist_ids.index(item[2]), item[2], k)) + break + case 2: + default_playlist.append(item[2]) + case 3: + default_playlist.extend(self.click_genre(item[1], get_list=True)) + case 5: + default_playlist.extend(self.click_meta(item[1], get_list=True)) + case 6: + default_playlist.extend(self.click_composer(item[1], get_list=True)) + case 7: + default_playlist.extend(self.click_year(item[1], get_list=True)) + case 8: + default_playlist.extend(pctl.multi_playlist[pl].playlist_ids) + case 12: + tauon.spot_ctl.append_track(item[2]) + reload_albums() + + gui.pl_update += 1 + elif show: + match n: + case 0 | 1 | 2 | 3 | 5 | 6 | 7 | 10: + pctl.show_current(index=item[2], playing=False) + if album_mode: + show_in_gal(0) + case 8: + pl = id_to_pl(item[3]) + if pl: + switch_playlist(pl) + + elif go: + match n: + case 0: + self.click_artist(item[1]) + case 10: + show_message(_("Searching for albums by artist: ") + item[1], _("This may take a moment")) + shoot = threading.Thread(target=tauon.spot_ctl.artist_playlist, args=([item[2]])) + shoot.daemon = True + shoot.start() + case 1 | 2: + self.click_album(item[2]) + pctl.show_current(index=item[2]) + pctl.playlist_view_position = pctl.selected_in_playlist + case 3: + self.click_genre(item[1]) + case 5: + self.click_meta(item[1]) + case 6: + self.click_composer(item[1]) + case 7: + self.click_year(item[1]) + case 8: + pl = id_to_pl(item[3]) + if pl: + switch_playlist(pl) + case 11: + tauon.spot_ctl.album_playlist(item[2]) + reload_albums() + case 12: + tauon.spot_ctl.append_track(item[2]) + reload_albums() + + if n in (2,) and keymaps.test("add-to-queue") and fade == 1: + queue_object = queue_item_gen( + item[2], + pctl.multi_playlist[id_to_pl(item[3])].playlist_ids.index(item[2]), + item[3]) + pctl.force_queue.append(queue_object) + queue_timer_set(queue_object=queue_object) + + # ---- + + # --- + if i > 40: + break + if yy > window_size[1] - (100 * gui.scale): + break + + continue + + if clear: + self.active = False + self.search_text.text = "" + self.results.clear() + self.searched_text = "" + +class MessageBox: + + def __init__(self): + pass + + def get_rect(self): + + w1 = ddt.get_text_w(gui.message_text, 15) + 74 * gui.scale + w2 = ddt.get_text_w(gui.message_subtext, 12) + 74 * gui.scale + w3 = ddt.get_text_w(gui.message_subtext2, 12) + 74 * gui.scale + w = max(w1, w2, w3) + + w = max(w, 210 * gui.scale) + + h = round(60 * gui.scale) + if gui.message_subtext2: + h += round(15 * gui.scale) + + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) + + return x, y, w, h + + def render(self): + + if inp.mouse_click or inp.key_return_press or right_click or key_esc_press or inp.backspace_press \ + or keymaps.test("quick-find") or (k_input and message_box_min_timer.get() > 1.2): + + if not key_focused and message_box_min_timer.get() > 0.4: + gui.message_box = False + gui.update += 1 + inp.key_return_press = False + + x, y, w, h = self.get_rect() + + ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), + colours.box_text_border) + ddt.rect_a((x, y), (w, h), colours.message_box_bg) + + ddt.text_background_colour = colours.message_box_bg + + if gui.message_mode == "info": + message_info_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) + elif gui.message_mode == "warning": + message_warning_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) + elif gui.message_mode == "done": + message_tick_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) + elif gui.message_mode == "arrow": + message_arrow_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) + elif gui.message_mode == "download": + message_download_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) + elif gui.message_mode == "error": + message_error_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_error_icon.h / 2) - 1) + elif gui.message_mode == "bubble": + message_bubble_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_bubble_icon.h / 2) - 1) + elif gui.message_mode == "link": + message_info_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_bubble_icon.h / 2) - 1) + elif gui.message_mode == "confirm": + message_info_icon.render(x + 14 * gui.scale, y + int(h / 2) - int(message_info_icon.h / 2) - 1) + ddt.text((x + 62 * gui.scale, y + 9 * gui.scale), gui.message_text, colours.message_box_text, 15) + if draw.button("Yes", (w // 2 + x) - 70 * gui.scale, y + 32 * gui.scale, w=60*gui.scale): + gui.message_box_confirm_callback(*gui.message_box_confirm_reference) + if draw.button("No", (w // 2 + x) + 25 * gui.scale, y + 32 * gui.scale, w=60*gui.scale): + gui.message_box = False + return + + if gui.message_subtext: + ddt.text((x + 62 * gui.scale, y + 11 * gui.scale), gui.message_text, colours.message_box_text, 15) + if gui.message_mode == "bubble" or gui.message_mode == "link": + link_pa = draw_linked_text((x + 63 * gui.scale, y + (9 + 22) * gui.scale), gui.message_subtext, + colours.message_box_text, 12) + link_activate(x + 63 * gui.scale, y + (9 + 22) * gui.scale, link_pa) + else: + ddt.text((x + 63 * gui.scale, y + (9 + 22) * gui.scale), gui.message_subtext, colours.message_box_text, + 12) + + if gui.message_subtext2: + ddt.text((x + 63 * gui.scale, y + (9 + 42) * gui.scale), gui.message_subtext2, colours.message_box_text, + 12) + + else: + ddt.text((x + 62 * gui.scale, y + 20 * gui.scale), gui.message_text, colours.message_box_text, 15) + +class NagBox: + def __init__(self): + self.wiggle_timer = Timer(10) + + def draw(self): + w = 485 * gui.scale + h = 165 * gui.scale + x = int(window_size[0] / 2) - int(w / 2) + # if self.wiggle_timer.get() < 0.5: + # gui.update += 1 + # x += math.sin(core_timer.get() * 40) * 4 + y = int(window_size[1] / 2) - int(h / 2) + + # xx = x - round(8 * gui.scale) + # hh = 0.0 #349 / 360 + # while xx < x + w + round(8 * gui.scale): + # re = [xx, y - round(8 * gui.scale), 3, h + round(8 * gui.scale) + round(8 * gui.scale)] + # hh -= 0.0007 + # c = hsl_to_rgb(hh, 0.9, 0.7) + # #c = hsl_to_rgb(hh, 0.63, 0.43) + # ddt.rect(re, c) + # xx += 3 + + ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), + colours.box_text_border) + ddt.rect_a((x, y), (w, h), colours.message_box_bg) + + # if gui.level_2_click and not coll((x, y, w, h)): + # if core_timer.get() < 2: + # self.wiggle_timer.set() + # else: + # prefs.show_nag = False + # + # gui.update += 1 + + ddt.text_background_colour = colours.message_box_bg + + x += round(10 * gui.scale) + y += round(13 * gui.scale) + ddt.text((x, y), _("Welcome to v7.2.0!"), colours.message_box_text, 212) + y += round(20 * gui.scale) + + link_pa = draw_linked_text( + (x, y), + _("You can check out the release notes on the https://") + "github.com/Taiko2k/TauonMusicBox/releases", + colours.message_box_text, 12, replace=_("Github release page.")) + link_activate(x, y, link_pa, click=gui.level_2_click) + + heart_notify_icon.render(x + round(425 * gui.scale), y + round(80 * gui.scale), [255, 90, 90, 255]) + + y += round(30 * gui.scale) + ddt.text((x, y), _("New supporter bonuses!"), colours.message_box_text, 212) + + y += round(20 * gui.scale) + + ddt.text((x, y), _("A new supporter bonus theme is now available! Check it out at the above link!"), + colours.message_box_text, 12) + # link_activate(x, y, link_pa, click=gui.level_2_click) + + y += round(20 * gui.scale) + ddt.text((x, y), _("Your support means a lot! Love you!"), colours.message_box_text, 12) + + y += round(30 * gui.scale) + + if draw.button("Close", x, y, press=gui.level_2_click): + prefs.show_nag = False + # show_message("Oh... :( 💔") + # if draw.button("Show supporter page", x + round(304 * gui.scale), y, background_colour=[60, 140, 60, 255], background_highlight_colour=[60, 150, 60, 255], press=gui.level_2_click): + # webbrowser.open("https://github.com/sponsors/Taiko2k", new=2, autoraise=True) + # prefs.show_nag = False + # if draw.button("I already am!", x + round(360), y, press=gui.level_2_click): + # show_message("Oh hey, thanks! :)") + # prefs.show_nag = False + +class PowerTag: + + def __init__(self): + self.name = "BLANK" + self.path = "" + self.position = 0 + self.colour = None + + self.peak_x = 0 + self.ani_timer = Timer() + self.ani_timer.force_set(10) + +class Over: + def __init__(self): + + global window_size + + self.init2done = False + + self.about_image = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-a.png") + self.about_image2 = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-b.png") + self.about_image3 = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-c.png") + self.about_image4 = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-d.png") + self.about_image5 = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-e.png") + self.about_image6 = asset_loader(scaled_asset_directory, loaded_asset_dc, "v4-f.png") + self.title_image = asset_loader(scaled_asset_directory, loaded_asset_dc, "title.png", True) + + # self.tab_width = round(115 * gui.scale) + self.w = 100 + self.h = 100 + + self.box_x = 100 + self.box_y = 100 + self.item_x_offset = round(25 * gui.scale) + + self.current_path = os.path.expanduser("~") + self.view_offset = 0 + self.ext_ratio = {} + self.last_db_size = -1 + + self.enabled = False + self.click = False + self.right_click = False + self.scroll = 0 + self.lock = False + + self.drives = [] + + self.temp_lastfm_user = "" + self.temp_lastfm_pass = "" + self.lastfm_input_box = 0 + + self.func_page = 0 + self.tab_active = 0 + self.tabs = [ + [_("Function"), self.funcs], + [_("Audio"), self.audio], + [_("Tracklist"), self.config_v], + [_("Theme"), self.theme], + [_("Window"), self.config_b], + [_("View"), self.view2], + [_("Transcode"), self.codec_config], + [_("Lyrics"), self.lyrics], + [_("Accounts"), self.last_fm_box], + [_("Stats"), self.stats], + [_("About"), self.about], + ] + + self.stats_timer = Timer() + self.stats_timer.force_set(1000) + self.stats_pl_timer = Timer() + self.stats_pl_timer.force_set(1000) + self.total_albums = 0 + self.stats_pl = 0 + self.stats_pl_albums = 0 + self.stats_pl_length = 0 + + self.ani_cred = 0 + self.cred_page = 0 + self.ani_fade_on_timer = Timer(force=10) + self.ani_fade_off_timer = Timer(force=10) + + self.device_scroll_bar_position = 0 + + self.lyrics_panel = False + self.account_view = 0 + self.view_view = 0 + self.chart_view = 0 + self.eq_view = False + self.rg_view = False + self.sync_view = False + + self.account_text_field = -1 + + self.themes = [] + self.view_supporters = False + self.key_box = TextBox2() + self.key_box_focused = False + + def theme(self, x0, y0, w0, h0): + + global album_mode_art_size + global update_layout + + y = y0 + 13 * gui.scale + x = x0 + 25 * gui.scale + + ddt.text_background_colour = colours.box_background + ddt.text((x, y), _("Theme"), colours.box_text_label, 12) + + y += 25 * gui.scale + + self.toggle_square(x, y, toggle_auto_bg, _("Use album art as background")) + + y += 23 * gui.scale + + old = prefs.enable_fanart_bg + prefs.enable_fanart_bg = self.toggle_square(x + 10 * gui.scale, y, prefs.enable_fanart_bg, + _("Prefer artist backgrounds")) + if prefs.enable_fanart_bg and prefs.enable_fanart_bg != old: + if not prefs.auto_dl_artist_data: + prefs.auto_dl_artist_data = True + show_message(_("Also enabling 'auto-fech artist data' to scrape last.fm."), _("You can toggle this back off under Settings > Function")) + y += 23 * gui.scale + + self.toggle_square(x + 10 * gui.scale, y, toggle_auto_bg_strong, _("Stronger")) + # self.toggle_square(x + 10 * gui.scale, y, toggle_auto_bg_strong1, _("Lo")) + # self.toggle_square(x + 54 * gui.scale, y, toggle_auto_bg_strong2, _("Md")) + # self.toggle_square(x + 105 * gui.scale, y, toggle_auto_bg_strong3, _("Hi")) + + #y += 23 * gui.scale + self.toggle_square(x + 120 * gui.scale, y, toggle_auto_bg_blur, _("Blur")) + + y += 23 * gui.scale + self.toggle_square(x + 10 * gui.scale, y, toggle_auto_bg_showcase, _("Showcase only")) + + y += 23 * gui.scale + # prefs.center_bg = self.toggle_square(x + 10 * gui.scale, y, prefs.center_bg, _("Always center")) + prefs.showcase_overlay_texture = self.toggle_square( + x + 20 * gui.scale, y, prefs.showcase_overlay_texture, _("Pattern style")) + + y += 25 * gui.scale + + self.toggle_square(x, y, toggle_auto_theme, _("Auto-theme from album art")) + + y += 55 * gui.scale + + square = round(8 * gui.scale) + border = round(4 * gui.scale) + outer_border = round(2 * gui.scale) + + # theme_files = get_themes() + xx = x + yy = y + hover_name = None + for c, theme_name, theme_number in self.themes: + + if theme_name == gui.theme_name: + rect = [ + xx - outer_border, yy - outer_border, border * 2 + square * 2 + outer_border * 2, + border * 2 + square * 2 + outer_border * 2] + ddt.rect(rect, colours.box_text_label) + + rect = [xx, yy, border * 2 + square * 2, border * 2 + square * 2] + ddt.rect(rect, [5, 5, 5, 255]) + + rect = grow_rect(rect, 3) + fields.add(rect) + if coll(rect): + hover_name = theme_name + if self.click: + global theme + theme = theme_number + gui.reload_theme = True + + c1 = c.playlist_panel_background + c2 = c.artist_playing + c3 = c.title_playing + c4 = c.bottom_panel_colour + + if theme_name == "Carbon": + c1 = c.title_playing + c2 = c.playlist_panel_background + c3 = c.top_panel_background + + if theme_name == "Lavender Light": + c1 = c.tab_background_active + + if theme_name == "Neon Love": + c2 = c.artist_text + c4 = [118, 85, 194, 255] + c1 = c4 + + if theme_name == "Sky": + c2 = c.artist_text + + if theme_name == "Sunken": + c2 = c.title_text + c3 = c.artist_text + c4 = [59, 115, 109, 255] + c1 = c4 + + if c2 == c3 and colour_value(c1) < 200: + rect = [(xx + border + square) - (square // 2), (yy + border + square) - (square // 2), square, square] + ddt.rect(rect, c2) + else: + + # tl + rect = [xx + border, yy + border, square, square] + ddt.rect(rect, c1) + + # tr + rect = [xx + border + square, yy + border, square, square] + ddt.rect(rect, c2) + + # bl + rect = [xx + border, yy + border + square, square, square] + ddt.rect(rect, c3) + + # br + rect = [xx + border + square, yy + border + square, square, square] + ddt.rect(rect, c4) + + yy += round(27 * gui.scale) + if yy > y + 40 * gui.scale: + yy = y + xx += round(27 * gui.scale) + + name = gui.theme_name + if hover_name: + name = hover_name + ddt.text((x, y - 23 * gui.scale), name, colours.box_text_label, 214) + if gui.theme_name == "Neon Love" and not hover_name: + x += 95 * gui.scale + y -= 23 * gui.scale + # x += 165 * gui.scale + # y += -19 * gui.scale + + link_pa = draw_linked_text((x, y), + _("Based on") + " " + "https://love.holllo.cc/", colours.box_text_label, 312, replace="love.holllo.cc") + link_activate(x, y, link_pa, click=self.click) + + def rg(self, x0, y0, w0, h0): + y = y0 + 55 * gui.scale + x = x0 + 130 * gui.scale + + if self.button(x - 110 * gui.scale, y + 180 * gui.scale, _("Return"), width=75 * gui.scale): + self.rg_view = False + + y = y0 + round(15 * gui.scale) + x = x0 + round(50 * gui.scale) + + ddt.text((x, y), _("ReplayGain"), colours.box_text_label, 14) + y += round(25 * gui.scale) + + self.toggle_square(x, y, switch_rg_off, _("Off")) + self.toggle_square(x + round(80 * gui.scale), y, switch_rg_auto, _("Auto")) + y += round(22 * gui.scale) + self.toggle_square(x, y, switch_rg_album, _("Preserve album dynamics")) + y += round(22 * gui.scale) + self.toggle_square(x, y, switch_rg_track, _("Tracks equal loudness")) + + y += round(25 * gui.scale) + ddt.text((x, y), _("Will only have effect if ReplayGain metadata is present."), colours.box_text_label, 12) + y += round(26 * gui.scale) + + ddt.text((x, y), _("Pre-amp"), colours.box_text_label, 14) + y += round(26 * gui.scale) + + sw = round(170 * gui.scale) + sh = round(2 * gui.scale) + + slider = (x, y, sw, sh) + + gh = round(14 * gui.scale) + gw = round(8 * gui.scale) + grip = [0, y - (gh // 2), gw, gh] + + grip[0] = x + + bp = prefs.replay_preamp + 15 + + grip[0] += (bp / 30 * sw) + + m1 = (x, y, sh, sh * 2) + m2 = ((x + sw // 2), y, sh, sh * 2) + m3 = ((x + sw), y, sh, sh * 2) + + if coll(grow_rect(slider, 15)) and mouse_down: + bp = (mouse_position[0] - x) / sw * 30 + gui.update += 1 + + bp = round(bp) + bp = max(bp, 0) + bp = min(bp, 30) + prefs.replay_preamp = bp - 15 + + # grip[0] += (bp / 30 * sw) + + ddt.rect(slider, colours.box_text_border) + ddt.rect(m1, colours.box_text_border) + ddt.rect(m2, colours.box_text_border) + ddt.rect(m3, colours.box_text_border) + ddt.rect(grip, colours.box_text_label) + + text = f"{prefs.replay_preamp} dB" + if prefs.replay_preamp > 0: + text = "+" + text + + colour = colours.box_sub_text + if prefs.replay_preamp == 0: + colour = colours.box_text_label + ddt.text((x + sw + round(14 * gui.scale), y - round(8 * gui.scale)), text, colour, 11) + #logging.info(prefs.replay_preamp) + + y += round(18 * gui.scale) + ddt.text( + (x, y, 4, 310 * gui.scale, 300 * gui.scale), + _("Lower pre-amp values improve normalisation but will require a higher system volume."), + colours.box_text_label, 12) + + def eq(self, x0, y0, w0, h0): + + y = y0 + 55 * gui.scale + x = x0 + 130 * gui.scale + + if self.button(x - 110 * gui.scale, y + 180 * gui.scale, _("Return"), width=75 * gui.scale): + self.eq_view = False + + base_dis = 160 * gui.scale + center = base_dis // 2 + width = 25 * gui.scale + + range = 12 + + self.toggle_square(x - 90 * gui.scale, y - 35 * gui.scale, toggle_eq, _("Enable")) + + ddt.text((x - 17 * gui.scale, y + 2 * gui.scale), "+", colours.grey(130), 16) + ddt.text((x - 17 * gui.scale, y + base_dis - 15 * gui.scale), "-", colours.grey(130), 16) + + for i, q in enumerate(prefs.eq): + + bar = [x, y, width, base_dis] + + ddt.rect(bar, [255, 255, 255, 20]) + + bar[0] -= 2 * gui.scale + bar[1] -= 10 * gui.scale + bar[2] += 4 * gui.scale + bar[3] += 20 * gui.scale + + if coll(bar): + + if mouse_down: + target = mouse_position[1] - y - center + target = (target / center) * range + target = min(target, range) + target = max(target, range * -1) + if -0.1 < target < 0.1: + target = 0 + + prefs.eq[i] = target + + pctl.playerCommand = "seteq" + pctl.playerCommandReady = True + + if self.right_click: + prefs.eq[i] = 0 + pctl.playerCommand = "seteq" + pctl.playerCommandReady = True + + start = (q / range) * center + + bar = [x, y + center, width, start] + + ddt.rect(bar, [100, 200, 100, 255]) + + x += round(29 * gui.scale) + + def audio(self, x0, y0, w0, h0): + + global mouse_down + + ddt.text_background_colour = colours.box_background + y = y0 + 40 * gui.scale + x = x0 + 20 * gui.scale + + if self.eq_view: + self.eq(x0, y0, w0, h0) + return + + if self.rg_view: + self.rg(x0, y0, w0, h0) + return + + colour = colours.box_sub_text + + # if system == "Linux": + if not phazor_exists(tauon.pctl): + x += round(20 * gui.scale) + ddt.text((x, y - 25 * gui.scale), _("PHAzOR DLL not found!"), colour, 213) + + elif prefs.backend == 4: + + y = y0 + round(20 * gui.scale) + x = x0 + 20 * gui.scale + + x += round(2 * gui.scale) + + self.toggle_square(x, y, toggle_pause_fade, _("Use fade on pause/stop")) + y += round(23 * gui.scale) + self.toggle_square(x, y, toggle_jump_crossfade, _("Use fade on track jump")) + y += round(23 * gui.scale) + prefs.back_restarts = self.toggle_square(x, y, prefs.back_restarts, _("Back restarts to beginning")) + + y += round(40 * gui.scale) + if self.button(x, y, _("ReplayGain")): + mouse_down = False + self.rg_view = True + + y += round(45 * gui.scale) + prefs.precache = self.toggle_square(x, y, prefs.precache, _("Cache local files (for smb/nfs)")) + y += round(23 * gui.scale) + old = prefs.tmp_cache + prefs.tmp_cache = self.toggle_square(x, y, prefs.tmp_cache ^ True, _("Use persistent network cache")) ^ True + if old != prefs.tmp_cache and tauon.cachement: + tauon.cachement.__init__() + + y += round(22 * gui.scale) + ddt.text((x + round(22 * gui.scale), y), _("Cache size"), colours.box_text, 312) + y += round(18 * gui.scale) + prefs.cache_limit = int( + self.slide_control( + x + round(22 * gui.scale), y, None, _(" GB"), prefs.cache_limit / 1000, 0.5, + 1000, 0.5) * 1000) + + y += round(30 * gui.scale) + # prefs.device_buffer = self.slide_control(x + round(270 * gui.scale), y, _("Output buffer"), 'ms', + # prefs.device_buffer, 10, + # 500, 10, self.reload_device) + + # if prefs.device_buffer > 100: + # prefs.pa_fast_seek = True + # else: + # prefs.pa_fast_seek = False + + y = y0 + 37 * gui.scale + x = x0 + 270 * gui.scale + ddt.text_background_colour = colours.box_background + ddt.text((x, y - 22 * gui.scale), _("Set audio output device"), colours.box_text_label, 212) + + if platform_system == "Linux": + old = prefs.pipewire + prefs.pipewire = self.toggle_square(x + round(gui.scale * 110), self.box_y + self.h - 50 * gui.scale, + prefs.pipewire, _("PipeWire (unstable)")) + prefs.pipewire = self.toggle_square(x, self.box_y + self.h - 50 * gui.scale, + prefs.pipewire ^ True, _("PulseAudio")) ^ True + if old != prefs.pipewire: + show_message(_("Please restart Tauon for this change to take effect")) + + old = prefs.avoid_resampling + prefs.avoid_resampling = self.toggle_square(x, self.box_y + self.h - 27 * gui.scale, prefs.avoid_resampling, _("Avoid resampling")) + if prefs.avoid_resampling != old: + pctl.playerCommand = "reload" + pctl.playerCommandReady = True + if not old: + show_message( + _("Tip: To get samplerate to DAC you may need to check some settings, see:"), + "https://github.com/Taiko2k/Tauon/wiki/Audio-Specs", mode="link") + + self.device_scroll_bar_position -= pref_box.scroll + self.device_scroll_bar_position = max(self.device_scroll_bar_position, 0) + if self.device_scroll_bar_position > len(prefs.phazor_devices) - 11 > 11: + self.device_scroll_bar_position = len(prefs.phazor_devices) - 11 + + if len(prefs.phazor_devices) > 13: + self.device_scroll_bar_position = device_scroll.draw( + x + 250 * gui.scale, y, 11, 180, + self.device_scroll_bar_position, + len(prefs.phazor_devices) - 11, click=self.click) + + i = 0 + reload = False + for name in prefs.phazor_devices: + + if i < self.device_scroll_bar_position: + continue + if y > self.box_y + self.h - 40 * gui.scale: + break + + rect = (x, y + 4 * gui.scale, 245 * gui.scale, 13) + + if self.click and coll(rect): + prefs.phazor_device_selected = name + reload = True + + line = trunc_line(name, 10, 245 * gui.scale) + + fields.add(rect) + + if prefs.phazor_device_selected == name: + ddt.text((x, y), line, colours.box_sub_text, 10) + ddt.text((x - 12 * gui.scale, y + 1 * gui.scale), ">", colours.box_sub_text, 213) + elif coll(rect): + ddt.text((x, y), line, colours.box_sub_text, 10) + else: + ddt.text((x, y), line, colours.box_text_label, 10) + y += 14 * gui.scale + i += 1 + + if reload: + pctl.playerCommand = "set-device" + pctl.playerCommandReady = True + + def reload_device(self, _): + + pctl.playerCommand = "reload" + pctl.playerCommandReady = True + + def toggle_lyrics_view(self): + self.lyrics_panel ^= True + + def lyrics(self, x0, y0, w0, h0): + + x = x0 + 25 * gui.scale + y = y0 - 10 * gui.scale + y += 30 * gui.scale + + ddt.text_background_colour = colours.box_background + + # self.toggle_square(x, y, toggle_auto_lyrics, _("Auto search lyrics")) + if prefs.auto_lyrics: + if prefs.auto_lyrics_checked: + if self.button(x, y, _("Reset failed list")): + prefs.auto_lyrics_checked.clear() + y += 30 * gui.scale + + + #self.toggle_square(x, y, toggle_guitar_chords, _("Enable chord lyrics")) + + y += 40 * gui.scale + ddt.text((x, y), _("Sources:"), colours.box_text_label, 11) + y += 23 * gui.scale + + for name in lyric_sources.keys(): + enabled = name in prefs.lyrics_enables + title = _(name) + if name in uses_scraping: + title += "*" + new = self.toggle_square(x, y, enabled, title) + y += round(23 * gui.scale) + if new != enabled: + if enabled: + prefs.lyrics_enables.clear() + else: + prefs.lyrics_enables.append(name) + + y += round(6 * gui.scale) + ddt.text((x + 12 * gui.scale, y), _("*Uses scraping. Enable at your own discretion."), colours.box_text_label, 11) + y += 20 * gui.scale + ddt.text((x + 12 * gui.scale, y), _("Tip: The order enabled will be the order searched."), colours.box_text_label, 11) + y += 20 * gui.scale + + def view2(self, x0, y0, w0, h0): + + x = x0 + 25 * gui.scale + y = y0 + 20 * gui.scale + + ddt.text_background_colour = colours.box_background + + ddt.text((x, y), _("Metadata side panel"), colours.box_text_label, 12) + + y += 25 * gui.scale + self.toggle_square(x, y, toggle_side_panel_layout, _("Use centered style")) + y += 25 * gui.scale + old = prefs.zoom_art + prefs.zoom_art = self.toggle_square(x, y, prefs.zoom_art, _("Zoom album art to fit")) + if prefs.zoom_art != old: + album_art_gen.clear_cache() + + global album_mode_art_size + global update_layout + y += 35 * gui.scale + ddt.text((x, y), _("Gallery"), colours.box_text_label, 12) + + y += 25 * gui.scale + # self.toggle_square(x, y, toggle_dim_albums, "Dim gallery when playing") + self.toggle_square(x, y, toggle_gallery_click, _("Single click to play")) + y += 25 * gui.scale + self.toggle_square(x, y, toggle_gallery_combine, _("Combine multi-discs")) + y += 25 * gui.scale + self.toggle_square(x, y, toggle_galler_text, _("Show titles")) + y += 25 * gui.scale + # self.toggle_square(x, y, toggle_gallery_row_space, _("Increase row spacing")) + # y += 25 * gui.scale + prefs.center_gallery_text = self.toggle_square( + x + round(10 * gui.scale), y, prefs.center_gallery_text, _("Center alignment")) + + y += 30 * gui.scale + + # y += 25 * gui.scale + + x -= 80 * gui.scale + x += ddt.get_text_w(_("Thumbnail size"), 312) + # x += 20 * gui.scale + + if album_mode_art_size < 160: + self.toggle_square(x + 235 * gui.scale, y + 2 * gui.scale, toggle_gallery_thin, _("Prefer thinner padding")) + + # ddt.text((x, y), _("Gallery art size"), colours.grey(220), 11) + + album_mode_art_size = self.slide_control( + x + 25 * gui.scale, y, _("Thumbnail size"), "px", album_mode_art_size, 70, 400, 10, img_slide_update_gall) + + def funcs(self, x0, y0, w0, h0): + + x = x0 + 25 * gui.scale + y = y0 - 10 * gui.scale + + ddt.text_background_colour = colours.box_background + + if self.func_page == 0: + + y += 23 * gui.scale + + self.toggle_square( + x, y, toggle_enable_web, _("Enable Listen Along"), subtitle=_("Start server for remote web playback")) + + if toggle_enable_web(1): + + link_pa2 = draw_linked_text( + (x + 300 * gui.scale, y - 1 * gui.scale), + f"http://localhost:{prefs.metadata_page_port!s}/listenalong", + colours.grey_blend_bg(190), 13) + link_rect2 = [x + 300 * gui.scale, y - 1 * gui.scale, link_pa2[1], 20 * gui.scale] + fields.add(link_rect2) + + if coll(link_rect2): + if not self.click: + gui.cursor_want = 3 + + if self.click: + webbrowser.open(link_pa2[2], new=2, autoraise=True) + + y += 38 * gui.scale + + old = gui.artist_info_panel + new = self.toggle_square( + x, y, gui.artist_info_panel, + _("Show artist info panel"), + subtitle=_("You can also toggle this with ctrl+o")) + if new != old: + view_box.artist_info(True) + + y += 38 * gui.scale + + self.toggle_square( + x, y, toggle_auto_artist_dl, + _("Auto fetch artist data"), + subtitle=_("Downloads data in background when artist panel is open")) + + y += 38 * gui.scale + prefs.always_auto_update_playlists = self.toggle_square( + x, y, prefs.always_auto_update_playlists, + _("Auto regenerate playlists"), + subtitle=_("Generated playlists reload when re-entering")) + + y += 38 * gui.scale + self.toggle_square( + x, y, toggle_top_tabs, _("Tabs in top panel"), + subtitle=_("Uncheck to disable the tab pin function")) + + y += 45 * gui.scale + # y += 30 * gui.scale + + wa = ddt.get_text_w(_("Open config file"), 211) + 10 * gui.scale + # wb = ddt.get_text_w(_("Open keymap file"), 211) + 10 * gui.scale + wc = ddt.get_text_w(_("Open data folder"), 211) + 10 * gui.scale + + ww = max(wa, wc) + + self.button(x, y, _("Open config file"), open_config_file, width=ww) + bg = None + if gui.opened_config_file: + bg = [90, 50, 130, 255] + self.button(x + ww + wc + 25 * gui.scale, y, _("Reload"), reload_config_file, bg=bg) + + self.button(x + wa + round(20 * gui.scale), y, _("Open data folder"), open_data_directory, ww) + + elif self.func_page == 1: + y += 23 * gui.scale + ddt.text((x, y), _("Enable/Disable track context menu functions:"), colours.box_text_label, 11) + y += 25 * gui.scale + + self.toggle_square(x, y, toggle_wiki, _("Wikipedia artist search")) + y += 23 * gui.scale + self.toggle_square(x, y, toggle_rym, _("Sonemic artist search")) + y += 23 * gui.scale + self.toggle_square(x, y, toggle_band, _("Bandcamp artist page search")) + # y += 23 * gui.scale + # self.toggle_square(x, y, toggle_gimage, _("Google image search")) + y += 23 * gui.scale + self.toggle_square(x, y, toggle_gen, _("Genius track search")) + y += 23 * gui.scale + self.toggle_square(x, y, toggle_transcode, _("Transcode folder")) + + y += 28 * gui.scale + + x = x0 + self.item_x_offset + + ddt.text((x, y), _("End of playlist action"), colours.box_text_label, 12) + + y += 25 * gui.scale + wa = ddt.get_text_w(_("Stop playback"), 13) + 10 * gui.scale + wb = ddt.get_text_w(_("Repeat playlist"), 13) + 10 * gui.scale + wc = max(wa, wb) + 20 * gui.scale + + self.toggle_square(x, y, self.set_playlist_stop, _("Stop playback")) + y += 25 * gui.scale + self.toggle_square(x, y, self.set_playlist_repeat, _("Repeat playlist")) + # y += 25 + y -= 25 * gui.scale + x += wc + self.toggle_square(x, y, self.set_playlist_advance, _("Play next playlist")) + y += 25 * gui.scale + self.toggle_square(x, y, self.set_playlist_cycle, _("Cycle all playlists")) + + elif self.func_page == 2: + y += 23 * gui.scale + # ddt.text((x, y), _("Auto download monitor and archive extractor"), colours.box_text_label, 11) + # y += 25 * gui.scale + self.toggle_square( + x, y, toggle_extract, _("Extract archives"), + subtitle=_("Extracts zip archives on drag and drop")) + y += 38 * gui.scale + self.toggle_square( + x + 10 * gui.scale, y, toggle_dl_mon, _("Enable download monitor"), + subtitle=_("One click import new archives and folders from downloads folder")) + y += 38 * gui.scale + self.toggle_square(x + 10 * gui.scale, y, toggle_ex_del, _("Trash archive after extraction")) + y += 23 * gui.scale + self.toggle_square(x + 10 * gui.scale, y, toggle_music_ex, _("Always extract to Music folder")) + + y += 38 * gui.scale + if not msys: + self.toggle_square(x, y, toggle_use_tray, _("Show icon in system tray")) + + y += 25 * gui.scale + self.toggle_square(x + round(10 * gui.scale), y, toggle_min_tray, _("Close to tray")) + + y += 25 * gui.scale + self.toggle_square(x + round(10 * gui.scale), y, toggle_text_tray, _("Show title text")) + + old = prefs.tray_theme + if not self.toggle_square(x + round(190 * gui.scale), y, prefs.tray_theme == "gray", _("Monochrome")): + prefs.tray_theme = "pink" + else: + prefs.tray_theme = "gray" + if prefs.tray_theme != old: + tauon.set_tray_icons(force=True) + show_message(_("Restart Tauon for change to take effect")) + + else: + self.toggle_square(x, y, toggle_min_tray, _("Close to tray")) + + + + elif self.func_page == 4: + y += 23 * gui.scale + prefs.use_gamepad = self.toggle_square( + x, y, prefs.use_gamepad, _("Enable use of gamepad as input"), + subtitle=_("Change requires restart")) + y += 37 * gui.scale + + elif self.func_page == 3: + y += 23 * gui.scale + old = prefs.enable_remote + prefs.enable_remote = self.toggle_square( + x, y, prefs.enable_remote, _("Enable remote control"), + subtitle=_("Change requires restart")) + y += 37 * gui.scale + + if prefs.enable_remote and prefs.enable_remote != old: + show_message( + _("Notice: This API is not security hardened."), + _("Only enable in a trusted LAN and do not expose port (7814) to the internet"), + mode="warning") + + old = prefs.block_suspend + prefs.block_suspend = self.toggle_square( + x, y, prefs.block_suspend, _("Block suspend"), + subtitle=_("Prevent system suspend during playback")) + y += 37 * gui.scale + old = prefs.block_suspend + prefs.resume_play_wake = self.toggle_square( + x, y, prefs.resume_play_wake, _("Resume from suspend"), + subtitle=_("Continue playback when waking from sleep")) + + y += 37 * gui.scale + old = prefs.auto_rec + prefs.auto_rec = self.toggle_square( + x, y, prefs.auto_rec, _("Record Radio"), + subtitle=_("Record and split songs when playing internet radio")) + if prefs.auto_rec != old and prefs.auto_rec: + show_message( + _("Tracks will now be recorded. Restart any playback for change to take effect."), + _("Tracks will be saved to \"Saved Radio Tracks\" playlist."), + mode="info") + + if tauon.update_play_lock is None: + prefs.block_suspend = False + # if flatpak_mode: + # show_message("Sandbox support not implemented") + elif old != prefs.block_suspend: + tauon.update_play_lock() + + y += 37 * gui.scale + ddt.text((x, y), "Discord", colours.box_text_label, 11) + y += 25 * gui.scale + old = prefs.discord_enable + prefs.discord_enable = self.toggle_square(x, y, prefs.discord_enable, _("Enable Discord Rich Presence")) + + if flatpak_mode: + if self.button(x + 215 * gui.scale, y, _("?")): + show_message( + _("For troubleshooting Discord RP"), + "https://github.com/Taiko2k/TauonMusicBox/wiki/Discord-RP", mode="link") + + if prefs.discord_enable and not old: + if snap_mode: + show_message(_("Sorry, this feature is unavailable with snap"), mode="error") + prefs.discord_enable = False + elif not discord_allow: + show_message(_("Missing dependency python-pypresence")) + prefs.discord_enable = False + else: + hit_discord() + + if old and not prefs.discord_enable: + if prefs.discord_active: + prefs.disconnect_discord = True + + y += 22 * gui.scale + text = _("Disabled") + if prefs.discord_enable: + text = gui.discord_status + ddt.text((x, y), _("Status: {state}").format(state=text), colours.box_text, 11) + + # Switcher + pages = 5 + x = x0 + round(18 * gui.scale) + y = (y0 + h0) - round(29 * gui.scale) + ww = round(40 * gui.scale) + + for p in range(pages): + if self.button2(x, y, str(p + 1), width=ww, center_text=True, force_on=self.func_page == p): + self.func_page = p + x += ww + + # self.button(x, y, _("Open keymap file"), open_keymap_file, width=wc) + + def button(self, x, y, text, plug=None, width=0, bg=None): + + w = width + if w == 0: + w = ddt.get_text_w(text, 211) + round(10 * gui.scale) + + h = round(20 * gui.scale) + border_size = round(2 * gui.scale) + + rect = (round(x), round(y), round(w), round(h)) + rect2 = (rect[0] - border_size, rect[1] - border_size, rect[2] + border_size * 2, rect[3] + border_size * 2) + + if bg is None: + bg = colours.box_background + + real_bg = bg + hit = False + + ddt.rect(rect2, colours.box_check_border) + ddt.rect(rect, bg) + + fields.add(rect) + if coll(rect): + ddt.rect(rect, [255, 255, 255, 15]) + real_bg = alpha_blend([255, 255, 255, 15], bg) + ddt.text((x + int(w / 2), rect[1] + 1 * gui.scale, 2), text, colours.box_title_text, 211, bg=real_bg) + if self.click: + hit = True + if plug is not None: + plug() + else: + ddt.text((x + int(w / 2), rect[1] + 1 * gui.scale, 2), text, colours.box_sub_text, 211, bg=real_bg) + + return hit + + def button2(self, x, y, text, width=0, center_text=False, force_on=False): + w = width + if w == 0: + w = ddt.get_text_w(text, 211) + 10 * gui.scale + rect = (x, y, w, 20 * gui.scale) + + bg_colour = colours.box_button_background + real_bg = bg_colour + + ddt.rect(rect, bg_colour) + fields.add(rect) + hit = False + + text_position = (x + int(7 * gui.scale), rect[1] + 1 * gui.scale) + if center_text: + text_position = (x + rect[2] // 2, rect[1] + 1 * gui.scale, 2) + + if coll(rect) or force_on: + ddt.rect(rect, colours.box_button_background_highlight) + bg_colour = colours.box_button_background + real_bg = alpha_blend(colours.box_button_background_highlight, bg_colour) + ddt.text(text_position, text, colours.box_button_text_highlight, 211, bg=real_bg) + if self.click and not force_on: + hit = True + else: + ddt.text(text_position, text, colours.box_button_text, 211, bg=real_bg) + return hit + + def toggle_square(self, x, y, function, text: str , click: bool = False, subtitle: str = "") -> bool: + + x = round(x) + y = round(y) + + border = round(2 * gui.scale) + gap = round(2 * gui.scale) + inner_square = round(6 * gui.scale) + + full_w = border * 2 + gap * 2 + inner_square + + if subtitle: + le = ddt.text((x + 20 * gui.scale, y - 1 * gui.scale), text, colours.box_text, 13) + se = ddt.text((x + 20 * gui.scale, y + 14 * gui.scale), subtitle, colours.box_text_label, 13) + hit_rect = (x - 10 * gui.scale, y - 3 * gui.scale, max(le, se) + 30 * gui.scale, 34 * gui.scale) + y += round(8 * gui.scale) + + else: + le = ddt.text((x + 20 * gui.scale, y - 1 * gui.scale), text, colours.box_text, 13) + hit_rect = (x - 10 * gui.scale, y - 3 * gui.scale, le + 30 * gui.scale, 22 * gui.scale) + + # Border outline + ddt.rect_a((x, y), (full_w, full_w), colours.box_check_border) + # Inner background + ddt.rect_a( + (x + border, y + border), (gap * 2 + inner_square, gap * 2 + inner_square), + alpha_blend([255, 255, 255, 14], colours.box_background)) + + # Check if box clicked + clicked = False + if (self.click or click) and coll(hit_rect): + clicked = True + + # There are two mode, function type, and passthrough bool type + active = False + if type(function) is bool: + active = function + else: + active = function(1) + + if clicked: + if type(function) is bool: + active ^= True + else: + function() + active = function(1) + + # Draw inner check mark if enabled + if active: + ddt.rect_a((x + border + gap, y + border + gap), (inner_square, inner_square), colours.toggle_box_on) + + return active + + def last_fm_box(self, x0, y0, w0, h0): + + x = x0 + round(20 * gui.scale) + y = y0 + round(15 * gui.scale) + + ddt.text_background_colour = colours.box_background + + text = "Last.fm" + if prefs.use_libre_fm: + text = "Libre.fm" + if self.button2(x, y, text, width=84 * gui.scale): + self.account_view = 1 + self.toggle_square(x + 105 * gui.scale, y + 2 * gui.scale, toggle_lfm_auto, _("Enable")) + + y += 28 * gui.scale + + if self.button2(x, y, "ListenBrainz", width=84 * gui.scale): + self.account_view = 2 + self.toggle_square(x + 105 * gui.scale, y + 2 * gui.scale, toggle_lb, _("Enable")) + + y += 28 * gui.scale + + if self.button2(x, y, "Maloja", width=84 * gui.scale): + self.account_view = 9 + self.toggle_square(x + 105 * gui.scale, y + 2 * gui.scale, toggle_maloja, _("Enable")) + + # if self.button2(x, y, "Discogs", width=84*gui.scale): + # self.account_view = 3 + + y += 28 * gui.scale + + if self.button2(x, y, "fanart.tv", width=84 * gui.scale): + self.account_view = 4 + + y += 28 * gui.scale + y += 28 * gui.scale + + y += 15 * gui.scale + + if key_shift_down and self.button2(x + round(95 * gui.scale), y, "koel", width=84 * gui.scale): + self.account_view = 6 + + if self.button2(x, y, "Jellyfin", width=84 * gui.scale): + self.account_view = 10 + + if self.button2(x + round(95 * gui.scale), y, "TIDAL", width=84 * gui.scale): + self.account_view = 12 + + y += 28 * gui.scale + + if self.button2(x, y, "Airsonic", width=84 * gui.scale): + self.account_view = 7 + + if self.button2(x + round(95 * gui.scale), y, "PLEX", width=84 * gui.scale): + self.account_view = 5 + + y += 28 * gui.scale + + if self.button2(x, y, "Spotify", width=84 * gui.scale): + self.account_view = 8 + + if self.button2(x + round(95 * gui.scale), y, "Satellite", width=84 * gui.scale): + self.account_view = 11 + + if self.account_view in (9, 2): + self.toggle_square( + x0 + 230 * gui.scale, y + 2 * gui.scale, toggle_scrobble_mark, + _("Show threshold marker")) + + x = x0 + 230 * gui.scale + y = y0 + round(20 * gui.scale) + + if self.account_view == 12: + ddt.text((x, y), "TIDAL", colours.box_sub_text, 213) + + y += round(30 * gui.scale) + + if os.path.isfile(tauon.tidal.save_path): + if self.button2(x, y, _("Logout"), width=84 * gui.scale): + tauon.tidal.logout() + elif tauon.tidal.login_stage == 0: + if self.button2(x, y, _("Login"), width=84 * gui.scale): + # webThread = threading.Thread(target=authserve, args=[tauon]) + # webThread.daemon = True + # webThread.start() + # time.sleep(0.1) + tauon.tidal.login1() + else: + ddt.text( + (x + 0 * gui.scale, y), _("Copy the full URL of the resulting 'oops' page"), colours.box_text_label, 11) + y += round(25 * gui.scale) + if self.button2(x, y, _("Paste Redirect URL"), width=84 * gui.scale): + text = copy_from_clipboard() + if text: + tauon.tidal.login2(text) + + if os.path.isfile(tauon.tidal.save_path): + y += round(30 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Paste TIDAL URL's into Tauon using ctrl+v"), colours.box_text_label, 11) + y += round(30 * gui.scale) + if self.button(x, y, _("Import Albums")): + show_message(_("Fetching playlist...")) + shooter(tauon.tidal.fav_albums) + + y += round(30 * gui.scale) + if self.button(x, y, _("Import Tracks")): + show_message(_("Fetching playlist...")) + shooter(tauon.tidal.fav_tracks) + + if self.account_view == 11: + ddt.text((x, y), "Tauon Satellite", colours.box_sub_text, 213) + + y += round(30 * gui.scale) + + field_width = round(245 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("IP"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 0 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_sat_url.text = prefs.sat_url + text_sat_url.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.sat_url = text_sat_url.text.strip() + + y += round(25 * gui.scale) + + y += round(30 * gui.scale) + + field_width = round(245 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Playlist name"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 1 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_sat_playlist.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, + width=rect1[2] - 8 * gui.scale, click=self.click) + + y += round(25 * gui.scale) + + if self.button(x, y, _("Get playlist")): + if tau.processing: + show_message(_("An operation is already running")) + else: + shooter(tau.get_playlist()) + + elif self.account_view == 9: + + ddt.text((x, y), _("Maloja Server"), colours.box_sub_text, 213) + if self.button(x + 260 * gui.scale, y, _("?")): + show_message( + _("Maloja is a self-hosted scrobble server."), + _("See here to learn more: {link}").format(link="https://github.com/krateng/maloja"), mode="link") + + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 + + field_width = round(245 * gui.scale) + + y += round(25 * gui.scale) + ddt.text( + (x + 0 * gui.scale, y), _("Server URL"), + colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 0 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_maloja_url.text = prefs.maloja_url + text_maloja_url.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.maloja_url = text_maloja_url.text.strip() + + y += round(23 * gui.scale) + ddt.text( + (x + 0 * gui.scale, y), _("API Key"), + colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 1 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_maloja_key.text = prefs.maloja_key + text_maloja_key.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.maloja_key = text_maloja_key.text.strip() + + y += round(35 * gui.scale) + + if self.button(x, y, _("Test connectivity")): + + if not prefs.maloja_url or not prefs.maloja_key: + show_message(_("One or more fields is missing.")) + else: + url = prefs.maloja_url + if not url.endswith("/mlj_1"): + if not url.endswith("/"): + url += "/" + url += "apis/mlj_1" + url += "/test" + + try: + r = requests.get(url, params={"key": prefs.maloja_key}, timeout=10) + if r.status_code == 403: + show_message(_("Connection appeared successful but the API key was invalid"), mode="warning") + elif r.status_code == 200: + show_message(_("Connection to Maloja server was successful."), mode="done") + else: + show_message(_("The Maloja server returned an error"), r.text, mode="warning") + except Exception: + logging.exception("Could not communicate with the Maloja server") + show_message(_("Could not communicate with the Maloja server"), mode="warning") + + y += round(30 * gui.scale) + + ws = ddt.get_text_w(_("Get scrobble counts"), 211) + 10 * gui.scale + wcc = ddt.get_text_w(_("Clear"), 211) + 15 * gui.scale + if self.button(x, y, _("Get scrobble counts")): + shooter(maloja_get_scrobble_counts) + self.button(x + ws + round(12 * gui.scale), y, _("Clear"), self.clear_scrobble_counts, width=wcc) + + if self.account_view == 8: + + ddt.text((x, y), "Spotify", colours.box_sub_text, 213) + + prefs.spot_mode = self.toggle_square(x + 80 * gui.scale, y + 2 * gui.scale, prefs.spot_mode, _("Enable")) + y += round(30 * gui.scale) + + if self.button(x, y, _("View setup instructions")): + webbrowser.open("https://github.com/Taiko2k/Tauon/wiki/Spotify", new=2, autoraise=True) + + field_width = round(245 * gui.scale) + + y += round(26 * gui.scale) + + ddt.text( + (x + 0 * gui.scale, y), _("Client ID"), + colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 0 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_spot_client.text = prefs.spot_client + text_spot_client.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.spot_client = text_spot_client.text.strip() + + y += round(19 * gui.scale) + ddt.text( + (x + 0 * gui.scale, y), _("Client Secret"), + colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 1 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_spot_secret.text = prefs.spot_secret + text_spot_secret.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.spot_secret = text_spot_secret.text.strip() + + y += round(27 * gui.scale) + + if prefs.spotify_token: + if self.button(x, y, _("Forget Account")): + tauon.spot_ctl.delete_token() + tauon.spot_ctl.cache_saved_albums.clear() + prefs.spot_username = "" + if not prefs.launch_spotify_local: + prefs.spot_password = "" + elif self.button(x, y, _("Authorise")): + webThread = threading.Thread(target=authserve, args=[tauon]) + webThread.daemon = True + webThread.start() + time.sleep(0.1) + + tauon.spot_ctl.auth() + + y += round(31 * gui.scale) + prefs.launch_spotify_web = self.toggle_square( + x, y, prefs.launch_spotify_web, + _("Prefer launching web player")) + + y += round(24 * gui.scale) + + old = prefs.launch_spotify_local + prefs.launch_spotify_local = self.toggle_square( + x, y, prefs.launch_spotify_local, + _("Enable local audio playback")) + + if prefs.launch_spotify_local and not tauon.enable_librespot: + show_message(_("Librespot not installed?")) + prefs.launch_spotify_local = False + + + if self.account_view == 7: + + ddt.text((x, y), _("Airsonic/Subsonic network streaming"), colours.box_sub_text, 213) + + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 + + field_width = round(245 * gui.scale) + + y += round(25 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Username / Email"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 0 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_air_usr.text = prefs.subsonic_user + text_air_usr.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.subsonic_user = text_air_usr.text + + y += round(23 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Password"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 1 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_air_pas.text = prefs.subsonic_password + text_air_pas.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, + width=rect1[2] - 8 * gui.scale, click=self.click, secret=True) + prefs.subsonic_password = text_air_pas.text + + y += round(23 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Server URL"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 2 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_air_ser.text = prefs.subsonic_server + text_air_ser.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 2, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.subsonic_server = text_air_ser.text + + y += round(40 * gui.scale) + self.button(x, y, _("Import music to playlist"), sub_get_album_thread) + + y += round(35 * gui.scale) + prefs.subsonic_password_plain = self.toggle_square( + x, y, prefs.subsonic_password_plain, + _("Use plain text authentication"), + subtitle=_("Needed for Nextcloud Music")) + + if self.account_view == 10: + + ddt.text((x, y), _("Jellyfin network streaming"), colours.box_sub_text, 213) + + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 + + field_width = round(245 * gui.scale) + + y += round(25 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Username"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 0 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_jelly_usr.text = prefs.jelly_username + text_jelly_usr.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.jelly_username = text_jelly_usr.text + + y += round(23 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Password"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 1 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_jelly_pas.text = prefs.jelly_password + text_jelly_pas.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, + width=rect1[2] - 8 * gui.scale, click=self.click, secret=True) + prefs.jelly_password = text_jelly_pas.text + + y += round(23 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Server URL"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 2 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_jelly_ser.text = prefs.jelly_server_url + text_jelly_ser.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 2, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.jelly_server_url = text_jelly_ser.text + + y += round(30 * gui.scale) + + self.button(x, y, _("Import music to playlist"), jellyfin_get_library_thread) + + y += round(30 * gui.scale) + if self.button(x, y, _("Import playlists")): + found = False + for item in pctl.gen_codes.values(): + if item.startswith("jelly"): + found = True + break + if not found: + gui.show_message(_("Run music import first")) + else: + jellyfin_get_playlists_thread() + + y += round(35 * gui.scale) + if self.button(x, y, _("Test connectivity")): + jellyfin.test() + + if self.account_view == 6: + + ddt.text((x, y), _("koel network streaming"), colours.box_sub_text, 213) + + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 + + field_width = round(245 * gui.scale) + + y += round(25 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Username / Email"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 0 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_koel_usr.text = prefs.koel_username + text_koel_usr.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.koel_username = text_koel_usr.text + + y += round(23 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Password"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 1 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_koel_pas.text = prefs.koel_password + text_koel_pas.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, + width=rect1[2] - 8 * gui.scale, click=self.click, secret=True) + prefs.koel_password = text_koel_pas.text + + y += round(23 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Server URL"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 2 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_koel_ser.text = prefs.koel_server_url + text_koel_ser.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 2, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.koel_server_url = text_koel_ser.text + + y += round(40 * gui.scale) + + self.button(x, y, _("Import music to playlist"), koel_get_album_thread) + + if self.account_view == 5: + + ddt.text((x, y), _("PLEX network streaming"), colours.box_sub_text, 213) + + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 + + field_width = round(245 * gui.scale) + + y += round(25 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Username / Email"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 0 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_plex_usr.text = prefs.plex_username + text_plex_usr.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 0, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.plex_username = text_plex_usr.text + + y += round(23 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Password"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 1 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_plex_pas.text = prefs.plex_password + text_plex_pas.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 1, + width=rect1[2] - 8 * gui.scale, click=self.click, secret=True) + prefs.plex_password = text_plex_pas.text + + y += round(23 * gui.scale) + ddt.text((x + 0 * gui.scale, y), _("Server name"), colours.box_text_label, 11) + y += round(19 * gui.scale) + rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) + fields.add(rect1) + if coll(rect1) and (self.click or level_2_right_click): + self.account_text_field = 2 + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + text_plex_ser.text = prefs.plex_servername + text_plex_ser.draw( + x + round(4 * gui.scale), y, colours.box_input_text, self.account_text_field == 2, + width=rect1[2] - 8 * gui.scale, click=self.click) + prefs.plex_servername = text_plex_ser.text + + y += round(40 * gui.scale) + self.button(x, y, _("Import music to playlist"), plex_get_album_thread) + + if self.account_view == 4: + + ddt.text((x, y), "fanart.tv", colours.box_sub_text, 213) + + y += 25 * gui.scale + ddt.text( + (x + 0 * gui.scale, y, 4, 270 * gui.scale, 600), + _("Fanart.tv can be used for sourcing of artist images and cover art."), + colours.box_text_label, 11) + y += 17 * gui.scale + + y += 22 * gui.scale + # . Limited space available. Limit 55 chars + link_pa2 = draw_linked_text( + (x + 0 * gui.scale, y), + _("They encourage you to contribute at {link}").format(link="https://fanart.tv"), + colours.box_text_label, 11) + link_activate(x, y, link_pa2) + + y += 35 * gui.scale + prefs.enable_fanart_cover = self.toggle_square( + x, y, prefs.enable_fanart_cover, + _("Cover art (Manual only)")) + y += 25 * gui.scale + prefs.enable_fanart_artist = self.toggle_square( + x, y, prefs.enable_fanart_artist, + _("Artist images (Automatic)")) + #y += 25 * gui.scale + # prefs.enable_fanart_bg = self.toggle_square(x, y, prefs.enable_fanart_bg, + # _("Artist backgrounds (Automatic)")) + y += 25 * gui.scale + x += 23 * gui.scale + if self.button(x, y, _("Flip current")): + if key_shift_down: + prefs.bg_flips.clear() + show_message(_("Reset flips"), mode="done") + else: + tr = pctl.playing_object() + artist = get_artist_safe(tr) + if artist: + if artist not in prefs.bg_flips: + prefs.bg_flips.add(artist) + else: + prefs.bg_flips.remove(artist) + style_overlay.flush() + show_message(_("OK"), mode="done") + + # if self.account_view == 3: + # + # ddt.text((x, y), 'Discogs', colours.box_sub_text, 213) + # + # y += 25 * gui.scale + # hh = ddt.text((x + 0 * gui.scale, y, 4, 260 * gui.scale, 300 * gui.scale), _("Discogs can be used for sourcing artist images. For this you will need a \"Personal Access Token\".\n\nYou can generate one with a Discogs account here:"), + # colours.box_text_label, 11) + # + # + # y += hh + # #y += 15 * gui.scale + # link_pa2 = draw_linked_text((x + 0 * gui.scale, y), "https://www.discogs.com/settings/developers",colours.box_text_label, 12) + # link_rect2 = [x + 0 * gui.scale, y, link_pa2[1], 20 * gui.scale] + # fields.add(link_rect2) + # if coll(link_rect2): + # if not self.click: + # gui.cursor_want = 3 + # if self.click: + # webbrowser.open(link_pa2[2], new=2, autoraise=True) + # + # y += 40 * gui.scale + # if self.button(x, y, _("Paste Token")): + # + # text = copy_from_clipboard() + # if text == "": + # show_message(_("There is no text in the clipboard", mode='error') + # elif len(text) == 40: + # prefs.discogs_pat = text + # + # # Reset caches ------------------- + # prefs.failed_artists.clear() + # artist_list_box.to_fetch = "" + # for key, value in artist_list_box.thumb_cache.items(): + # if value: + # SDL_DestroyTexture(value[0]) + # artist_list_box.thumb_cache.clear() + # artist_list_box.to_fetch = "" + # + # direc = os.path.join(a_cache_dir) + # if os.path.isdir(direc): + # for item in os.listdir(direc): + # if "-lfm.txt" in item: + # os.remove(os.path.join(direc, item)) + # # ----------------------------------- + # + # else: + # show_message(_("That is not a valid token", mode='error') + # y += 30 * gui.scale + # if self.button(x, y, _("Clear")): + # if not prefs.discogs_pat: + # show_message(_("There wasn't any token saved.") + # prefs.discogs_pat = "" + # save_prefs() + # + # y += 30 * gui.scale + # if prefs.discogs_pat: + # ddt.text((x + 0 * gui.scale, y - 0 * gui.scale), prefs.discogs_pat, colours.box_input_text, 211) + # + + if self.account_view == 1: + + text = "Last.fm" + if prefs.use_libre_fm: + text = "Libre.fm" + + ddt.text((x, y), text, colours.box_sub_text, 213) + + ww = ddt.get_text_w(_("Username:"), 212) + ddt.text((x + 65 * gui.scale, y - 0 * gui.scale), _("Username:"), colours.box_text_label, 212) + ddt.text( + (x + ww + 65 * gui.scale + 7 * gui.scale, y - 0 * gui.scale), prefs.last_fm_username, + colours.box_sub_text, 213) + + y += 25 * gui.scale + + if prefs.last_fm_token is None: + ww = ddt.get_text_w(_("Login"), 211) + 10 * gui.scale + ww2 = ddt.get_text_w(_("Done"), 211) + 40 * gui.scale + self.button(x, y, _("Login"), lastfm.auth1) + self.button(x + ww + 10 * gui.scale, y, _("Done"), lastfm.auth2) + + if prefs.last_fm_token is None and lastfm.url is None: + prefs.use_libre_fm = self.toggle_square( + x + ww + ww2, y + round(1 * gui.scale), prefs.use_libre_fm, _("Use LibreFM")) + + y += 25 * gui.scale + ddt.text( + (x + 2 * gui.scale, y, 4, 270 * gui.scale, 300 * gui.scale), + _("Click login to open the last.fm web authorisation page and follow prompt. Then return here and click \"Done\"."), + colours.box_text_label, 11, max_w=270 * gui.scale) + + else: + self.button(x, y, _("Forget account"), lastfm.auth3) + + x = x0 + 230 * gui.scale + y = y0 + round(130 * gui.scale) + + # self.toggle_square(x, y, toggle_scrobble_mark, "Show scrobble marker") + + wa = ddt.get_text_w(_("Get user loves"), 211) + 10 * gui.scale + wb = ddt.get_text_w(_("Clear local loves"), 211) + 10 * gui.scale + wc = ddt.get_text_w(_("Get friend loves"), 211) + 10 * gui.scale + ws = ddt.get_text_w(_("Get scrobble counts"), 211) + 10 * gui.scale + wcc = ddt.get_text_w(_("Clear"), 211) + 15 * gui.scale + # wd = ddt.get_text_w(_("Clear friend loves"),211) + 10 * gui.scale + ww = max(wa, wb, wc, ws) + + self.button(x, y, _("Get user loves"), self.get_user_love, width=ww) + self.button(x + ww + round(12 * gui.scale), y, _("Clear"), self.clear_local_loves, width=wcc) + + # y += 26 * gui.scale + # self.button(x, y, _("Clear local loves"), self.clear_local_loves, width=ww) + + y += 26 * gui.scale + + self.button(x, y, _("Get friend loves"), self.get_friend_love, width=ww) + self.button(x + ww + round(12 * gui.scale), y, _("Clear"), lastfm.clear_friends_love, width=wcc) + + y += 26 * gui.scale + self.button(x, y, _("Get scrobble counts"), self.get_scrobble_counts, width=ww) + self.button(x + ww + round(12 * gui.scale), y, _("Clear"), self.clear_scrobble_counts, width=wcc) + + + y += 33 * gui.scale + + old = prefs.lastfm_pull_love + prefs.lastfm_pull_love = self.toggle_square( + x, y, prefs.lastfm_pull_love, + _("Pull love on scrobble/rescan")) + if old != prefs.lastfm_pull_love and prefs.lastfm_pull_love: + show_message(_("Note that this will overwrite the local loved status if different to last.fm status")) + + y += 25 * gui.scale + + self.toggle_square( + x, y, toggle_scrobble_mark, + _("Show threshold marker")) + + if self.account_view == 2: + + ddt.text((x, y), "ListenBrainz", colours.box_sub_text, 213) + + y += 30 * gui.scale + self.button(x, y, _("Paste Token"), lb.paste_key) + + self.button(x + ddt.get_text_w(_("Paste Token"), 211) + 21 * gui.scale, y, _("Clear"), lb.clear_key) + + y += 35 * gui.scale + + if prefs.lb_token: + line = prefs.lb_token + ddt.text((x + 0 * gui.scale, y - 0 * gui.scale), line, colours.box_input_text, 212) + + y += 25 * gui.scale + link_pa2 = draw_linked_text((x + 0 * gui.scale, y), "https://listenbrainz.org/profile/", + colours.box_sub_text, 12) + link_rect2 = [x + 0 * gui.scale, y, link_pa2[1], 20 * gui.scale] + fields.add(link_rect2) + + if coll(link_rect2): + if not self.click: + gui.cursor_want = 3 + + if self.click: + webbrowser.open(link_pa2[2], new=2, autoraise=True) + + def clear_local_loves(self): + + if not key_shift_down: + show_message( + _("This will mark all tracks in local database as unloved!"), + _("Press button again while holding shift key if you're sure you want to do that."), + mode="warning") + return + + for key, star in star_store.db.items(): + star[1] = star[1].replace("L", "") + star_store.db[key] = star + + gui.pl_update += 1 + show_message(_("Cleared all loves"), mode="done") + + def get_scrobble_counts(self): + + if not key_shift_down: + t = lastfm.get_all_scrobbles_estimate_time() + if not t: + show_message(_("Error, not connected to last.fm")) + return + show_message( + _("Warning: This process will take approximately {T} minutes to complete.").format(T=(t // 60)), + _("Press again while holding Shift if you understand"), mode="warning") + return + + if not lastfm.scanning_friends and not lastfm.scanning_scrobbles and not lastfm.scanning_loves: + shoot_dl = threading.Thread(target=lastfm.get_all_scrobbles) + shoot_dl.daemon = True + shoot_dl.start() + else: + show_message(_("A process is already running. Wait for it to finish.")) + + def clear_scrobble_counts(self): + + for track in pctl.master_library.values(): + track.lfm_scrobbles = 0 + + show_message(_("Cleared all scrobble counts"), mode="done") + + def get_friend_love(self): + + if not key_shift_down: + show_message( + _("Warning: This process can take a long time to complete! (up to an hour or more)"), + _("This feature is not recommended for accounts that have many friends."), + _("Press again while holding Shift if you understand"), mode="warning") + return + + if not lastfm.scanning_friends and not lastfm.scanning_scrobbles and not lastfm.scanning_loves: + logging.info("Launch friend love thread") + shoot_dl = threading.Thread(target=lastfm.get_friends_love) + shoot_dl.daemon = True + shoot_dl.start() + else: + show_message(_("A process is already running. Wait for it to finish.")) + + def get_user_love(self): + + if not lastfm.scanning_friends and not lastfm.scanning_scrobbles and not lastfm.scanning_loves: + shoot_dl = threading.Thread(target=lastfm.dl_love) + shoot_dl.daemon = True + shoot_dl.start() + else: + show_message(_("A process is already running. Wait for it to finish.")) + + def codec_config(self, x0, y0, w0, h0): + + x = x0 + round(25 * gui.scale) + y = y0 + + y += 20 * gui.scale + ddt.text_background_colour = colours.box_background + + if self.sync_view: + + pl = None + if prefs.sync_playlist: + pl = id_to_pl(prefs.sync_playlist) + if pl is None: + prefs.sync_playlist = None + + y += 5 * gui.scale + if prefs.sync_playlist: + ww = ddt.text((x, y), _("Selected playlist:") + " ", colours.box_text_label, 11) + ddt.text((x + ww, y), pctl.multi_playlist[pl].title, colours.box_sub_text, 12, 400 * gui.scale) + else: + ddt.text((x, y), _("No sync playlist selected!"), colours.box_text_label, 11) + + y += 25 * gui.scale + ww = ddt.text((x, y), _("Path to device music folder: "), colours.box_text_label, 11) + y += 20 * gui.scale + + rect1 = (x + 0 * gui.scale, y, round(450 * gui.scale), round(17 * gui.scale)) + fields.add(rect1) + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + sync_target.draw( + x + round(4 * gui.scale), y, colours.box_input_text, not gui.sync_progress, + width=rect1[2] - 8 * gui.scale, click=self.click) + + rect = [x + rect1[2] + 11 * gui.scale, y - 2 * gui.scale, 15 * gui.scale, 19 * gui.scale] + fields.add(rect) + colour = colours.box_text_label + if coll(rect): + colour = [225, 160, 0, 255] + if self.click: + paths = auto_get_sync_targets() + if paths: + sync_target.text = paths[0] + show_message(_("A mounted music folder was found!"), mode="done") + else: + show_message( + _("Could not auto-detect mounted device path."), + _("Make sure the device is mounted and path is accessible.")) + + power_bar_icon.render(rect[0], rect[1], colour) + y += 30 * gui.scale + + prefs.sync_deletes = self.toggle_square(x, y, prefs.sync_deletes, _("Delete all other folders in target")) + y += 25 * gui.scale + prefs.bypass_transcode = self.toggle_square( + x, y, prefs.bypass_transcode ^ True, + _("Transcode files")) ^ True + y += 25 * gui.scale + prefs.smart_bypass = self.toggle_square( + x + round(10 * gui.scale), y, prefs.smart_bypass ^ True, + _("Bypass low bitrate")) ^ True + y += 30 * gui.scale + + text = _("Start Transcode and Sync") + ww = ddt.get_text_w(text, 211) + 25 * gui.scale + if prefs.bypass_transcode: + text = _("Start Sync") + + xx = (rect1[0] + (rect1[2] // 2)) - (ww // 2) + if gui.stop_sync: + self.button(xx, y, _("Stopping..."), width=ww) + elif not gui.sync_progress: + if self.button(xx, y, text, width=ww): + if pl is not None: + auto_sync(pl) + else: + show_message( + _("Select a source playlist"), + _("Right click tab > Misc... > Set as sync playlist")) + elif self.button(xx, y, _("Stop"), width=ww): + gui.stop_sync = True + gui.sync_progress = _("Aborting Sync") + + y += 60 * gui.scale + + if self.button(x, y, _("Return"), width=round(75 * gui.scale)): + self.sync_view = False + + if self.button(x + 485 * gui.scale, y, _("?")): + show_message( + _("See here for detailed instructions"), + "https://github.com/Taiko2k/Tauon/wiki/Transcode-and-Sync", mode="link") + + return + + # ---------- + + ddt.text((x, y + 13 * gui.scale), _("Output codec setting:"), colours.box_text_label, 11) + + ww = ddt.get_text_w(_("Open output folder"), 211) + 25 * gui.scale + self.button(x0 + w0 - ww, y - 4 * gui.scale, _("Open output folder"), open_encode_out) + + ww = ddt.get_text_w(_("Sync..."), 211) + 25 * gui.scale + if self.button(x0 + w0 - ww, y + 25 * gui.scale, _("Sync...")): + self.sync_view = True + + y += 40 * gui.scale + self.toggle_square(x, y, switch_flac, "FLAC") + y += 25 * gui.scale + self.toggle_square(x, y, switch_opus, "OPUS") + if prefs.transcode_codec == "opus": + self.toggle_square(x + 120 * gui.scale, y, switch_opus_ogg, _("Save opus as .ogg extension")) + y += 25 * gui.scale + self.toggle_square(x, y, switch_ogg, "OGG Vorbis") + y += 25 * gui.scale + + # if not flatpak_mode: + self.toggle_square(x, y, switch_mp3, "MP3") + # if prefs.transcode_codec == 'mp3' and not shutil.which("lame"): + # ddt.draw_text((x + 90 * gui.scale, y - 3 * gui.scale), "LAME not detected!", [220, 110, 110, 255], 12) + + if prefs.transcode_codec != "flac": + y += 35 * gui.scale + + prefs.transcode_bitrate = self.slide_control(x, y, _("Bitrate"), "kbs", prefs.transcode_bitrate, 32, 320, 8) + + y -= 1 * gui.scale + x += 280 * gui.scale + + x = x0 + round(20 * gui.scale) + y = y0 + 215 * gui.scale + + self.toggle_square(x, y, toggle_transcode_output, _("Save to output folder")) + y += 25 * gui.scale + self.toggle_square(x, y, toggle_transcode_inplace, _("Save and overwrite files inplace")) + + def devance_theme(self): + global theme + + theme -= 1 + gui.reload_theme = True + if theme < 0: + theme = len(get_themes()) + + def config_b(self, x0, y0, w0, h0): + + global album_mode_art_size + global update_layout + + ddt.text_background_colour = colours.box_background + x = x0 + round(25 * gui.scale) + y = y0 + round(20 * gui.scale) + + # ddt.text((x, y), _("Window"),colours.box_text_label, 12) + + if system == "Linux": + self.toggle_square(x, y, toggle_notifications, _("Emit track change notifications")) + + y += 25 * gui.scale + self.toggle_square(x, y, toggle_borderless, _("Draw own window decorations")) + + # y += 25 * gui.scale + # prefs.save_window_position = self.toggle_square(x, y, prefs.save_window_position, + # _("Restore window position on restart")) + + y += 25 * gui.scale + if not draw_border: + self.toggle_square(x, y, toggle_titlebar_line, _("Show playing in titlebar")) + + #y += 25 * gui.scale + # if system != 'windows' and (flatpak_mode or snap_mode): + # self.toggle_square(x, y, toggle_force_subpixel, _("Enable RGB text antialiasing")) + + y += 25 * gui.scale + old = prefs.mini_mode_on_top + prefs.mini_mode_on_top = self.toggle_square(x, y, prefs.mini_mode_on_top, _("Mini-mode always on top")) + if wayland and prefs.mini_mode_on_top and prefs.mini_mode_on_top != old: + show_message(_("Always-on-top feature not yet implemented for Wayland mode"), _("You can enable the x11 setting below as a workaround")) + + y += 25 * gui.scale + self.toggle_square(x, y, toggle_level_meter, _("Top-panel visualiser")) + + y += 25 * gui.scale + if prefs.backend == 4: + self.toggle_square(x, y, toggle_showcase_vis, _("Showcase visualisation")) + + y += round(30 * gui.scale) + # if not msys: + # y += round(15 * gui.scale) + + ddt.text((x, y), _("UI scale for HiDPI displays"), colours.box_text_label, 12) + + y += round(25 * gui.scale) + + sw = round(200 * gui.scale) + sh = round(2 * gui.scale) + + slider = (x, y, sw, sh) + + gh = round(14 * gui.scale) + gw = round(8 * gui.scale) + grip = [0, y - (gh // 2), gw, gh] + + grip[0] = x + grip[0] += ((prefs.scale_want - 0.5) / 3 * sw) + + m1 = (x + ((1.0 - 0.5) / 3 * sw), y, sh, sh * 2) + m2 = (x + ((2.0 - 0.5) / 3 * sw), y, sh, sh * 2) + m3 = (x + ((3.0 - 0.5) / 3 * sw), y, sh, sh * 2) + + if coll(grow_rect(slider, round(16 * gui.scale))) and mouse_down: + prefs.scale_want = ((mouse_position[0] - x) / sw * 3) + 0.5 + prefs.x_scale = False + gui.update_on_drag = True + prefs.scale_want = max(prefs.scale_want, 0.5) + prefs.scale_want = min(prefs.scale_want, 3.5) + prefs.scale_want = round(round(prefs.scale_want / 0.05) * 0.05, 2) + if prefs.scale_want == 0.95 or prefs.scale_want == 1.05: + prefs.scale_want = 1.0 + if prefs.scale_want == 1.95 or prefs.scale_want == 2.05: + prefs.scale_want = 2.0 + if prefs.scale_want == 2.95 or prefs.scale_want == 3.05: + prefs.scale_want = 3.0 + + text = str(prefs.scale_want) + if len(text) == 3: + text += "0" + text += "x" + + if prefs.x_scale: + text = "auto" + + font = 13 + if not prefs.x_scale and (prefs.scale_want == 1.0 or prefs.scale_want == 2.0 or prefs.scale_want == 3.0): + font = 313 + + ddt.text((x + sw + round(14 * gui.scale), y - round(8 * gui.scale)), text, colours.box_sub_text, font) + # ddt.text((x + sw + round(14 * gui.scale), y + round(10 * gui.scale)), _("Restart app to apply any changes"), colours.box_text_label, 11) + + ddt.rect(slider, colours.box_text_border) + ddt.rect(m1, colours.box_text_border) + ddt.rect(m2, colours.box_text_border) + ddt.rect(m3, colours.box_text_border) + ddt.rect(grip, colours.box_text_label) + + y += round(23 * gui.scale) + self.toggle_square(x, y, self.toggle_x_scale, _("Auto scale")) + + if prefs.scale_want != gui.scale: + gui.update += 1 + if not mouse_down: + gui.update_layout() + + y += round(25 * gui.scale) + if not msys and not macos: + x11_path = str(user_directory / "x11") + x11 = os.path.exists(x11_path) + old = x11 + x11 = self.toggle_square(x, y, x11, _("Prefer x11 when running in Wayland")) + if old is False and x11 is True: + with open(x11_path, "a"): + pass + elif old is True and x11 is False: + os.remove(x11_path) + + def toggle_x_scale(self, mode=0): + if mode == 1: + return prefs.x_scale + prefs.x_scale ^= True + auto_scale() + gui.update_layout() + + def about(self, x0, y0, w0, h0): + + x = x0 + int(w0 * 0.3) - 10 * gui.scale + y = y0 + 85 * gui.scale + + ddt.text_background_colour = colours.box_background + + icon_rect = (x - 110 * gui.scale, y - 15 * gui.scale, self.about_image.w, self.about_image.h) + + genre = "" + if pctl.playing_object() is not None: + genre = pctl.playing_object().genre.lower() + + if any(s in genre for s in ["ock", "lt"]): + self.about_image2.render(icon_rect[0], icon_rect[1]) + elif any(s in genre for s in ["kpop", "k-pop", "anime"]): + self.about_image6.render(icon_rect[0], icon_rect[1]) + elif any(s in genre for s in ["syn", "pop"]): + self.about_image3.render(icon_rect[0], icon_rect[1]) + elif any(s in genre for s in ["tro", "cid"]): + self.about_image4.render(icon_rect[0], icon_rect[1]) + elif any(s in genre for s in ["uture"]): + self.about_image5.render(icon_rect[0], icon_rect[1]) + else: + genre = "" + + if not genre: + self.about_image.render(icon_rect[0], icon_rect[1]) + + x += 20 * gui.scale + y -= 10 * gui.scale + + self.title_image.render(x - 1, y, alpha_mod(colours.box_sub_text, 240)) + + credit_pages = 5 + + if self.click and coll(icon_rect) and self.ani_cred == 0: + self.ani_cred = 1 + self.ani_fade_on_timer.set() + + fade = 0 + + if self.ani_cred == 1: + t = self.ani_fade_on_timer.get() + fade = round(t / 0.7 * 255) + fade = min(fade, 255) + + if t > 0.7: + self.ani_cred = 2 + self.cred_page += 1 + if self.cred_page > credit_pages: + self.cred_page = 0 + self.ani_fade_on_timer.set() + + gui.update = 2 + + if self.ani_cred == 2: + + t = self.ani_fade_on_timer.get() + fade = 255 - round(t / 0.7 * 255) + fade = max(fade, 0) + if t > 0.7: + self.ani_cred = 0 + + gui.update = 2 + + y += 32 * gui.scale + + block_y = y - 10 * gui.scale + + if self.cred_page == 0: + + ddt.text((x, y - 6 * gui.scale), t_version, colours.box_text_label, 313) + y += 19 * gui.scale + ddt.text((x, y), "Copyright © 2015-2024 Taiko2k captain.gxj@gmail.com", colours.box_sub_text, 13) + + y += 19 * gui.scale + link_pa = draw_linked_text( + (x, y), "https://tauonmusicbox.rocks", colours.box_sub_text, 12, + replace="tauonmusicbox.rocks") + link_rect = [x, y, link_pa[1], 18 * gui.scale] + if coll(link_rect): + if not self.click: + gui.cursor_want = 3 + if self.click: + webbrowser.open(link_pa[2], new=2, autoraise=True) + + fields.add(link_rect) + + y += 27 * gui.scale + ddt.text((x, y), _("This program comes with absolutely no warranty."), colours.box_text_label, 12) + y += 16 * gui.scale + link_gpl = "https://www.gnu.org/licenses/gpl-3.0.html" + link_pa = draw_linked_text( + (x, y), _("See the {link} license for details.").format(link=link_gpl), + colours.box_text_label, 12, replace="GNU GPLv3+") + link_rect = [x + link_pa[0], y, link_pa[1], 18 * gui.scale] + if coll(link_rect): + if not self.click: + gui.cursor_want = 3 + if self.click: + webbrowser.open(link_pa[2], new=2, autoraise=True) + fields.add(link_rect) + + elif self.cred_page == 1: + + y += 15 * gui.scale + + ddt.text((x, y + 1 * gui.scale), _("Created by"), colours.box_text_label, 13) + ddt.text((x + 120 * gui.scale, y + 1 * gui.scale), "Taiko2k", colours.box_sub_text, 13) + + y += 40 * gui.scale + link_pa = draw_linked_text( + (x, y), "https://github.com/Taiko2k/Tauon/graphs/contributors", + colours.box_sub_text, 12, replace=_("Contributors")) + link_rect = [x, y, link_pa[1], 18 * gui.scale] + if coll(link_rect): + if not self.click: + gui.cursor_want = 3 + if self.click: + webbrowser.open(link_pa[2], new=2, autoraise=True) + fields.add(link_rect) + + + elif self.cred_page == 2: + xx = x + round(160 * gui.scale) + xxx = x + round(240 * gui.scale) + ddt.text((x, y), _("Open source software used"), colours.box_text_label, 13) + font = 12 + spacing = round(18 * gui.scale) + y += spacing + ddt.text((x, y), "Simple DirectMedia Layer", colours.box_sub_text, font) + ddt.text((xx, y), "zlib", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://www.libsdl.org/", colours.box_sub_text, font, click=self.click, replace="libsdl.org") + + y += spacing + ddt.text((x, y), "Cairo Graphics", colours.box_sub_text, font) + ddt.text((xx, y), "MPL", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://www.cairographics.org/", colours.box_sub_text, font, click=self.click, replace="cairographics.org") + + y += spacing + ddt.text((x, y), "Pango", colours.box_sub_text, font) + ddt.text((xx, y), "LGPL", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://pango.gnome.org/", colours.box_sub_text, font, click=self.click, replace="pango.gnome.org") + + y += spacing + ddt.text((x, y), "FFmpeg", colours.box_sub_text, font) + ddt.text((xx, y), "GPL", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://ffmpeg.org/", colours.box_sub_text, font, click=self.click, replace="ffmpeg.org") + + y += spacing + ddt.text((x, y), "Pillow", colours.box_sub_text, font) + ddt.text((xx, y), "PIL License", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://python-pillow.org/", colours.box_sub_text, font, click=self.click, replace="python-pillow.org") + + + elif self.cred_page == 4: + xx = x + round(140 * gui.scale) + xxx = x + round(240 * gui.scale) + ddt.text((x, y), _("Open source software used (cont'd)"), colours.box_text_label, 13) + font = 12 + spacing = round(18 * gui.scale) + y += spacing + ddt.text((x, y), "PySDL2", colours.box_sub_text, font) + ddt.text((xx, y), _("Public Domain"), colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://github.com/marcusva/py-sdl2", colours.box_sub_text, font, click=self.click, replace="github") + + y += spacing + ddt.text((x, y), "Tekore", colours.box_sub_text, font) + ddt.text((xx, y), "MIT", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://github.com/felix-hilden/tekore", colours.box_sub_text, font, click=self.click, replace="github") + + y += spacing + ddt.text((x, y), "pyLast", colours.box_sub_text, font) + ddt.text((xx, y), "Apache 2.0", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://github.com/pylast/pylast", colours.box_sub_text, font, click=self.click, replace="github") + + y += spacing + ddt.text((x, y), "Noto Sans font", colours.box_sub_text, font) + ddt.text((xx, y), "Apache 2.0", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://fonts.google.com/specimen/Noto+Sans", colours.box_sub_text, font, click=self.click, replace="fonts.google.com") + + # y += spacing + # ddt.text((x, y), "Stagger", colours.box_sub_text, font) + # ddt.text((xx, y), "BSD 2-Clause", colours.box_text_label, font) + # d"raw_linked_text2(xxx, y, "https://github.com/staggerpkg/stagger", colours.box_sub_text, font, click=self.click, replace="github") + + y += spacing + ddt.text((x, y), "KISS FFT", colours.box_sub_text, font) + ddt.text((xx, y), "New BSD License", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://github.com/mborgerding/kissfft", colours.box_sub_text, font, click=self.click, replace="github") + + elif self.cred_page == 3: + xx = x + round(130 * gui.scale) + xxx = x + round(240 * gui.scale) + ddt.text((x, y), _("Open source software used (cont'd)"), colours.box_text_label, 13) + font = 12 + spacing = round(18 * gui.scale) + y += spacing + ddt.text((x, y), "libFLAC", colours.box_sub_text, font) + ddt.text((xx, y), "New BSD License", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://xiph.org/flac/", colours.box_sub_text, font, click=self.click, replace="xiph.org") + + y += spacing + ddt.text((x, y), "libvorbis", colours.box_sub_text, font) + ddt.text((xx, y), "BSD License", colours.box_text_label, font) + draw_linked_text2(xxx, y, "https://xiph.org/vorbis/", colours.box_sub_text, font, click=self.click, replace="xiph.org") + + y += spacing + ddt.text((x, y), "opusfile", colours.box_sub_text, font) + ddt.text((xx, y), "New BSD license", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://opus-codec.org/", colours.box_sub_text, font, click=self.click, replace="opus-codec.org") + + y += spacing + ddt.text((x, y), "mpg123", colours.box_sub_text, font) + ddt.text((xx, y), "LGPL 2.1", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://www.mpg123.de/", colours.box_sub_text, font, click=self.click, replace="mpg123.de") + + y += spacing + ddt.text((x, y), "Secret Rabbit Code", colours.box_sub_text, font) + ddt.text((xx, y), "BSD 2-Clause", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "http://www.mega-nerd.com/SRC/index.html", colours.box_sub_text, font, click=self.click, replace="mega-nerd.com") + + y += spacing + ddt.text((x, y), "libopenmpt", colours.box_sub_text, font) + ddt.text((xx, y), "New BSD License", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://lib.openmpt.org/libopenmpt", colours.box_sub_text, font, click=self.click, replace="lib.openmpt.org") + + elif self.cred_page == 5: + xx = x + round(130 * gui.scale) + xxx = x + round(240 * gui.scale) + ddt.text((x, y), _("Open source software used (cont'd)"), colours.box_text_label, 13) + font = 12 + spacing = round(18 * gui.scale) + y += spacing + ddt.text((x, y), "Mutagen", colours.box_sub_text, font) + ddt.text((xx, y), "GPLv2+", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://github.com/quodlibet/mutagen", colours.box_sub_text, font, click=self.click, replace="github") + + y += spacing + ddt.text((x, y), "unidecode", colours.box_sub_text, font) + ddt.text((xx, y), "GPL-2.0+", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://github.com/avian2/unidecode", colours.box_sub_text, font, click=self.click, replace="github") + + y += spacing + ddt.text((x, y), "pypresence", colours.box_sub_text, font) + ddt.text((xx, y), "MIT", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://github.com/qwertyquerty/pypresence", colours.box_sub_text, font, click=self.click, replace="github") + + y += spacing + ddt.text((x, y), "musicbrainzngs", colours.box_sub_text, font) + ddt.text((xx, y), "Simplified BSD", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://github.com/alastair/python-musicbrainzngs", colours.box_sub_text, font, click=self.click, replace="github") + + y += spacing + ddt.text((x, y), "Send2Trash", colours.box_sub_text, font) + ddt.text((xx, y), "New BSD License", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://github.com/arsenetar/send2trash", colours.box_sub_text, font, click=self.click, replace="github") + + y += spacing + ddt.text((x, y), "GTK/PyGObject", colours.box_sub_text, font) + ddt.text((xx, y), "LGPLv2.1+", colours.box_text_label, font) + draw_linked_text2( + xxx, y, "https://gitlab.gnome.org/GNOME/pygobject", colours.box_sub_text, font, click=self.click, replace="gitlab.gnome.org") + + ddt.rect((x, block_y, 369 * gui.scale, 140 * gui.scale), alpha_mod(colours.box_background, fade)) + + y = y0 + h0 - round(33 * gui.scale) + x = x0 + w0 - 0 * gui.scale + + w = max(ddt.get_text_w(_("Credits"), 211), ddt.get_text_w(_("Next"), 211)) + x -= w + round(40 * gui.scale) + + text = _("Credits") + if self.cred_page != 0: + text = _("Next") + if self.button(x, y, text, width=w + round(25 * gui.scale)): + self.ani_cred = 1 + self.ani_fade_on_timer.set() + + def topchart(self, x0, y0, w0, h0): + + x = x0 + round(25 * gui.scale) + y = y0 + 20 * gui.scale + + ddt.text_background_colour = colours.box_background + + ddt.text((x, y), _("Chart Grid Generator"), colours.box_text, 214) + + y += 25 * gui.scale + ww = ddt.text((x, y), _("Target playlist: "), colours.box_sub_text, 312) + ddt.text( + (x + ww, y), pctl.multi_playlist[pctl.active_playlist_viewing].title, colours.box_text_label, 12, + 400 * gui.scale) + # x -= 210 * gui.scale + + y += 30 * gui.scale + + if prefs.chart_cascade: + if prefs.chart_d1: + prefs.chart_c1 = self.slide_control(x, y, _("Level 1"), "", prefs.chart_c1, 2, 20, 1, width=35) + y += 22 * gui.scale + if prefs.chart_d2: + prefs.chart_c2 = self.slide_control(x, y, _("Level 2"), "", prefs.chart_c2, 2, 20, 1, width=35) + y += 22 * gui.scale + if prefs.chart_d3: + prefs.chart_c3 = self.slide_control(x, y, _("Level 3"), "", prefs.chart_c3, 2, 20, 1, width=35) + + y -= 44 * gui.scale + x += 133 * gui.scale + prefs.chart_d1 = self.slide_control(x, y, _("by"), "", prefs.chart_d1, 0, 10, 1, width=35) + y += 22 * gui.scale + prefs.chart_d2 = self.slide_control(x, y, _("by"), "", prefs.chart_d2, 0, 10, 1, width=35) + y += 22 * gui.scale + prefs.chart_d3 = self.slide_control(x, y, _("by"), "", prefs.chart_d3, 0, 10, 1, width=35) + x -= 133 * gui.scale + + else: + + prefs.chart_rows = self.slide_control(x, y, _("Rows"), "", prefs.chart_rows, 1, 100, 1, width=35) + y += 22 * gui.scale + prefs.chart_columns = self.slide_control(x, y, _("Columns"), "", prefs.chart_columns, 1, 100, 1, width=35) + y += 22 * gui.scale + + y += 35 * gui.scale + x += 5 * gui.scale + + prefs.chart_cascade = self.toggle_square(x, y, prefs.chart_cascade, _("Cascade style")) + y += 25 * gui.scale + prefs.chart_tile = self.toggle_square(x, y, prefs.chart_tile ^ True, _("Use padding")) ^ True + + y -= 25 * gui.scale + x += 170 * gui.scale + + prefs.chart_text = self.toggle_square(x, y, prefs.chart_text, _("Include album titles")) + y += 25 * gui.scale + prefs.topchart_sorts_played = self.toggle_square(x, y, prefs.topchart_sorts_played, _("Sort by top played")) + + x = x0 + 15 * gui.scale + 320 * gui.scale + y = y0 + 100 * gui.scale + + # . Limited width. Max 13 chars + if self.button(x, y, _("Randomise BG")): + + r = round(random.random() * 40) + g = round(random.random() * 40) + b = round(random.random() * 40) + + prefs.chart_bg = [r, g, b] + + d = random.randrange(0, 4) + + if d == 1: + c = 5 + round(random.random() * 20) + prefs.chart_bg = [c, c, c] + + x += 100 * gui.scale + y -= 20 * gui.scale + + display_colour = (prefs.chart_bg[0], prefs.chart_bg[1], prefs.chart_bg[2], 255) + + rect = (x, y, 70 * gui.scale, 70 * gui.scale) + ddt.rect(rect, display_colour) + + ddt.rect_s(rect, (50, 50, 50, 255), round(1 * gui.scale)) + + # x = self.box_x + self.item_x_offset + 200 * gui.scale + # y = self.box_y + 180 * gui.scale + + x = x0 + 260 * gui.scale + y = y0 + 180 * gui.scale + + dex = reload_albums(quiet=True, return_playlist=pctl.active_playlist_viewing) + + x = x0 + round(110 * gui.scale) + y = y0 + 240 * gui.scale + + # . Limited width. Max 9 chars + if self.button(x, y, _("Generate"), width=80 * gui.scale): + if gui.generating_chart: + show_message(_("Be patient!")) + elif not prefs.chart_font: + show_message(_("No font set in config"), mode="error") + else: + shoot = threading.Thread(target=gen_chart) + shoot.daemon = True + shoot.start() + gui.generating_chart = True + + x += round(95 * gui.scale) + if gui.generating_chart: + ddt.text((x, y + round(1 * gui.scale)), _("Generating..."), colours.box_text_label, 12) + else: + + count = prefs.chart_rows * prefs.chart_columns + if prefs.chart_cascade: + count = prefs.chart_c1 * prefs.chart_d1 + prefs.chart_c2 * prefs.chart_d2 + prefs.chart_c3 * prefs.chart_d3 + + line = _("{N} Album chart").format(N=str(count)) + + ww = ddt.text((x, y + round(1 * gui.scale)), line, colours.box_text_label, 12) + + if len(dex) < count: + ddt.text( + (x + ww + round(10 * gui.scale), y + 1 * gui.scale), _("Not enough albums in the playlist!"), + [255, 120, 125, 255], 12) + + x = x0 + round(20 * gui.scale) + y = y0 + 240 * gui.scale + + # . Limited width. Max 8 chars + if self.button(x, y, _("Return"), width=75 * gui.scale): + self.chart_view = 0 + + def stats(self, x0, y0, w0, h0): + + x = x0 + 10 * gui.scale + y = y0 + + if self.chart_view == 1: + self.topchart(x0, y0, w0, h0) + return + + ww = ddt.get_text_w(_("Chart generator..."), 211) + 30 * gui.scale + if system == "Linux" and self.button(x0 + w0 - ww, y + 15 * gui.scale, _("Chart generator...")): + self.chart_view = 1 + + ddt.text_background_colour = colours.box_background + lt_font = 312 + lt_colour = colours.box_text_label + + w1 = ddt.get_text_w(_("Tracks in playlist"), 12) + w2 = ddt.get_text_w(_("Albums in playlist"), 12) + w3 = ddt.get_text_w(_("Playlist duration"), 12) + w4 = ddt.get_text_w(_("Tracks in database"), 12) + w5 = ddt.get_text_w(_("Total albums"), 12) + w6 = ddt.get_text_w(_("Total playtime"), 12) + + x1 = x + (8 + 10 + 10) * gui.scale + x2 = x1 + max(w1, w2, w3, w4, w5, w6) + 20 * gui.scale + y1 = y + 50 * gui.scale + + if self.stats_pl != pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int or self.stats_pl_timer.get() > 5: + self.stats_pl = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + self.stats_pl_timer.set() + + album_names = set() + folder_names = set() + count = 0 + + for track_id in default_playlist: + tr = pctl.get_track(track_id) + + if not tr.album: + if tr.parent_folder_path not in folder_names: + count += 1 + folder_names.add(tr.parent_folder_path) + else: + if tr.parent_folder_path not in folder_names and tr.album not in album_names: + count += 1 + folder_names.add(tr.parent_folder_path) + album_names.add(tr.album) + + self.stats_pl_albums = count + + self.stats_pl_length = 0 + for item in default_playlist: + self.stats_pl_length += pctl.master_library[item].length + + line = seconds_to_day_hms(self.stats_pl_length, strings.day, strings.days) + + ddt.text((x1, y1), _("Tracks in playlist"), lt_colour, lt_font) + ddt.text((x2, y1), py_locale.format_string("%d", len(default_playlist), True), colours.box_sub_text, 12) + y1 += 20 * gui.scale + ddt.text((x1, y1), _("Albums in playlist"), lt_colour, lt_font) + ddt.text((x2, y1), str(self.stats_pl_albums), colours.box_sub_text, 12) + y1 += 20 * gui.scale + ddt.text((x1, y1), _("Playlist duration"), lt_colour, lt_font) + + ddt.text((x2, y1), line, colours.box_sub_text, 12) + + if self.stats_timer.get() > 5: + album_names = set() + folder_names = set() + count = 0 + + for pl in pctl.multi_playlist: + for track_id in pl.playlist_ids: + tr = pctl.get_track(track_id) + + if not tr.album: + if tr.parent_folder_path not in folder_names: + count += 1 + folder_names.add(tr.parent_folder_path) + else: + if tr.parent_folder_path not in folder_names and tr.album not in album_names: + count += 1 + folder_names.add(tr.parent_folder_path) + album_names.add(tr.album) + + self.total_albums = count + + self.stats_timer.set() + + y1 += 40 * gui.scale + ddt.text((x1, y1), _("Tracks in database"), lt_colour, lt_font) + ddt.text((x2, y1), py_locale.format_string("%d", len(pctl.master_library), True), colours.box_sub_text, 12) + y1 += 20 * gui.scale + ddt.text((x1, y1), _("Total albums"), lt_colour, lt_font) + ddt.text((x2, y1), str(self.total_albums), colours.box_sub_text, 12) + + y1 += 20 * gui.scale + ddt.text((x1, y1), _("Total playtime"), lt_colour, lt_font) + ddt.text((x2, y1), seconds_to_day_hms(pctl.total_playtime, strings.day, strings.days), colours.box_sub_text, 15) + + # Ratio bar + if len(pctl.master_library) > 115 * gui.scale: + x = x0 + y = y0 + h0 - 7 * gui.scale + + full_rect = [x, y, w0, 7 * gui.scale] + d = 0 + + # Stats + try: + if self.last_db_size != len(pctl.master_library): + self.last_db_size = len(pctl.master_library) + self.ext_ratio = {} + for key, value in pctl.master_library.items(): + if value.file_ext in self.ext_ratio: + self.ext_ratio[value.file_ext] += 1 + else: + self.ext_ratio[value.file_ext] = 1 + + for key, value in self.ext_ratio.items(): + + colour = [200, 200, 200, 255] + if key in format_colours: + colour = format_colours[key] + + colour = colorsys.rgb_to_hls(colour[0] / 255, colour[1] / 255, colour[2] / 255) + colour = colorsys.hls_to_rgb(1 - colour[0], colour[1] * 0.8, colour[2] * 0.8) + colour = [int(colour[0] * 255), int(colour[1] * 255), int(colour[2] * 255), 255] + + h = int(round(value / len(pctl.master_library) * full_rect[2])) + block_rect = [full_rect[0] + d, full_rect[1], h, full_rect[3]] + + ddt.rect(block_rect, colour) + d += h + + block_rect = (block_rect[0], block_rect[1], block_rect[2] - 1, block_rect[3]) + fields.add(block_rect) + if coll(block_rect): + xx = block_rect[0] + int(block_rect[2] / 2) + xx = max(xx, x + 30 * gui.scale) + xx = min(xx, x0 + w0 - 30 * gui.scale) + ddt.text((xx, y0 + h0 - 35 * gui.scale, 2), key, colours.grey_blend_bg(220), 13) + + if self.click: + gen_codec_pl(key) + except Exception: + logging.exception("Error draw ext bar") + + def config_v(self, x0, y0, w0, h0): + + ddt.text_background_colour = colours.box_background + + x = x0 + self.item_x_offset + y = y0 + 17 * gui.scale + + self.toggle_square(x, y, rating_toggle, _("Track ratings")) + y += round(25 * gui.scale) + self.toggle_square(x, y, album_rating_toggle, _("Album ratings")) + y += round(35 * gui.scale) + + self.toggle_square(x, y, heart_toggle, " ") + heart_row_icon.render(x + round(23 * gui.scale), y + round(2 * gui.scale), colours.box_text) + rect = (x, y + round(2 * gui.scale), 40 * gui.scale, 15 * gui.scale) + fields.add(rect) + if coll(rect): + ex_tool_tip(x + round(45 * gui.scale), y - 20 * gui.scale, 0, _("Show track loves"), 12) + + x += (55 * gui.scale) + self.toggle_square(x, y, star_toggle, " ") + star_row_icon.render(x + round(22 * gui.scale), y + round(0 * gui.scale), colours.box_text) + rect = (x, y + round(2 * gui.scale), 40 * gui.scale, 15 * gui.scale) + fields.add(rect) + if coll(rect): + ex_tool_tip(x + round(35 * gui.scale), y - 20 * gui.scale, 0, _("Represent playtime as stars"), 12) + + x += (55 * gui.scale) + self.toggle_square(x, y, star_line_toggle, " ") + ddt.rect( + (x + round(21 * gui.scale), y + round(6 * gui.scale), round(15 * gui.scale), round(1 * gui.scale)), + colours.box_text) + rect = (x, y + round(2 * gui.scale), 40 * gui.scale, 15 * gui.scale) + fields.add(rect) + if coll(rect): + ex_tool_tip(x + round(35 * gui.scale), y - 20 * gui.scale, 0, _("Represent playcount as lines"), 12) + + x = x0 + self.item_x_offset + + # y += round(25 * gui.scale) + + # self.toggle_square(x, y, star_line_toggle, _('Show playtime lines')) + y += round(15 * gui.scale) + + # if gui.show_ratings: + # x += round(10 * gui.scale) + # #self.toggle_square(x, y, star_toggle, _('Show playtime stars')) + # if gui.show_ratings: + # x -= round(10 * gui.scale) + + + y += round(25 * gui.scale) + + if self.toggle_square(x, y, prefs.row_title_format == 2, _("Left align title style")): + prefs.row_title_format = 2 + else: + prefs.row_title_format = 1 + + y += round(25 * gui.scale) + + prefs.row_title_genre = self.toggle_square(x + round(10 * gui.scale), y, prefs.row_title_genre, _("Show album genre")) + y += round(25 * gui.scale) + + self.toggle_square(x, y, toggle_append_date, _("Show album release year")) + y += round(25 * gui.scale) + + self.toggle_square(x, y, toggle_append_total_time, _("Show album duration")) + y += round(35 * gui.scale) + + if self.toggle_square(x, y, prefs.row_title_separator_type == 0, " - "): + prefs.row_title_separator_type = 0 + if self.toggle_square(x + round(55 * gui.scale), y, prefs.row_title_separator_type == 1, " ‒ "): + prefs.row_title_separator_type = 1 + if self.toggle_square(x + round(110 * gui.scale), y, prefs.row_title_separator_type == 2, " ⦁ "): + prefs.row_title_separator_type = 2 + x = x0 + 330 * gui.scale + y = y0 + 25 * gui.scale + + prefs.playlist_font_size = self.slide_control(x, y, _("Font Size"), "", prefs.playlist_font_size, 12, 17) + y += 25 * gui.scale + prefs.playlist_row_height = self.slide_control(x, y, _("Row Size"), "px", prefs.playlist_row_height, 15, 45) + y += 25 * gui.scale + prefs.tracklist_y_text_offset = self.slide_control( + x, y, _("Baseline offset"), "px", prefs.tracklist_y_text_offset, -10, 10) + y += 25 * gui.scale + + x += 65 * gui.scale + self.button(x, y, _("Thin default"), self.small_preset, 124 * gui.scale) + y += 27 * gui.scale + self.button(x, y, _("Thick default"), self.large_preset, 124 * gui.scale) + + + def set_playlist_cycle(self, mode=0): + if mode == 1: + return True if prefs.end_setting == "cycle" else False + prefs.end_setting = "cycle" + # global pl_follow + # pl_follow = False + + def set_playlist_advance(self, mode=0): + if mode == 1: + return True if prefs.end_setting == "advance" else False + prefs.end_setting = "advance" + # global pl_follow + # pl_follow = False + + def set_playlist_stop(self, mode=0): + if mode == 1: + return True if prefs.end_setting == "stop" else False + prefs.end_setting = "stop" + + def set_playlist_repeat(self, mode=0): + if mode == 1: + return True if prefs.end_setting == "repeat" else False + prefs.end_setting = "repeat" + + def small_preset(self): + + prefs.playlist_row_height = round(22 * prefs.ui_scale) + prefs.playlist_font_size = 15 + prefs.tracklist_y_text_offset = 0 + gui.update_layout() + + def large_preset(self): + + prefs.playlist_row_height = round(27 * prefs.ui_scale) + prefs.playlist_font_size = 15 + gui.update_layout() + + def slide_control(self, x, y, label, units, value, lower_limit, upper_limit, step=1, callback=None, width=58): + + width = round(width * gui.scale) + + if label is not None: + ddt.text((x + 55 * gui.scale, y, 1), label, colours.box_text, 312) + x += 65 * gui.scale + y += 1 * gui.scale + rect = (x, y, 33 * gui.scale, 15 * gui.scale) + fields.add(rect) + ddt.rect(rect, colours.box_button_background) + abg = [255, 255, 255, 40] + if coll(rect): + + if self.click: + if value > lower_limit: + value -= step + gui.update_layout() + if callback is not None: + callback(value) + + if mouse_down: + abg = [230, 120, 20, 255] + else: + abg = [220, 150, 20, 255] + + if colour_value(colours.box_background) > 300: + abg = colours.box_sub_text + + dec_arrow.render(x + 1 * gui.scale, y, abg) + + x += 33 * gui.scale + + ddt.rect((x, y, width, 15 * gui.scale), alpha_mod(colours.box_button_background, 120)) + ddt.text((x + width / 2, y, 2), str(value) + units, colours.box_sub_text, 312) + + x += width + + rect = (x, y, 33 * gui.scale, 15 * gui.scale) + fields.add(rect) + ddt.rect(rect, colours.box_button_background) + abg = [255, 255, 255, 40] + if coll(rect): + + if self.click: + if value < upper_limit: + value += step + gui.update_layout() + if callback is not None: + callback(value) + if mouse_down: + abg = [230, 120, 20, 255] + else: + abg = [220, 150, 20, 255] + + if colour_value(colours.box_background) > 300: + abg = colours.box_sub_text + + inc_arrow.render(x + 1 * gui.scale, y, abg) + + return value + + # def style_up(self): + # prefs.line_style += 1 + # if prefs.line_style > 5: + # prefs.line_style = 1 + + def inside(self): + + return coll((self.box_x, self.box_y, self.w, self.h)) + + def init2(self): + + self.init2done = True + + def close(self): + self.enabled = False + fader.fall() + if gui.opened_config_file: + reload_config_file() + + def render(self): + + if self.init2done is False: + self.init2() + + if key_esc_press: + self.close() + + tab_width = 115 * gui.scale + + side_width = 115 * gui.scale + header_width = 0 + + top_mode = False + if window_size[0] < 700 * gui.scale: + top_mode = True + side_width = 0 * gui.scale + header_width = round(48 * gui.scale) # 48 + + content_width = round(545 * gui.scale) + content_height = round(275 * gui.scale) # 275 + full_width = content_width + full_height = content_height + + full_width += side_width + full_height += header_width + + x = int(window_size[0] / 2) - int(full_width / 2) + y = int(window_size[1] / 2) - int(full_height / 2) + + self.box_x = x + self.box_y = y + self.w = full_width + self.h = full_height + + border_colour = colours.box_border + + ddt.rect( + (x - 5 * gui.scale, y - 5 * gui.scale, full_width + 10 * gui.scale, full_height + 10 * gui.scale), border_colour) + ddt.rect_a((x, y), (full_width, full_height), colours.box_background) + + current_tab = 0 + tab_height = round(24 * gui.scale) # 30 + + tab_bg = colours.sys_tab_bg + tab_hl = colours.sys_tab_hl + tab_text = rgb_add_hls(tab_bg, 0, 0.3, -0.15) + if is_light(tab_bg): + h, l, s = rgb_to_hls(tab_bg[0], tab_bg[1], tab_bg[2]) + l = 0.1 + tab_text = hls_to_rgb(h, l, s) + tab_over = alpha_mod(rgb_add_hls(tab_bg, 0, 0.5, 0), 13) + + if top_mode: + + xx = x + yy = y + tab_width = 90 * gui.scale + + ddt.rect_a((x, y), (full_width, header_width), tab_bg) + + for item in self.tabs: + + if self.click and gui.message_box: + gui.message_box = False + + box = [xx, yy, tab_width, tab_height] + box2 = [xx, yy, tab_width, tab_height - 1] + fields.add(box2) + + if self.click and coll(box2): + self.tab_active = current_tab + self.lyrics_panel = False + + if current_tab == self.tab_active: + colour = copy.deepcopy(colours.sys_tab_hl) + ddt.text_background_colour = colour + ddt.rect(box, colour) + else: + ddt.text_background_colour = tab_bg + ddt.rect(box, tab_bg) + + if coll(box2): + ddt.rect(box, tab_over) + + alpha = 100 + if current_tab == self.tab_active: + alpha = 240 + + ddt.text((xx + (tab_width // 2), yy + 4 * gui.scale, 2), item[0], tab_text, 212) + + current_tab += 1 + xx += tab_width + if current_tab == 6: + yy += round(24 * gui.scale) # 30 + xx = x + + else: + + ddt.rect_a((x, y), (tab_width, full_height), tab_bg) + + for item in self.tabs: + + if self.click and gui.message_box: + if not coll(message_box.get_rect()): + gui.message_box = False + else: + inp.mouse_click = True + self.click = False + + box = [x, y + (current_tab * tab_height), tab_width, tab_height] + box2 = [x, y + (current_tab * tab_height), tab_width, tab_height - 1] + fields.add(box2) + + if self.click and coll(box2): + self.tab_active = current_tab + self.lyrics_panel = False + + if current_tab == self.tab_active: + bg_colour = copy.deepcopy(colours.sys_tab_hl) + ddt.text_background_colour = bg_colour + ddt.rect(box, bg_colour) + else: + ddt.text_background_colour = tab_bg + ddt.rect(box, tab_bg) + + if coll(box2): + ddt.rect(box, tab_over) + + yy = box[1] + 4 * gui.scale + + if current_tab == self.tab_active: + ddt.text( + (box[0] + (tab_width // 2), yy, 2), item[0], alpha_blend(colours.tab_text_active, ddt.text_background_colour), 213) + else: + ddt.text( + (box[0] + (tab_width // 2), yy, 2), item[0], tab_text, 213) + + current_tab += 1 + + # ddt.line(x + 110, self.box_y + 1, self.box_x + 110, self.box_y + self.h, colours.grey(50)) + + self.tabs[self.tab_active][1](x + side_width, y + header_width, content_width, content_height) + + self.click = False + self.right_click = False + + ddt.text_background_colour = colours.box_background + +class Fields: + def __init__(self): + + self.id = [] + self.last_id = [] + + self.field_array = [] + self.force = False + + def add(self, rect, callback=None): + + self.field_array.append((rect, callback)) + + def test(self): + + if self.force: + self.force = False + return True + + self.last_id = self.id + #logging.info(len(self.id)) + self.id = [] + + for f in self.field_array: + if coll(f[0]): + self.id.append(1) # += "1" + if f[1] is not None: # Call callback if present + f[1]() + else: + self.id.append(0) # += "0" + + if self.last_id == self.id: + return False + + return True + + def clear(self): + + self.field_array = [] + +class TopPanel: + def __init__(self): + + self.height = gui.panelY + self.ty = 0 + + self.start_space_left = round(46 * gui.scale) + self.start_space_compact_left = 46 * gui.scale + + self.tab_text_font = fonts.tabs + self.tab_extra_width = round(17 * gui.scale) + self.tab_text_start_space = 8 * gui.scale + self.tab_text_y_offset = 7 * gui.scale + self.tab_spacing = 0 + + self.ini_menu_space = 17 * gui.scale # 17 + self.menu_space = 17 * gui.scale + self.click_buffer = 4 * gui.scale + + self.tabs_right_x = 0 # computed for drag and drop code elsewhere (hacky) + self.tabs_left_x = 1 + + self.prime_tab = gui.saved_prime_tab + self.prime_side = gui.saved_prime_direction # 0=left, 1=right + self.shown_tabs = [] + + # --- + self.space_left = 0 + self.tab_text_spaces = [] + self.index_playing = -1 + self.drag_zone_start_x = 300 * gui.scale + + self.exit_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "ex.png", True) + self.maximize_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "max.png", True) + self.restore_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "restore.png", True) + self.restore_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "restore.png", True) + self.playlist_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "playlist.png", True) + self.return_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "return.png", True) + self.artist_list_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "artist-list.png", True) + self.folder_list_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "folder-list.png", True) + self.dl_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "dl.png", True) + self.overflow_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "overflow.png", True) + + self.drag_slide_timer = Timer(100) + self.tab_d_click_timer = Timer(10) + self.tab_d_click_ref = None + + self.adds = [] + + def left_overflow_switch_playlist(self, pl): + self.prime_side = 0 + self.prime_tab = pl + switch_playlist(pl) + + def right_overflow_switch_playlist(self, pl): + self.prime_side = 1 + self.prime_tab = pl + switch_playlist(pl) + + def render(self): + + # C-TD + global quick_drag + global update_layout + + hh = gui.panelY2 + yy = gui.panelY - hh + self.height = hh + + if quick_drag is True: + # gui.pl_update = 1 + gui.update_on_drag = True + + # Draw the background + ddt.rect((0, 0, window_size[0], gui.panelY), colours.top_panel_background) + + if prefs.shuffle_lock and not gui.compact_bar: + colour = [250, 250, 250, 255] + if colours.lm: + colour = [10, 10, 10, 255] + text = _("Tauon Music Box SHUFFLE!") + if prefs.album_shuffle_lock_mode: + text = _("Tauon Music Box ALBUM SHUFFLE!") + ddt.text((window_size[0] // 2, 8 * gui.scale, 2), text, colour, 212, bg=colours.top_panel_background) + if gui.top_bar_mode2: + tr = pctl.playing_object() + if tr: + album_art_gen.display(tr, (window_size[0] - gui.panelY - 1, 0), (gui.panelY, gui.panelY)) + if loading_in_progress or \ + to_scan or \ + cm_clean_db or \ + lastfm.scanning_friends or \ + after_scan or \ + move_in_progress or \ + plex.scanning or \ + transcode_list or tauon.spot_ctl.launching_spotify or tauon.spot_ctl.spotify_com or subsonic.scanning or \ + koel.scanning or gui.sync_progress or lastfm.scanning_scrobbles: + ddt.rect( + (window_size[0] - (gui.panelY + 20), gui.panelY - gui.panelY2, gui.panelY + 25, gui.panelY2), + colours.top_panel_background) + + maxx = window_size[0] - (gui.panelY + 30 * gui.scale) + title_colour = colours.grey(249) + if colours.lm: + title_colour = colours.grey(30) + title = tr.title + if not title: + title = tr.filename + artist = tr.artist + + if pctl.playing_state == 3 and not radiobox.dummy_track.title: + title = pctl.tag_meta + artist = radiobox.loaded_url # pctl.url + + ddt.text_background_colour = colours.top_panel_background + + ddt.text((round(14 * gui.scale), round(15 * gui.scale)), title, title_colour, 215, max_w=maxx) + ddt.text((round(14 * gui.scale), round(40 * gui.scale)), artist, colours.grey(120), 315, max_w=maxx) + + wwx = 0 + if prefs.left_window_control and not gui.compact_bar: + if gui.macstyle: + wwx = 24 + # wwx = round(64 * gui.scale) + if draw_min_button: + wwx += 20 + if draw_max_button: + wwx += 20 + wwx = round(wwx * gui.scale) + else: + wwx = 26 + # wwx = round(90 * gui.scale) + if draw_min_button: + wwx += 35 + if draw_max_button: + wwx += 33 + wwx = round(wwx * gui.scale) + + rect = (wwx + 9 * gui.scale, yy + 4 * gui.scale, 34 * gui.scale, 25 * gui.scale) + fields.add(rect) + + if coll(rect) and not prefs.shuffle_lock: + if inp.mouse_click: + + if gui.combo_mode: + gui.switch_showcase_off = True + else: + gui.lsp ^= True + + update_layout = True + gui.update += 1 + if mouse_down and quick_drag: + gui.lsp = True + update_layout = True + gui.update += 1 + + if middle_click: + toggle_left_last() + update_layout = True + gui.update += 1 + + if right_click: + # prefs.artist_list ^= True + lsp_menu.activate(position=(5 * gui.scale, gui.panelY)) + update_layout_do() + + colour = colours.corner_button # [230, 230, 230, 255] + + if gui.lsp: + colour = colours.corner_button_active + if gui.combo_mode: + colour = colours.corner_button + if coll(rect): + colour = colours.corner_button_active + + if not prefs.shuffle_lock: + if gui.combo_mode: + self.return_icon.render(wwx + 14 * gui.scale, yy + 8 * gui.scale, colour) + elif prefs.left_panel_mode == "artist list": + self.artist_list_icon.render(wwx + 13 * gui.scale, yy + 8 * gui.scale, colour) + elif prefs.left_panel_mode == "folder view": + self.folder_list_icon.render(wwx + 14 * gui.scale, yy + 8 * gui.scale, colour) + else: + self.playlist_icon.render(wwx + 13 * gui.scale, yy + 8 * gui.scale, colour) + + # if prefs.artist_list: + # self.artist_list_icon.render(13 * gui.scale, yy + 8 * gui.scale, colour) + # else: + # self.playlist_icon.render(13 * gui.scale, yy + 8 * gui.scale, colour) + + if playlist_box.drag: + drag_mode = False + + # Need to test length + self.tab_text_spaces = [] + + if gui.radio_view: + for item in pctl.radio_playlists: + le = ddt.get_text_w(item["name"], self.tab_text_font) + self.tab_text_spaces.append(le) + else: + for i, item in enumerate(pctl.multi_playlist): + le = ddt.get_text_w(pctl.multi_playlist[i].title, self.tab_text_font) + self.tab_text_spaces.append(le) + + x = self.start_space_left + wwx + y = yy # self.ty + + # Calculate position for playing text and text + offset = 15 * gui.scale + if draw_border and not prefs.left_window_control: + offset += 61 * gui.scale + if draw_max_button: + offset += 61 * gui.scale + if gui.turbo: + offset += 90 * gui.scale + if gui.vis == 3: + offset += 57 * gui.scale + if gui.top_bar_mode2: + offset = 0 + + p_text_len = 180 * gui.scale + right_space_es = p_text_len + offset + + x_start = x + + if playlist_box.drag and not gui.radio_view: + if mouse_up: + if mouse_up_position[0] > (gui.lspw if gui.lsp else 0) and mouse_up_position[1] > gui.panelY: + playlist_box.drag = False + if prefs.drag_to_unpin: + if playlist_box.drag_source == 0: + pctl.multi_playlist[playlist_box.drag_on].hidden = True + else: + pctl.multi_playlist[playlist_box.drag_on].hidden = False + gui.update += 1 + gui.update_on_drag = True + + # List all tabs eligible to be shown + #logging.info("-------------") + ready_tabs = [] + show_tabs = [] + + if prefs.tabs_on_top or gui.radio_view: + if gui.radio_view: + for i, tab in enumerate(pctl.radio_playlists): + ready_tabs.append(i) + self.prime_tab = min(self.prime_tab, len(pctl.radio_playlists) - 1) + else: + for i, tab in enumerate(pctl.multi_playlist): + # Skip if hide flag is set + if tab.hidden: + continue + ready_tabs.append(i) + self.prime_tab = min(self.prime_tab, len(pctl.multi_playlist) - 1) + max_w = window_size[0] - (x + right_space_es + round(34 * gui.scale)) + + left_tabs = [] + right_tabs = [] + if prefs.shuffle_lock: + for p in ready_tabs: + left_tabs.append(p) + + else: + for p in ready_tabs: + if p < self.prime_tab: + left_tabs.append(p) + + for p in ready_tabs: + if p > self.prime_tab: + right_tabs.append(p) + left_tabs.reverse() + + run = max_w + + if self.prime_tab in ready_tabs: + size = self.tab_text_spaces[self.prime_tab] + self.tab_extra_width + if size < run: + show_tabs.append(self.prime_tab) + run -= size + + if self.prime_side == 0: + for tab in right_tabs: + size = self.tab_text_spaces[tab] + self.tab_extra_width + if size < run: + show_tabs.append(tab) + run -= size + else: + break + for tab in left_tabs: + size = self.tab_text_spaces[tab] + self.tab_extra_width + if size < run: + show_tabs.insert(0, tab) + run -= size + else: + break + else: + for tab in left_tabs: + size = self.tab_text_spaces[tab] + self.tab_extra_width + if size < run: + show_tabs.insert(0, tab) + run -= size + else: + break + for tab in right_tabs: + size = self.tab_text_spaces[tab] + self.tab_extra_width + if size < run: + show_tabs.append(tab) + run -= size + else: + break + + # for tab in show_tabs: + # logging.info(pctl.multi_playlist[tab].title) + #logging.info("---") + left_overflow = [x for x in left_tabs if x not in show_tabs] + right_overflow = [x for x in right_tabs if x not in show_tabs] + self.shown_tabs = show_tabs + + if left_overflow: + hh = round(20 * gui.scale) + rect = [x, y + (self.height - hh), 17 * gui.scale, hh] + ddt.rect(rect, colours.tab_background) + self.overflow_icon.render(rect[0] + round(3 * gui.scale), rect[1] + round(4 * gui.scale), colours.tab_text) + + x += 17 * gui.scale + x_start = x + + if inp.mouse_click and coll(rect): + overflow_menu.items.clear() + for tab in reversed(left_overflow): + if gui.radio_view: + overflow_menu.add( + MenuItem(pctl.radio_playlists[tab]["name"], self.left_overflow_switch_playlist, + pass_ref=True, set_ref=tab)) + else: + overflow_menu.add( + MenuItem(pctl.multi_playlist[tab].title, self.left_overflow_switch_playlist, + pass_ref=True, set_ref=tab)) + overflow_menu.activate(0, (rect[0], rect[1] + rect[3])) + + xx = x + (max_w - run) # + round(6 * gui.scale) + self.tabs_left_x = x_start + + if right_overflow: + hh = round(20 * gui.scale) + rect = [xx, y + (self.height - hh), 17 * gui.scale, hh] + ddt.rect(rect, colours.tab_background) + self.overflow_icon.render( + rect[0] + round(3 * gui.scale), rect[1] + round(4 * gui.scale), + colours.tab_text) + if inp.mouse_click and coll(rect): + overflow_menu.items.clear() + for tab in right_overflow: + if gui.radio_view: + overflow_menu.add( + MenuItem( + pctl.radio_playlists[tab]["name"], self.left_overflow_switch_playlist, pass_ref=True, set_ref=tab)) + else: + overflow_menu.add( + MenuItem( + pctl.multi_playlist[tab].title, self.left_overflow_switch_playlist, pass_ref=True, set_ref=tab)) + overflow_menu.activate(0, (rect[0], rect[1] + rect[3])) + + if gui.radio_view: + if not mouse_down and pctl.radio_playlist_viewing not in show_tabs and pctl.radio_playlist_viewing in ready_tabs: + if pctl.radio_playlist_viewing < self.prime_tab: + self.prime_side = 0 + elif pctl.radio_playlist_viewing > self.prime_tab: + self.prime_side = 1 + self.prime_tab = pctl.radio_playlist_viewing + gui.update += 1 + elif not mouse_down and pctl.active_playlist_viewing not in show_tabs and pctl.active_playlist_viewing in ready_tabs: + if pctl.active_playlist_viewing < self.prime_tab: + self.prime_side = 0 + elif pctl.active_playlist_viewing > self.prime_tab: + self.prime_side = 1 + self.prime_tab = pctl.active_playlist_viewing + gui.update += 1 + + if playlist_box.drag and mouse_position[0] > xx and mouse_position[1] < gui.panelY: + gui.update += 1 + if 0.5 < self.drag_slide_timer.get() < 1 and show_tabs and right_overflow: + self.drag_slide_timer.set() + self.prime_side = 1 + self.prime_tab = right_overflow[0] + if self.drag_slide_timer.get() > 1: + self.drag_slide_timer.set() + if playlist_box.drag and mouse_position[0] < x and mouse_position[1] < gui.panelY: + gui.update += 1 + if 0.5 < self.drag_slide_timer.get() < 1 and show_tabs and left_overflow: + self.drag_slide_timer.set() + self.prime_side = 0 + self.prime_tab = left_overflow[0] + if self.drag_slide_timer.get() > 1: + self.drag_slide_timer.set() + + # TAB INPUT PROCESSING + target = pctl.multi_playlist + if gui.radio_view: + target = pctl.radio_playlists + for i, tab in enumerate(target): + + if not gui.radio_view: + if not prefs.tabs_on_top or prefs.shuffle_lock: + break + + if len(pctl.multi_playlist) != len(self.tab_text_spaces): + break + + if i not in show_tabs: + continue + + # Determine the tab width + tab_width = self.tab_text_spaces[i] + self.tab_extra_width + + # Save the far right boundary of the tabs (hacky) + self.tabs_right_x = x + tab_width + + # Detect mouse over and add tab to mouse over detection + f_rect = [x, y + 1, tab_width - 1, self.height - 1] + tab_hit = coll(f_rect) + + # Tab functions + if tab_hit: + if not gui.radio_view: + # Double click to play + if mouse_up and pl_to_id(i) == self.tab_d_click_ref == pl_to_id(pctl.active_playlist_viewing) and \ + self.tab_d_click_timer.get() < 0.25 and point_distance( + last_click_location, mouse_up_position) < 5 * gui.scale: + + if pctl.playing_state == 2 and pctl.active_playlist_playing == i: + pctl.play() + elif pctl.selected_ready() and (pctl.playing_state != 1 or pctl.active_playlist_playing != i): + pctl.jump(default_playlist[pctl.selected_in_playlist], pl_position=pctl.selected_in_playlist) + if mouse_up: + self.tab_d_click_timer.set() + self.tab_d_click_ref = pl_to_id(i) + + # Click to change playlist + if inp.mouse_click: + gui.pl_update = 1 + playlist_box.drag = True + playlist_box.drag_source = 0 + playlist_box.drag_on = i + if gui.radio_view: + pctl.radio_playlist_viewing = i + else: + switch_playlist(i) + set_drag_source() + + # Drag to move playlist + if mouse_up and playlist_box.drag and coll_point(mouse_up_position, f_rect): + + if gui.radio_view: + move_radio_playlist(playlist_box.drag_on, i) + else: + if playlist_box.drag_source == 1: + pctl.multi_playlist[playlist_box.drag_on].hidden = False + + if i != playlist_box.drag_on: + + # # Reveal the tab in case it has been hidden + # pctl.multi_playlist[playlist_box.drag_on].hidden = False + + if key_shift_down: + pctl.multi_playlist[i].playlist_ids += pctl.multi_playlist[playlist_box.drag_on].playlist_ids + delete_playlist(playlist_box.drag_on, check_lock=True, force=True) + else: + move_playlist(playlist_box.drag_on, i) + + playlist_box.drag = False + gui.update += 1 + + # Delete playlist on wheel click + elif tab_menu.active is False and middle_click: + # delete_playlist(i) + delete_playlist_ask(i) + break + + # Activate menu on right click + elif right_click: + if gui.radio_view: + radio_tab_menu.activate(copy.deepcopy(i)) + else: + tab_menu.activate(copy.deepcopy(i)) + gui.tab_menu_pl = i + + # Quick drop tracks + elif quick_drag is True and mouse_up: + self.tab_d_click_ref = -1 + self.tab_d_click_timer.force_set(100) + if (pctl.gen_codes.get(pl_to_id(i)) and "self" not in pctl.gen_codes[pl_to_id(i)]): + clear_gen_ask(pl_to_id(i)) + quick_drag = False + modified = False + gui.pl_update += 1 + + for item in shift_selection: + pctl.multi_playlist[i].playlist_ids.append(default_playlist[item]) + modified = True + if len(shift_selection) > 0: + modified = True + self.adds.append( + [pctl.multi_playlist[i].uuid_int, len(shift_selection), Timer()]) # ID, num, timer + + if modified: + pctl.after_import_flag = True + pctl.notify_change() + pctl.update_shuffle_pool(pctl.multi_playlist[i].uuid_int) + tree_view_box.clear_target_pl(i) + tauon.thread_manager.ready("worker") + + if mouse_up and radio_view.drag: + pctl.radio_playlists[i]["items"].append(radio_view.drag) + toast(_("Added station to: ") + pctl.radio_playlists[i]["name"]) + + radio_view.drag = None + + x += tab_width + self.tab_spacing + + # Test dupelicate tab function + if playlist_box.drag: + rect = (0, x, self.height, window_size[0]) + fields.add(rect) + + if mouse_up and playlist_box.drag and mouse_position[0] > x and mouse_position[1] < self.height: + if gui.radio_view: + pass + elif key_ctrl_down: + gen_dupe(playlist_box.drag_on) + + else: + if playlist_box.drag_source == 1: + pctl.multi_playlist[playlist_box.drag_on].hidden = False + + move_playlist(playlist_box.drag_on, i) + playlist_box.drag = False + + # Need to test length again + # Need to test length + self.tab_text_spaces = [] + + if gui.radio_view: + for item in pctl.radio_playlists: + le = ddt.get_text_w(item["name"], self.tab_text_font) + self.tab_text_spaces.append(le) + else: + for i, item in enumerate(pctl.multi_playlist): + le = ddt.get_text_w(pctl.multi_playlist[i].title, self.tab_text_font) + self.tab_text_spaces.append(le) + + # Reset X draw position + x = x_start + bar_highlight_size = round(2 * gui.scale) + + # TAB DRAWING + shown = [] + for i, tab in enumerate(target): + + if not gui.radio_view: + if not prefs.tabs_on_top or prefs.shuffle_lock: + break + + if len(pctl.multi_playlist) != len(self.tab_text_spaces): + break + + # if tab.hidden is True: + # continue + + if i not in show_tabs: + continue + + # if window_size[0] - x - (self.tab_text_spaces[i] + self.tab_extra_width) < right_space_es: + # break + + shown.append(i) + + tab_width = self.tab_text_spaces[i] + self.tab_extra_width + rect = [x, y, tab_width, self.height] + + # Detect mouse over and add tab to mouse over detection + f_rect = [x, y + 1, tab_width - 1, self.height - 1] + fields.add(f_rect) + tab_hit = coll(f_rect) + playing_hint = False + active = False + + # Determine tab background colour + if not gui.radio_view: + if i == pctl.active_playlist_viewing: + bg = colours.tab_background_active + active = True + elif ( + tab_menu.active is True and tab_menu.reference == i) or (tab_menu.active is False and tab_hit and not playlist_box.drag): + bg = colours.tab_highlight + elif i == pctl.active_playlist_playing: + bg = colours.tab_background + playing_hint = True + else: + bg = colours.tab_background + elif pctl.radio_playlist_viewing == i: + bg = colours.tab_background_active + active = True + else: + bg = colours.tab_background + + # Draw tab background + ddt.rect(rect, bg) + if playing_hint: + ddt.rect(rect, [255, 255, 255, 7]) + + # Determine text colour + if active: + fg = colours.tab_text_active + else: + fg = colours.tab_text + + # Draw tab text + if gui.radio_view: + text = tab["name"] + else: + text = tab.title + ddt.text((x + self.tab_text_start_space, y + self.tab_text_y_offset), text, fg, self.tab_text_font, bg=bg) + + # Drop pulse + if gui.pl_pulse and gui.drop_playlist_target == i: + if tab_pulse.render(x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size, r=200, + g=130) is False: + gui.pl_pulse = False + + # Drag to move playlist + if tab_hit: + if mouse_down and i != playlist_box.drag_on and playlist_box.drag is True: + + if key_shift_down: + ddt.rect((x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size), [80, 160, 200, 255]) + elif playlist_box.drag_on < i: + ddt.rect((x + tab_width - bar_highlight_size, y, bar_highlight_size, gui.panelY2), [80, 160, 200, 255]) + else: + ddt.rect((x, y, bar_highlight_size, gui.panelY2), [80, 160, 200, 255]) + + elif quick_drag is True and pl_is_mut(i): + ddt.rect((x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size), [80, 200, 180, 255]) + # Drag yellow line highlight if single track already in playlist + elif quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15 * gui.scale): + for item in shift_selection: + if item < len(default_playlist) and default_playlist[item] in tab.playlist_ids: + ddt.rect((x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size), [190, 160, 20, 255]) + break + # Drag red line highlight if playlist is generator playlist + if quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15 * gui.scale): + if not pl_is_mut(i): + ddt.rect((x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size), [200, 70, 50, 255]) + + if not gui.radio_view: + if len(self.adds) > 0: + for k in reversed(range(len(self.adds))): + if pctl.multi_playlist[i].uuid_int == self.adds[k][0]: + if self.adds[k][2].get() > 0.3: + del self.adds[k] + else: + ay = y + 4 + ay -= 6 * self.adds[k][2].get() / 0.3 + + ddt.text( + (x + tab_width - 3, int(round(ay)), 1), "+" + str(self.adds[k][1]), colours.pluse_colour, 212, bg=bg) + gui.update += 1 + + x += tab_width + self.tab_spacing + + # Quick drag single track onto bar to create new playlist function and indicator + if prefs.tabs_on_top: + if quick_drag and mouse_position[0] > x and mouse_position[1] < gui.panelY and quick_d_timer.get() > 1: + ddt.rect((x, y, 2 * gui.scale, gui.panelY2), [80, 200, 180, 255]) + + if mouse_up: + drop_tracks_to_new_playlist(shift_selection) + + # Draw end drag tab indicator + if playlist_box.drag and mouse_position[0] > x and mouse_position[1] < gui.panelY: + if key_ctrl_down: + ddt.rect((x, y, 2 * gui.scale, gui.panelY2), [255, 190, 0, 255]) + else: + ddt.rect((x, y, 2 * gui.scale, gui.panelY2), [80, 160, 200, 255]) + + if prefs.tabs_on_top and right_overflow: + x += 24 * gui.scale + self.tabs_right_x += 24 * gui.scale + + # ------------- + # Other input + if mouse_up: + quick_drag = False + playlist_box.drag = False + radio_view.drag = None + + # Scroll anywhere on panel to cycle playlist + # (This is a bit complicated because we need to skip over hidden playlists) + if mouse_wheel != 0 and 1 < mouse_position[1] < gui.panelY + 1 and len(pctl.multi_playlist) > 1 and mouse_position[0] > 5: + + cycle_playlist_pinned(mouse_wheel) + + gui.pl_update = 1 + if not prefs.tabs_on_top: + if pctl.active_playlist_viewing not in shown: # and not gui.lsp: + gui.mode_toast_text = _(pctl.multi_playlist[pctl.active_playlist_viewing].title) + toast_mode_timer.set() + gui.frame_callback_list.append(TestTimer(1)) + else: + toast_mode_timer.force_set(10) + gui.mode_toast_text = "" + # --------- + # Menu Bar + + x += self.ini_menu_space + y += 7 * gui.scale + ddt.text_background_colour = colours.top_panel_background + + # MENU ----------------------------- + + word = _("MENU") + word_length = ddt.get_text_w(word, 212) + rect = [x - self.click_buffer, yy + self.ty + 1, word_length + self.click_buffer * 2, self.height - 1] + hit = coll(rect) + fields.add(rect) + + if (x_menu.active or hit) and not tab_menu.active: + bg = colours.status_text_over + else: + bg = colours.status_text_normal + ddt.text((x, y), word, bg, 212) + + if hit and inp.mouse_click: + if x_menu.active: + x_menu.active = False + else: + xx = x + if x > window_size[0] - (210 * gui.scale): + xx = window_size[0] - round(210 * gui.scale) + x_menu.activate(position=(xx + round(12 * gui.scale), gui.panelY)) + view_box.activate(xx) + + # if True: + # border = round(3 * gui.scale) + # border_colour = colours.grey(30) + # rect = (5 * gui.scale, gui.panelY, round(90 * gui.scale), round(25 * gui.scale)) + # + + dl = len(dl_mon.ready) + watching = len(dl_mon.watching) + + if (dl > 0 or watching > 0) and core_timer.get() > 2 and prefs.auto_extract and prefs.monitor_downloads: + x += 52 * gui.scale + rect = (x - 5 * gui.scale, y - 2 * gui.scale, 30 * gui.scale, 23 * gui.scale) + fields.add(rect) + + if coll(rect): + colour = colours.corner_button_active + # if colours.lm: + # colour = [40, 40, 40, 255] + if dl > 0 or watching > 0: + if right_click: + dl_menu.activate(position=(mouse_position[0], gui.panelY)) + if dl > 0: + if inp.mouse_click: + pln = 0 + for item in dl_mon.ready: + load_order = LoadClass() + load_order.target = item + pln = pctl.active_playlist_viewing + load_order.playlist = pctl.multi_playlist[pln].uuid_int + + for i, pl in enumerate(pctl.multi_playlist): + if prefs.download_playlist is not None: + if pl.uuid_int == prefs.download_playlist: + load_order.playlist = pl.uuid_int + pln = i + break + else: + for i, pl in enumerate(pctl.multi_playlist): + if pl.title.lower() == "downloads": + load_order.playlist = pl.uuid_int + pln = i + break + + load_orders.append(copy.deepcopy(load_order)) + + if len(dl_mon.ready) > 0: + dl_mon.ready.clear() + switch_playlist(pln) + + pctl.playlist_view_position = len(default_playlist) + logging.debug("Position changed by track import") + gui.update += 1 + else: + colour = colours.corner_button # [60, 60, 60, 255] + # if colours.lm: + # colour = [180, 180, 180, 255] + if inp.mouse_click: + inp.mouse_click = False + show_message( + _("It looks like something is being downloaded..."), _("Let's check back later..."), mode="info") + + + else: + colour = colours.corner_button # [60, 60, 60, 255] + if colours.lm: + # colour = [180, 180, 180, 255] + if dl_mon.ready: + colour = colours.corner_button_active # [60, 60, 60, 255] + + self.dl_button.render(x, y + 1 * gui.scale, colour) + if dl > 0: + ddt.text((x + 18 * gui.scale, y - 4 * gui.scale), str(dl), colours.pluse_colour, 209) # [244, 223, 66, 255] + # [166, 244, 179, 255] + + # LAYOUT -------------------------------- + x += self.menu_space + word_length + + self.drag_zone_start_x = x - 5 * gui.scale + status = True + + if loading_in_progress: + + bg = colours.status_info_text + if to_got == "xspf": + text = _("Importing XSPF playlist") + elif to_got == "xspfl": + text = _("Importing XSPF playlist...") + elif to_got == "ex": + text = _("Extracting Archive...") + else: + text = _("Importing... ") + str(to_got) # + "/" + str(to_get) + if right_click and coll([x, y, 180 * gui.scale, 18 * gui.scale]): + cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) + elif after_scan: + # bg = colours.status_info_text + bg = [100, 200, 100, 255] + text = _("Scanning Tags... {N} remaining").format(N=str(len(after_scan))) + elif move_in_progress: + text = _("File copy in progress...") + bg = colours.status_info_text + elif cm_clean_db and to_get > 0: + per = str(int(to_got / to_get * 100)) + text = _("Cleaning db... ") + per + "%" + bg = [100, 200, 100, 255] + elif to_scan: + text = _("Rescanning Tags... {N} remaining").format(N=str(len(to_scan))) + bg = [100, 200, 100, 255] + elif plex.scanning: + text = _("Accessing PLEX library...") + if gui.to_got: + text += f" {gui.to_got}" + bg = [229, 160, 13, 255] + elif tauon.spot_ctl.launching_spotify: + text = _("Launching Spotify...") + bg = [30, 215, 96, 255] + elif tauon.spot_ctl.preparing_spotify: + text = _("Preparing Spotify Playback...") + bg = [30, 215, 96, 255] + elif tauon.spot_ctl.spotify_com: + text = _("Accessing Spotify library...") + bg = [30, 215, 96, 255] + elif subsonic.scanning: + text = _("Accessing AIRSONIC library...") + if gui.to_got: + text += f" {gui.to_got}" + bg = [58, 194, 224, 255] + elif koel.scanning: + text = _("Accessing KOEL library...") + bg = [111, 98, 190, 255] + elif jellyfin.scanning: + text = _("Accessing JELLYFIN library...") + bg = [90, 170, 240, 255] + elif tauon.chrome_mode: + text = _("Chromecast Mode") + bg = [207, 94, 219, 255] + elif gui.sync_progress and not transcode_list: + text = gui.sync_progress + bg = [100, 200, 100, 255] + if right_click and coll([x, y, 280 * gui.scale, 18 * gui.scale]): + cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) + elif transcode_list and gui.tc_cancel: + bg = [150, 150, 150, 255] + text = _("Stopping transcode...") + elif lastfm.scanning_friends or lastfm.scanning_loves: + text = _("Scanning: ") + lastfm.scanning_username + bg = [200, 150, 240, 255] + elif lastfm.scanning_scrobbles: + text = _("Scanning Scrobbles...") + bg = [219, 88, 18, 255] + elif gui.buffering: + text = _("Buffering... ") + text += gui.buffering_text + bg = [18, 180, 180, 255] + + elif lfm_scrobbler.queue and scrobble_warning_timer.get() < 260: + text = _("Network error. Will try again later.") + bg = [250, 250, 250, 255] + last_fm_icon.render(x - 4 * gui.scale, y + 4 * gui.scale, [250, 40, 40, 255]) + x += 21 * gui.scale + elif tauon.listen_alongers: + new = {} + for ip, timer in tauon.listen_alongers.items(): + if timer.get() < 6: + new[ip] = timer + tauon.listen_alongers = new + + text = _("{N} listening along").format(N=len(tauon.listen_alongers)) + bg = [40, 190, 235, 255] + else: + status = False + + if status: + x += ddt.text((x, y), text, bg, 311) + # x += ddt.get_text_w(text, 11) + # TODO list listenieng clients + elif transcode_list: + bg = colours.status_info_text + # if key_ctrl_down and key_c_press: + # del transcode_list[1:] + # gui.tc_cancel = True + if right_click and coll([x, y, 280 * gui.scale, 18 * gui.scale]): + cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) + + w = 100 * gui.scale + x += ddt.text((x, y), _("Transcoding"), bg, 311) + 8 * gui.scale + + if gui.transcoding_batch_total: + + # c1 = [40, 40, 40, 255] + # c2 = [60, 60, 60, 255] + # c3 = [130, 130, 130, 255] + # + # if colours.lm: + # c1 = [100, 100, 100, 255] + # c2 = [130, 130, 130, 255] + # c3 = [180, 180, 180, 255] + + c1 = [40, 40, 40, 255] + c2 = [100, 59, 200, 200] + c3 = [150, 70, 200, 255] + + if colours.lm: + c1 = [100, 100, 100, 255] + c2 = [170, 140, 255, 255] + c3 = [230, 170, 255, 255] + + yy = y + 4 * gui.scale + h = 9 * gui.scale + box = [x, yy, w, h] + # ddt.rect_r(box, [100, 100, 100, 255]) + ddt.rect(box, c1) + + done = round(gui.transcoding_bach_done / gui.transcoding_batch_total * 100) + doing = round(core_use / gui.transcoding_batch_total * 100) + + ddt.rect([x, yy, done, h], c3) + ddt.rect([x + done, yy, doing, h], c2) + + x += w + 8 * gui.scale + + if gui.sync_progress: + text = gui.sync_progress + else: + text = _("{N} Folder Remaining {T}").format(N=str(len(transcode_list)), T=transcode_state) + if len(transcode_list) > 1: + text = _("{N} Folders Remaining {T}").format(N=str(len(transcode_list)), T=transcode_state) + + x += ddt.text((x, y), text, bg, 311) + 8 * gui.scale + + + if colours.lm: + colours.tb_line = colours.grey(200) + ddt.rect((0, int(gui.panelY - 1 * gui.scale), window_size[0], int(1 * gui.scale)), colours.tb_line) + +class BottomBarType1: + def __init__(self): + + self.mode = 0 + + self.seek_time = 0 + + self.seek_down = False + self.seek_hit = False + self.volume_hit = False + self.volume_bar_being_dragged = False + self.control_line_bottom = 35 * gui.scale + self.repeat_click_off = False + self.random_click_off = False + + self.seek_bar_position = [300 * gui.scale, window_size[1] - gui.panelBY] + self.seek_bar_size = [window_size[0] - (300 * gui.scale), 15 * gui.scale] + self.volume_bar_size = [135 * gui.scale, 14 * gui.scale] + self.volume_bar_position = [0, 45 * gui.scale] + + self.play_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "play.png", True) + self.forward_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "ff.png", True) + self.back_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "bb.png", True) + self.repeat_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_repeat.png", True) + self.repeat_button_off = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_repeat_off.png", True) + self.shuffle_button_off = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_shuffle_off.png", True) + self.shuffle_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_shuffle.png", True) + self.repeat_button_a = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_repeat_a.png", True) + self.shuffle_button_a = asset_loader(scaled_asset_directory, loaded_asset_dc, "tauon_shuffle_a.png", True) + + self.buffer_shard = asset_loader(scaled_asset_directory, loaded_asset_dc, "shard.png", True) + + self.scrob_stick = 0 + + def update(self): + + if self.mode == 0: + self.volume_bar_position[0] = window_size[0] - (210 * gui.scale) + self.volume_bar_position[1] = window_size[1] - (27 * gui.scale) + self.seek_bar_position[1] = window_size[1] - gui.panelBY + + seek_bar_x = 300 * gui.scale + if window_size[0] < 600 * gui.scale: + seek_bar_x = 250 * gui.scale + + self.seek_bar_size[0] = window_size[0] - seek_bar_x + self.seek_bar_position[0] = seek_bar_x + + # if gui.bb_show_art: + # self.seek_bar_position[0] = 300 + gui.panelBY + # self.seek_bar_size[0] = window_size[0] - 300 - gui.panelBY + + # self.seek_bar_position[0] = 0 + # self.seek_bar_size[0] = window_size[0] + + def render(self): + + global volume_store + global clicked + global right_click + + ddt.rect_a((0, window_size[1] - gui.panelBY), (window_size[0], gui.panelBY), colours.bottom_panel_colour) + + ddt.rect_a(self.seek_bar_position, self.seek_bar_size, colours.seek_bar_background) + + right_offset = 0 + if gui.display_time_mode >= 2: + right_offset = 22 * gui.scale + + if window_size[0] < 670 * gui.scale: + right_offset -= 90 * gui.scale + # Scrobble marker + + if prefs.scrobble_mark and ( + prefs.auto_lfm or lb.enable or prefs.maloja_enable) and not prefs.scrobble_hold and pctl.playing_length > 0 and 3 > pctl.playing_state > 0: + if pctl.master_library[pctl.track_queue[pctl.queue_step]].length > 240 * 2: + l_target = 240 + else: + l_target = int(pctl.master_library[pctl.track_queue[pctl.queue_step]].length * 0.50) + l_lead = l_target - pctl.a_time + + if l_lead > 0 and pctl.master_library[pctl.track_queue[pctl.queue_step]].length > 30: + l_x = self.seek_bar_position[0] + int(math.ceil( + pctl.playing_time * self.seek_bar_size[0] / int(pctl.playing_length))) + l_x += int(math.ceil(self.seek_bar_size[0] / int(pctl.playing_length) * l_lead)) + + if abs(self.scrob_stick - l_x) < 2: + l_x = self.scrob_stick + else: + self.scrob_stick = l_x + ddt.rect((self.scrob_stick, self.seek_bar_position[1], 2 * gui.scale, self.seek_bar_size[1]), [240, 10, 10, 80]) + + # # MINI ALBUM ART + # if gui.bb_show_art: + # rect = [self.seek_bar_position[0] - gui.panelBY, self.seek_bar_position[1], gui.panelBY, gui.panelBY] + # ddt.rect_r(rect, [255, 255, 255, 8], True) + # if 3 > pctl.playing_state > 0: + # album_art_gen.display(pctl.track_queue[pctl.queue_step], (rect[0], rect[1]), (rect[2], rect[3])) + + # ddt.rect_r(rect, [255, 255, 255, 20]) + + # SEEK BAR------------------ + if pctl.playing_time < 1: + self.seek_time = 0 + + if inp.mouse_click and coll_point( + mouse_position, + self.seek_bar_position + [self.seek_bar_size[0]] + [ + self.seek_bar_size[1] + 2]): + self.seek_down = True + self.volume_hit = True + if right_click and coll_point( + mouse_position, self.seek_bar_position + [self.seek_bar_size[0]] + [self.seek_bar_size[1] + 2]): + pctl.pause() + if pctl.playing_state == 0: + pctl.play() + + fields.add(self.seek_bar_position + self.seek_bar_size) + if coll(self.seek_bar_position + self.seek_bar_size): + + if middle_click and pctl.playing_state > 0: + gui.seek_cur_show = True + + clicked = True + if mouse_wheel != 0: + pctl.seek_time(pctl.playing_time + (mouse_wheel * 3)) + + if gui.seek_cur_show: + gui.update += 1 + + # fields.add([mouse_position[0] - 1, mouse_position[1] - 1, 1, 1]) + # ddt.rect_r([mouse_position[0] - 1, mouse_position[1] - 1, 1, 1], [255,0,0,180], True) + + bargetX = mouse_position[0] + bargetX = min(bargetX, self.seek_bar_position[0] + self.seek_bar_size[0]) + bargetX = max(bargetX, self.seek_bar_position[0]) + bargetX -= self.seek_bar_position[0] + seek = bargetX / self.seek_bar_size[0] + gui.cur_time = get_display_time(pctl.playing_object().length * seek) + + if self.seek_down is True: + if mouse_position[0] == 0: + self.seek_down = False + self.seek_hit = True + + if (mouse_up and coll(self.seek_bar_position + self.seek_bar_size) and coll_point( + last_click_location, self.seek_bar_position + self.seek_bar_size) + and coll_point( + click_location, self.seek_bar_position + self.seek_bar_size)) or (mouse_up and self.volume_hit) or self.seek_hit: + + self.volume_hit = False + self.seek_down = False + self.seek_hit = False + + bargetX = mouse_position[0] + bargetX = min(bargetX, self.seek_bar_position[0] + self.seek_bar_size[0]) + bargetX = max(bargetX, self.seek_bar_position[0]) + bargetX -= self.seek_bar_position[0] + seek = bargetX / self.seek_bar_size[0] + + pctl.seek_decimal(seek) + #logging.info(seek) + + self.seek_time = pctl.playing_time + + if radiobox.load_connecting or gui.buffering: + x = self.seek_bar_position[0] - round(26 - gui.scale) + y = self.seek_bar_position[1] + while x < self.seek_bar_position[0] + self.seek_bar_size[0]: + offset = (math.floor(((core_timer.get() * 1) % 1) * 13) / 13) * self.buffer_shard.w + gui.delay_frame(0.01) + + # colour = colours.seek_bar_fill + h, l, s = rgb_to_hls( + colours.seek_bar_background[0], colours.seek_bar_background[1], colours.seek_bar_background[2]) + l = min(1, l + 0.05) + colour = hls_to_rgb(h, l, s) + colour[3] = colours.seek_bar_background[3] + + self.buffer_shard.render(x + offset, y, colour) + x += self.buffer_shard.w + + ddt.rect( + (self.seek_bar_position[0] - self.buffer_shard.w, y, self.buffer_shard.w, self.buffer_shard.h), + colours.bottom_panel_colour) + + if pctl.playing_length > 0: + + if pctl.download_time != 0: + + if pctl.download_time == -1: + pctl.download_time = pctl.playing_length + + colour = (255, 255, 255, 10) + if gui.theme_name == "Lavender Light" or gui.theme_name == "Carbon": + colour = (255, 255, 255, 40) + + gui.seek_bar_rect = ( + self.seek_bar_position[0], self.seek_bar_position[1], + int(pctl.download_time * self.seek_bar_size[0] / pctl.playing_length), + self.seek_bar_size[1]) + ddt.rect(gui.seek_bar_rect, colour) + + gui.seek_bar_rect = ( + self.seek_bar_position[0], self.seek_bar_position[1], + int(self.seek_time * self.seek_bar_size[0] / pctl.playing_length), + self.seek_bar_size[1]) + ddt.rect(gui.seek_bar_rect, colours.seek_bar_fill) + + if gui.seek_cur_show: + + if coll( + [self.seek_bar_position[0] - 50, self.seek_bar_position[1] - 50, self.seek_bar_size[0] + 50, self.seek_bar_size[1] + 100]): + if mouse_position[0] > self.seek_bar_position[0] - 1: + cur = [mouse_position[0] - 40, self.seek_bar_position[1] - 25, 42, 19] + ddt.rect(cur, colours.grey(15)) + # ddt.rect_r(cur, colours.grey(80)) + ddt.text( + (mouse_position[0] - 40 + 3, self.seek_bar_position[1] - 24), gui.cur_time, + colours.grey(180), 213, + bg=colours.grey(15)) + + ddt.rect( + [mouse_position[0], self.seek_bar_position[1], 2, self.seek_bar_size[1]], + [100, 100, 20, 255]) + + else: + gui.seek_cur_show = False + + if gui.buffering and pctl.buffering_percent: + ddt.rect_a((self.seek_bar_position[0], self.seek_bar_position[1] + self.seek_bar_size[1] - round(3 * gui.scale)), (self.seek_bar_size[0] * pctl.buffering_percent / 100, round(3 * gui.scale)), [255, 255, 255, 50]) + # Volume mouse wheel control ----------------------------------------- + if mouse_wheel != 0 and mouse_position[1] > self.seek_bar_position[1] + 4 and not coll_point( + mouse_position, self.seek_bar_position + self.seek_bar_size): + + pctl.player_volume += mouse_wheel * prefs.volume_wheel_increment + if pctl.player_volume < 1: + pctl.player_volume = 0 + elif pctl.player_volume > 100: + pctl.player_volume = 100 + + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() + + # Volume Bar 2 ------------------------------------------------ + if window_size[0] < 670 * gui.scale: + x = window_size[0] - right_offset - 207 * gui.scale + y = window_size[1] - round(14 * gui.scale) + + rect = (x - 8 * gui.scale, y - 17 * gui.scale, 55 * gui.scale, 23 * gui.scale) + # ddt.rect(rect, [255,255,255,25]) + if coll(rect) and mouse_down: + gui.update_on_drag = True + + h_rect = (x - 6 * gui.scale, y - 17 * gui.scale, 4 * gui.scale, 23 * gui.scale) + if coll(h_rect) and mouse_down: + pctl.player_volume = 0 + + step = round(1 * gui.scale) + min_h = round(4 * gui.scale) + spacing = round(5 * gui.scale) + + if right_click and coll((h_rect[0], h_rect[1], h_rect[2] + 50 * gui.scale, h_rect[3])): + if right_click: + pctl.toggle_mute() + + for bar in range(8): + + h = min_h + bar * step + rect = (x, y - h, 3 * gui.scale, h) + h_rect = (x - 1 * gui.scale, y - 17 * gui.scale, 4 * gui.scale, 23 * gui.scale) + + if coll(h_rect): + if mouse_down or mouse_up: + gui.update_on_drag = True + + if bar == 0: + pctl.player_volume = 5 + if bar == 1: + pctl.player_volume = 10 + if bar == 2: + pctl.player_volume = 20 + if bar == 3: + pctl.player_volume = 30 + if bar == 4: + pctl.player_volume = 45 + if bar == 5: + pctl.player_volume = 55 + if bar == 6: + pctl.player_volume = 70 + if bar == 7: + pctl.player_volume = 100 + + pctl.set_volume() + + colour = colours.mode_button_off + + if bar == 0 and pctl.player_volume > 0: + colour = colours.mode_button_active + elif bar == 1 and pctl.player_volume >= 10: + colour = colours.mode_button_active + elif bar == 2 and pctl.player_volume >= 20: + colour = colours.mode_button_active + elif bar == 3 and pctl.player_volume >= 30: + colour = colours.mode_button_active + elif bar == 4 and pctl.player_volume >= 45: + colour = colours.mode_button_active + elif bar == 5 and pctl.player_volume >= 55: + colour = colours.mode_button_active + elif bar == 6 and pctl.player_volume >= 70: + colour = colours.mode_button_active + elif bar == 7 and pctl.player_volume >= 95: + colour = colours.mode_button_active + + ddt.rect(rect, colour) + x += spacing + + # Volume Bar -------------------------------------------------------- + else: + if (inp.mouse_click and coll(( + self.volume_bar_position[0] - right_offset, self.volume_bar_position[1], self.volume_bar_size[0], + self.volume_bar_size[1] + 4))) or \ + self.volume_bar_being_dragged is True: + clicked = True + + if inp.mouse_click is True or self.volume_bar_being_dragged is True: + gui.update = 2 + + self.volume_bar_being_dragged = True + volgetX = mouse_position[0] + volgetX = min(volgetX, self.volume_bar_position[0] + self.volume_bar_size[0] - right_offset) + volgetX = max(volgetX, self.volume_bar_position[0] - right_offset) + volgetX -= self.volume_bar_position[0] - right_offset + pctl.player_volume = volgetX / self.volume_bar_size[0] * 100 + + time.sleep(0.02) + + if mouse_down is False: + self.volume_bar_being_dragged = False + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume(True) + + if mouse_down: + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume(False) + + if right_click and coll(( + self.volume_bar_position[0] - 15 * gui.scale, self.volume_bar_position[1] - 10 * gui.scale, + self.volume_bar_size[0] + 30 * gui.scale, + self.volume_bar_size[1] + 20 * gui.scale)): + + if pctl.player_volume > 0: + volume_store = pctl.player_volume + pctl.player_volume = 0 + else: + pctl.player_volume = volume_store + + pctl.set_volume() + + ddt.rect_a( + (self.volume_bar_position[0] - right_offset, self.volume_bar_position[1]), + self.volume_bar_size, colours.volume_bar_background) # 22 + + gui.volume_bar_rect = ( + self.volume_bar_position[0] - right_offset, self.volume_bar_position[1], + int(pctl.player_volume * self.volume_bar_size[0] / 100), self.volume_bar_size[1]) + + ddt.rect(gui.volume_bar_rect, colours.volume_bar_fill) + + fields.add(self.volume_bar_position + self.volume_bar_size) + if pctl.active_replaygain != 0 and (coll(( + self.volume_bar_position[0], self.volume_bar_position[1], self.volume_bar_size[0], + self.volume_bar_size[1])) or self.volume_bar_being_dragged): + + if pctl.player_volume > 50: + ddt.text( + (self.volume_bar_position[0] - right_offset + 8 * gui.scale, + self.volume_bar_position[1] - 1 * gui.scale), str(pctl.active_replaygain) + " dB", + colours.volume_bar_background, + 11, bg=colours.volume_bar_fill) + else: + ddt.text( + (self.volume_bar_position[0] - right_offset + 85 * gui.scale, + self.volume_bar_position[1] - 1 * gui.scale), str(pctl.active_replaygain) + " dB", + colours.volume_bar_fill, + 11, bg=colours.volume_bar_background) + + gui.show_bottom_title = gui.showed_title ^ True + if not prefs.hide_bottom_title: + gui.show_bottom_title = True + + if gui.show_bottom_title and pctl.playing_state > 0 and window_size[0] > 820 * gui.scale: + line = pctl.title_text() + + x = self.seek_bar_position[0] + 1 + mx = window_size[0] - 710 * gui.scale + # if gui.bb_show_art: + # x += 10 * gui.scale + # mx -= gui.panelBY - 10 + + # line = trunc_line(line, 213, mx) + ddt.text( + (x, self.seek_bar_position[1] + 24 * gui.scale), line, colours.bar_title_text, + fonts.panel_title, max_w=mx) + + if (inp.mouse_click or right_click) and coll(( + self.seek_bar_position[0] - 10 * gui.scale, self.seek_bar_position[1] + 20 * gui.scale, + window_size[0] - 710 * gui.scale, 30 * gui.scale)): + # if pctl.playing_state == 3: + # copy_to_clipboard(pctl.tag_meta) + # show_message("Copied text to clipboard") + # if input.mouse_click or right_click: + # input.mouse_click = False + # right_click = False + # else: + if inp.mouse_click and pctl.playing_state != 3: + pctl.show_current() + + if pctl.playing_ready() and not gui.fullscreen: + + if right_click: + mode_menu.activate() + + if d_click_timer.get() < 0.3 and inp.mouse_click: + set_mini_mode() + gui.update += 1 + return + d_click_timer.set() + + # TIME---------------------- + + x = window_size[0] - 57 * gui.scale + y = window_size[1] - 29 * gui.scale + + r_start = x - 10 * gui.scale + if gui.display_time_mode in (2, 3): + r_start -= 20 * gui.scale + rect = (r_start, y - 3 * gui.scale, 80 * gui.scale, 27 * gui.scale) + # ddt.rect_r(rect, [255, 0, 0, 40], True) + if inp.mouse_click and coll(rect): + gui.display_time_mode += 1 + if gui.display_time_mode > 3: + gui.display_time_mode = 0 + + if gui.display_time_mode == 0: + text_time = get_display_time(pctl.playing_time) + ddt.text( + (x + 1 * gui.scale, y), text_time, colours.time_playing, + fonts.bottom_panel_time) + elif gui.display_time_mode == 1: + if pctl.playing_state == 0: + text_time = get_display_time(0) + else: + text_time = get_display_time(pctl.playing_length - pctl.playing_time) + ddt.text( + (x + 1 * gui.scale, y), text_time, colours.time_playing, + fonts.bottom_panel_time) + ddt.text( + (x - 5 * gui.scale, y), "-", colours.time_playing, + fonts.bottom_panel_time) + elif gui.display_time_mode == 2: + + # colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) + + x -= 4 + text_time = get_display_time(pctl.playing_time) + ddt.text( + (x - 25 * gui.scale, y), text_time, colours.time_playing, + fonts.bottom_panel_time) + + offset1 = 10 * gui.scale + + if system == "Windows": + offset1 += 2 * gui.scale + + offset2 = offset1 + 7 * gui.scale + + ddt.text( + (x + offset1, y), "/", colours.time_sub, + fonts.bottom_panel_time) + text_time = get_display_time(pctl.playing_length) + if pctl.playing_state == 0: + text_time = get_display_time(0) + elif pctl.playing_state == 3: + text_time = "-- : --" + ddt.text( + (x + offset2, y), text_time, colours.time_sub, + fonts.bottom_panel_time) + + elif gui.display_time_mode == 3: + + # colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) + + track = pctl.playing_object() + if track and track.index != gui.dtm3_index: + + gui.dtm3_cum = 0 + gui.dtm3_total = 0 + run = True + collected = [] + for item in default_playlist: + if pctl.master_library[item].parent_folder_path == track.parent_folder_path: + if item not in collected: + collected.append(item) + gui.dtm3_total += pctl.master_library[item].length + if item == track.index: + run = False + if run: + gui.dtm3_cum += pctl.master_library[item].length + gui.dtm3_index = track.index + + x -= 4 + text_time = get_display_time(gui.dtm3_cum + pctl.playing_time) + + ddt.text( + (x - 25 * gui.scale, y), text_time, colours.time_playing, + fonts.bottom_panel_time) + + offset1 = 10 * gui.scale + if system == "Windows": + offset1 += 2 * gui.scale + offset2 = offset1 + 7 * gui.scale + + ddt.text( + (x + offset1, y), "/", colours.time_sub, + fonts.bottom_panel_time) + text_time = get_display_time(gui.dtm3_total) + if pctl.playing_state == 0: + text_time = get_display_time(0) + elif pctl.playing_state == 3: + text_time = "-- : --" + ddt.text( + (x + offset2, y), text_time, colours.time_sub, + fonts.bottom_panel_time) + + # BUTTONS + # bottom buttons + + if gui.mode == 1: + + # PLAY--- + buttons_x_offset = 0 + compact = False + if window_size[0] < 650 * gui.scale: + compact = True + + play_colour = colours.media_buttons_off + pause_colour = colours.media_buttons_off + stop_colour = colours.media_buttons_off + forward_colour = colours.media_buttons_off + back_colour = colours.media_buttons_off + + if pctl.playing_state == 1: + play_colour = colours.media_buttons_active + + if pctl.auto_stop: + stop_colour = colours.media_buttons_active + + if pctl.playing_state == 2 or (tauon.spot_ctl.coasting and tauon.spot_ctl.paused): + pause_colour = colours.media_buttons_active + play_colour = colours.media_buttons_active + elif pctl.playing_state == 3: + play_colour = colours.media_buttons_active + if tauon.stream_proxy.encode_running: + play_colour = [220, 50, 50, 255] + + if not compact or (compact and pctl.playing_state != 1): + rect = ( + buttons_x_offset + (10 * gui.scale), window_size[1] - self.control_line_bottom - (13 * gui.scale), + 50 * gui.scale, 40 * gui.scale) + fields.add(rect) + if coll(rect): + play_colour = colours.media_buttons_over + if inp.mouse_click: + if compact and pctl.playing_state == 1: + pctl.pause() + elif pctl.playing_state == 1 or tauon.spot_ctl.coasting: + pctl.show_current(highlight=True) + else: + pctl.play() + inp.mouse_click = False + tool_tip2.test(33 * gui.scale, y - 35 * gui.scale, _("Play, RC: Go to playing")) + + if right_click: + pctl.show_current(highlight=True) + + self.play_button.render(29 * gui.scale, window_size[1] - self.control_line_bottom, play_colour) + # ddt.rect_r(rect,[255,0,0,255], True) + + # PAUSE--- + if compact: + buttons_x_offset = -46 * gui.scale + + x = (75 * gui.scale) + buttons_x_offset + y = window_size[1] - self.control_line_bottom + + if not compact or (compact and pctl.playing_state == 1): + + rect = (x - 15 * gui.scale, y - 13 * gui.scale, 50 * gui.scale, 40 * gui.scale) + fields.add(rect) + if coll(rect) and not (pctl.playing_state == 3 and not tauon.spot_ctl.coasting): + pause_colour = colours.media_buttons_over + if inp.mouse_click: + pctl.pause() + if right_click: + pctl.show_current(highlight=True) + tool_tip2.test(x, y - 35 * gui.scale, _("Pause")) + + # ddt.rect_r(rect,[255,0,0,255], True) + ddt.rect_a((x, y + 0), (4 * gui.scale, 13 * gui.scale), pause_colour) + ddt.rect_a((x + 10 * gui.scale, y + 0), (4 * gui.scale, 13 * gui.scale), pause_colour) + + # STOP--- + x = 125 * gui.scale + buttons_x_offset + rect = (x - 14 * gui.scale, y - 13 * gui.scale, 50 * gui.scale, 40 * gui.scale) + fields.add(rect) + if coll(rect): + stop_colour = colours.media_buttons_over + if inp.mouse_click: + pctl.stop() + if right_click: + pctl.auto_stop ^= True + tool_tip2.test(x, y - 35 * gui.scale, _("Stop, RC: Toggle auto-stop")) + + ddt.rect_a((x, y + 0), (13 * gui.scale, 13 * gui.scale), stop_colour) + # ddt.rect_r(rect,[255,0,0,255], True) + + if compact: + buttons_x_offset -= 5 * gui.scale + + # FORWARD--- + rect = (buttons_x_offset + 230 * gui.scale, window_size[1] - self.control_line_bottom - 10 * gui.scale, + 50 * gui.scale, 35 * gui.scale) + fields.add(rect) + if coll(rect) and not (pctl.playing_state == 3 and not tauon.spot_ctl.coasting): + forward_colour = colours.media_buttons_over + if inp.mouse_click: + pctl.advance() + gui.tool_tip_lock_off_f = True + if right_click: + # pctl.random_mode ^= True + toggle_random() + gui.tool_tip_lock_off_f = True + # if window_size[0] < 600 * gui.scale: + # . Shuffle set to on + gui.mode_toast_text = _("Shuffle On") + if not pctl.random_mode: + # . Shuffle set to off + gui.mode_toast_text = _("Shuffle Off") + toast_mode_timer.set() + gui.delay_frame(1) + if middle_click: + pctl.advance(rr=True) + gui.tool_tip_lock_off_f = True + # tool_tip.test(buttons_x_offset + 230 * gui.scale + 50 * gui.scale, window_size[1] - self.control_line_bottom - 20 * gui.scale, "Advance") + # if not gui.tool_tip_lock_off_f: + # tool_tip2.test(x + 45 * gui.scale, y - 35 * gui.scale, _("Forward, RC: Toggle shuffle, MC: Radio random")) + else: + gui.tool_tip_lock_off_f = False + + self.forward_button.render( + buttons_x_offset + 240 * gui.scale, 1 + window_size[1] - self.control_line_bottom, forward_colour) + + # ddt.rect_r(rect,[255,0,0,255], True) + + # BACK--- + rect = (buttons_x_offset + 170 * gui.scale, window_size[1] - self.control_line_bottom - 10 * gui.scale, + 50 * gui.scale, 35 * gui.scale) + fields.add(rect) + if coll(rect) and not (pctl.playing_state == 3 and not tauon.spot_ctl.coasting): + back_colour = colours.media_buttons_over + if inp.mouse_click: + pctl.back() + gui.tool_tip_lock_off_b = True + if right_click: + toggle_repeat() + gui.tool_tip_lock_off_b = True + # if window_size[0] < 600 * gui.scale: + # . Repeat set to on + gui.mode_toast_text = _("Repeat On") + if not pctl.repeat_mode: + # . Repeat set to off + gui.mode_toast_text = _("Repeat Off") + toast_mode_timer.set() + gui.delay_frame(1) + if middle_click: + pctl.revert() + gui.tool_tip_lock_off_b = True + if not gui.tool_tip_lock_off_b: + tool_tip2.test(x, y - 35 * gui.scale, _("Back, RC: Toggle repeat, MC: Revert")) + else: + gui.tool_tip_lock_off_b = False + + self.back_button.render(buttons_x_offset + 180 * gui.scale, 1 + window_size[1] - self.control_line_bottom, + back_colour) + # ddt.rect_r(rect,[255,0,0,255], True) + + # menu button + + x = window_size[0] - 252 * gui.scale - right_offset + y = window_size[1] - round(26 * gui.scale) + rpbc = colours.mode_button_off + rect = (x - 9 * gui.scale, y - 5 * gui.scale, 40 * gui.scale, 25 * gui.scale) + fields.add(rect) + if coll(rect): + if not extra_menu.active: + tool_tip.test(x, y - 28 * gui.scale, _("Playback menu")) + rpbc = colours.mode_button_over + if inp.mouse_click: + extra_menu.activate(position=(x - 115 * gui.scale, y - 6 * gui.scale)) + elif right_click: + mode_menu.activate(position=(x - 115 * gui.scale, y - 6 * gui.scale)) + if extra_menu.active: + rpbc = colours.mode_button_active + + spacing = round(5 * gui.scale) + ddt.rect_a((x, y), (24 * gui.scale, 2 * gui.scale), rpbc) + y += spacing + ddt.rect_a((x, y), (24 * gui.scale, 2 * gui.scale), rpbc) + y += spacing + ddt.rect_a((x, y), (24 * gui.scale, 2 * gui.scale), rpbc) + + if self.mode == 0 and window_size[0] > 530 * gui.scale: + + # shuffle button + x = window_size[0] - 318 * gui.scale - right_offset + y = window_size[1] - 27 * gui.scale + + rect = (x - 5 * gui.scale, y - 5 * gui.scale, 60 * gui.scale, 25 * gui.scale) + fields.add(rect) + + rpbc = colours.mode_button_off + off = True + if (inp.mouse_click or right_click) and coll(rect): + + if inp.mouse_click: + # pctl.random_mode ^= True + toggle_random() + if pctl.random_mode is False: + self.random_click_off = True + else: + shuffle_menu.activate(position=(x + 30 * gui.scale, y - 7 * gui.scale)) + + if pctl.random_mode: + rpbc = colours.mode_button_active + off = False + if coll(rect): + tool_tip.test(x, y - 28 * gui.scale, _("Shuffle")) + elif coll(rect): + tool_tip.test(x, y - 28 * gui.scale, _("Shuffle")) + if self.random_click_off is True: + rpbc = colours.mode_button_off + elif pctl.random_mode is True: + rpbc = colours.mode_button_active + else: + rpbc = colours.mode_button_over + else: + self.random_click_off = False + + # Keep hover highlight on if menu is open + if shuffle_menu.active and not pctl.random_mode: + rpbc = colours.mode_button_over + + #self.shuffle_button.render(x + round(1 * gui.scale), y + round(1 * gui.scale), rpbc) + + #y += round(3 * gui.scale) + #ddt.rect_a((x, y), (25 * gui.scale, 3 * gui.scale), rpbc) + + if pctl.album_shuffle_mode: + self.shuffle_button_a.render(x + round(1 * gui.scale), y + round(1 * gui.scale), rpbc) + elif off: + self.shuffle_button_off.render(x + round(1 * gui.scale), y + round(1 * gui.scale), rpbc) + else: + self.shuffle_button.render(x + round(1 * gui.scale), y + round(1 * gui.scale), rpbc) + + #ddt.rect_a((x + 25 * gui.scale, y), (23 * gui.scale, 3 * gui.scale), rpbc) + + #y += round(5 * gui.scale) + #ddt.rect_a((x, y), (48 * gui.scale, 3 * gui.scale), rpbc) + + # REPEAT + x = window_size[0] - round(380 * gui.scale) - right_offset + y = window_size[1] - round(27 * gui.scale) + + rpbc = colours.mode_button_off + off = True + + rect = (x - 6 * gui.scale, y - 5 * gui.scale, 61 * gui.scale, 25 * gui.scale) + fields.add(rect) + if (inp.mouse_click or right_click) and coll(rect): + + if inp.mouse_click: + toggle_repeat() + if pctl.repeat_mode is False: + self.repeat_click_off = True + else: # right click + repeat_menu.activate(position=(x + 30 * gui.scale, y - 7 * gui.scale)) + # pctl.album_repeat_mode ^= True + # if not pctl.repeat_mode: + # self.repeat_click_off = True + + if pctl.repeat_mode: + rpbc = colours.mode_button_active + off = False + if coll(rect): + if pctl.album_repeat_mode: + tool_tip.test(x, y - 28 * gui.scale, _("Repeat album")) + else: + tool_tip.test(x, y - 28 * gui.scale, _("Repeat track")) + elif coll(rect): + + # Tooltips. But don't show tooltips if menus open + if not repeat_menu.active and not shuffle_menu.active: + if pctl.album_repeat_mode: + tool_tip.test(x, y - 28 * gui.scale, _("Repeat album")) + else: + tool_tip.test(x, y - 28 * gui.scale, _("Repeat track")) + + if self.repeat_click_off is True: + rpbc = colours.mode_button_off + elif pctl.repeat_mode is True: + rpbc = colours.mode_button_active + else: + rpbc = colours.mode_button_over + else: + self.repeat_click_off = False + + # Keep hover highlight on if menu is open + if repeat_menu.active and not pctl.repeat_mode: + rpbc = colours.mode_button_over + + rpbc = alpha_blend(rpbc, colours.bottom_panel_colour) # bake in alpha in case of overlap + + y += round(3 * gui.scale) + w = round(3 * gui.scale) + y = round(y) + x = round(x) + + ar = x + round(50 * gui.scale) + h = round(5 * gui.scale) + + if pctl.album_repeat_mode: + self.repeat_button_a.render(ar - round(45 * gui.scale), y - round(2 * gui.scale), rpbc) + #ddt.rect_a((x + round(4 * gui.scale), y), (round(25 * gui.scale), w), rpbc) + elif off: + self.repeat_button_off.render(ar - round(45 * gui.scale), y - round(2 * gui.scale), rpbc) + else: + self.repeat_button.render(ar - round(45 * gui.scale), y - round(2 * gui.scale), rpbc) + #ddt.rect_a((ar - round(25 * gui.scale), y), (round(25 * gui.scale), w), rpbc) + #ddt.rect_a((ar - w, y), (w, h), rpbc) + #ddt.rect_a((ar - round(50 * gui.scale), y + h), (round(50 * gui.scale), w), rpbc) + + # ddt.rect_a((x + round(25 * gui.scale), y), (round(25 * gui.scale), w), rpbc, True) + # ddt.rect_a((x + round(4 * gui.scale), y + round(5 * gui.scale)), (math.floor(46 * gui.scale), w), rpbc, True) + # ddt.rect_a((x + 50 * gui.scale - w, y), (w, 8 * gui.scale), rpbc, True) + # ddt.rect_a((x + round(50 * gui.scale) - w, y + w), (w, round(4 * gui.scale)), rpbc, True) + +class BottomBarType_ao1: + def __init__(self): + + self.mode = 0 + + self.seek_time = 0 + + self.seek_down = False + self.seek_hit = False + self.volume_hit = False + self.volume_bar_being_dragged = False + self.control_line_bottom = 35 * gui.scale + self.repeat_click_off = False + self.random_click_off = False + + self.seek_bar_position = [300 * gui.scale, window_size[1] - gui.panelBY] + self.seek_bar_size = [window_size[0] - (300 * gui.scale), 15 * gui.scale] + self.volume_bar_size = [135 * gui.scale, 14 * gui.scale] + self.volume_bar_position = [0, 45 * gui.scale] + + self.play_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "play.png", True) + self.forward_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "ff.png", True) + self.back_button = asset_loader(scaled_asset_directory, loaded_asset_dc, "bb.png", True) + + self.scrob_stick = 0 + + def update(self): + + if self.mode == 0: + self.volume_bar_position[0] = window_size[0] - (210 * gui.scale) + self.volume_bar_position[1] = window_size[1] - (27 * gui.scale) + self.seek_bar_position[1] = window_size[1] - gui.panelBY + + seek_bar_x = 300 * gui.scale + if window_size[0] < 600 * gui.scale: + seek_bar_x = 250 * gui.scale + + self.seek_bar_size[0] = window_size[0] - seek_bar_x + self.seek_bar_position[0] = seek_bar_x + + # if gui.bb_show_art: + # self.seek_bar_position[0] = 300 + gui.panelBY + # self.seek_bar_size[0] = window_size[0] - 300 - gui.panelBY + + # self.seek_bar_position[0] = 0 + # self.seek_bar_size[0] = window_size[0] + + def render(self): + + global volume_store + global clicked + global right_click + + ddt.rect_a((0, window_size[1] - gui.panelBY), (window_size[0], gui.panelBY), colours.bottom_panel_colour) + + right_offset = 0 + if gui.display_time_mode >= 2: + right_offset = 22 * gui.scale + + if window_size[0] < 670 * gui.scale: + right_offset -= 90 * gui.scale + + # # MINI ALBUM ART + # if gui.bb_show_art: + # rect = [self.seek_bar_position[0] - gui.panelBY, self.seek_bar_position[1], gui.panelBY, gui.panelBY] + # ddt.rect_r(rect, [255, 255, 255, 8], True) + # if 3 > pctl.playing_state > 0: + # album_art_gen.display(pctl.track_queue[pctl.queue_step], (rect[0], rect[1]), (rect[2], rect[3])) + + # ddt.rect_r(rect, [255, 255, 255, 20]) + + # Volume mouse wheel control ----------------------------------------- + if mouse_wheel != 0 and mouse_position[1] > self.seek_bar_position[1] + 4 and not coll_point( + mouse_position, self.seek_bar_position + self.seek_bar_size): + + pctl.player_volume += mouse_wheel * prefs.volume_wheel_increment + if pctl.player_volume < 1: + pctl.player_volume = 0 + elif pctl.player_volume > 100: + pctl.player_volume = 100 + + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() + + # mode menu + if right_click: + if mouse_position[0] > 190 * gui.scale and \ + mouse_position[1] > window_size[1] - gui.panelBY and \ + mouse_position[0] < window_size[0] - 190 * gui.scale: + mode_menu.activate() + + # Volume Bar 2 ------------------------------------------------ + if True: + x = window_size[0] - right_offset - 120 * gui.scale + y = window_size[1] - round(21 * gui.scale) + + if gui.compact_bar: + x -= 90 * gui.scale + + rect = (x - 8 * gui.scale, y - 17 * gui.scale, 55 * gui.scale, 23 * gui.scale) + # ddt.rect(rect, [255,255,255,25]) + if coll(rect) and mouse_down: + gui.update_on_drag = True + + h_rect = (x - 6 * gui.scale, y - 17 * gui.scale, 4 * gui.scale, 23 * gui.scale) + if coll(h_rect) and mouse_down: + pctl.player_volume = 0 + + step = round(1 * gui.scale) + min_h = round(4 * gui.scale) + spacing = round(5 * gui.scale) + + if right_click and coll((h_rect[0], h_rect[1], h_rect[2] + 50 * gui.scale, h_rect[3])): + if right_click: + if pctl.player_volume > 0: + volume_store = pctl.player_volume + pctl.player_volume = 0 + else: + pctl.player_volume = volume_store + + pctl.set_volume() + + for bar in range(8): + + h = min_h + bar * step + rect = (x, y - h, 3 * gui.scale, h) + h_rect = (x - 1 * gui.scale, y - 17 * gui.scale, 4 * gui.scale, 23 * gui.scale) + + if coll(h_rect): + if mouse_down: + gui.update_on_drag = True + + if bar == 0: + pctl.player_volume = 5 + if bar == 1: + pctl.player_volume = 10 + if bar == 2: + pctl.player_volume = 20 + if bar == 3: + pctl.player_volume = 30 + if bar == 4: + pctl.player_volume = 45 + if bar == 5: + pctl.player_volume = 55 + if bar == 6: + pctl.player_volume = 70 + if bar == 7: + pctl.player_volume = 100 + + pctl.set_volume() + + colour = colours.mode_button_off + + if bar == 0 and pctl.player_volume > 0: + colour = colours.mode_button_active + elif bar == 1 and pctl.player_volume >= 10: + colour = colours.mode_button_active + elif bar == 2 and pctl.player_volume >= 20: + colour = colours.mode_button_active + elif bar == 3 and pctl.player_volume >= 30: + colour = colours.mode_button_active + elif bar == 4 and pctl.player_volume >= 45: + colour = colours.mode_button_active + elif bar == 5 and pctl.player_volume >= 55: + colour = colours.mode_button_active + elif bar == 6 and pctl.player_volume >= 70: + colour = colours.mode_button_active + elif bar == 7 and pctl.player_volume >= 95: + colour = colours.mode_button_active + + ddt.rect(rect, colour) + x += spacing + + # TIME---------------------- + + x = window_size[0] - 57 * gui.scale + y = window_size[1] - 35 * gui.scale + + r_start = x - 10 * gui.scale + if gui.display_time_mode in (2, 3): + r_start -= 20 * gui.scale + rect = (r_start, y - 3 * gui.scale, 80 * gui.scale, 27 * gui.scale) + # ddt.rect_r(rect, [255, 0, 0, 40], True) + if inp.mouse_click and coll(rect): + gui.display_time_mode += 1 + if gui.display_time_mode > 3: + gui.display_time_mode = 0 + + if gui.display_time_mode == 0: + text_time = get_display_time(pctl.playing_time) + ddt.text((x + 1 * gui.scale, y), text_time, colours.time_playing, fonts.bottom_panel_time) + elif gui.display_time_mode == 1: + if pctl.playing_state == 0: + text_time = get_display_time(0) + else: + text_time = get_display_time(pctl.playing_length - pctl.playing_time) + ddt.text((x + 1 * gui.scale, y), text_time, colours.time_playing, fonts.bottom_panel_time) + ddt.text((x - 5 * gui.scale, y), "-", colours.time_playing, fonts.bottom_panel_time) + elif gui.display_time_mode == 2: + + colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) + + x -= 4 + text_time = get_display_time(pctl.playing_time) + ddt.text((x - 25 * gui.scale, y), text_time, colours.time_playing, fonts.bottom_panel_time) + + offset1 = 10 * gui.scale + + if system == "Windows": + offset1 += 2 * gui.scale + + offset2 = offset1 + 7 * gui.scale + + ddt.text((x + offset1, y), "/", colours.time_sub, fonts.bottom_panel_time) + text_time = get_display_time(pctl.playing_length) + if pctl.playing_state == 0: + text_time = get_display_time(0) + elif pctl.playing_state == 3: + text_time = "-- : --" + ddt.text((x + offset2, y), text_time, colours.time_sub, fonts.bottom_panel_time) + + elif gui.display_time_mode == 3: + + colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) + + track = pctl.playing_object() + if track and track.index != gui.dtm3_index: + + gui.dtm3_cum = 0 + gui.dtm3_total = 0 + run = True + collected = [] + for item in default_playlist: + if pctl.master_library[item].parent_folder_path == track.parent_folder_path: + if item not in collected: + collected.append(item) + gui.dtm3_total += pctl.master_library[item].length + if item == track.index: + run = False + if run: + gui.dtm3_cum += pctl.master_library[item].length + gui.dtm3_index = track.index + + x -= 4 + text_time = get_display_time(gui.dtm3_cum + pctl.playing_time) + + ddt.text((x - 25 * gui.scale, y), text_time, colours.time_playing, fonts.bottom_panel_time) + + offset1 = 10 * gui.scale + if system == "Windows": + offset1 += 2 * gui.scale + offset2 = offset1 + 7 * gui.scale + + ddt.text((x + offset1, y), "/", colours.time_sub, fonts.bottom_panel_time) + text_time = get_display_time(gui.dtm3_total) + if pctl.playing_state == 0: + text_time = get_display_time(0) + elif pctl.playing_state == 3: + text_time = "-- : --" + ddt.text((x + offset2, y), text_time, colours.time_sub, fonts.bottom_panel_time) + + # BUTTONS + # bottom buttons + + if gui.mode == 1: + + # PLAY--- + buttons_x_offset = 0 + compact = False + if window_size[0] < 650 * gui.scale: + compact = True + + play_colour = colours.media_buttons_off + pause_colour = colours.media_buttons_off + stop_colour = colours.media_buttons_off + forward_colour = colours.media_buttons_off + back_colour = colours.media_buttons_off + + if pctl.playing_state == 1: + play_colour = colours.media_buttons_active + + if pctl.auto_stop: + stop_colour = colours.media_buttons_active + + if pctl.playing_state == 2: + pause_colour = colours.media_buttons_active + play_colour = colours.media_buttons_active + elif pctl.playing_state == 3: + play_colour = colours.media_buttons_active + if pctl.record_stream: + play_colour = [220, 50, 50, 255] + + if not compact or (compact and pctl.playing_state != 2): + rect = ( + buttons_x_offset + (10 * gui.scale), window_size[1] - self.control_line_bottom - (13 * gui.scale), + 50 * gui.scale, 40 * gui.scale) + fields.add(rect) + if coll(rect): + play_colour = colours.media_buttons_over + if inp.mouse_click: + if compact and pctl.playing_state == 1: + pctl.pause() + elif pctl.playing_state == 1: + pctl.show_current(highlight=True) + else: + pctl.play() + inp.mouse_click = False + tool_tip2.test(33 * gui.scale, y - 35 * gui.scale, _("Play, RC: Go to playing")) + + if right_click: + pctl.show_current(highlight=True) + + self.play_button.render(29 * gui.scale, window_size[1] - self.control_line_bottom, play_colour) + # ddt.rect_r(rect,[255,0,0,255], True) + + # PAUSE--- + if compact: + buttons_x_offset = -46 * gui.scale + + x = (75 * gui.scale) + buttons_x_offset + y = window_size[1] - self.control_line_bottom + + if not compact or (compact and pctl.playing_state == 2): + + rect = (x - 15 * gui.scale, y - 13 * gui.scale, 50 * gui.scale, 40 * gui.scale) + fields.add(rect) + if coll(rect) and pctl.playing_state != 3: + pause_colour = colours.media_buttons_over + if inp.mouse_click: + pctl.pause() + if right_click: + pctl.show_current(highlight=True) + tool_tip2.test(x, y - 35 * gui.scale, _("Pause")) + + # ddt.rect_r(rect,[255,0,0,255], True) + ddt.rect_a((x, y + 0), (4 * gui.scale, 13 * gui.scale), pause_colour) + ddt.rect_a((x + 10 * gui.scale, y + 0), (4 * gui.scale, 13 * gui.scale), pause_colour) + + # FORWARD--- + rect = (buttons_x_offset + 125 * gui.scale, window_size[1] - self.control_line_bottom - 10 * gui.scale, + 50 * gui.scale, 35 * gui.scale) + fields.add(rect) + if coll(rect) and pctl.playing_state != 3: + forward_colour = colours.media_buttons_over + if inp.mouse_click: + pctl.advance() + gui.tool_tip_lock_off_f = True + if right_click: + # pctl.random_mode ^= True + toggle_random() + gui.tool_tip_lock_off_f = True + # if window_size[0] < 600 * gui.scale: + # . Shuffle set to on + gui.mode_toast_text = _("Shuffle On") + if not pctl.random_mode: + # . Shuffle set to off + gui.mode_toast_text = _("Shuffle Off") + toast_mode_timer.set() + gui.delay_frame(1) + if middle_click: + pctl.advance(rr=True) + gui.tool_tip_lock_off_f = True + # tool_tip.test(buttons_x_offset + 230 * gui.scale + 50 * gui.scale, window_size[1] - self.control_line_bottom - 20 * gui.scale, "Advance") + # if not gui.tool_tip_lock_off_f: + # tool_tip2.test(x + 45 * gui.scale, y - 35 * gui.scale, _("Forward, RC: Toggle shuffle, MC: Radio random")) + else: + gui.tool_tip_lock_off_f = False + + self.forward_button.render( + buttons_x_offset + 125 * gui.scale, + 1 + window_size[1] - self.control_line_bottom, forward_colour) + +class MiniMode: + def __init__(self): + self.save_position = None + self.was_borderless = True + self.volume_timer = Timer() + self.volume_timer.force_set(100) + + self.left_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "left-slide.png", True) + self.right_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "right-slide.png", True) + self.repeat = asset_loader(scaled_asset_directory, loaded_asset_dc, "repeat-mini-mode.png", True) + self.shuffle = asset_loader(scaled_asset_directory, loaded_asset_dc, "shuffle-mini-mode.png", True) + + self.shuffle_fade_timer = Timer(100) + self.repeat_fade_timer = Timer(100) + + def render(self): + # We only set seek_r and seek_w if track is currently on, but use it anyway later, so make sure it exists + if 'seek_r' not in locals(): + seek_r = [0, 0, 0, 0] + seek_w = 0 + + w = window_size[0] + h = window_size[1] + + y1 = w + if w == h: + y1 -= 79 * gui.scale + + h1 = h - y1 + + # Draw background + bg = colours.mini_mode_background + # bg = [250, 250, 250, 255] + + ddt.rect((0, 0, w, h), bg) + ddt.text_background_colour = bg + + detect_mouse_rect = (3, 3, w - 6, h - 6) + fields.add(detect_mouse_rect) + mouse_in = coll(detect_mouse_rect) + + # Play / Pause when right clicking below art + if right_click: # and mouse_position[1] > y1: + pctl.play_pause() + + # Volume change on scroll + if mouse_wheel != 0: + self.volume_timer.set() + + pctl.player_volume += mouse_wheel * prefs.volume_wheel_increment * 3 + if pctl.player_volume < 1: + pctl.player_volume = 0 + elif pctl.player_volume > 100: + pctl.player_volume = 100 + + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() + + track = pctl.playing_object() + + control_hit_area = (3, y1 - 15 * gui.scale, w - 6, h1 - 3 + 15 * gui.scale) + mouse_in_area = coll(control_hit_area) + fields.add(control_hit_area) + + ddt.rect((0, 0, w, w), (0, 0, 0, 45)) + if track is not None: + + # Render album art + album_art_gen.display(track, (0, 0), (w, w)) + + line1c = colours.mini_mode_text_1 + line2c = colours.mini_mode_text_2 + + if h == w and mouse_in_area: + # ddt.pretty_rect = (0, 260 * gui.scale, w, 100 * gui.scale) + ddt.rect((0, y1, w, h1), [0, 0, 0, 220]) + line1c = [255, 255, 255, 240] + line2c = [255, 255, 255, 77] + + # Double click bottom text to return to full window + text_hit_area = (60 * gui.scale, y1 + 4, 230 * gui.scale, 50 * gui.scale) + + if coll(text_hit_area): + if inp.mouse_click: + if d_click_timer.get() < 0.3: + restore_full_mode() + gui.update += 1 + return + d_click_timer.set() + + # Draw title texts + line1 = track.artist + line2 = track.title + + # Calculate seek bar position + seek_w = int(w * 0.70) + + seek_r = [(w - seek_w) // 2, y1 + 58 * gui.scale, seek_w, 6 * gui.scale] + seek_r_hit = [seek_r[0], seek_r[1] - 4 * gui.scale, seek_r[2], seek_r[3] + 8 * gui.scale] + + if w != h or mouse_in_area: + + if not line1 and not line2: + ddt.text((w // 2, y1 + 18 * gui.scale, 2), track.filename, line1c, 214, window_size[0] - 30 * gui.scale) + else: + + ddt.text((w // 2, y1 + 10 * gui.scale, 2), line1, line2c, 514, window_size[0] - 30 * gui.scale) + + ddt.text((w // 2, y1 + 31 * gui.scale, 2), line2, line1c, 414, window_size[0] - 30 * gui.scale) + + # Test click to seek + if mouse_up and coll(seek_r_hit): + + click_x = mouse_position[0] + click_x = min(click_x, seek_r[0] + seek_r[2]) + click_x = max(click_x, seek_r[0]) + click_x -= seek_r[0] + + if click_x < 6 * gui.scale: + click_x = 0 + seek = click_x / seek_r[2] + + pctl.seek_decimal(seek) + + # Draw progress bar background + ddt.rect(seek_r, [255, 255, 255, 32]) + + # Calculate and draw bar foreground + progress_w = 0 + if pctl.playing_length > 1: + progress_w = pctl.playing_time * seek_w / pctl.playing_length + seek_colour = [210, 210, 210, 255] + if gui.theme_name == "Carbon": + seek_colour = colours.bottom_panel_colour + + if pctl.playing_state != 1: + seek_colour = [210, 40, 100, 255] + + seek_r[2] = progress_w + + if self.volume_timer.get() < 0.9: + progress_w = pctl.player_volume * (seek_w - (4 * gui.scale)) / 100 + gui.update += 1 + seek_colour = [210, 210, 210, 255] + seek_r[2] = progress_w + seek_r[0] += 2 * gui.scale + seek_r[1] += 2 * gui.scale + seek_r[3] -= 4 * gui.scale + + ddt.rect(seek_r, seek_colour) + + left_area = (1, y1, seek_r[0] - 1, 45 * gui.scale) + right_area = (seek_r[0] + seek_w, y1, seek_r[0] - 2, 45 * gui.scale) + + fields.add(left_area) + fields.add(right_area) + + hint = 0 + if coll(control_hit_area): + hint = 30 + if coll(left_area): + hint = 240 + if hint and not prefs.shuffle_lock: + self.left_slide.render(16 * gui.scale, y1 + 17 * gui.scale, [255, 255, 255, hint]) + + hint = 0 + if coll(control_hit_area): + hint = 30 + if coll(right_area): + hint = 240 + if hint: + self.right_slide.render(window_size[0] - self.right_slide.w - 16 * gui.scale, y1 + 17 * gui.scale, + [255, 255, 255, hint]) + + # Shuffle + + shuffle_area = (seek_r[0] + seek_w, seek_r[1] - 10 * gui.scale, 50 * gui.scale, 30 * gui.scale) + # fields.add(shuffle_area) + # ddt.rect_r(shuffle_area, [255, 0, 0, 100], True) + + if coll(control_hit_area) and not prefs.shuffle_lock: + colour = [255, 255, 255, 20] + if inp.mouse_click and coll(shuffle_area): + # pctl.random_mode ^= True + toggle_random() + if pctl.random_mode: + colour = [255, 255, 255, 190] + + sx = seek_r[0] + seek_w + 12 * gui.scale + sy = seek_r[1] - 2 * gui.scale + self.shuffle.render(sx, sy, colour) + + + # sx = seek_r[0] + seek_w + 8 * gui.scale + # sy = seek_r[1] - 1 * gui.scale + # ddt.rect_a((sx, sy), (14 * gui.scale, 2 * gui.scale), colour) + # sy += 4 * gui.scale + # ddt.rect_a((sx, sy), (28 * gui.scale, 2 * gui.scale), colour) + + shuffle_area = (seek_r[0] - 41 * gui.scale, seek_r[1] - 10 * gui.scale, 40 * gui.scale, 30 * gui.scale) + if coll(control_hit_area) and not prefs.shuffle_lock: + colour = [255, 255, 255, 20] + if inp.mouse_click and coll(shuffle_area): + toggle_repeat() + if pctl.repeat_mode: + colour = [255, 255, 255, 190] + + + sx = seek_r[0] - 36 * gui.scale + sy = seek_r[1] - 1 * gui.scale + self.repeat.render(sx, sy, colour) + + + # sx = seek_r[0] - 39 * gui.scale + # sy = seek_r[1] - 1 * gui.scale + + #tw = 2 * gui.scale + # ddt.rect_a((sx + 15 * gui.scale, sy), (13 * gui.scale, tw), colour) + # ddt.rect_a((sx + 4 * gui.scale, sy + 4 * gui.scale), (25 * gui.scale, tw), colour) + # ddt.rect_a((sx + 30 * gui.scale - tw, sy), (tw, 6 * gui.scale), colour) + + + # Forward and back clicking + if inp.mouse_click: + if coll(left_area) and not prefs.shuffle_lock: + pctl.back() + if coll(right_area): + pctl.advance() + + # Show exit/min buttons when mosue over + tool_rect = [window_size[0] - 110 * gui.scale, 2, 108 * gui.scale, 45 * gui.scale] + if prefs.left_window_control: + tool_rect[0] = 0 + fields.add(tool_rect) + if coll(tool_rect): + draw_window_tools() + + if w != h: + ddt.rect_s((1, 1, w - 2, h - 2), colours.mini_mode_border, 1 * gui.scale) + if gui.scale == 2: + ddt.rect_s((2, 2, w - 4, h - 4), colours.mini_mode_border, 1 * gui.scale) + +class MiniMode2: + + def __init__(self): + + self.save_position = None + self.was_borderless = True + self.volume_timer = Timer() + self.volume_timer.force_set(100) + + self.left_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "left-slide.png", True) + self.right_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "right-slide.png", True) + + def render(self): + + w = window_size[0] + h = window_size[1] + + x1 = h + + # Draw background + ddt.rect((0, 0, w, h), colours.mini_mode_background) + ddt.text_background_colour = colours.mini_mode_background + + detect_mouse_rect = (2, 2, w - 4, h - 4) + fields.add(detect_mouse_rect) + mouse_in = coll(detect_mouse_rect) + + # Play / Pause when right clicking below art + if right_click: # and mouse_position[1] > y1: + pctl.play_pause() + + # Volume change on scroll + if mouse_wheel != 0: + self.volume_timer.set() + + pctl.player_volume += mouse_wheel * prefs.volume_wheel_increment * 3 + if pctl.player_volume < 1: + pctl.player_volume = 0 + elif pctl.player_volume > 100: + pctl.player_volume = 100 + + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() + + track = pctl.playing_object() + + if track is not None: + + # Render album art + album_art_gen.display(track, (0, 0), (h, h)) + + text_hit_area = (x1, 0, w, h) + + if coll(text_hit_area): + if inp.mouse_click: + if d_click_timer.get() < 0.3: + restore_full_mode() + gui.update += 1 + return + d_click_timer.set() + + # Draw title texts + line1 = track.artist + line2 = track.title + + if not line1 and not line2: + + ddt.text( + (x1 + 15 * gui.scale, 44 * gui.scale), track.filename, colours.grey(150), 315, + window_size[0] - x1 - 30 * gui.scale) + else: + + # if ddt.get_text_w(line2, 215) > window_size[0] - x1 - 30 * gui.scale: + # ddt.text((x1 + 15 * gui.scale, 19 * gui.scale), line2, colours.grey(249), 413, + # window_size[0] - x1 - 35 * gui.scale) + # + # ddt.text((x1 + 15 * gui.scale, 43 * gui.scale), line1, colours.grey(110), 513, + # window_size[0] - x1 - 35 * gui.scale) + # else: + + ddt.text( + (x1 + 15 * gui.scale, 18 * gui.scale), line2, colours.grey(249), 514, + window_size[0] - x1 - 30 * gui.scale) + + ddt.text( + (x1 + 15 * gui.scale, 43 * gui.scale), line1, colours.grey(110), 514, + window_size[0] - x1 - 30 * gui.scale) + + # Show exit/min buttons when mosue over + tool_rect = [window_size[0] - 110 * gui.scale, 2, 108 * gui.scale, 45 * gui.scale] + if prefs.left_window_control: + tool_rect[0] = 0 + fields.add(tool_rect) + if coll(tool_rect): + draw_window_tools() + + # Seek bar + bg_rect = (h, h - round(5 * gui.scale), w - h, round(5 * gui.scale)) + ddt.rect(bg_rect, [255, 255, 255, 18]) + + if pctl.playing_state > 0: + + hit_rect = h - 5 * gui.scale, h - 12 * gui.scale, w - h + 5 * gui.scale, 13 * gui.scale + + if coll(hit_rect) and mouse_up: + p = (mouse_position[0] - h) / (w - h) + + if p < 0 or mouse_position[0] - h < 6 * gui.scale: + pctl.seek_time(0) + elif p > .96: + pctl.advance() + else: + pctl.seek_decimal(p) + + if pctl.playing_length: + seek_rect = ( + h, h - round(5 * gui.scale), round((w - h) * (pctl.playing_time / pctl.playing_length)), + round(5 * gui.scale)) + colour = colours.artist_text + if gui.theme_name == "Carbon": + colour = colours.bottom_panel_colour + if pctl.playing_state != 1: + colour = [210, 40, 100, 255] + ddt.rect(seek_rect, colour) + +class MiniMode3: + + def __init__(self): + + self.save_position = None + self.was_borderless = True + self.volume_timer = Timer() + self.volume_timer.force_set(100) + + self.left_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "left-slide.png", True) + self.right_slide = asset_loader(scaled_asset_directory, loaded_asset_dc, "right-slide.png", True) + + self.shuffle_fade_timer = Timer(100) + self.repeat_fade_timer = Timer(100) + + def render(self): + # We only set seek_r and seek_w if track is currently on, but use it anyway later, so make sure it exists + if 'seek_r' not in locals(): + seek_r = [0, 0, 0, 0] + seek_w = 0 + volume_r = [0, 0, 0, 0] + volume_w = 0 + + w = window_size[0] + h = window_size[1] + + y1 = w #+ 10 * gui.scale + # if w == h: + # y1 -= 79 * gui.scale + + h1 = h - y1 + + # Draw background + bg = colours.mini_mode_background + bg = [0, 0, 0, 0] + # bg = [250, 250, 250, 255] + + ddt.rect((0, 0, w, h), bg) + + style_overlay.display() + + transit = False + #ddt.text_background_colour = list(gui.center_blur_pixel) + [255,] #bg + if style_overlay.fade_on_timer.get() < 0.4 or style_overlay.stage != 2: + ddt.alpha_bg = True + transit = True + + detect_mouse_rect = (3, 3, w - 6, h - 6) + fields.add(detect_mouse_rect) + mouse_in = coll(detect_mouse_rect) + + # Play / Pause when right clicking below art + if right_click: # and mouse_position[1] > y1: + pctl.play_pause() + + # Volume change on scroll + if mouse_wheel != 0: + self.volume_timer.set() + + pctl.player_volume += mouse_wheel * prefs.volume_wheel_increment * 3 + if pctl.player_volume < 1: + pctl.player_volume = 0 + elif pctl.player_volume > 100: + pctl.player_volume = 100 + + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() + + track = pctl.playing_object() + + control_hit_area = (3, y1 - 15 * gui.scale, w - 6, h1 - 3 + 15 * gui.scale) + mouse_in_area = coll(control_hit_area) + fields.add(control_hit_area) + + #ddt.rect((0, 0, w, w), (0, 0, 0, 45)) + if track is not None: + + # Render album art + + wid = (w // 2) + round(60 * gui.scale) + ins = (window_size[0] - wid) / 2 + off = round(4 * gui.scale) + + drop_shadow.render(ins + off, ins + off, wid + off * 2, wid + off * 2) + ddt.rect((ins, ins, wid, wid), [20, 20, 20, 255]) + album_art_gen.display(track, (ins, ins), (wid, wid)) + + line1c = [255, 255, 255, 255] #colours.mini_mode_text_1 + line2c = [255, 255, 255, 255] #colours.mini_mode_text_2 + + # if h == w and mouse_in_area: + # # ddt.pretty_rect = (0, 260 * gui.scale, w, 100 * gui.scale) + # ddt.rect((0, y1, w, h1), [0, 0, 0, 220]) + # line1c = [255, 255, 255, 240] + # line2c = [255, 255, 255, 77] + + # Double click bottom text to return to full window + text_hit_area = (60 * gui.scale, y1 + 4, 230 * gui.scale, 50 * gui.scale) + + if coll(text_hit_area): + if inp.mouse_click: + if d_click_timer.get() < 0.3: + restore_full_mode() + gui.update += 1 + return + d_click_timer.set() + + # Draw title texts + line1 = track.artist + line2 = track.title + key = None + if not line1 and not line2: + if not ddt.alpha_bg: + key = (track.filename, 214, style_overlay.current_track_id) + ddt.text( + (w // 2, y1 + 18 * gui.scale, 2), track.filename, line1c, 214, + window_size[0] - 30 * gui.scale, real_bg=not transit, key=key) + else: + + if not ddt.alpha_bg: + key = (line1, 515, style_overlay.current_track_id) + ddt.text( + (w // 2, y1 + 5 * gui.scale, 2), line1, line2c, 515, + window_size[0] - 30 * gui.scale, real_bg=not transit, key=key) + if not ddt.alpha_bg: + key = (line2, 415, style_overlay.current_track_id) + ddt.text( + (w // 2, y1 + 31 * gui.scale, 2), line2, line1c, 415, + window_size[0] - 30 * gui.scale, real_bg=not transit, key=key) + + y1 += round(10 * gui.scale) + + # Calculate seek bar position + seek_w = int(w * 0.80) + + seek_r = [(w - seek_w) // 2, y1 + 58 * gui.scale, seek_w, 9 * gui.scale] + seek_r_hit = [seek_r[0], seek_r[1] - 5 * gui.scale, seek_r[2], seek_r[3] + 12 * gui.scale] + + if w != h or mouse_in_area: + + + # Test click to seek + if mouse_up and coll(seek_r_hit): + + click_x = mouse_position[0] + click_x = min(click_x, seek_r[0] + seek_r[2]) + click_x = max(click_x, seek_r[0]) + click_x -= seek_r[0] + + if click_x < 6 * gui.scale: + click_x = 0 + seek = click_x / seek_r[2] + + pctl.seek_decimal(seek) + + # Draw progress bar background + ddt.rect(seek_r, [255, 255, 255, 32]) + + # Calculate and draw bar foreground + progress_w = 0 + if pctl.playing_length > 1: + progress_w = pctl.playing_time * seek_w / pctl.playing_length + seek_colour = [210, 210, 210, 255] + if gui.theme_name == "Carbon": + seek_colour = colours.bottom_panel_colour + + if pctl.playing_state != 1: + seek_colour = [210, 40, 100, 255] + + seek_r[2] = progress_w + + ddt.rect(seek_r, seek_colour) + + + + volume_w = int(w * 0.50) + volume_r = [(w - volume_w) // 2, y1 + 80 * gui.scale, volume_w, 6 * gui.scale] + volume_r_hit = [volume_r[0], volume_r[1] - 5 * gui.scale, volume_r[2], volume_r[3] + 10 * gui.scale] + + # Test click to volume + if (mouse_up or mouse_down) and coll(volume_r_hit): + gui.update_on_drag = True + click_x = mouse_position[0] + click_x = min(click_x, volume_r[0] + volume_r[2]) + click_x = max(click_x, volume_r[0]) + click_x -= volume_r[0] + + if click_x < 6 * gui.scale: + click_x = 0 + volume = click_x / volume_r[2] + + pctl.player_volume = int(volume * 100) + pctl.set_volume() + + ddt.rect(volume_r, [255, 255, 255, 32]) + + #if self.volume_timer.get() < 0.9: + progress_w = pctl.player_volume * (volume_w - (4 * gui.scale)) / 100 + volume_colour = [210, 210, 210, 255] + volume_r[2] = progress_w + volume_r[0] += 2 * gui.scale + volume_r[1] += 2 * gui.scale + volume_r[3] -= 4 * gui.scale + + ddt.rect(volume_r, volume_colour) + + + left_area = (1, y1, volume_r[0] - 1, 45 * gui.scale) + right_area = (volume_r[0] + volume_w, y1, volume_r[0] - 2, 45 * gui.scale) + + fields.add(left_area) + fields.add(right_area) + + hint = 0 + if True: #coll(control_hit_area): + hint = 30 + if coll(left_area): + hint = 240 + if hint and not prefs.shuffle_lock: + self.left_slide.render(16 * gui.scale, y1 + 10 * gui.scale, [255, 255, 255, hint]) + + hint = 0 + if True: #coll(control_hit_area): + hint = 30 + if coll(right_area): + hint = 240 + if hint: + self.right_slide.render( + window_size[0] - self.right_slide.w - 16 * gui.scale, y1 + 10 * gui.scale, [255, 255, 255, hint]) + + # Shuffle + shuffle_area = (volume_r[0] + volume_w, volume_r[1] - 10 * gui.scale, 50 * gui.scale, 30 * gui.scale) + # fields.add(shuffle_area) + # ddt.rect_r(shuffle_area, [255, 0, 0, 100], True) + + if True: #coll(control_hit_area) and not prefs.shuffle_lock: + colour = [255, 255, 255, 20] + if inp.mouse_click and coll(shuffle_area): + # pctl.random_mode ^= True + toggle_random() + if pctl.random_mode: + colour = [255, 255, 255, 190] + + sx = volume_r[0] + volume_w + 12 * gui.scale + sy = volume_r[1] - 3 * gui.scale + mini_mode.shuffle.render(sx, sy, colour) + + # + # sx = volume_r[0] + volume_w + 8 * gui.scale + # sy = volume_r[1] - 1 * gui.scale + # ddt.rect_a((sx, sy), (14 * gui.scale, 2 * gui.scale), colour) + # sy += 4 * gui.scale + # ddt.rect_a((sx, sy), (28 * gui.scale, 2 * gui.scale), colour) + + shuffle_area = (volume_r[0] - 41 * gui.scale, volume_r[1] - 10 * gui.scale, 40 * gui.scale, 30 * gui.scale) + if True: #coll(control_hit_area) and not prefs.shuffle_lock: + colour = [255, 255, 255, 20] + if inp.mouse_click and coll(shuffle_area): + toggle_repeat() + if pctl.repeat_mode: + colour = [255, 255, 255, 190] + + sx = volume_r[0] - 39 * gui.scale + sy = volume_r[1] - 1 * gui.scale + mini_mode.repeat.render(sx, sy, colour) + + # sx = volume_r[0] - 39 * gui.scale + # sy = volume_r[1] - 1 * gui.scale + # + # tw = 2 * gui.scale + # ddt.rect_a((sx + 15 * gui.scale, sy), (13 * gui.scale, tw), colour) + # ddt.rect_a((sx + 4 * gui.scale, sy + 4 * gui.scale), (25 * gui.scale, tw), colour) + # ddt.rect_a((sx + 30 * gui.scale - tw, sy), (tw, 6 * gui.scale), colour) + + # Forward and back clicking + if inp.mouse_click: + if coll(left_area) and not prefs.shuffle_lock: + pctl.back() + if coll(right_area): + pctl.advance() + + search_over.render() + + + # Show exit/min buttons when mosue over + tool_rect = [window_size[0] - 110 * gui.scale, 2, 108 * gui.scale, 45 * gui.scale] + if prefs.left_window_control: + tool_rect[0] = 0 + fields.add(tool_rect) + if coll(tool_rect): + draw_window_tools() + + + # if w != h: + # ddt.rect_s((1, 1, w - 2, h - 2), colours.mini_mode_border, 1 * gui.scale) + # if gui.scale == 2: + # ddt.rect_s((2, 2, w - 4, h - 4), colours.mini_mode_border, 1 * gui.scale) + ddt.alpha_bg = False + +class StandardPlaylist: + def __init__(self): + pass + + def full_render(self): + + global highlight_left + global highlight_right + + global playlist_hold + global playlist_hold_position + global shift_selection + + global click_time + global quick_drag + global mouse_down + global mouse_up + global selection_stage + + global r_menu_index + global r_menu_position + + left = gui.playlist_left + width = gui.plw + + highlight_width = gui.tracklist_highlight_width + highlight_left = gui.tracklist_highlight_left + inset_width = gui.tracklist_inset_width + inset_left = gui.tracklist_inset_left + center_mode = gui.tracklist_center_mode + + w = 0 + gui.row_extra = 0 + cv = 0 # update gui.playlist_current_visible_tracks + + # Draw the background + SDL_SetRenderTarget(renderer, gui.tracklist_texture) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_RenderClear(renderer) + + rect = (left, gui.panelY, width, window_size[1]) + ddt.rect(rect, colours.playlist_panel_background) + + # This draws an optional background image + if pl_bg: + x = (left + highlight_width) - (pl_bg.w + round(60 * gui.scale)) + pl_bg.render(x, window_size[1] - gui.panelBY - pl_bg.h) + ddt.pretty_rect = (x, window_size[1] - gui.panelBY - pl_bg.h, pl_bg.w, pl_bg.h) + ddt.alpha_bg = True + else: + xx = left + inset_left + inset_width + if center_mode: + xx -= round(15 * gui.scale) + deco.draw(ddt, xx, window_size[1] - gui.panelBY, pretty_text=True) + + # Mouse wheel scrolling + if mouse_wheel != 0 and window_size[1] - gui.panelBY - 1 > mouse_position[ + 1] > gui.panelY - 2 and gui.playlist_left < mouse_position[0] < gui.playlist_left + gui.plw \ + and not (coll(pl_rect)) and not search_over.active and not radiobox.active: + + # Set scroll speed + mx = 4 + + if gui.playlist_view_length < 25: + mx = 3 + if gui.playlist_view_length < 10: + mx = 2 + pctl.playlist_view_position -= mouse_wheel * mx + + if gui.playlist_view_length > 40: + pctl.playlist_view_position -= mouse_wheel + + #if mouse_wheel: + #logging.debug("Position changed by mouse wheel scroll: " + str(mouse_wheel)) + + pctl.playlist_view_position = min(pctl.playlist_view_position, len(default_playlist)) + #logging.debug("Position changed by range bound") + if pctl.playlist_view_position < 1: + pctl.playlist_view_position = 0 + if default_playlist: + # edge_playlist.pulse() + edge_playlist2.pulse() + + scroll_hide_timer.set() + gui.frame_callback_list.append(TestTimer(0.9)) + + # Show notice if playlist empty + if len(default_playlist) == 0: + colour = alpha_mod(colours.index_text, 200) # colours.playlist_text_missing + + top_a = gui.panelY + if gui.artist_info_panel: + top_a += gui.artist_panel_height + + b = window_size[1] - top_a - gui.panelBY + half = int(top_a + (b * 0.60)) + + if pl_bg: + rect = (left + int(width / 2) - 80 * gui.scale, half - 10 * gui.scale, + 190 * gui.scale, 60 * gui.scale) + ddt.pretty_rect = rect + ddt.alpha_bg = True + + ddt.text( + (left + int(width / 2) + 10 * gui.scale, half, 2), + _("Playlist is empty"), colour, 213, bg=colours.playlist_panel_background) + ddt.text( + (left + int(width / 2) + 10 * gui.scale, half + 30 * gui.scale, 2), + _("Drag and drop files to import"), colour, 13, bg=colours.playlist_panel_background) + + ddt.pretty_rect = None + ddt.alpha_bg = False + + # Show notice if at end of playlist + elif pctl.playlist_view_position > len(default_playlist) - 1: + colour = alpha_mod(colours.index_text, 200) + + top_a = gui.panelY + if gui.artist_info_panel: + top_a += gui.artist_panel_height + + b = window_size[1] - top_a - gui.panelBY + half = int(top_a + (b * 0.17)) + + if pl_bg: + rect = (left + int(width / 2) - 60 * gui.scale, half - 5 * gui.scale, + 140 * gui.scale, 30 * gui.scale) + ddt.pretty_rect = rect + ddt.alpha_bg = True + + ddt.text( + (left + int(width / 2) + 10 * gui.scale, half, 2), _("End of Playlist"), + colour, 213) + + ddt.pretty_rect = None + ddt.alpha_bg = False + + # line = "Contains " + str(len(default_playlist)) + ' track' + # if len(default_playlist) > 1: + # line += "s" + # + # ddt.draw_text((left + int(width / 2) + 10 * gui.scale, half + 24 * gui.scale, 2), line, + # colour, 12) + + # Process Input + + # type (0 is track, 1 is fold title), track_position, track_object, box, input_box, + list_items = [] + number = 0 + + for i in range(gui.playlist_view_length + 1): + + track_position = i + pctl.playlist_view_position + + # Make sure the view position is valid + pctl.playlist_view_position = max(pctl.playlist_view_position, 0) + + # Break if we are at end of playlist + if len(default_playlist) <= track_position or number > gui.playlist_view_length: + break + + track_object = pctl.get_track(default_playlist[track_position]) + track_id = track_object.index + move_on_title = False + + line_y = gui.playlist_top + gui.playlist_row_height * number + + track_box = ( + left + highlight_left, line_y, highlight_width, + gui.playlist_row_height - 1) + + input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) + + # Are folder titles enabled? + if not pctl.multi_playlist[pctl.active_playlist_viewing].hide_title and break_enable: + # Is this track from a different folder than the last? + if track_position == 0 or track_object.parent_folder_path != pctl.get_track( + default_playlist[track_position - 1]).parent_folder_path: + # Make folder title + + highlight = False + drag_highlight = False + + # Shift selection highlight + if (track_position in shift_selection and len(shift_selection) > 1): + highlight = True + + # Tracks have been dropped? + if playlist_hold is True and coll(input_box): + if mouse_up: + move_on_title = True + + # Ignore click in ratings box + click_title = (inp.mouse_click or right_click or middle_click) and coll(input_box) + if click_title and gui.show_album_ratings: + if mouse_position[0] > (input_box[0] + input_box[2]) - 80 * gui.scale: + click_title = False + + # Detect folder title click + if click_title and mouse_position[1] < window_size[1] - gui.panelBY: + + gui.pl_update += 1 + # Add folder to queue if middle click + if middle_click and is_level_zero(): + if key_ctrl_down: # Add as ungrouped tracks + i = track_position + parent = pctl.get_track(default_playlist[i]).parent_folder_path + while i < len(default_playlist) and parent == pctl.get_track( + default_playlist[i]).parent_folder_path: + pctl.force_queue.append(queue_item_gen(default_playlist[i], i, pl_to_id( + pctl.active_playlist_viewing))) + i += 1 + queue_timer_set(plural=True) + if prefs.stop_end_queue: + pctl.auto_stop = False + + else: # Add as grouped album + add_album_to_queue(track_id, track_position) + pctl.selected_in_playlist = track_position + shift_selection = [pctl.selected_in_playlist] + gui.pl_update += 1 + + # Play if double click: + if d_mouse_click and track_position in shift_selection and coll_point( + last_click_location, (input_box)): + click_time -= 1.5 + pctl.jump(track_id, track_position) + line_hit = False + inp.mouse_click = False + + if album_mode: + goto_album(pctl.playlist_playing_position) + + # Show selection menu if right clicked after select + if right_click: + folder_menu.activate(track_id) + r_menu_position = track_position + selection_stage = 2 + gui.pl_update = 1 + + if track_position not in shift_selection: + shift_selection = [] + pctl.selected_in_playlist = track_position + u = track_position + while u < len(default_playlist) and track_object.parent_folder_path == \ + pctl.master_library[ + default_playlist[u]].parent_folder_path: + shift_selection.append(u) + u += 1 + + # Add folder to selection if clicked + if inp.mouse_click and not ( + scroll_enable and mouse_position[0] < 30 * gui.scale) and not side_drag: + + quick_drag = True + set_drag_source() + + if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: + playlist_hold = True + + selection_stage = 1 + temp = get_folder_tracks_local(track_position) + pctl.selected_in_playlist = track_position + + if len(shift_selection) > 0 and key_shift_down: + if track_position < shift_selection[0]: + for item in reversed(temp): + if item not in shift_selection: + shift_selection.insert(0, item) + else: + for item in temp: + if item not in shift_selection: + shift_selection.append(item) + + else: + shift_selection = copy.copy(temp) + + # Should draw drag highlight? + + if mouse_down and playlist_hold and coll(input_box) and track_position not in shift_selection: + + if len(shift_selection) < 2 and not key_shift_down: + pass + else: + drag_highlight = True + + # Something to do with quick search, I forgot + if pctl.selected_in_playlist > track_position + 1: + gui.row_extra += 1 + + list_items.append( + (1, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, False)) + number += 1 + + if number > gui.playlist_view_length: + break + + # Standard track --------------------------------------------------------------------- + playing = False + + highlight = False + drag_highlight = False + line_y = gui.playlist_top + gui.playlist_row_height * number + + track_box = ( + left + highlight_left, line_y, highlight_width, + gui.playlist_row_height - 1) + + input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) + + # Test if line has mouse over or been clicked + line_over = False + line_hit = False + if coll(input_box) and mouse_position[1] < window_size[1] - gui.panelBY: + line_over = True + if (inp.mouse_click or right_click or (middle_click and is_level_zero())): + line_hit = True + gui.pl_update += 1 + + else: + line_hit = False + else: + line_hit = False + line_over = False + + # Prevent click if near scroll bar + if scroll_enable and mouse_position[0] < 30: + line_hit = False + + # Double click to play + if key_shift_down is False and d_mouse_click and line_hit and track_position == pctl.selected_in_playlist and coll_point( + last_click_location, input_box): + + pctl.jump(track_id, track_position) + + click_time -= 1.5 + quick_drag = False + mouse_down = False + mouse_up = False + line_hit = False + + if album_mode: + goto_album(pctl.playlist_playing_position) + + if len(pctl.track_queue) > 0 and pctl.track_queue[pctl.queue_step] == track_id: + if track_position == pctl.playlist_playing_position and pctl.active_playlist_viewing == pctl.active_playlist_playing: + this_line_playing = True + + # Add to queue on middle click + if middle_click and line_hit: + pctl.force_queue.append( + queue_item_gen(track_id, + track_position, pl_to_id(pctl.active_playlist_viewing))) + pctl.selected_in_playlist = track_position + shift_selection = [pctl.selected_in_playlist] + gui.pl_update += 1 + queue_timer_set() + if prefs.stop_end_queue: + pctl.auto_stop = False + + # Deselect multiple if one clicked on and not dragged (mouse up is probably a bit of a hacky way of doing it) + if len(shift_selection) > 1 and mouse_up and line_over and not key_shift_down and not key_ctrl_down and point_proximity_test( + gui.drag_source_position, mouse_position, 15): # and not playlist_hold: + shift_selection = [track_position] + pctl.selected_in_playlist = track_position + gui.pl_update = 1 + gui.update = 2 + + # # Begin drag block selection + # if mouse_down and line_over and track_position in shift_selection and len(shift_selection) > 1: + # if not pl_is_locked(pctl.active_playlist_viewing): + # playlist_hold = True + # elif key_shift_down: + # playlist_hold = True + + # Begin drag single track + if inp.mouse_click and line_hit and not side_drag: + quick_drag = True + set_drag_source() + + # Shift Move Selection + if move_on_title or (mouse_up and playlist_hold is True and coll(( + left + highlight_left, line_y, highlight_width, gui.playlist_row_height))): + + if len(shift_selection) > 1 or key_shift_down: + if track_position not in shift_selection: # p_track != playlist_hold_position and + + if len(shift_selection) == 0: + + ref = default_playlist[playlist_hold_position] + default_playlist[playlist_hold_position] = "old" + if move_on_title: + default_playlist.insert(track_position, "new") + else: + default_playlist.insert(track_position + 1, "new") + default_playlist.remove("old") + pctl.selected_in_playlist = default_playlist.index("new") + default_playlist[default_playlist.index("new")] = ref + + gui.pl_update = 1 + + + else: + ref = [] + selection_stage = 2 + for item in shift_selection: + ref.append(default_playlist[item]) + + for item in shift_selection: + default_playlist[item] = "old" + + for item in shift_selection: + if move_on_title: + default_playlist.insert(track_position, "new") + else: + default_playlist.insert(track_position + 1, "new") + + for b in reversed(range(len(default_playlist))): + if default_playlist[b] == "old": + del default_playlist[b] + shift_selection = [] + for b in range(len(default_playlist)): + if default_playlist[b] == "new": + shift_selection.append(b) + default_playlist[b] = ref.pop(0) + + pctl.selected_in_playlist = shift_selection[0] + gui.pl_update += 1 + + reload_albums(True) + pctl.notify_change() + + # Test show drag indicator + if mouse_down and playlist_hold and coll(input_box) and track_position not in shift_selection: + if len(shift_selection) > 1 or key_shift_down: + drag_highlight = True + + # Right click menu activation + if right_click and line_hit and mouse_position[0] > gui.playlist_left + 10: + + if len(shift_selection) > 1 and track_position in shift_selection: + selection_menu.activate(default_playlist[track_position]) + selection_stage = 2 + else: + r_menu_index = default_playlist[track_position] + r_menu_position = track_position + track_menu.activate(default_playlist[track_position]) + gui.pl_update += 1 + gui.update += 1 + + if track_position not in shift_selection: + pctl.selected_in_playlist = track_position + shift_selection = [pctl.selected_in_playlist] + + if line_over and inp.mouse_click: + + if track_position in shift_selection: + pass + else: + selection_stage = 2 + if key_shift_down: + start_s = track_position + end_s = pctl.selected_in_playlist + if end_s < start_s: + end_s, start_s = start_s, end_s + for y in range(start_s, end_s + 1): + if y not in shift_selection: + shift_selection.append(y) + shift_selection.sort() + pctl.selected_in_playlist = track_position + elif key_ctrl_down: + shift_selection.append(track_position) + else: + pctl.selected_in_playlist = track_position + shift_selection = [pctl.selected_in_playlist] + + if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: + playlist_hold = True + playlist_hold_position = track_position + + # Activate drag if shift key down + if quick_drag and pl_is_locked(pctl.active_playlist_viewing) and mouse_down: + if key_shift_down: + playlist_hold = True + else: + playlist_hold = False + + # Multi Select Highlight + if track_position in shift_selection or track_position == pctl.selected_in_playlist: + highlight = True + + if pctl.playing_state != 3 and len(pctl.track_queue) > 0 and pctl.track_queue[pctl.queue_step] == \ + default_playlist[track_position]: + if track_position == pctl.playlist_playing_position and pctl.active_playlist_viewing == pctl.active_playlist_playing: + playing = True + + list_items.append( + (0, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, playing)) + number += 1 + + if number > gui.playlist_view_length: + break + # --------------------------------------------------------------------------------------- + + # For every track in view + # for i in range(gui.playlist_view_length + 1): + gui.tracklist_bg_is_light = test_lumi(colours.playlist_panel_background) < 0.55 + + for type, track_position, tr, track_box, input_box, highlight, number, drag_highlight, playing in list_items: + + line_y = gui.playlist_top + gui.playlist_row_height * number + + ddt.text_background_colour = colours.playlist_panel_background + + if type == 1: + + # Is type ALBUM TITLE + separator = " - " + if prefs.row_title_separator_type == 1: + separator = " ‒ " + if prefs.row_title_separator_type == 2: + separator = " ⦁ " + + date = "" + duration = "" + + line = tr.parent_folder_name + + # Use folder name if mixed/singles? + if len(default_playlist) > track_position + 1 and pctl.get_track( + default_playlist[track_position + 1]).album != tr.album and \ + pctl.get_track(default_playlist[track_position + 1]).parent_folder_path == tr.parent_folder_path: + line = tr.parent_folder_name + else: + + if tr.album_artist != "" and tr.album != "": + line = tr.album_artist + separator + tr.album + + if prefs.left_align_album_artist_title and not True: + album_artist_mode = True + line = tr.album + + if len(line) < 6 and "CD" in line: + line = tr.album + + if prefs.append_date and year_search.search(tr.date): + year = d_date_display2(tr) + if not year: + year = d_date_display(tr) + date = "(" + year + ")" + + if line.endswith(")"): + b = line.split("(") + if len(b) > 1 and len(b[1]) <= 11: + + match = year_search.search(b[1]) + + if match: + line = b[0] + date = "(" + b[1] + + elif line.startswith("("): + + b = line.split(")") + if len(b) > 1 and len(b[0]) <= 11: + + match = year_search.search(b[0]) + + if match: + line = b[1] + date = b[0] + ")" + + if "(" in line and year_search.search(line): + date = "" + + line = line.replace(" - ", separator) + + qq = 0 + d_date = date + title_line = line + + # Calculate folder duration + + q = track_position + + total_time = 0 + while q < len(default_playlist): + + if pctl.get_track(default_playlist[q]).parent_folder_path != tr.parent_folder_path: + break + + total_time += pctl.get_track(default_playlist[q]).length + + q += 1 + qq += 1 + + if qq > 1: + duration = " [ " + get_display_time(total_time) + " ]" # Hair space inside brackets for better visual spacing + + if prefs.append_total_time: + date += duration + + ex = left + highlight_left + highlight_width - 7 * gui.scale + + height = line_y + gui.playlist_row_height - 19 * gui.scale # gui.pl_title_y_offset + + star_offset = 0 + if gui.show_album_ratings: + star_offset = round(72 * gui.scale) + ex -= star_offset + draw_rating_widget(ex + 6 * gui.scale, height, tr, album=True) + + light_offset = 0 + if colours.lm: + light_offset = 3 * gui.scale + ex -= light_offset + + if qq > 1: + ex += 1 * gui.scale + + ddt.text_background_colour = colours.playlist_panel_background + + if gui.scale == 2: + height += 1 + + if highlight: + ddt.text_background_colour = alpha_blend( + colours.row_select_highlight, + colours.playlist_panel_background) + ddt.rect_a( + (left + highlight_left, gui.playlist_top + gui.playlist_row_height * number), + (highlight_width, gui.playlist_row_height), colours.row_select_highlight) + + + #logging.info(d_date) # date of album release / release year + #logging.info(tr.parent_folder_name) # folder name + #logging.info(tr.album) + #logging.info(tr.artist) + #logging.info(tr.album_artist) + #logging.info(tr.genre) + + + + if prefs.row_title_format == 2: + + separator = " | " + + start_offset = round(15 * gui.scale) + xx = left + highlight_left + start_offset + ww = highlight_width + + was = False + run = 0 + duration = get_display_time(total_time) + colour = colours.folder_title + colour = [colour[0], colour[1], colour[2], max(colour[3] - 50, 0)] + + if prefs.append_total_time and duration: + was = True + run += ddt.text( + (ex - run, height, 1), duration, colour, + gui.row_font_size + gui.pl_title_font_offset) + if d_date: + if was: + run += ddt.text( + (ex - run, height, 1), separator, colour, + gui.row_font_size + gui.pl_title_font_offset) + was = True + run += ddt.text( + (ex - run, height, 1), d_date.rstrip(")").lstrip("("), colour, + gui.row_font_size + gui.pl_title_font_offset) + if tr.genre and prefs.row_title_genre: + if was: + run += ddt.text( + (ex - run, height, 1), separator, colour, + gui.row_font_size + gui.pl_title_font_offset) + was = True + run += ddt.text( + (ex - run, height, 1), tr.genre, colour, + gui.row_font_size + gui.pl_title_font_offset) + + + w2 = ddt.text((xx, height), title_line, colours.folder_title, gui.row_font_size + gui.pl_title_font_offset, max_w=ww - (start_offset + run + round(10 * gui.scale))) + + + + + else: + date_w = 0 + if date: + date_w = ddt.text( + (ex, height, 1), date, colours.folder_title, + gui.row_font_size + gui.pl_title_font_offset) + date_w += 4 * gui.scale + if qq > 1: + date_w -= 1 * gui.scale + + aa = 0 + + ft_width = ddt.get_text_w(line, gui.row_font_size + gui.pl_title_font_offset) + + left_align = highlight_width - date_w - 13 * gui.scale - light_offset + + left_align -= star_offset + + extra = aa + + left_align -= extra + + if ft_width > left_align: + date_w += 19 * gui.scale + ddt.text( + (left + highlight_left + 8 * gui.scale + extra, height), line, + colours.folder_title, + gui.row_font_size + gui.pl_title_font_offset, + highlight_width - date_w - extra - star_offset) + + else: + ddt.text( + (ex - date_w, height, 1), line, + colours.folder_title, + gui.row_font_size + gui.pl_title_font_offset) + + # ----- + + # Draw separation line below title + ddt.rect( + (left + highlight_left, line_y + gui.playlist_row_height - 1 * gui.scale, highlight_width, + 1 * gui.scale), colours.folder_line) + + # Draw blue highlight insert line + if drag_highlight: + ddt.rect( + [left + highlight_left, line_y + gui.playlist_row_height - 1 * gui.scale, + highlight_width, 3 * gui.scale], [135, 145, 190, 255]) + + continue + + # Draw playing highlight + if playing: + ddt.rect(track_box, colours.row_playing_highlight) + ddt.text_background_colour = alpha_blend(colours.row_playing_highlight, ddt.text_background_colour) + + if tr.file_ext == "SPTY": + # if not tauon.spot_ctl.started_once: + # ddt.rect((track_box[0], track_box[1], track_box[2], track_box[3] + 1), [40, 190, 40, 20]) + # ddt.text_background_colour = alpha_blend([40, 190, 40, 20], ddt.text_background_colour) + ddt.rect((track_box[0] + track_box[2] - round(2 * gui.scale), track_box[1] + round(2 * gui.scale), round(2 * gui.scale), track_box[3] - round(3 * gui.scale)), [40, 190, 40, 230]) + + + # Blue drop line + if drag_highlight: # playlist_hold_position != p_track: + + ddt.rect( + [left + highlight_left, line_y + gui.playlist_row_height - 1 * gui.scale, highlight_width, + 3 * gui.scale], [125, 105, 215, 255]) + + # Highlight + if highlight: + ddt.rect_a( + (left + highlight_left, line_y), (highlight_width, gui.playlist_row_height), + colours.row_select_highlight) + + ddt.text_background_colour = alpha_blend(colours.row_select_highlight, ddt.text_background_colour) + + if track_position > 0 and track_position < len(default_playlist) and tr.disc_number != "" and tr.disc_number != "0" and tr.album and tr.disc_number != pctl.get_track(default_playlist[track_position - 1]).disc_number \ + and tr.album == pctl.get_track(default_playlist[track_position - 1]).album and tr.parent_folder_path == pctl.get_track(default_playlist[track_position - 1]).parent_folder_path: + # Draw disc change line + ddt.rect( + (left + highlight_left, line_y + 0 * gui.scale, highlight_width, + 1 * gui.scale), colours.folder_line) + + if not gui.set_mode: + + line_render( + tr, track_position, gui.playlist_text_offset + line_y, + playing, 255, left + inset_left, inset_width, 1, line_y) + + else: + # NEE --------------------------------------------------------- + n_track = tr + p_track = track_position + this_line_playing = playing + + start = 18 * gui.scale + + if center_mode: + start = inset_left + + elif gui.lsp: + start += gui.lspw + + run = start + end = start + gui.plw + + if center_mode: + end = highlight_width + start + + # gui.tracklist_center_mode = center_mode + # gui.tracklist_inset_left = inset_left - round(20 * gui.scale) + # gui.tracklist_inset_width = inset_width + round(20 * gui.scale) + + for h, item in enumerate(gui.pl_st): + + wid = item[1] - 20 * gui.scale + y = gui.playlist_text_offset + gui.playlist_top + gui.playlist_row_height * number + ry = gui.playlist_top + gui.playlist_row_height * number + + if run > end - 50 * gui.scale: + break + + if len(gui.pl_st) == h + 1: + wid -= 6 * gui.scale + + if item[0] == "Rating": + if wid > 50 * gui.scale: + yy = ry + (gui.playlist_row_height // 2) - (6 * gui.scale) + draw_rating_widget(run + 4 * gui.scale, yy, n_track) + + if item[0] == "Starline": + + total = star_store.get_by_object(n_track) + + if total > 0 and n_track.length != 0 and wid > 0: + if gui.star_mode == "star": + + star = star_count(total, n_track.length) - 1 + rr = 0 + if star > -1: + if gui.tracklist_bg_is_light: + colour = alpha_blend([0, 0, 0, 200], ddt.text_background_colour) + else: + colour = alpha_blend([255, 255, 255, 50], ddt.text_background_colour) + + sx = run + 6 * gui.scale + sy = ry + (gui.playlist_row_height // 2) - (6 * gui.scale) + for count in range(8): + if star < count or rr > wid + round(6 * gui.scale): + break + star_pc_icon.render(sx, sy, colour) + sx += round(13) * gui.scale + rr += round(13) * gui.scale + + else: + + ratio = total / n_track.length + if ratio > 0.55: + star_x = int(ratio * (4 * gui.scale)) + star_x = min(star_x, wid) + + colour = colours.star_line + if playing and colours.star_line_playing is not None: + colour = colours.star_line_playing + + sy = (gui.playlist_top + gui.playlist_row_height * number) + int( + gui.playlist_row_height / 2) + ddt.rect((run + 4 * gui.scale, sy, star_x, 1 * gui.scale), colour) + + else: + text = "" + font = gui.row_font_size + colour = [200, 200, 200, 255] + norm_colour = colour + y_off = 0 + if item[0] == "Title": + colour = colours.title_text + if n_track.title != "": + text = n_track.title + else: + text = n_track.filename + # colour = colours.index_playing + if this_line_playing is True: + colour = colours.title_playing + + elif item[0] == "Artist": + text = n_track.artist + colour = colours.artist_text + norm_colour = colour + if this_line_playing is True: + colour = colours.artist_playing + elif item[0] == "Album": + text = n_track.album + colour = colours.album_text + norm_colour = colour + if this_line_playing is True: + colour = colours.album_playing + elif item[0] == "Album Artist": + text = n_track.album_artist + if not text and prefs.column_aa_fallback_artist: + text = n_track.artist + colour = colours.artist_text + norm_colour = colour + if this_line_playing is True: + colour = colours.artist_playing + elif item[0] == "Composer": + text = n_track.composer + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "Comment": + text = n_track.comment.replace("\n", " ").replace("\r", " ") + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "S": + if n_track.lfm_scrobbles > 0: + text = str(n_track.lfm_scrobbles) + + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "#": + + if prefs.use_absolute_track_index and pctl.multi_playlist[pctl.active_playlist_viewing].hide_title: + text = str(p_track) + else: + text = track_number_process(n_track.track_number) + + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "Date": + text = n_track.date + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "Filepath": + text = clean_string(n_track.fullpath) + colour = colours.index_text + norm_colour = colour + elif item[0] == "Filename": + text = clean_string(n_track.filename) + colour = colours.index_text + norm_colour = colour + elif item[0] == "Disc": + text = str(n_track.disc_number) + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "Codec": + text = n_track.file_ext + if text == "JELY" and "container" in tr.misc: + text = tr.misc["container"] + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "Lyrics": + text = "" + if n_track.lyrics != "": + text = "Y" + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "CUE": + text = "" + if n_track.is_cue: + text = "Y" + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "Genre": + text = n_track.genre + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "Bitrate": + text = str(n_track.bitrate) + if text == "0": + text = "" + + ex = n_track.file_ext + if n_track.misc.get("container") is not None: + ex = n_track.misc.get("container") + if ex == "FLAC" or ex == "WAV" or ex == "APE": + text = str(round(n_track.samplerate / 1000, 1)).rstrip("0").rstrip(".") + "|" + str( + n_track.bit_depth) + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "Time": + text = get_display_time(n_track.length) + colour = colours.bar_time + norm_colour = colour + # colour = colours.time_text + if this_line_playing is True: + colour = colours.time_text + elif item[0] == "❤": + # col love + u = 5 * gui.scale + yy = ry + (gui.playlist_row_height // 2) - (5 * gui.scale) + if gui.scale == 1.25: + yy += 1 + + if get_love(n_track): + + j = 0 # justify right + if run < start + 100 * gui.scale: + j = 1 # justify left + display_you_heart(run + 6 * gui.scale, yy, j) + u += 18 * gui.scale + + if "spotify-liked" in n_track.misc: + j = 0 # justify right + if run < start + 100 * gui.scale: + j = 1 # justify left + display_spot_heart(run + u, yy, j) + u += 18 * gui.scale + + count = 0 + for name in n_track.lfm_friend_likes: + spacing = 6 * gui.scale + if u + (heart_row_icon.w + spacing) * count > wid + 7 * gui.scale: + break + + x = run + u + (heart_row_icon.w + spacing) * count + + j = 0 # justify right + if run < start + 100 * gui.scale: + j = 1 # justify left + + display_friend_heart(x, yy, name, j) + count += 1 + + # if n_track.track_number == 1 or n_track.track_number == "1": + # ss = wid - (wid % 15) + # tauon.gall_ren.render(n_track, (run, y), ss) + + + elif item[0] == "P": + ratio = 0 + total = star_store.get_by_object(n_track) + if total > 0 and n_track.length > 2: + if n_track.length > 15: + total += 2 + ratio = total / (n_track.length - 1) + + text = str(str(int(ratio))) + if text == "0": + text = "" + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + + if prefs.dim_art and album_mode and \ + n_track.parent_folder_name \ + != pctl.master_library[pctl.track_queue[pctl.queue_step]].parent_folder_name: + colour = alpha_mod(colour, 150) + if n_track.found is False: + colour = colours.playlist_text_missing + + if text: + if item[0] in colours.column_colours: + colour = colours.column_colours[item[0]] + + if this_line_playing and item[0] in colours.column_colours_playing: + colour = colours.column_colours_playing[item[0]] + + if run + 6 * gui.scale + wid > end: + wid = end - run - 40 * gui.scale + if center_mode: + wid += 25 * gui.scale + + wid = max(0, wid) + + # # Hacky. Places a dark background behind light text for readability over mascot + # if pl_bg and gui.set_mode and colour_value(norm_colour) < 400 and not colours.lm: + # w, h = ddt.get_text_wh(text, font, wid) + # quick_box = [run + round(5 * gui.scale), y + y_off, w + round(2 * gui.scale), h] + # if coll_rect((left + width - pl_bg.w - 60 * gui.scale, window_size[1] - gui.panelBY - pl_bg.h, pl_bg.w, pl_bg.h), quick_box): + # quick_box = (run, ry, item[1], gui.playlist_row_height) + # ddt.rect(quick_box, [0, 0, 0, 40], True) + # ddt.rect(quick_box, alpha_mod(colours.playlist_panel_background, 150), True) + + ddt.text( + (run + 6 * gui.scale, y + y_off), + text, + colour, + font, + max_w=wid) + + if ddt.was_truncated: + #logging.info(text) + rect = (run, y, wid - 1, gui.playlist_row_height - 1) + gui.heart_fields.append(rect) + + if coll(rect): + columns_tool_tip.set(run - 7 * gui.scale, y, text, font, rect) + + run += item[1] + + # ----------------------------------------------------------------- + # Count the number if visable tracks (used by Show Current function) + if gui.playlist_top + gui.playlist_row_height * w > window_size[0] - gui.panelBY - gui.playlist_row_height: + pass + else: + cv += 1 + + # w += 1 + # if w > gui.playlist_view_length: + # break + + # This is a bit hacky since its only generated after drawing + # Used to keep track of how many tracks are actually in view + gui.playlist_current_visible_tracks = cv + gui.playlist_current_visible_tracks_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + + if (right_click and gui.playlist_top + 5 * gui.scale + gui.playlist_row_height * len(list_items) < + mouse_position[1] < window_size[ + 1] - 55 and width + left > mouse_position[0] > gui.playlist_left + 15): + playlist_menu.activate() + + SDL_SetRenderTarget(renderer, gui.main_texture) + SDL_RenderCopy(renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) + + if mouse_down is False: + playlist_hold = False + + ddt.pretty_rect = None + ddt.alpha_bg = False + + def cache_render(self): + + SDL_RenderCopy(renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) + +class ArtBox: + + def __init__(self): + pass + + def draw(self, x, y, w, h, target_track=None, tight_border=False, default_border=None): + + # Draw a background for whole area + ddt.rect((x, y, w, h), colours.side_panel_background) + # ddt.rect_r((x, y, w ,h), [255, 0, 0, 200], True) + + # We need to find the size of the inner square for the artwork + # box = min(w, h) + + box_w = w + box_h = h + + box_w -= 17 * gui.scale # Inset the square a bit + box_h -= 17 * gui.scale # Inset the square a bit + + box_x = x + ((w - box_w) // 2) + box_y = y + ((h - box_h) // 2) + + # And position the square + rect = (box_x, box_y, box_w, box_h) + gui.main_art_box = rect + + # Draw the album art. If side bar is being dragged set quick draw flag + showc = None + result = 1 + + if target_track: # Only show if song playing or paused + result = album_art_gen.display(target_track, (rect[0], rect[1]), (box_w, box_h), side_drag) + showc = album_art_gen.get_info(target_track) + + # Draw faint border on album art + if tight_border: + if result == 0 and gui.art_drawn_rect: + border = gui.art_drawn_rect + ddt.rect_s(gui.art_drawn_rect, colours.art_box, 1 * gui.scale) + elif default_border: + border = default_border + ddt.rect_s(default_border, colours.art_box, 1 * gui.scale) + else: + border = rect + else: + ddt.rect_s(rect, colours.art_box, 1 * gui.scale) + border = rect + + fields.add(border) + + # Draw image downloading indicator + if gui.image_downloading: + ddt.text( + (x + int(box_w / 2), 38 * gui.scale + int(box_h / 2), 2), _("Fetching image..."), + colours.side_bar_line1, + 14, bg=colours.side_panel_background) + gui.update = 2 + + # Input for album art + if target_track: + + # Cycle images on click + + if coll(gui.main_art_box) and inp.mouse_click is True and key_focused == 0: + + album_art_gen.cycle_offset(target_track) + + if pctl.mpris: + pctl.mpris.update(force=True) + + # Activate picture context menu on right click + if tight_border and gui.art_drawn_rect: + if right_click and coll(gui.art_drawn_rect) and target_track: + picture_menu.activate(in_reference=target_track) + elif right_click and coll(rect) and target_track: + picture_menu.activate(in_reference=target_track) + + # Draw picture metadata + if showc is not None and coll(border) \ + and rename_track_box.active is False \ + and radiobox.active is False \ + and pref_box.enabled is False \ + and gui.rename_playlist_box is False \ + and gui.message_box is False \ + and track_box is False \ + and gui.layer_focus == 0: + + padding = 6 * gui.scale + + xw = box_x + box_w + yh = box_y + box_h + if tight_border and gui.art_drawn_rect and gui.art_drawn_rect[2] > 50 * gui.scale: + xw = gui.art_drawn_rect[0] + gui.art_drawn_rect[2] + yh = gui.art_drawn_rect[1] + gui.art_drawn_rect[3] + + art_metadata_overlay(xw, yh, showc) + + +class ScrollBox: + + def __init__(self): + + self.held = False + self.slide_hold = False + self.source_click_y = 0 + self.source_bar_y = 0 + self.direction_lock = -1 + self.d_position = 0 + + def draw( + self, x, y, w, h, value, max_value, force_dark_theme=False, click=None, r_click=False, jump_distance=4, extend_field=0): + + if max_value < 2: + return 0 + + if click is None: + click = inp.mouse_click + + bar_height = round(90 * gui.scale) + + if h > 400 * gui.scale and max_value < 20: + bar_height = round(180 * gui.scale) + + bg = [255, 255, 255, 7] + fg = [255, 255, 255, 30] + fg_h = [255, 255, 255, 40] + fg_off = [255, 255, 255, 15] + + if colours.lm and not force_dark_theme: + bg = [0, 0, 0, 15] + fg_off = [0, 0, 0, 30] + fg = [0, 0, 0, 60] + fg_h = [0, 0, 0, 70] + + ddt.rect((x, y, w, h), bg) + + half = bar_height // 2 + + ratio = value / max_value + + mi = y + half + mo = y + h - half + distance = mo - mi + position = int(round(distance * ratio)) + + fw = w + extend_field + fx = x - extend_field + + if coll((fx, y, fw, h)): + + if mouse_down: + gui.update += 1 + + if r_click: + p = mouse_position[1] - half - y + p = max(0, p) + + range = h - bar_height + p = min(p, range) + + per = p / range + + value = int(round(max_value * per)) + + ratio = value / max_value + + mi = y + half + mo = y + h - half + distance = mo - mi + position = int(round(distance * ratio)) + + in_bar = False + if coll((x, mi + position - half, w, bar_height)): + in_bar = True + if click: + self.held = True + + # p_y = pointer(c_int(0)) + # SDL_GetGlobalMouseState(None, p_y) + get_sdl_input.mouse_capture_want = True + self.source_click_y = mouse_position[1] + self.source_bar_y = position + + if pctl.playlist_view_position < 0: + pctl.playlist_view_position = 0 + + + elif mouse_down and not self.held: + + if click and not in_bar: + self.slide_hold = True + self.direction_lock = 1 + if mouse_position[1] - y < position: + self.direction_lock = 0 + + self.d_position = value / max_value + + if self.slide_hold: + if (self.direction_lock == 1 and mouse_position[1] - y < position + half) or \ + (self.direction_lock == 0 and mouse_position[1] - y > position + half): + pass + else: + + tt = scroll_timer.hit() + if tt > 0.1: + tt = 0 + + flip = -1 + if self.direction_lock: + flip = 1 + + self.d_position = min(max(self.d_position + (((tt * jump_distance) / max_value) * flip), 0), 1) + + else: + self.slide_hold = False + + if (self.held and mouse_up) or not mouse_down: + self.held = False + + if self.held and not window_is_focused(): + self.held = False + + if self.held: + get_sdl_input.mouse_capture_want = True + new_y = mouse_position[1] + gui.update += 1 + + offset = new_y - self.source_click_y + + position = self.source_bar_y + offset + + position = max(position, 0) + position = min(position, distance) + + ratio = position / distance + value = int(round(max_value * ratio)) + + colour = fg_off + rect = (x, mi + position - half, w, bar_height) + fields.add(rect) + if coll(rect): + colour = fg + if self.held: + colour = fg_h + + ddt.rect(rect, colour) + + if self.slide_hold: + return round(max_value * self.d_position) + + return value + + +class RadioBox: + + def __init__(self): + + self.active = False + self.station_editing = None + self.edit_mode = True + self.add_mode = False + self.radio_field_active = 1 + self.radio_field = TextBox2() + self.radio_field_title = TextBox2() + self.radio_field_search = TextBox2() + + self.x = 1 + self.y = 1 + self.w = 1 + self.h = 1 + self.center = False + + self.scroll_position = 0 + self.scroll = ScrollBox() + + self.dummy_track = TrackClass() + self.dummy_track.index = -2 + self.dummy_track.is_network = True + self.dummy_track.art_url_key = "" # radio" + self.dummy_track.file_ext = "RADIO" + self.playing_title = "" + + self.proxy_started = False + self.loaded_url = None + self.loaded_station = None + self.load_connecting = False + self.load_failed = False + self.searching = False + self.load_failed_timer = Timer() + self.right_clicked_station = None + self.right_clicked_station_p = None + self.click_point = (0, 0) + + self.song_key = "" + + self.drag = None + + self.tab = 0 + self.temp_list = [] + + self.hosts = None + self.host = None + + self.search_menu = Menu(170) + self.search_menu.add(MenuItem(_("Search Tag"), self.search_tag, pass_ref=True)) + self.search_menu.add(MenuItem(_("Search Country Code"), self.search_country, pass_ref=True)) + self.search_menu.add(MenuItem(_("Search Title"), self.search_title, pass_ref=True)) + + self.websocket = None + self.ws_interval = 4.5 + self.websocket_source_urls = ("https://listen.moe/kpop/stream", "https://listen.moe/stream") + self.run_proxy = True + + def parse_vorbis_okay(self): + return ( + self.loaded_url not in self.websocket_source_urls) and \ + "radio.plaza.one" not in self.loaded_url and \ + "gensokyoradio.net" not in self.loaded_url + + def search_country(self, text): + + if len(text) == 2 and text.isalpha(): + self.search_radio_browser( + "/json/stations/search?countrycode=" + text + "&order=votes&limit=250&reverse=true") + else: + self.search_radio_browser( + "/json/stations/search?country=" + text + "&order=votes&limit=250&reverse=true") + + def search_tag(self, text): + + text = text.lower() + self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&tag=" + text) + + def search_title(self, text): + + text = text.lower() + self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&name=" + text) + + def is_m3u(self, url): + return url.lower().endswith(".m3u") or url.lower().endswith(".m3u8") + + def extract_stream_m3u(self, url, recursion_limit=5): + if recursion_limit <= 0: + return None + logging.info("Fetching M3U...") + + try: + response = requests.get(url, timeout=10) + if response.status_code != 200: + logging.error(f"M3U Fetch error code: {response.status_code}") + return None + + content = response.text + lines = content.strip().split("\n") + + for line in lines: + line = line.strip() + if not line.startswith("#") and len(line) > 0: + if self.is_m3u(line): + next_url = urllib.parse.urljoin(url, line) + return self.extract_stream_m3u(next_url, recursion_limit - 1) + return urllib.parse.urljoin(url, line) + + return None + + except Exception: + logging.exception("Failed to extract M3U") + return None + + def start(self, item): + url = item["stream_url"] + logging.info("Start radio") + logging.info(url) + if self.is_m3u(url): + url = self.extract_stream_m3u(url) + logging.info(f"Extracted URL is: {url}") + if not url: + logging.info("Failed to extract stream from M3U") + return + + if self.load_connecting: + return + + if tauon.spot_ctl.playing or tauon.spot_ctl.coasting: + tauon.spot_ctl.control("stop") + + try: + self.websocket.close() + logging.info("Websocket closed") + except Exception: + logging.exception("No socket to close?") + + self.playing_title = "" + self.playing_title = item["title"] + self.dummy_track.art_url_key = "" + self.dummy_track.title = "" + self.dummy_track.artist = "" + self.dummy_track.album = "" + self.dummy_track.date = "" + pctl.radio_meta_on = "" + + album_art_gen.clear_cache() + + if not tauon.test_ffmpeg(): + prefs.auto_rec = False + return + + self.run_proxy = True + if url.endswith(".ts"): + self.run_proxy = False + + if self.run_proxy and not self.proxy_started and prefs.backend != 4: + shoot = threading.Thread(target=stream_proxy, args=[tauon]) + shoot.daemon = True + shoot.start() + self.proxy_started = True + + # pctl.url = url + pctl.url = f"http://127.0.0.1:{7812}" + if not self.run_proxy: + pctl.url = item["stream_url"] + self.loaded_url = None + pctl.tag_meta = "" + pctl.radio_meta_on = "" + pctl.found_tags = {} + self.song_key = "" + pctl.playing_time = 0 + pctl.decode_time = 0 + self.loaded_station = item + + if tauon.stream_proxy.download_running: + tauon.stream_proxy.abort = True + + self.load_connecting = True + self.load_failed = False + + shoot = threading.Thread(target=self.start2, args=[url]) + shoot.daemon = True + shoot.start() + + def start2(self, url): + + if self.run_proxy and not tauon.stream_proxy.start_download(url): + self.load_failed_timer.set() + self.load_failed = True + self.load_connecting = False + gui.update += 1 + logging.error("Starting radio failed") + # show_message(_("Failed to establish a connection"), mode="error") + return + + self.loaded_url = url + pctl.playing_state = 0 + pctl.record_stream = False + pctl.playerCommand = "url" + pctl.playerCommandReady = True + pctl.playing_state = 3 + pctl.playing_time = 0 + pctl.decode_time = 0 + pctl.playing_length = 0 + tauon.thread_manager.ready_playback() + hit_discord() + + if tauon.update_play_lock is not None: + tauon.update_play_lock() + + time.sleep(0.1) + self.load_connecting = False + self.load_failed = False + gui.update += 1 + + wss = "" + if url == "https://listen.moe/kpop/stream": + wss = "wss://listen.moe/kpop/gateway_v2" + if url == "https://listen.moe/stream": + wss = "wss://listen.moe/gateway_v2" + if wss: + logging.info("Connecting to Listen.moe") + import websocket + import _thread as th + + def send_heartbeat(ws): + #logging.info(self.ws_interval) + time.sleep(self.ws_interval) + ws.send("{\"op\":9}") + logging.info("Send heatbeat") + + def on_message(ws, message): + logging.info(message) + d = json.loads(message) + if d["op"] == 10: + shoot = threading.Thread(target=send_heartbeat, args=[ws]) + shoot.daemon = True + shoot.start() + + if d["op"] == 0: + self.ws_interval = d["d"]["heartbeat"] / 1000 + ws.send("{\"op\":9}") + + if d["op"] == 1: + try: + + found_tags = {} + found_tags["title"] = d["d"]["song"]["title"] + if d["d"]["song"]["artists"]: + found_tags["artist"] = d["d"]["song"]["artists"][0]["name"] + line = "" + if "title" in found_tags: + line += found_tags["title"] + if "artist" in found_tags: + line = found_tags["artist"] + " - " + line + + pctl.found_tags = found_tags + pctl.tag_meta = line + + filename = d["d"]["song"]["albums"][0]["image"] + fulllink = "https://cdn.listen.moe/covers/" + filename + + #logging.info(fulllink) + art_response = requests.get(fulllink, timeout=10) + #logging.info(art_response.status_code) + + if art_response.status_code == 200: + if pctl.radio_image_bin: + pctl.radio_image_bin.close() + pctl.radio_image_bin = None + pctl.radio_image_bin = io.BytesIO(art_response.content) + pctl.radio_image_bin.seek(0) + radiobox.dummy_track.art_url_key = "ok" + logging.info("Got new art") + + + except Exception: + logging.exception("No image") + if pctl.radio_image_bin: + pctl.radio_image_bin.close() + pctl.radio_image_bin = None + gui.clear_image_cache_next += 1 + gui.update += 1 + + def on_error(ws, error): + logging.error(error) + + def on_close(ws): + logging.info("### closed ###") + + def on_open(ws): + def run(*args): + pass + # for i in range(3): + # time.sleep(4.5) + # ws.send("{\"op\":9}") + # time.sleep(10) + # ws.close() + #logging.info("thread terminating...") + + th.start_new_thread(run, ()) + + # websocket.enableTrace(True) + #logging.info(wss) + ws = websocket.WebSocketApp(wss, + on_message=on_message, + on_error=on_error) + ws.on_open = on_open + self.websocket = ws + shoot = threading.Thread(target=ws.run_forever) + shoot.daemon = True + shoot.start() + + def delete_radio_entry(self, item): + for i, saved in enumerate(prefs.radio_urls): + if saved["stream_url"] == item["stream_url"] and saved["title"] == item["title"]: + del prefs.radio_urls[i] + + def delete_radio_entry_after(self, item): + p = radiobox.right_clicked_station_p + del prefs.radio_urls[p + 1:] + + def edit_entry(self, item): + radio = item + self.radio_field_title.text = radio["title"] + self.radio_field.text = radio["stream_url"] + + def browser_get_hosts(self): + + import socket + """ + Get all base urls of all currently available radiobrowser servers + + Returns: + list: a list of strings + + """ + hosts = [] + # get all hosts from DNS + ips = socket.getaddrinfo( + "all.api.radio-browser.info", 80, 0, 0, socket.IPPROTO_TCP) + for ip_tupple in ips: + try: + ip = ip_tupple[4][0] + + # do a reverse lookup on every one of the ips to have a nice name for it + host_addr = socket.gethostbyaddr(ip) + # add the name to a list if not already in there + if host_addr[0] not in hosts: + hosts.append(host_addr[0]) + except Exception: + logging.exception("IPv4 lookup fail") + + # sort list of names + hosts.sort() + # add "https://" in front to make it an url + return list(map(lambda x: "https://" + x, hosts)) + + def search_page(self): + + y = self.y + x = self.x + w = self.w + h = self.h + + yy = y + round(40 * gui.scale) + + width = round(330 * gui.scale) + rect = (x + 8 * gui.scale, yy - round(2 * gui.scale), width, 22 * gui.scale) + fields.add(rect) + # if (coll(rect) and gui.level_2_click) or (input.key_tab_press and self.radio_field_active == 2): + # self.radio_field_active = 1 + # input.key_tab_press = False + if not self.radio_field_search.text and not editline: + ddt.text((x + 14 * gui.scale, yy), _("Search text…"), colours.box_text_label, 312) + self.radio_field_search.draw( + x + 14 * gui.scale, yy, colours.box_input_text, + active=True, + width=width, click=gui.level_2_click) + + ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) + + if draw.button( + _("Search"), x + width + round(21 * gui.scale), yy - round(3 * gui.scale), + press=gui.level_2_click, w=round(80 * gui.scale)) or inp.level_2_enter: + + text = self.radio_field_search.text.replace("/", "").replace(":", "").replace("\\", "").replace(".", "").replace( + "-", "").upper() + text = urllib.parse.quote(text) + if len(text) > 1: + self.search_menu.activate(text, position=(x + width + round(21 * gui.scale), yy + round(20 * gui.scale))) + if draw.button(_("Get Top Voted"), x + round(8 * gui.scale), yy + round(30 * gui.scale), press=gui.level_2_click): + self.search_radio_browser("/json/stations?order=votes&limit=250&reverse=true") + + ww = ddt.get_text_w(_("Get Top Voted"), 212) + if key_shift_down: + if draw.button(_("Developer Picks"), x + ww + round(35 * gui.scale), yy + round(30 * gui.scale), press=gui.level_2_click): + self.temp_list.clear() + + radio = {} + radio["title"] = "Nightwave Plaza" + radio["stream_url_unresolved"] = "https://radio.plaza.one/ogg" + radio["stream_url"] = "https://radio.plaza.one/ogg" + radio["website_url"] = "https://plaza.one/" + radio["icon"] = "https://plaza.one/icons/apple-touch-icon.png" + radio["country"] = "Japan" + self.temp_list.append(radio) + + radio = {} + radio["title"] = "Gensokyo Radio" + radio["stream_url_unresolved"] = " https://stream.gensokyoradio.net/GensokyoRadio-enhanced.m3u" + radio["stream_url"] = "https://stream.gensokyoradio.net/1" + radio["website_url"] = "https://gensokyoradio.net/" + radio["icon"] = "https://gensokyoradio.net/favicon.ico" + radio["country"] = "Japan" + self.temp_list.append(radio) + + radio = {} + radio["title"] = "Listen.moe | Jpop" + radio["stream_url_unresolved"] = "https://listen.moe/stream" + radio["stream_url"] = "https://listen.moe/stream" + radio["website_url"] = "https://listen.moe/" + radio["icon"] = "https://avatars.githubusercontent.com/u/26034028?s=200&v=4" + radio["country"] = "Japan" + self.temp_list.append(radio) + + radio = {} + radio["title"] = "Listen.moe | Kpop" + radio["stream_url_unresolved"] = "https://listen.moe/kpop/stream" + radio["stream_url"] = "https://listen.moe/kpop/stream" + radio["website_url"] = "https://listen.moe/" + radio["icon"] = "https://avatars.githubusercontent.com/u/26034028?s=200&v=4" + radio["country"] = "Korea" + + self.temp_list.append(radio) + + radio = {} + radio["title"] = "HBR1 Dream Factory | Ambient" + radio["stream_url_unresolved"] = "http://radio.hbr1.com:19800/ambient.ogg" + radio["stream_url"] = "http://radio.hbr1.com:19800/ambient.ogg" + radio["website_url"] = "http://www.hbr1.com/" + self.temp_list.append(radio) + + radio = {} + radio["title"] = "Yggdrasil Radio | Anime & Jpop" + radio["stream_url_unresolved"] = "http://shirayuki.org:9200/" + radio["stream_url"] = "http://shirayuki.org:9200/" + radio["website_url"] = "https://yggdrasilradio.net/" + self.temp_list.append(radio) + + for station in primary_stations: + self.temp_list.append(station) + + def search_radio_browser(self, param): + if self.searching: + return + self.searching = True + shoot = threading.Thread(target=self.search_radio_browser2, args=[param]) + shoot.daemon = True + shoot.start() + + def search_radio_browser2(self, param): + + if not self.hosts: + self.hosts = self.browser_get_hosts() + if not self.host: + self.host = random.choice(self.hosts) + + uri = self.host + param + req = urllib.request.Request(uri) + req.add_header("User-Agent", t_agent) + req.add_header("Content-Type", "application/json") + response = urllib.request.urlopen(req, context=ssl_context) + data = response.read() + data = json.loads(data.decode()) + self.parse_data(data) + self.searching = False + + def parse_data(self, data): + + self.temp_list.clear() + + for station in data: + radio: dict[str, int | str] = {} + #logging.info(station) + radio["title"] = station["name"] + radio["stream_url_unresolved"] = station["url"] + radio["stream_url"] = station["url_resolved"] + radio["icon"] = station["favicon"] + radio["country"] = station["country"] + if radio["country"] == "The Russian Federation": + radio["country"] = "Russia" + elif radio["country"] == "The United States Of America": + radio["country"] = "USA" + elif radio["country"] == "The United Kingdom Of Great Britain And Northern Ireland": + radio["country"] = "United Kingdom" + elif radio["country"] == "Islamic Republic Of Iran": + radio["country"] = "Iran" + elif len(station["country"]) > 20: + radio["country"] = station["countrycode"] + radio["website_url"] = station["homepage"] + if "homepage" in station: + radio["website_url"] = station["homepage"] + self.temp_list.append(radio) + gui.update += 1 + + def render(self) -> None: + + if self.edit_mode: + w = round(510 * gui.scale) + h = round(120 * gui.scale) # + sh + + self.w = w + self.h = h + # self.x = x + # self.y = y + width = w + if self.center: + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) + yy = y + self.y = y + self.x = x + else: + yy = self.y + y = self.y + x = self.x + ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) + ddt.rect_a((x, y), (w, h), colours.box_background) + ddt.text_background_colour = colours.box_background + if key_esc_press or (gui.level_2_click and not coll((x, y, w, h))): + self.active = False + + if self.add_mode: + ddt.text((x + 10 * gui.scale, yy + 8 * gui.scale), _("Add Station"), colours.box_title_text, 213) + else: + ddt.text((x + 10 * gui.scale, yy + 8 * gui.scale), _("Edit Station"), colours.box_title_text, 213) + + self.saved() + return + + w = round(510 * gui.scale) + h = round(356 * gui.scale) # + sh + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) + + self.w = w + self.h = h + self.x = x + self.y = y + + yy = y + + ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) + ddt.rect_a((x, y), (w, h), colours.box_background) + + ddt.text_background_colour = colours.box_background + + if key_esc_press or (gui.level_2_click and not coll((x, y, w, h))): + self.active = False + + ddt.text((x + 10 * gui.scale, yy + 8 * gui.scale), _("Station Browser"), colours.box_title_text, 213) + + # --- + if self.load_connecting: + ddt.text((x + 495 * gui.scale, yy + 8 * gui.scale, 1), _("Connecting..."), colours.box_title_text, 311) + elif self.load_failed: + ddt.text((x + 495 * gui.scale, yy + 8 * gui.scale, 1), _("Failed to connect!"), colours.box_title_text, 311) + if self.load_failed_timer.get() > 3: + gui.delay_frame(0.2) + self.load_failed = False + + elif self.searching: + ddt.text((x + 495 * gui.scale, yy + 8 * gui.scale, 1), _("Searching..."), colours.box_title_text, 311) + elif pctl.playing_state == 3: + + text = "" + if tauon.stream_proxy.s_format: + text = str(tauon.stream_proxy.s_format) + if tauon.stream_proxy.s_bitrate and tauon.stream_proxy.s_bitrate.isnumeric(): + text += " " + tauon.stream_proxy.s_bitrate + _("kbps") + + ddt.text((x + 495 * gui.scale, yy + 8 * gui.scale, 1), text, colours.box_title_text, 311) + # if tauon.stream_proxy.s_format: + # ddt.text((x + 425 * gui.scale, yy + 8 * gui.scale,), tauon.stream_proxy.s_format, colours.box_title_text, 311) + # if tauon.stream_proxy.s_bitrate: + # ddt.text((x + 454 * gui.scale, yy + 8 * gui.scale,), tauon.stream_proxy.s_bitrate + "kbps", colours.box_title_text, 311) + + # --- ---------------------------------------------------------------------- + if self.tab == 1: + self.search_page() + elif self.tab == 0: + self.saved() + self.draw_list() + # self.footer() + return + + def saved(self): + y = self.y + x = self.x + w = self.w + h = self.h + + yy = y + round(40 * gui.scale) + + width = round(370 * gui.scale) + + rect = (x + 8 * gui.scale, yy - round(2 * gui.scale), width, 22 * gui.scale) + fields.add(rect) + if (coll(rect) and gui.level_2_click) or (inp.key_tab_press and self.radio_field_active == 2): + self.radio_field_active = 1 + inp.key_tab_press = False + if not self.radio_field_title.text and not (self.radio_field_active == 1 and editline): + ddt.text((x + 14 * gui.scale, yy), _("Name / Title"), colours.box_text_label, 312) + self.radio_field_title.draw(x + 14 * gui.scale, yy, colours.box_input_text, + active=self.radio_field_active == 1, + width=width, click=gui.level_2_click) + + ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) + + yy += round(30 * gui.scale) + + rect = (x + 8 * gui.scale, yy - round(2 * gui.scale), width, 22 * gui.scale) + ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) + fields.add(rect) + if (coll(rect) and gui.level_2_click) or (inp.key_tab_press and self.radio_field_active == 1): + self.radio_field_active = 2 + inp.key_tab_press = False + + if not self.radio_field.text and not (self.radio_field_active == 2 and editline): + ddt.text((x + 14 * gui.scale, yy), _("Raw Stream URL http://example.stream:1234"), colours.box_text_label, 312) + self.radio_field.draw( + x + 14 * gui.scale, yy, colours.box_input_text, active=self.radio_field_active == 2, + width=width, click=gui.level_2_click) + + if draw.button(_("Save"), x + width + round(21 * gui.scale), yy - round(20 * gui.scale), press=gui.level_2_click): + + if not self.radio_field.text: + show_message(_("Enter a stream URL")) + elif "http://" in self.radio_field.text or "https://" in self.radio_field.text: + radio = self.station_editing + if self.add_mode: + radio: dict[str, int | str] = {} + radio["title"] = self.radio_field_title.text + radio["stream_url"] = self.radio_field.text + radio["website_url"] = "" + + if self.add_mode: + pctl.radio_playlists[pctl.radio_playlist_viewing]["items"].append(radio) + self.active = False + + else: + show_message(_("Could not validate URL. Must start with https:// or http://")) + + def draw_list(self): + + x = self.x + y = self.y + w = self.w + h = self.h + + if self.drag: + gui.update_on_drag = True + + yy = y + round(100 * gui.scale) + x += round(10 * gui.scale) + + radio_list = prefs.radio_urls + if self.tab == 1: + radio_list = self.temp_list + + rect = (x, y, w, h) + if coll(rect): + self.scroll_position += mouse_wheel * -1 + self.scroll_position = max(self.scroll_position, 0) + self.scroll_position = min(self.scroll_position, len(radio_list) // 2 - 7) + + if len(radio_list) // 2 > 9: + self.scroll_position = self.scroll.draw( + (x + w) - round(35 * gui.scale), yy, round(15 * gui.scale), + round(210 * gui.scale), self.scroll_position, + len(radio_list) // 2 - 7, True, click=gui.level_2_click) + + self.scroll_position = max(self.scroll_position, 0) + + p = self.scroll_position * 2 + offset = 0 + to_delete = None + swap = None + + while True: + + if p > len(radio_list) - 1: + break + + xx = x + offset + item = radio_list[p] + + rect = (xx, yy, round(233 * gui.scale), round(40 * gui.scale)) + fields.add(rect) + + bg = colours.box_background + text_colour = colours.box_input_text + + playing = pctl.playing_state == 3 and self.loaded_url == item["stream_url"] + + if playing: + # bg = colours.box_sub_highlight + # ddt.rect(rect, bg, True) + + bg = colours.tab_background_active + text_colour = colours.tab_text_active + ddt.rect(rect, bg) + + if radio_view.drag: + if item == radio_view.drag: + text_colour = colours.box_sub_text + bg = [255, 255, 255, 10] + ddt.rect(rect, bg) + elif (radio_entry_menu.active and radio_entry_menu.reference == p) or \ + ((not radio_entry_menu.active and coll(rect)) and not playing): + text_colour = colours.box_sub_text + bg = [255, 255, 255, 10] + ddt.rect(rect, bg) + + if coll(rect): + + if gui.level_2_click: + # self.drag = p + # self.click_point = copy.copy(mouse_position) + radio_view.drag = item + radio_view.click_point = copy.copy(mouse_position) + if mouse_up: # gui.level_2_click: + gui.update += 1 + # if self.drag is not None and p != self.drag: + # swap = p + if point_proximity_test(radio_view.click_point, mouse_position, round(4 * gui.scale)): + self.start(item) + if middle_click: + to_delete = p + if level_2_right_click: + self.right_clicked_station = item + self.right_clicked_station_p = p + radio_entry_menu.activate(item) + + bg = alpha_blend(bg, colours.box_background) + + boxx = round(32 * gui.scale) + toff = boxx + round(10 * gui.scale) + if item["title"]: + ddt.text( + (xx + toff, yy + round(3 * gui.scale)), item["title"], text_colour, 212, bg=bg, + max_w=rect[2] - (15 * gui.scale + toff)) + else: + ddt.text( + (xx + toff, yy + round(3 * gui.scale)), item["stream_url"], text_colour, 212, bg=bg, + max_w=rect[2] - (15 * gui.scale + toff)) + + country = item.get("country") + if country: + ddt.text( + (xx + toff, yy + round(18 * gui.scale)), country, text_colour, 11, bg=bg, + max_w=rect[2] - (15 * gui.scale + toff)) + + b_rect = (xx + round(4 * gui.scale), yy + round(4 * gui.scale), boxx, boxx) + ddt.rect(b_rect, colours.box_thumb_background) + radio_thumb_gen.draw(item, b_rect[0], b_rect[1], b_rect[2]) + + if offset == 0: + offset = rect[2] + round(4 * gui.scale) + else: + offset = 0 + yy += round(43 * gui.scale) + + if yy > y + 300 * gui.scale: + break + + p += 1 + + # if to_delete is not None: + # del radio_list[to_delete] + # + # if mouse_up and self.drag and mouse_position[1] > yy + round(22 * gui.scale): + # swap = len(radio_list) + + # if self.drag and not point_proximity_test(self.click_point, mouse_position, round(4 * gui.scale)): + # ddt.rect(( + # mouse_position[0] + round(8 * gui.scale), mouse_position[1] - round(8 * gui.scale), 45 * gui.scale, + # 13 * gui.scale), colours.grey(70)) + + # if swap is not None: + # + # old = radio_list[self.drag] + # radio_list[self.drag] = None + # + # if swap > self.drag: + # swap += 1 + # + # radio_list.insert(swap, old) + # radio_list.remove(None) + # + # self.drag = None + # gui.update += 1 + + # if not mouse_down: + # self.drag = None + + def footer(self): + + y = self.y + x = self.x + round(15 * gui.scale) + w = self.w + h = self.h + + yy = y + round(328 * gui.scale) + if pctl.playing_state == 3 and not prefs.auto_rec: + old = prefs.auto_rec + if not old and pref_box.toggle_square( + x, yy, prefs.auto_rec, _("Record and auto split songs"), + click=gui.level_2_click): + show_message(_("Please stop playback first before toggling this setting")) + elif pctl.playing_state == 3: + old = prefs.auto_rec + if old and not pref_box.toggle_square( + x, yy, prefs.auto_rec, _("Record and auto split songs"), + click=gui.level_2_click): + show_message(_("Please stop playback first to end current recording")) + + else: + old = prefs.auto_rec + prefs.auto_rec = pref_box.toggle_square( + x, yy, prefs.auto_rec, _("Record and auto split songs"), + click=gui.level_2_click) + if prefs.auto_rec != old and prefs.auto_rec: + show_message( + _("Tracks will now be recorded."), + _("Tip: You can press F9 to view the output folder."), mode="info") + + if self.tab == 0: + if draw.button( + _("Browse"), (x + w) - round(130 * gui.scale), yy - round(3 * gui.scale), + press=gui.level_2_click, w=round(100 * gui.scale)): + self.tab = 1 + elif self.tab == 1: + if draw.button( + _("Saved"), (x + w) - round(130 * gui.scale), yy - round(3 * gui.scale), + press=gui.level_2_click, w=round(100 * gui.scale)): + self.tab = 0 + gui.level_2_click = False + +class RenamePlaylistBox: + + def __init__(self): + + self.x = 300 + self.y = 300 + self.playlist_index = 0 + + self.edit_generator = False + + def toggle_edit_gen(self): + + self.edit_generator ^= True + if self.edit_generator: + + if len(rename_text_area.text) > 0: + pctl.multi_playlist[self.playlist_index].title = rename_text_area.text + + pl = self.playlist_index + id = pl_to_id(pl) + + text = pctl.gen_codes.get(id) + if not text: + text = "" + + rename_text_area.set_text(text) + rename_text_area.highlight_none() + + gui.regen_single = rename_playlist_box.playlist_index + tauon.thread_manager.ready("worker") + + + else: + rename_text_area.set_text(pctl.multi_playlist[self.playlist_index].title) + rename_text_area.highlight_none() + # rename_text_area.highlight_all() + + def render(self): + + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False + + if inp.key_tab_press: + self.toggle_edit_gen() + + text_w = ddt.get_text_w(rename_text_area.text, 315) + min_w = max(250 * gui.scale, text_w + 50 * gui.scale) + + rect = [self.x, self.y, min_w, 37 * gui.scale] + bg = [40, 40, 40, 255] + if self.edit_generator: + bg = [70, 50, 100, 255] + ddt.text_background_colour = bg + + # Draw background + ddt.rect(rect, bg) + + # Draw text entry + rename_text_area.draw( + rect[0] + 10 * gui.scale, rect[1] + 8 * gui.scale, colours.alpha_grey(250), + width=350 * gui.scale, font=315) + + # Draw accent + rect2 = [self.x, self.y + rect[3] - 4 * gui.scale, min_w, 4 * gui.scale] + ddt.rect(rect2, [255, 255, 255, 60]) + + if self.edit_generator: + pl = self.playlist_index + id = pl_to_id(pl) + pctl.gen_codes[id] = rename_text_area.text + + if input_text or key_backspace_press: + gui.regen_single = rename_playlist_box.playlist_index + tauon.thread_manager.ready("worker") + + # regenerate_playlist(rename_playlist_box.playlist_index) + # if gui.gen_code_errors: + # del_icon.render(rect[0] + rect[2] - 21 * gui.scale, rect[1] + 10 * gui.scale, (255, 70, 70, 255)) + ddt.text_background_colour = [4, 4, 4, 255] + hint_rect = [rect[0], rect[1] + round(50 * gui.scale), round(560 * gui.scale), round(300 * gui.scale)] + + if hint_rect[0] + hint_rect[2] > window_size[0]: + hint_rect[0] = window_size[0] - hint_rect[2] + + ddt.rect(hint_rect, [0, 0, 0, 245]) + xx0 = hint_rect[0] + round(15 * gui.scale) + xx = hint_rect[0] + round(25 * gui.scale) + xx2 = hint_rect[0] + round(85 * gui.scale) + yy = hint_rect[1] + round(10 * gui.scale) + + text_colour = [150, 150, 150, 255] + title_colour = text_colour + code_colour = [250, 250, 250, 255] + hint_colour = [110, 110, 110, 255] + + title_font = 311 + code_font = 311 + hint_font = 310 + + # ddt.pretty_rect = hint_rect + + ddt.text( + (xx0, yy), _("Type codes separated by spaces. Codes will be executed left to right."), text_colour, title_font) + yy += round(18 * gui.scale) + ddt.text((xx0, yy), _("Select sources: (default: all playlists)"), title_colour, title_font) + yy += round(14 * gui.scale) + ddt.text((xx, yy), "s\"name\"", code_colour, code_font) + ddt.text((xx2, yy), _("Select source playlist by name"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "self", code_colour, code_font) + ddt.text((xx2, yy), _("Select playlist itself"), hint_colour, hint_font) + + yy += round(16 * gui.scale) + ddt.text((xx0, yy), _("Add tracks from sources: (at least 1 required)"), title_colour, title_font) + yy += round(14 * gui.scale) + + ddt.text((xx, yy), "a\"name\"", code_colour, code_font) + ddt.text((xx2, yy), _("Search artist name"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "g\"genre\"", code_colour, code_font) + ddt.text((xx2, yy), _("Search genre"), hint_colour, hint_font) + # yy += round(12 * gui.scale) + # ddt.text((xx, yy), "p\"text\"", code_colour, code_font) + # ddt.text((xx2, yy), "Search filepath segment", hint_colour, hint_font) + + yy += round(12 * gui.scale) + ddt.text((xx, yy), "f\"terms\"", code_colour, code_font) + ddt.text((xx2, yy), _("Find / Search / Path"), hint_colour, hint_font) + + # yy += round(12 * gui.scale) + # ddt.text((xx, yy), "ext\"flac\"", code_colour, code_font) + # ddt.text((xx2, yy), "Search by file type", hint_colour, hint_font) + + yy += round(12 * gui.scale) + ddt.text((xx, yy), "a", code_colour, code_font) + ddt.text((xx2, yy), _("Add all tracks"), hint_colour, hint_font) + + yy += round(16 * gui.scale) + ddt.text((xx0, yy), _("Filters"), title_colour, title_font) + yy += round(14 * gui.scale) + ddt.text((xx, yy), "n123", code_colour, code_font) + ddt.text((xx2, yy), _("Limit to number of tracks"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "y>1999", code_colour, code_font) + ddt.text((xx2, yy), _("Year: >, <, ="), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "pc>5", code_colour, code_font) + ddt.text((xx2, yy), _("Play count: >, <"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "d>120", code_colour, code_font) + ddt.text((xx2, yy), _("Duration (seconds): >, <"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "rat>3.5", code_colour, code_font) + ddt.text((xx2, yy), _("Track rating 0-5: >, <, ="), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "l", code_colour, code_font) + ddt.text((xx2, yy), _("Loved tracks"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "ly", code_colour, code_font) + ddt.text((xx2, yy), _("Has lyrics"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "ff\"terms\"", code_colour, code_font) + ddt.text((xx2, yy), _("Search and keep"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "fx\"terms\"", code_colour, code_font) + ddt.text((xx2, yy), _("Search and exclude"), hint_colour, hint_font) + + # yy += round(12 * gui.scale) + # ddt.text((xx, yy), "com\"text\"", code_colour, code_font) + # ddt.text((xx2, yy), "Search in comment", hint_colour, hint_font) + # yy += round(12 * gui.scale) + + xx += round(260 * gui.scale) + xx2 += round(260 * gui.scale) + xx0 += round(260 * gui.scale) + yy = hint_rect[1] + round(10 * gui.scale) + yy += round(18 * gui.scale) + + # yy += round(16 * gui.scale) + ddt.text((xx0, yy), _("Sorters"), title_colour, title_font) + yy += round(14 * gui.scale) + + ddt.text((xx, yy), "st", code_colour, code_font) + ddt.text((xx2, yy), _("Shuffle tracks"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "ra", code_colour, code_font) + ddt.text((xx2, yy), _("Shuffle albums"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "y>", code_colour, code_font) + ddt.text((xx2, yy), _("Year: >, <"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "d>", code_colour, code_font) + ddt.text((xx2, yy), _("Duration: >, <"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "pt>", code_colour, code_font) + ddt.text((xx2, yy), _("Track Playtime: >, <"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "pa>", code_colour, code_font) + ddt.text((xx2, yy), _("Album playtime: >, <"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "rv", code_colour, code_font) + ddt.text((xx2, yy), _("Invert tracks"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "rva", code_colour, code_font) + ddt.text((xx2, yy), _("Invert albums"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "rat>", code_colour, code_font) + ddt.text((xx2, yy), _("Track rating: >, <"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "rata>", code_colour, code_font) + ddt.text((xx2, yy), _("Album rating: >, <"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "m>", code_colour, code_font) + ddt.text((xx2, yy), _("Modification date: >, <"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "path", code_colour, code_font) + ddt.text((xx2, yy), _("Filepath"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "tn", code_colour, code_font) + ddt.text((xx2, yy), _("Track number per album"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "ypa", code_colour, code_font) + ddt.text((xx2, yy), _("Year per artist"), hint_colour, hint_font) + yy += round(12 * gui.scale) + ddt.text((xx, yy), "\"artist\">", code_colour, code_font) + ddt.text((xx2, yy), _("Sort by column name: >, <"), hint_colour, hint_font) + + yy += round(16 * gui.scale) + ddt.text((xx0, yy), _("Special"), title_colour, title_font) + yy += round(14 * gui.scale) + ddt.text((xx, yy), "auto", code_colour, code_font) + ddt.text((xx2, yy), _("Automatically reload on imports"), hint_colour, hint_font) + + yy += round(24 * gui.scale) + # xx += round(80 * gui.scale) + xx2 = xx + xx2 += ddt.text((xx2, yy), _("Status:"), [90, 90, 90, 255], 212) + round(6 * gui.scale) + if rename_text_area.text: + if gui.gen_code_errors: + if gui.gen_code_errors == "playlist": + ddt.text((xx2, yy), _("Playlist not found"), [255, 100, 100, 255], 212) + elif gui.gen_code_errors == "empty": + ddt.text((xx2, yy), _("Result is empty"), [250, 190, 100, 255], 212) + elif gui.gen_code_errors == "close": + ddt.text((xx2, yy), _("Close quotation..."), [110, 110, 110, 255], 212) + else: + ddt.text((xx2, yy), "...", [255, 100, 100, 255], 212) + else: + ddt.text((xx2, yy), _("OK"), [100, 255, 100, 255], 212) + else: + ddt.text((xx2, yy), _("Disabled"), [110, 110, 110, 255], 212) + + # ddt.pretty_rect = None + + # If enter or click outside of box: save and close + if inp.key_return_press or (key_esc_press and len(editline) == 0) \ + or ((inp.mouse_click or level_2_right_click) and not coll(rect)): + gui.rename_playlist_box = False + + if self.edit_generator: + pass + elif len(rename_text_area.text) > 0: + if gui.radio_view: + pctl.radio_playlists[self.playlist_index]["name"] = rename_text_area.text + else: + pctl.multi_playlist[self.playlist_index].title = rename_text_area.text + inp.key_return_press = False + +class PlaylistBox: + + def recalc(self): + self.tab_h = round(25 * gui.scale) + self.gap = round(2 * gui.scale) + + self.text_offset = 2 * gui.scale + if gui.scale == 1.25: + self.text_offset = 3 + + def __init__(self): + + self.scroll_on = prefs.old_playlist_box_position + self.drag = False + self.drag_source = 0 + self.drag_on = -1 + + self.adds = [] + + self.indicate_w = round(2 * gui.scale) + + self.lock_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "lock-corner.png", True) + self.pin_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "dia-pin.png", True) + self.gen_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "gen-gear.png", True) + self.spot_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "spot-playlist.png", True) + + + # if gui.scale == 1.25: + self.tab_h = 0 + self.gap = 0 + + self.text_offset = 2 * gui.scale + self.recalc() + + def draw(self, x, y, w, h): + + global quick_drag + + # ddt.rect_r((x, y, w, h), colours.side_panel_background, True) + ddt.rect((x, y, w, h), colours.playlist_box_background) + ddt.text_background_colour = colours.playlist_box_background + + max_tabs = (h - 10 * gui.scale) // (self.gap + self.tab_h) + + tab_title_colour = [230, 230, 230, 255] + + bg_lumi = test_lumi(colours.playlist_box_background) + light_mode = False + + if bg_lumi < 0.55: + light_mode = True + tab_title_colour = [20, 20, 20, 255] + + dark_mode = False + if bg_lumi > 0.8: + dark_mode = True + + if light_mode: + indicate_w = round(3 * gui.scale) + else: + indicate_w = round(2 * gui.scale) + + show_scroll = False + tab_start = x + 10 * gui.scale + + if window_size[0] < 700 * gui.scale: + tab_start = x + 4 * gui.scale + + if mouse_wheel != 0 and coll((x, y, w, h)): + self.scroll_on -= mouse_wheel + + self.scroll_on = min(self.scroll_on, len(pctl.multi_playlist) - max_tabs + 1) + + self.scroll_on = max(self.scroll_on, 0) + + if len(pctl.multi_playlist) > max_tabs: + show_scroll = True + else: + self.scroll_on = 0 + + if show_scroll: + tab_start += 15 * gui.scale + + if colours.lm: + w -= round(6 * gui.scale) + tab_width = w - tab_start # - 0 * gui.scale + + # Draw scroll bar + if show_scroll: + self.scroll_on = playlist_panel_scroll.draw(x + 2, y + 1, 15 * gui.scale, h, self.scroll_on, + len(pctl.multi_playlist) - max_tabs + 1) + + draw_pin_indicator = False # prefs.tabs_on_top + + # if not gui.album_tab_mode: + # if key_left_press or key_right_press: + # if pctl.active_playlist_viewing < self.scroll_on: + # self.scroll_on = pctl.active_playlist_viewing + # elif pctl.active_playlist_viewing + 1 > self.scroll_on + max_tabs: + # self.scroll_on = (pctl.active_playlist_viewing - max_tabs) + 1 + + # Process inputs + delete_pl = None + tab_on = 0 + yy = y + 5 * gui.scale + for i, pl in enumerate(pctl.multi_playlist): + + if tab_on >= max_tabs: + break + if i < self.scroll_on: + continue + + # if not pl.hidden and i in tabs_on_top: + # continue + + tab_on += 1 + + if coll((tab_start, yy - 1, tab_width, (self.tab_h + 1))): + if right_click: + if gui.radio_view: + radio_tab_menu.activate(i, mouse_position) + else: + tab_menu.activate(i, mouse_position) + gui.tab_menu_pl = i + + if tab_menu.active is False and middle_click: + delete_pl = i + # delete_playlist(i) + # break + + if mouse_up and self.drag and coll_point(mouse_up_position, (tab_start, yy - 1, tab_width, (self.tab_h + 1))): + + # If drag from top bar to side panel, make hidden + if self.drag_source == 0 and prefs.drag_to_unpin: + pctl.multi_playlist[self.drag_on].hidden = True + + # Move playlist tab + if i != self.drag_on and not point_proximity_test(gui.drag_source_position, mouse_position, 10 * gui.scale): + if key_shift_down: + pctl.multi_playlist[i].playlist_ids += pctl.multi_playlist[self.drag_on].playlist_ids + delete_playlist(self.drag_on, force=True) + else: + move_playlist(self.drag_on, i) + + gui.update += 1 + + # Double click to play + if mouse_up and pl_to_id(i) == top_panel.tab_d_click_ref == pl_to_id(pctl.active_playlist_viewing) and \ + top_panel.tab_d_click_timer.get() < 0.25 and \ + point_distance(last_click_location, mouse_up_position) < 5 * gui.scale: + + if pctl.playing_state == 2 and pctl.active_playlist_playing == i: + pctl.play() + elif pctl.selected_ready() and (pctl.playing_state != 1 or pctl.active_playlist_playing != i): + pctl.jump(default_playlist[pctl.selected_in_playlist], pl_position=pctl.selected_in_playlist) + if mouse_up: + top_panel.tab_d_click_timer.set() + top_panel.tab_d_click_ref = pl_to_id(i) + + if not draw_pin_indicator: + if inp.mouse_click: + switch_playlist(i) + self.drag_on = i + self.drag = True + self.drag_source = 1 + set_drag_source() + + # Process input of dragging tracks onto tab + if quick_drag is True and mouse_up: + top_panel.tab_d_click_ref = -1 + top_panel.tab_d_click_timer.force_set(100) + if (pctl.gen_codes.get(pl_to_id(i)) and "self" not in pctl.gen_codes[pl_to_id(i)]): + clear_gen_ask(pl_to_id(i)) + quick_drag = False + modified = False + gui.pl_update += 1 + + for item in shift_selection: + pctl.multi_playlist[i].playlist_ids.append(default_playlist[item]) + modified = True + if len(shift_selection) > 0: + self.adds.append( + [pctl.multi_playlist[i].uuid_int, len(shift_selection), Timer()]) # ID, num, timer + modified = True + if modified: + pctl.after_import_flag = True + tauon.thread_manager.ready("worker") + pctl.notify_change() + pctl.update_shuffle_pool(pctl.multi_playlist[i].uuid_int) + tree_view_box.clear_target_pl(i) + + # Toggle hidden flag on click + if draw_pin_indicator and inp.mouse_click and coll( + (tab_start + 5 * gui.scale, yy + 3 * gui.scale, 25 * gui.scale, 26 * gui.scale)): + pl.hidden ^= True + + yy += self.tab_h + self.gap + + # Draw tabs + # delete_pl = None + tab_on = 0 + yy = y + 5 * gui.scale + for i, pl in enumerate(pctl.multi_playlist): + + # if yy + self.tab_h > y + h: + # break + if tab_on >= max_tabs: + break + if i < self.scroll_on: + continue + + tab_on += 1 + + name = pl.title + hidden = pl.hidden + + # Background is insivible by default (for hightlighting if selected) + bg = [0, 0, 0, 0] + + # Highlight if playlist selected (viewing) + if i == pctl.active_playlist_viewing or (tab_menu.active and tab_menu.reference == i): + # bg = [255, 255, 255, 25] + + # Adjust highlight for different background brightnesses + bg = rgb_add_hls(colours.playlist_box_background, 0, 0.06, 0) + if light_mode: + bg = [0, 0, 0, 25] + + # Highlight target playlist when tragging tracks over + if coll( + (tab_start + 50 * gui.scale, yy - 1, tab_width - 50 * gui.scale, (self.tab_h + 1))) and quick_drag and not ( + pctl.gen_codes.get(pl_to_id(i)) and "self" not in pctl.gen_codes[pl_to_id(i)]): + # bg = [255, 255, 255, 15] + bg = rgb_add_hls(colours.playlist_box_background, 0, 0.04, 0) + if light_mode: + bg = [0, 0, 0, 16] + + # Get actual bg from blend for text bg + real_bg = alpha_blend(bg, colours.playlist_box_background) + + # Draw highlight + ddt.rect((tab_start, yy - round(1 * gui.scale), tab_width, self.tab_h), bg) + + # Draw title text + text_start = 10 * gui.scale + if draw_pin_indicator: + # text_start = 40 * gui.scale + text_start = 32 * gui.scale + + if pctl.gen_codes.get(pl_to_id(i), "")[:3] in ["sal", "slt", "spl"]: + text_start = 28 * gui.scale + self.spot_icon.render(tab_start + round(7 * gui.scale), yy + round(3 * gui.scale), alpha_mod(tab_title_colour, 170)) + + if not pl.hidden and prefs.tabs_on_top: + cl = [255, 255, 255, 25] + + if light_mode: + cl = [0, 0, 0, 40] + + xx = tab_start + tab_width - self.lock_icon.w + self.lock_icon.render(xx, yy, cl) + + text_max_w = tab_width - text_start - 15 * gui.scale + # if indicator_run_x: + # text_max_w = tab_width - (indicator_run_x + text_start + 17 * gui.scale + slide) + ddt.text( + (tab_start + text_start, yy + self.text_offset), name, tab_title_colour, 211, max_w=text_max_w, bg=real_bg) + + # Is mouse collided with tab? + hit = coll((tab_start + 50 * gui.scale, yy - 1, tab_width - 50 * gui.scale, (self.tab_h + 1))) + + # if not prefs.tabs_on_top: + if i == pctl.active_playlist_playing: + + indicator_colour = colours.title_playing + if colours.lm: + indicator_colour = colours.seek_bar_fill + + ddt.rect((tab_start + 0 - 2 * gui.scale, yy - round(1 * gui.scale), indicate_w, self.tab_h), indicator_colour) + + # # If mouse over + if hit: + # Draw indicator for dragging tracks + if quick_drag and pl_is_mut(i): + ddt.rect((tab_start + tab_width - self.indicate_w, yy, self.indicate_w, self.tab_h), [80, 200, 180, 255]) + + # Draw indicators for moving tab + if self.drag and i != self.drag_on and not point_proximity_test( + gui.drag_source_position, mouse_position, 10 * gui.scale): + if key_shift_down: + ddt.rect( + (tab_start + tab_width - 4 * gui.scale, yy, self.indicate_w, self.tab_h), + [80, 160, 200, 255]) + elif i < self.drag_on: + ddt.rect((tab_start, yy - self.indicate_w, tab_width, self.indicate_w), [80, 160, 200, 255]) + else: + ddt.rect((tab_start, yy + (self.tab_h - self.indicate_w), tab_width, self.indicate_w), [80, 160, 200, 255]) + + elif quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15 * gui.scale): + for item in shift_selection: + if len(default_playlist) > item and default_playlist[item] in pl.playlist_ids: + ddt.rect((tab_start + tab_width - self.indicate_w, yy, self.indicate_w, self.tab_h), [190, 170, 20, 255]) + break + # Drag red line highlight if playlist is generator playlist + if quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15 * gui.scale): + if not pl_is_mut(i): + ddt.rect((tab_start + tab_width - self.indicate_w, yy, self.indicate_w, self.tab_h), [200, 70, 50, 255]) + + # Draw effect of adding tracks to playlist + if len(self.adds) > 0: + for k in reversed(range(len(self.adds))): + if pctl.multi_playlist[i].uuid_int == self.adds[k][0]: + if self.adds[k][2].get() > 0.3: + del self.adds[k] + else: + ay = yy + 4 * gui.scale + ay -= 6 * gui.scale * self.adds[k][2].get() / 0.3 + + ddt.text( + (tab_start + tab_width - 10 * gui.scale, int(round(ay)), 1), + "+" + str(self.adds[k][1]), colours.pluse_colour, 212, bg=real_bg) + gui.update += 1 + + ddt.rect( + (tab_start + tab_width, yy, self.indicate_w, self.tab_h - self.indicate_w), + [244, 212, 66, int(255 * self.adds[k][2].get() / 0.3) * -1]) + + yy += self.tab_h + self.gap + + if delete_pl is not None: + # delete_playlist(delete_pl) + delete_playlist_ask(delete_pl) + gui.update += 1 + + # Create new playlist if drag in blank space after tabs + rect = (x, yy, w - 10 * gui.scale, h - (yy - y)) + fields.add(rect) + + if coll(rect): + if quick_drag: + ddt.rect((tab_start, yy, tab_width, self.indicate_w), [80, 160, 200, 255]) + if mouse_up: + drop_tracks_to_new_playlist(shift_selection) + + if right_click: + extra_tab_menu.activate(pctl.active_playlist_viewing) + + # Move tab to end playlist if dragged past end + if self.drag: + if mouse_up: + if key_ctrl_down: + # Duplicate playlist on ctrl + gen_dupe(playlist_box.drag_on) + gui.update += 2 + self.drag = False + else: + # If drag from top bar to side panel, make hidden + if self.drag_source == 0 and prefs.drag_to_unpin: + pctl.multi_playlist[self.drag_on].hidden = True + + move_playlist(self.drag_on, i) + gui.update += 2 + self.drag = False + elif key_ctrl_down: + ddt.rect((tab_start, yy, tab_width, self.indicate_w), [255, 190, 0, 255]) + else: + ddt.rect((tab_start, yy, tab_width, self.indicate_w), [80, 160, 200, 255]) + +class ArtistList: + + def __init__(self): + + self.tab_h = round(60 * gui.scale) + self.thumb_size = round(55 * gui.scale) + + self.current_artists = [] + self.current_album_counts = {} + self.current_artist_track_counts = {} + + self.thumb_cache = {} + + self.to_fetch = "" + self.to_fetch_mbid_a = "" + + self.scroll_position = 0 + + self.id_to_load = "" + + self.d_click_timer = Timer() + self.d_click_ref = -1 + + self.click_ref = -1 + self.click_highlight_timer = Timer() + + self.saves = {} + + self.load = False + + self.shown_letters = [] + + self.hover_on = "NONE" + self.hover_timer = Timer(10) + + self.sample_tracks = {} + + def load_img(self, artist): + + filepath = artist_info_box.get_data(artist, get_img_path=True) + + if filepath and os.path.isfile(filepath): + + try: + g = io.BytesIO() + g.seek(0) + + im = Image.open(filepath) + + w, h = im.size + if w != h: + m = min(w, h) + im = im.crop(( + round((w - m) / 2), + round((h - m) / 2), + round((w + m) / 2), + round((h + m) / 2), + )) + + im.thumbnail((self.thumb_size, self.thumb_size), Image.Resampling.LANCZOS) + + im.save(g, "PNG") + g.seek(0) + + wop = rw_from_object(g) + s_image = IMG_Load_RW(wop, 0) + texture = SDL_CreateTextureFromSurface(renderer, s_image) + SDL_FreeSurface(s_image) + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + SDL_QueryTexture(texture, None, None, tex_w, tex_h) + sdl_rect = SDL_Rect(0, 0) + sdl_rect.w = int(tex_w.contents.value) + sdl_rect.h = int(tex_h.contents.value) + + self.thumb_cache[artist] = [texture, sdl_rect] + except Exception: + logging.exception("Artist thumbnail processing error") + self.thumb_cache[artist] = None + + elif artist in prefs.failed_artists: + self.thumb_cache[artist] = None + elif not self.to_fetch: + + if prefs.auto_dl_artist_data: + self.to_fetch = artist + tauon.thread_manager.ready("worker") + + else: + self.thumb_cache[artist] = None + + def worker(self): + + if self.load: + + if after_scan: + return + + self.prep() + self.load = False + return + + if self.to_fetch: + + if get_lfm_wait_timer.get() < 2: + return + + artist = self.to_fetch + f_artist = filename_safe(artist) + filename = f_artist + "-lfm.png" + filename2 = f_artist + "-lfm.txt" + filename3 = f_artist + "-ftv.jpg" + filename4 = f_artist + "-dcg.jpg" + filepath = os.path.join(a_cache_dir, filename) + filepath2 = os.path.join(a_cache_dir, filename2) + filepath3 = os.path.join(a_cache_dir, filename3) + filepath4 = os.path.join(a_cache_dir, filename4) + got_image = False + try: + # Lookup artist info on last.fm + logging.info("lastfm lookup artist: " + artist) + mbid = lastfm.artist_mbid(artist) + get_lfm_wait_timer.set() + # if data[0] is not False: + # #cover_link = data[2] + # text = data[1] + # + # if not os.path.exists(filepath2): + # f = open(filepath2, 'w', encoding='utf-8') + # f.write(text) + # f.close() + + if mbid and prefs.enable_fanart_artist: + save_fanart_artist_thumb(mbid, filepath3, preview=True) + got_image = True + + except Exception: + logging.exception("Failed to find image from fanart.tv") + + if not got_image and verify_discogs(): + try: + save_discogs_artist_thumb(artist, filepath4) + except Exception: + logging.exception("Failed to find image from discogs") + + if os.path.exists(filepath3) or os.path.exists(filepath4): + gui.update += 1 + elif artist not in prefs.failed_artists: + logging.error("Failed fetching: " + artist) + prefs.failed_artists.append(artist) + + self.to_fetch = "" + + def prep(self): + self.scroll_position = 0 + + curren_pl_no = id_to_pl(self.id_to_load) + if curren_pl_no is None: + return + current_pl = pctl.multi_playlist[curren_pl_no] + + all = [] + artist_parents = {} + counts = {} + play_time = {} + filtered = 0 + b = 0 + + try: + + for item in current_pl.playlist_ids: + b += 1 + if b % 100 == 0: + time.sleep(0.001) + + track = pctl.get_track(item) + + if "artists" in track.misc: + artists = track.misc["artists"] + else: + if prefs.artist_list_prefer_album_artist and track.album_artist: + artists = track.album_artist + else: + artists = get_artist_strip_feat(track) + + artists = [x.strip() for x in artists.split(";")] + + pp = 0 + if prefs.artist_list_sort_mode == "play": + pp = star_store.get(item) + + for artist in artists: + + if artist: + + # Add play time + if prefs.artist_list_sort_mode == "play": + p = play_time.get(artist, 0) + play_time[artist] = p + pp + + # Get a sample track for fallback art + if artist not in self.sample_tracks: + self.sample_tracks[artist] = track + + # Confirm to final list if appeared at least 5 times + # if artist not in all: + if artist not in counts: + counts[artist] = 0 + counts[artist] += 1 + if artist not in all: + if counts[artist] > prefs.artist_list_threshold or len(current_pl.playlist_ids) < 1000: + all.append(artist) + else: + filtered += 1 + + if artist not in artist_parents: + artist_parents[artist] = [] + if track.parent_folder_path not in artist_parents[artist]: + artist_parents[artist].append(track.parent_folder_path) + + current_album_counts = artist_parents + + if prefs.artist_list_sort_mode == "popular": + all.sort(key=counts.get, reverse=True) + elif prefs.artist_list_sort_mode == "play": + all.sort(key=play_time.get, reverse=True) + else: + all.sort(key=lambda y: y.lower().removeprefix("the ")) + + except Exception: + logging.exception("Album scan failure") + time.sleep(4) + return + + # Artist-list, album-counts, scroll-position, playlist-length, number ignored + save = [all, current_album_counts, 0, len(current_pl.playlist_ids), counts, filtered] + + # Scroll to playing artist + scroll = 0 + if pctl.playing_ready(): + track = pctl.playing_object() + for i, item in enumerate(save[0]): + if item == track.artist or item == track.album_artist: + scroll = i + break + save[2] = scroll + + viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + if viewing_pl_id in self.saves: + self.saves[viewing_pl_id][2] = self.scroll_position # TODO(Martin): Is saves a list[TauonPlaylist] here? If so, [2] should be .playlist_ids + + self.saves[current_pl.uuid_int] = save + gui.update += 1 + + def locate_artist_letter(self, text): + + if not text or prefs.artist_list_sort_mode != "alpha": + return + + letter = text[0].lower() + letter_upper = letter.upper() + for i, item in enumerate(self.current_artists): + if item.startswith(("the ", "The ")): + if len(item) > 4 and (item[4] == letter or item[4] == letter_upper): + self.scroll_position = i + break + elif item and (item[0] == letter or item[0] == letter_upper): + self.scroll_position = i + break + + viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + if pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id: + viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id + if viewing_pl_id in self.saves: + self.saves[viewing_pl_id][2] = self.scroll_position + + def locate_artist(self, track: TrackClass): + + for i, item in enumerate(self.current_artists): + if item == track.artist or item == track.album_artist or ( + "artists" in track.misc and item in track.misc["artists"]): + self.scroll_position = i + break + + viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + if viewing_pl_id in self.saves: + self.saves[viewing_pl_id][2] = self.scroll_position + + def draw_card_text_only(self, artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg): + + album_mode = False + for albums in self.current_album_counts.values(): + if len(albums) > 1: + album_mode = True + break + + if not album_mode: + count = self.current_artist_track_counts[artist] + if count > 1: + text = _("{N} tracks").format(N=str(count)) + else: + text = _("{N} track").format(N=str(count)) + else: + album_count = len(self.current_album_counts[artist]) + if album_count > 1: + text = _("{N} tracks").format(N=str(album_count)) + else: + text = _("{N} track").format(N=str(album_count)) + + if gui.preview_artist_loading == artist: + # . Max 20 chars. Alt: Downloading image, Loading image + text = _("Downloading data...") + + x_text = round(10 * gui.scale) + artist_font = 313 + count_font = 312 + extra_text_space = 0 + ddt.text( + (x_text, y + round(2 * gui.scale)), artist, line1_colour, artist_font, + extra_text_space + w - x_text - 30 * gui.scale, bg=bg) + # ddt.text((x_text, y + self.tab_h // 2 - 2 * gui.scale), text, line2_colour, count_font, + # extra_text_space + w - x_text - 15 * gui.scale, bg=bg) + + def draw_card_with_thumbnail(self, artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg): + + if artist not in self.thumb_cache: + self.load_img(artist) + + thumb_x = round(x + 10 * gui.scale) + x_text = x + self.thumb_size + 19 * gui.scale + artist_font = 513 + count_font = 312 + extra_text_space = 0 + if thin_mode: + thumb_x = round(x + 10 * gui.scale) + x_text = x + self.thumb_size + 17 * gui.scale + artist_font = 211 + count_font = 311 + extra_text_space = 135 * gui.scale + thin_mode = True + area = (4 * gui.scale, y, w - 7 * gui.scale, self.tab_h - 2) + fields.add(area) + + back_colour = [30, 30, 30, 255] + back_colour_2 = [27, 27, 27, 255] + border_colour = [60, 60, 60, 255] + # if colours.lm: + # back_colour = [200, 200, 200, 255] + # back_colour_2 = [240, 240, 240, 255] + # border_colour = [160, 160, 160, 255] + rect = (thumb_x, round(y), self.thumb_size, self.thumb_size) + + if thin_mode and coll(area) and is_level_zero() and y + self.tab_h < window_size[1] - gui.panelBY: + tab_rect = (x, y - round(2 * gui.scale), round(190 * gui.scale), self.tab_h - round(1 * gui.scale)) + + for r in subtract_rect(tab_rect, rect): + r = SDL_Rect(r[0], r[1], r[2], r[3]) + style_overlay.hole_punches.append(r) + + ddt.rect(tab_rect, back_colour_2) + bg = back_colour_2 + + ddt.rect(rect, back_colour) + ddt.rect(rect, border_colour) + + fields.add(rect) + if coll(rect) and is_level_zero(True): + self.hover_any = True + + hover_delay = 0.5 + if gui.compact_artist_list: + hover_delay = 2 + + if gui.preview_artist != artist: + if self.hover_on != artist: + self.hover_on = artist + gui.preview_artist = "" + self.hover_timer.set() + gui.delay_frame(hover_delay) + elif self.hover_timer.get() > hover_delay and not gui.preview_artist_loading: + gui.preview_artist = "" + path = artist_info_box.get_data(artist, get_img_path=True) + if not path: + gui.preview_artist_loading = artist + shoot = threading.Thread( + target=get_artist_preview, + args=((artist, round(thumb_x + self.thumb_size), round(y)))) + shoot.daemon = True + shoot.start() + + if path: + set_artist_preview(path, artist, round(thumb_x + self.thumb_size), round(y)) + + if inp.mouse_click: + self.hover_timer.force_set(-2) + gui.delay_frame(2 + hover_delay) + + drawn = False + if artist in self.thumb_cache: + thumb = self.thumb_cache[artist] + if thumb is not None: + thumb[1].x = thumb_x + thumb[1].y = round(y) + SDL_RenderCopy(renderer, thumb[0], None, thumb[1]) + drawn = True + if prefs.art_bg: + rect = SDL_Rect(thumb_x, round(y), self.thumb_size, self.thumb_size) + if (rect.y + rect.h) > window_size[1] - gui.panelBY: + diff = (rect.y + rect.h) - (window_size[1] - gui.panelBY) + rect.h -= round(diff) + style_overlay.hole_punches.append(rect) + if not drawn: + track = self.sample_tracks.get(artist) + if track: + tauon.gall_ren.render(track, (round(thumb_x), round(y)), self.thumb_size) + + if thin_mode: + text = artist[:2].title() + if text not in self.shown_letters: + ww = ddt.get_text_w(text, 211) + ddt.rect( + (thumb_x + round(1 * gui.scale), y + self.tab_h - 20 * gui.scale, ww + 5 * gui.scale, 13 * gui.scale), + [20, 20, 20, 255]) + ddt.text( + (thumb_x + 3 * gui.scale, y + self.tab_h - 23 * gui.scale), text, [240, 240, 240, 255], 210, + bg=[20, 20, 20, 255]) + self.shown_letters.append(text) + + # Draw labels + if not thin_mode or (coll(area) and is_level_zero() and y + self.tab_h < window_size[1] - gui.panelBY): + + album_mode = False + for albums in self.current_album_counts.values(): + if len(albums) > 1: + album_mode = True + break + + if not album_mode: + count = self.current_artist_track_counts[artist] + if count > 1: + text = _("{N} tracks").format(N=str(count)) + else: + text = _("{N} track").format(N=str(count)) + else: + album_count = len(self.current_album_counts[artist]) + if album_count > 1: + text = _("{N} tracks").format(N=str(album_count)) + else: + text = _("{N} track").format(N=str(album_count)) + + if gui.preview_artist_loading == artist: + # . Max 20 chars. Alt: Downloading image, Loading image + text = _("Downloading data...") + + ddt.text( + (x_text, y + self.tab_h // 2 - 19 * gui.scale), artist, line1_colour, artist_font, + extra_text_space + w - x_text - 30 * gui.scale, bg=bg) + ddt.text( + (x_text, y + self.tab_h // 2 - 2 * gui.scale), text, line2_colour, count_font, + extra_text_space + w - x_text - 15 * gui.scale, bg=bg) + + def draw_card(self, artist, x, y, w): + + area = (4 * gui.scale, y, w - 26 * gui.scale, self.tab_h - 2) + if prefs.artist_list_style == 2: + area = (4 * gui.scale, y, w - 26 * gui.scale, self.tab_h - 1) + + fields.add(area) + + light_mode = False + line1_colour = [235, 235, 235, 255] + line2_colour = [255, 255, 255, 120] + fade_max = 50 + + thin_mode = False + if gui.compact_artist_list: + thin_mode = True + line2_colour = [115, 115, 115, 255] + + elif test_lumi(colours.side_panel_background) < 0.55 and not thin_mode: + light_mode = True + fade_max = 20 + line1_colour = [35, 35, 35, 255] + line2_colour = [100, 100, 100, 255] + + # Fade on click + bg = colours.side_panel_background + if not thin_mode: + + if coll(area) and is_level_zero( + True): # or pctl.get_track(default_playlist[pctl.playlist_view_position]).artist == artist: + ddt.rect(area, [50, 50, 50, 50]) + bg = alpha_blend([50, 50, 50, 50], colours.side_panel_background) + else: + + fade = 0 + t = self.click_highlight_timer.get() + if self.click_ref == artist and (t < 2.2 or artist_list_menu.active): + + if t < 1.9 or artist_list_menu.active: + fade = fade_max + else: + fade = fade_max - round((t - 1.9) / 0.3 * fade_max) + + gui.update += 1 + ddt.rect(area, [50, 50, 50, fade]) + + bg = alpha_blend([50, 50, 50, fade], colours.side_panel_background) + + if prefs.artist_list_style == 1: + self.draw_card_with_thumbnail(artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg) + else: + self.draw_card_text_only(artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg) + + if coll(area) and mouse_position[1] < window_size[1] - gui.panelBY: + if inp.mouse_click: + if self.click_ref != artist: + pctl.playlist_view_position = 0 + pctl.selected_in_playlist = 0 + self.click_ref = artist + + double_click = False + if self.d_click_timer.get() < 0.4 and self.d_click_ref == artist: + double_click = True + + self.click_highlight_timer.set() + + if pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id and \ + pctl.multi_playlist[pctl.active_playlist_viewing].title.startswith("Artist:"): + create_artist_pl(artist, replace=True) + + + blocks = [] + current_block = [] + + in_artist = False + this_artist = artist.casefold() + last_ref = None + on = 0 + + for i in range(len(default_playlist)): + track = pctl.get_track(default_playlist[i]) + if track.artist.casefold() == this_artist or track.album_artist.casefold() == this_artist or ( + "artists" in track.misc and artist in track.misc["artists"]): + # Matchin artist + if not in_artist: + in_artist = True + last_ref = track + current_block.append(i) + + elif (last_ref and track.album != last_ref.album) or track.parent_folder_path != last_ref.parent_folder_path: + current_block.append(i) + last_ref = track + # Not matching + elif in_artist: + blocks.append(current_block) + current_block = [] + in_artist = False + + if current_block: + blocks.append(current_block) + current_block = [] + + #logging.info(blocks) + # return + + # block_starts = [] + # current = False + # for i in range(len(default_playlist)): + # track = pctl.get_track(default_playlist[i]) + # if current is False: + # if track.artist == artist or track.album_artist == artist or ( + # 'artists' in track.misc and artist in track.misc['artists']): + # block_starts.append(i) + # current = True + # else: + # if track.artist != artist and track.album_artist != artist or ( + # 'artists' in track.misc and artist in track.misc['artists']): + # current = False + # + # if not block_starts: + # logging.info("No matching artists found in playlist") + # return + + if not blocks: + return + + #select = block_starts[0] + + # if len(block_starts) > 1: + # if -1 < pctl.selected_in_playlist < len(default_playlist): + # if pctl.selected_in_playlist in block_starts: + # scroll_hide_timer.set() + # gui.frame_callback_list.append(TestTimer(0.9)) + # if block_starts[-1] == pctl.selected_in_playlist: + # pass + # else: + # select = block_starts[block_starts.index(pctl.selected_in_playlist) + 1] + + gui.pl_update += 1 + + self.click_highlight_timer.set() + + select = blocks[0][0] + + if double_click: + # Stat first artist track in playlist + + pctl.jump(default_playlist[select], pl_position=select) + pctl.playlist_view_position = select + pctl.selected_in_playlist = select + shift_selection.clear() + self.d_click_timer.force_set(10) + else: + # Goto next artist section in playlist + c = pctl.selected_in_playlist + next = False + track = pctl.get_track_in_playlist(c, -1) + if track is None: + logging.error("Index out of range!") + pctl.selected_in_playlist = 0 + return + if track.artist.casefold != artist.casefold: + pctl.selected_in_playlist = 0 + pctl.playlist_view_position = 0 + if len(blocks) == 1: + block = blocks[0] + if len(block) > 1: + if c < block[0] or c >= block[-1]: + select = block[0] + toast(_("First of artist's albums ({N} albums)") + .format(N=len(block))) + else: + select = block[-1] + toast(_("Last of artist's albums ({N} albums)") + .format(N=len(block))) + else: + select = None + for bb, block in enumerate(blocks): + for i, al in enumerate(block): + if al <= c: + continue + next = True + if i == 0: + select = al + if len(block) > 1: + toast(_("Start of location {N} of {T} ({Nb} albums)") + .format(N=bb + 1, T=len(blocks), Nb=len(block))) + else: + toast(_("Location {N} of {T}") + .format(N=bb + 1, T=len(blocks))) + break + + if next and not select: + select = block[-1] + if len(block) > 1: + toast(_("End of location {N} of {T} ({Nb} albums)") + .format(N=bb + 1, T=len(blocks), Nb=len(block))) + else: + toast(_("Location {N} of {T}") + .format(N=bb, T=len(blocks))) + break + if select: + break + if not select: + select = blocks[0][0] + if len(blocks[0]) > 1: + if len(blocks) > 1: + toast(_("Start of location 1 of {N} ({Nb} albums)") + .format(N=len(blocks), Nb=len(blocks[0]))) + else: + toast(_("Location 1 of {N} ({Nb} albums)") + .format(N=len(blocks), Nb=len(blocks[0]))) + else: + toast(_("Location 1 of {N}") + .format(N=len(blocks))) + + pctl.playlist_view_position = select + pctl.selected_in_playlist = select + self.d_click_ref = artist + self.d_click_timer.set() + if album_mode: + goto_album(select) + + if middle_click: + self.click_ref = artist + self.click_highlight_timer.set() + create_artist_pl(artist) + + if right_click: + self.click_ref = artist + self.click_highlight_timer.set() + + artist_list_menu.activate(in_reference=artist) + + def render(self, x, y, w, h): + + if prefs.artist_list_style == 1: + self.tab_h = round(60 * gui.scale) + else: + self.tab_h = round(22 * gui.scale) + + viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + + # use parent playlst is set + if pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id: + + # test if parent still exists + new = id_to_pl(pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id) + if new is None or not pctl.multi_playlist[pctl.active_playlist_viewing].title.startswith("Artist:"): + pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id = "" + else: + viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id + + if viewing_pl_id in self.saves: + self.current_artists = self.saves[viewing_pl_id][0] + self.current_album_counts = self.saves[viewing_pl_id][1] + self.current_artist_track_counts = self.saves[viewing_pl_id][4] + self.scroll_position = self.saves[viewing_pl_id][2] + + if self.saves[viewing_pl_id][3] != len(pctl.multi_playlist[id_to_pl(viewing_pl_id)].playlist_ids): + del self.saves[viewing_pl_id] + return + + else: + + # if self.current_pl != viewing_pl_id: + self.id_to_load = viewing_pl_id + if not self.load: + # self.prep() + self.current_artists = [] + self.current_album_counts = [] + self.current_artist_track_counts = {} + self.load = True + tauon.thread_manager.ready("worker") + + area = (x, y, w, h) + area2 = (x + 1, y, w - 3, h) + + ddt.rect(area, colours.side_panel_background) + ddt.text_background_colour = colours.side_panel_background + + if coll(area) and mouse_wheel: + mx = 1 + if prefs.artist_list_style == 2: + mx = 3 + self.scroll_position -= mouse_wheel * mx + self.scroll_position = max(self.scroll_position, 0) + + range = (h // self.tab_h) - 1 + + whole_rage = math.floor(h // self.tab_h) + + if range > 4 and self.scroll_position > len(self.current_artists) - range: + self.scroll_position = len(self.current_artists) - range + + if len(self.current_artists) <= whole_rage: + self.scroll_position = 0 + + fields.add(area2) + scroll_x = x + w - 18 * gui.scale + if colours.lm: + scroll_x = x + w - 22 * gui.scale + if (coll(area2) or artist_list_scroll.held) and not pref_box.enabled: + scroll_width = 15 * gui.scale + inset = 0 + if gui.compact_artist_list: + pass + # scroll_width = round(6 * gui.scale) + # scroll_x += round(9 * gui.scale) + else: + self.scroll_position = artist_list_scroll.draw( + scroll_x, y + 1, scroll_width, h, self.scroll_position, + len(self.current_artists) - range, r_click=right_click, + jump_distance=35, extend_field=6 * gui.scale) + + if not self.current_artists: + text = _("No artists in playlist") + + if default_playlist: + text = _("Artist threshold not met") + if self.load: + text = _("Loading Artist List...") + if loading_in_progress or transcode_list or after_scan: + text = _("Busy...") + + ddt.text( + (x + w // 2, y + (h // 7), 2), text, alpha_mod(colours.side_bar_line2, 100), 212, + max_w=w - 17 * gui.scale) + + yy = y + 12 * gui.scale + + i = int(self.scroll_position) + + if viewing_pl_id in self.saves: + self.saves[viewing_pl_id][2] = self.scroll_position + + prefetch_mode = False + prefetch_distance = 22 + + self.shown_letters.clear() + + self.hover_any = False + + for i, artist in enumerate(self.current_artists[i:], start=i): + + if not prefetch_mode: + self.draw_card(artist, x, round(yy), w) + + yy += self.tab_h + + if yy - y > h - 24 * gui.scale: + prefetch_mode = True + continue + + if prefetch_mode: + if prefs.artist_list_style == 2: + break + prefetch_distance -= 1 + if prefetch_distance < 1: + break + if artist not in self.thumb_cache: + self.load_img(artist) + break + + if not self.hover_any: + gui.preview_artist = "" + self.hover_timer.force_set(10) + artist_preview_render.show = False + self.hover_on = False + +class TreeView: + + def __init__(self): + + self.trees = {} # Per playlist tree + self.rows = [] # For display (parsed from tree) + self.rows_id = "" + + self.opens = {} # Folders clicks to show per playlist + + self.scroll_positions = {} + + # Recursive gen_rows vars + self.count = 0 + self.depth = 0 + + self.background_processing = False + self.d_click_timer = Timer(100) + self.d_click_id = "" + + self.menu_selected = "" + self.folder_colour_cache = {} + self.dragging_name = "" + + self.force_opens = [] + self.click_drag_source = None + + self.tooltip_on = "" + self.tooltip_timer = Timer(10) + + self.lock_pl = None + + # self.bold_colours = ColourGenCache(0.6, 0.7) + + def clear_all(self): + self.rows_id = "" + self.trees.clear() + + def collapse_all(self): + pl_id = pl_to_id(pctl.active_playlist_viewing) + + if self.lock_pl: + pl_id = self.lock_pl + + opens = self.opens.get(pl_id) + if opens is None: + opens = [] + self.opens[pl_id] = opens + + opens.clear() + self.rows_id = "" + + def clear_target_pl(self, pl_number, pl_id=None): + + if pl_id is None: + pl_id = pl_to_id(pl_number) + + if gui.lsp and prefs.left_panel_mode == "folder view": + + if pl_id in self.trees: + if not self.background_processing: + self.background_processing = True + shoot_dl = threading.Thread(target=self.gen_tree, args=[pl_id]) + shoot_dl.daemon = True + shoot_dl.start() + elif pl_id in self.trees: + del self.trees[pl_id] + + def show_track(self, track: TrackClass) -> None: + + if track is None: + return + + # Get tree and opened folder data for this playlist + pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + opens = self.opens.get(pl_id) + if opens is None: + opens = [] + self.opens[pl_id] = opens + + tree = self.trees.get(pl_id) + if not tree: + return + + scroll_position = self.scroll_positions.get(pl_id) + if scroll_position is None: + scroll_position = 0 + + # Clear all opened folders + opens.clear() + + # Set every folder in path as opened + path = "" + crumbs = track.parent_folder_path.split("/")[1:] + for c in crumbs: + path += "/" + c + opens.append(path) + + # Regenerate row display + self.gen_rows(tree, opens) + + # Locate and set scroll position to playing folder + for i, row in enumerate(self.rows): + if row[1] + "/" + row[0] == track.parent_folder_path: + + scroll_position = i - 5 + scroll_position = max(scroll_position, 0) + break + + max_scroll = len(self.rows) - ((window_size[0] - (gui.panelY + gui.panelBY)) // round(22 * gui.scale)) + scroll_position = min(scroll_position, max_scroll) + scroll_position = max(scroll_position, 0) + + self.scroll_positions[pl_id] = scroll_position + + gui.update_layout() + gui.update += 1 + + def get_pl_id(self): + if self.lock_pl: + return self.lock_pl + return pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + + def render(self, x, y, w, h): + + global quick_drag + + pl_id = self.get_pl_id() + + tree = self.trees.get(pl_id) + + # Generate tree data if not done yet + if tree is None: + if not self.background_processing: + self.background_processing = True + shoot_dl = threading.Thread(target=self.gen_tree, args=[pl_id]) + shoot_dl.daemon = True + shoot_dl.start() + + self.playlist_id_on = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + + opens = self.opens.get(pl_id) + if opens is None: + opens = [] + self.opens[pl_id] = opens + + scroll_position = self.scroll_positions.get(pl_id) + if scroll_position is None: + scroll_position = 0 + + area = (x, y, w, h) + fields.add(area) + ddt.rect(area, colours.side_panel_background) + ddt.text_background_colour = colours.side_panel_background + + if self.background_processing and self.rows_id != pl_id: + ddt.text( + (x + w // 2, y + (h // 7), 2), _("Loading Folder Tree..."), alpha_mod(colours.side_bar_line2, 100), + 212, max_w=w - 17 * gui.scale) + return + + # if not tree or not self.rows: + # ddt.text((x + w // 2, y + (h // 7), 2), _("Folder Tree"), alpha_mod(colours.side_bar_line2, 100), + # 212, max_w=w - 17 * gui.scale) + # return + if not tree: + ddt.text( + (x + w // 2, y + (h // 7), 2), _("Folder Tree"), alpha_mod(colours.side_bar_line2, 100), + 212, max_w=w - 17 * gui.scale) + return + + if self.rows_id != pl_id: + if not self.background_processing: + self.gen_rows(tree, opens) + self.rows_id = pl_id + max_scroll = len(self.rows) - (h // round(22 * gui.scale)) + scroll_position = min(scroll_position, max_scroll) + + else: + return + + if not self.rows: + ddt.text( + (x + w // 2, y + (h // 7), 2), _("Folder Tree"), alpha_mod(colours.side_bar_line2, 100), + 212, max_w=w - 17 * gui.scale) + return + + yy = y + round(11 * gui.scale) + xx = x + round(22 * gui.scale) + + spacing = round(21 * gui.scale) + max_scroll = len(self.rows) - (h // round(22 * gui.scale)) + + mouse_in = coll(area) + + # Mouse wheel scrolling + if mouse_in and mouse_wheel: + scroll_position += mouse_wheel * -2 + scroll_position = max(scroll_position, 0) + scroll_position = min(scroll_position, max_scroll) + + focused = is_level_zero() + + # Draw scroll bar + if mouse_in or tree_view_scroll.held: + scroll_position = tree_view_scroll.draw( + x + w - round(12 * gui.scale), y + 1, round(11 * gui.scale), h, + scroll_position, + max_scroll, r_click=right_click, jump_distance=40) + + self.scroll_positions[pl_id] = scroll_position + + # Draw folder rows + playing_track = pctl.playing_object() + max_w = w - round(45 * gui.scale) + + light_mode = test_lumi(colours.side_panel_background) < 0.5 + semilight_mode = test_lumi(colours.side_panel_background) < 0.8 + + for i, item in enumerate(self.rows): + + if i < scroll_position: + continue + + if yy > y + h - spacing: + break + + target = item[1] + "/" + item[0] + + inset = item[2] * round(10 * gui.scale) + rect = (xx + inset - round(15 * gui.scale), yy, max_w - inset + round(15 * gui.scale), spacing - 1) + fields.add(rect) + + # text_colour = [255, 255, 255, 100] + text_colour = rgb_add_hls(colours.side_panel_background, 0, 0.35, -0.15) + + box_colour = [200, 100, 50, 255] + + if semilight_mode: + text_colour = [255, 255, 255, 180] + + if light_mode: + text_colour = [0, 0, 0, 200] + + full_folder_path = item[1] + "/" + item[0] + + # Hold highlight while menu open + if (folder_tree_menu.active or folder_tree_stem_menu.active) and full_folder_path == self.menu_selected: + text_colour = [255, 255, 255, 170] + if semilight_mode: + text_colour = (255, 255, 255, 255) + if light_mode: + text_colour = [0, 0, 0, 255] + + # Hold highlight while dragging folder + if quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15): + if shift_selection: + if pctl.get_track(pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids[shift_selection[0]]).fullpath.startswith( + full_folder_path + "/") and self.dragging_name and item[0].endswith(self.dragging_name): + text_colour = (255, 255, 255, 230) + if semilight_mode: + text_colour = (255, 255, 255, 255) + if light_mode: + text_colour = [0, 0, 0, 255] + + # Set highlight colours if folder is playing + if 0 < pctl.playing_state < 3 and playing_track: + if playing_track.parent_folder_path == full_folder_path or full_folder_path + "/" in playing_track.fullpath: + text_colour = [255, 255, 255, 225] + box_colour = [140, 220, 20, 255] + if semilight_mode: + text_colour = (255, 255, 255, 255) + if light_mode: + text_colour = [0, 0, 0, 255] + + if right_click: + mouse_in = coll(rect) and is_level_zero(False) + else: + mouse_in = coll(rect) and focused and not ( + quick_drag and not point_proximity_test(gui.drag_source_position, mouse_position, 15)) + + if mouse_in and not tree_view_scroll.held: + + if middle_click: + stem_to_new_playlist(full_folder_path) + + elif right_click: + + if item[3]: + + for p, id in enumerate(pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids): + if msys: + if pctl.get_track(id).fullpath.startswith(target.lstrip("/")): + folder_tree_menu.activate(in_reference=id) + self.menu_selected = full_folder_path + break + elif pctl.get_track(id).fullpath.startswith(target): + folder_tree_menu.activate(in_reference=id) + self.menu_selected = full_folder_path + break + elif msys: + folder_tree_stem_menu.activate(in_reference=full_folder_path.lstrip("/")) + self.menu_selected = full_folder_path.lstrip("/") + else: + folder_tree_stem_menu.activate(in_reference=full_folder_path) + self.menu_selected = full_folder_path + + elif inp.mouse_click: + # quick_drag = True + + if not self.click_drag_source: + self.click_drag_source = item + set_drag_source() + + elif mouse_up and self.click_drag_source == item: + # Click tree level folder to open/close branch + + if target not in opens: + opens.append(target) + else: + for s in reversed(range(len(opens))): + if opens[s].startswith(target): + del opens[s] + + if item[3]: + + # Locate the first track of folder in playlist + track_id = None + for p, id in enumerate(default_playlist): + if msys: + if pctl.get_track(id).fullpath.startswith(target.lstrip("/")): + track_id = id + break + elif pctl.get_track(id).fullpath.startswith(target): + track_id = id + break + else: # Fallback to folder name if full-path not found (hack for networked items) + for p, id in enumerate(default_playlist): + if pctl.get_track(id).parent_folder_name == item[0]: + track_id = id + break + + if track_id is not None: + # Single click base folder to locate in playlist + if self.d_click_timer.get() > 0.5 or self.d_click_id != target: + pctl.show_current(select=True, index=track_id, no_switch=True, highlight=True, folder_list=False) + self.d_click_timer.set() + self.d_click_id = target + + # Double click base folder to play + else: + pctl.jump(track_id) + + # Regenerate display rows after clicking + self.gen_rows(tree, opens) + + # Highlight folder text on mouse over + if (mouse_in and not mouse_down) or item == self.click_drag_source: + text_colour = (255, 255, 255, 235) + if semilight_mode: + text_colour = (255, 255, 255, 255) + if light_mode: + text_colour = [0, 0, 0, 255] + + # Render folder name text + if item[4] > 50: + font = 514 + text_label_colour = text_colour # self.bold_colours.get(full_folder_path) + else: + font = 414 + text_label_colour = text_colour + + if mouse_in: + tw = ddt.get_text_w(item[0], font) + + if self.tooltip_on != item: + self.tooltip_on = item + self.tooltip_timer.set() + gui.frame_callback_list.append(TestTimer(0.6)) + + if tw > max_w - inset and self.tooltip_on == item and self.tooltip_timer.get() >= 0.6: + rect = (xx + inset, yy - 2 * gui.scale, tw + round(20 * gui.scale), 20 * gui.scale) + ddt.rect(rect, ddt.text_background_colour) + ddt.text((xx + inset, yy), item[0], text_label_colour, font) + else: + ddt.text((xx + inset, yy), item[0], text_label_colour, font, max_w=max_w - inset) + else: + ddt.text((xx + inset, yy), item[0], text_label_colour, font, max_w=max_w - inset) + + # # Draw inset bars + # for m in range(item[2] + 1): + # if m == 0: + # continue + # colour = (255, 255, 255, 20) + # if semilight_mode: + # colour = (255, 255, 255, 30) + # if light_mode: + # colour = (0, 0, 0, 60) + # + # if i > 0 and self.rows[i - 1][2] == m - 1: # the top one needs to be slightly lower lower + # ddt.rect((x + (12 * m) + 2, yy - round(1 * gui.scale), round(1 * gui.scale), round(17 * gui.scale)), colour, True) + # else: + # ddt.rect((x + (12 * m) + 2, yy - round(5 * gui.scale), round(1 * gui.scale), round(21 * gui.scale)), colour, True) + + if prefs.folder_tree_codec_colours: + box_colour = self.folder_colour_cache.get(full_folder_path) + if box_colour is None: + box_colour = (150, 150, 150, 255) + + # Draw indicator box and +/- icons next to folder name + if item[3]: + rect = (xx + inset - round(9 * gui.scale), yy + round(7 * gui.scale), round(4 * gui.scale), + round(4 * gui.scale)) + if light_mode or semilight_mode: + border = round(1 * gui.scale) + ddt.rect((rect[0] - border, rect[1] - border, rect[2] + border * 2, rect[3] + border * 2), [0, 0, 0, 150]) + ddt.rect(rect, box_colour) + + elif True: + if not mouse_in or tree_view_scroll.held: + # text_colour = [255, 255, 255, 50] + text_colour = rgb_add_hls(colours.side_panel_background, 0, 0.2, -0.10) + if semilight_mode: + text_colour = [255, 255, 255, 70] + if light_mode: + text_colour = [0, 0, 0, 70] + if target in opens: + ddt.text((xx + inset - round(7 * gui.scale), yy + round(1 * gui.scale), 2), "-", text_colour, 19) + else: + ddt.text((xx + inset - round(7 * gui.scale), yy + round(1 * gui.scale), 2), "+", text_colour, 19) + + yy += spacing + + if self.click_drag_source and not point_proximity_test(gui.drag_source_position, mouse_position, 15) and \ + default_playlist is pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids: + quick_drag = True + global playlist_hold + playlist_hold = True + + self.dragging_name = self.click_drag_source[0] + logging.info(self.dragging_name) + + if "/" in self.dragging_name: + self.dragging_name = os.path.basename(self.dragging_name) + + shift_selection.clear() + set_drag_source() + for p, id in enumerate(pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids): + if msys: + if pctl.get_track(id).fullpath.startswith( + self.click_drag_source[1].lstrip("/") + "/" + self.click_drag_source[0] + "/"): + shift_selection.append(p) + elif pctl.get_track(id).fullpath.startswith(f"{self.click_drag_source[1]}/{self.click_drag_source[0]}/"): + shift_selection.append(p) + self.click_drag_source = None + + if self.dragging_name and not quick_drag: + self.dragging_name = "" + if not mouse_down: + self.click_drag_source = None + + def gen_row(self, tree_point, path, opens): + + for item in tree_point: + p = path + "/" + item[1] + self.count += 1 + enter_level = False + if len(tree_point) > 1 or path in self.force_opens: # Ignore levels that are only a single folder wide + + if path in opens or self.depth == 0 or path in self.force_opens: # Only show if parent stem is open, but always show the root displayed folders + + # If there is a single base folder in subfolder, combine the path and show it in upper level + if len(item[0]) == 1 and len(item[0][0][0]) == 1 and len(item[0][0][0][0][0]) == 0: + self.rows.append( + [item[1] + "/" + item[0][0][1] + "/" + item[0][0][0][0][1], path, self.depth, True, len(item[0])]) + elif len(item[0]) == 1 and len(item[0][0][0]) == 0: + self.rows.append([item[1] + "/" + item[0][0][1], path, self.depth, True, len(item[0])]) + + # Add normal base folder type + else: + self.rows.append([item[1], path, self.depth, len(item[0]) == 0, len(item[0])]) # Folder name, folder path, depth, is bottom + + # If folder is open and has only one subfolder, mark that subfolder as open + if len(item[0]) == 1 and (p in opens or p in self.force_opens): + self.force_opens.append(p + "/" + item[0][0][1]) + + self.depth += 1 + enter_level = True + + self.gen_row(item[0], p, opens) + + if enter_level: + self.depth -= 1 + + def gen_rows(self, tree, opens): + self.count = 0 + self.depth = 0 + self.rows.clear() + self.force_opens.clear() + + self.gen_row(tree, "", opens) + + gui.update_layout() + gui.update += 1 + + def gen_tree(self, pl_id): + pl_no = id_to_pl(pl_id) + if pl_no is None: + return + + playlist = pctl.multi_playlist[pl_no].playlist_ids + # Generate list of all unique folder paths + paths = [] + z = 5000 + for p in playlist: + + z += 1 + if z > 1000: + time.sleep(0.01) # Throttle thread + z = 0 + track = pctl.get_track(p) + path = track.parent_folder_path + if path not in paths: + paths.append(path) + self.folder_colour_cache[path] = format_colours.get(track.file_ext) + + # Genterate tree from folder paths + tree = [] + news = [] + for path in paths: + z += 1 + if z > 5000: + time.sleep(0.01) # Throttle thread + z = 0 + split_path = path.split("/") + on = tree + for level in split_path: + if not level: + continue + # Find if level already exists + for sub_level in on: + if sub_level[1] == level: + on = sub_level[0] + break + else: # Create new level + new = [[], level] + news.append(new) + on.append(new) + on = new[0] + + self.trees[pl_id] = tree + self.rows_id = "" + self.background_processing = False + gui.update += 1 + tauon.wake() + +class QueueBox: + + def recalc(self): + self.tab_h = 34 * gui.scale + def __init__(self): + + self.dragging = None + self.fq = [] + self.drag_start_y = 0 + self.drag_start_top = 0 + self.tab_h = 0 + self.scroll_position = 0 + self.right_click_id = None + self.d_click_ref = None + self.recalc() + + queue_menu.add(MenuItem(_("Remove This"), self.right_remove_item, show_test=self.queue_remove_show)) + queue_menu.add(MenuItem(_("Play Now"), self.play_now, show_test=self.queue_remove_show)) + queue_menu.add(MenuItem("Auto-Stop Here", self.toggle_auto_stop, self.toggle_auto_stop_deco, show_test=self.queue_remove_show)) + + queue_menu.add(MenuItem("Pause Queue", self.toggle_pause, queue_pause_deco)) + queue_menu.add(MenuItem(_("Clear Queue"), clear_queue, queue_deco, hint="Alt+Shift+Q")) + + queue_menu.add(MenuItem(_("↳ Except for This"), self.clear_queue_crop, show_test=self.except_for_this_show_test)) + + queue_menu.add(MenuItem(_("Queue to New Playlist"), self.make_as_playlist, queue_deco)) + # queue_menu.add("Finish Playing Album", finish_current, finish_current_deco) + + def except_for_this_show_test(self, _): + return self.queue_remove_show(_) and test_shift(_) + + def make_as_playlist(self): + + if pctl.force_queue: + playlist = [] + for item in pctl.force_queue: + + if item.type == 0: + playlist.append(item.track_id) + else: + + pl = id_to_pl(item.playlist_id) + if pl is None: + logging.info("Lost the target playlist") + continue + + pp = pctl.multi_playlist[pl].playlist_ids + + i = item.position # = pctl.playlist_playing_position + 1 + + parts = [] + album_parent_path = pctl.get_track(item.track_id).parent_folder_path + + while i < len(pp): + if pctl.get_track(pp[i]).parent_folder_path != album_parent_path: + break + + parts.append((pp[i], i)) + i += 1 + + for part in parts: + playlist.append(part[0]) + + pctl.multi_playlist.append( + pl_gen( + title=_("Queued Tracks"), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) + + def drop_tracks_insert(self, insert_position): + + global quick_drag + + if not shift_selection: + return + + # remove incomplete album from queue + if insert_position == 0 and pctl.force_queue and pctl.force_queue[0].album_stage == 1: + split_queue_album(pctl.force_queue[0].uuid_int) + + playlist_index = pctl.active_playlist_viewing + playlist_id = pl_to_id(pctl.active_playlist_viewing) + + main_track_position = shift_selection[0] + main_track_id = default_playlist[main_track_position] + quick_drag = False + + if len(shift_selection) > 1: + + # if shift selection contains only same folder + for position in shift_selection: + if pctl.get_track(default_playlist[position]).parent_folder_path != pctl.get_track( + main_track_id).parent_folder_path or key_ctrl_down: + break + else: + # Add as album type + pctl.force_queue.insert( + insert_position, queue_item_gen(main_track_id, main_track_position, playlist_id, 1)) + return + + if len(shift_selection) == 1: + pctl.force_queue.insert(insert_position, queue_item_gen(main_track_id, main_track_position, playlist_id)) + else: + # Add each track + for position in reversed(shift_selection): + pctl.force_queue.insert( + insert_position, queue_item_gen(default_playlist[position], position, playlist_id)) + + def clear_queue_crop(self): + + save = False + for item in pctl.force_queue: + if item.uuid_int == self.right_click_id: + save = item + break + + clear_queue() + if save: + pctl.force_queue.append(save) + + def play_now(self): + + queue_item = None + queue_index = 0 + for i, item in enumerate(pctl.force_queue): + if item.uuid_int == self.right_click_id: + queue_item = item + queue_index = i + break + else: + return + + del pctl.force_queue[queue_index] + # [trackid, position, pl_id, type, album_stage, uid_gen(), auto_stop] + + if pctl.force_queue and pctl.force_queue[0].album_stage == 1: + split_queue_album(None) + + target_track_id = queue_item.track_id + + pl = id_to_pl(queue_item.playlist_id) + if pl is not None: + pctl.active_playlist_playing = pl + + if target_track_id not in pctl.playing_playlist(): + pctl.advance() + return + + pctl.jump(target_track_id, queue_item.position) + + if queue_item.type == 1: # is album type + queue_item.album_stage = 1 # set as partway playing + pctl.force_queue.insert(0, queue_item) + + def toggle_auto_stop(self) -> None: + + for item in pctl.force_queue: + if item.uuid_int == self.right_click_id: + item.auto_stop ^= True + break + + def toggle_auto_stop_deco(self): + + enabled = False + for item in pctl.force_queue: + if item.uuid_int == self.right_click_id: + if item.auto_stop: + enabled = True + break + + if enabled: + return [colours.menu_text, colours.menu_background, _("Cancel Auto-Stop")] + return [colours.menu_text, colours.menu_background, _("Auto-Stop")] + + def queue_remove_show(self, id: int) -> bool: + + if self.right_click_id is not None: + return True + return False + + def right_remove_item(self) -> None: + + if self.right_click_id is None: + show_message(_("Eh?")) + + for u in reversed(range(len(pctl.force_queue))): + if pctl.force_queue[u].uuid_int == self.right_click_id: + del pctl.force_queue[u] + gui.pl_update += 1 + break + else: + show_message(_("Looks like it's gone now anyway")) + + def toggle_pause(self) -> None: + pctl.pause_queue ^= True + + def draw_card( + self, + x: int, y: int, + w: int, h: int, + yy: int, + track: TrackClass, fqo: TauonQueueItem, + draw_back: bool = False, draw_album_indicator: bool = True, + ) -> None: + + # text_colour = [230, 230, 230, 255] + bg = colours.queue_background + + # if fq[i].type == 0: + + rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h) + + if draw_back: + ddt.rect(rect, colours.queue_card_background) + bg = colours.queue_card_background + + text_colour1 = rgb_add_hls(bg, 0, 0.28, -0.15) # [255, 255, 255, 70] + text_colour2 = [255, 255, 255, 230] + if test_lumi(bg) < 0.2: + text_colour1 = [0, 0, 0, 130] + text_colour2 = [0, 0, 0, 230] + + tauon.gall_ren.render(track, (rect[0] + 4 * gui.scale, rect[1] + 4 * gui.scale), round(28 * gui.scale)) + + ddt.rect((rect[0] + 4 * gui.scale, rect[1] + 4 * gui.scale, 26, 26), [0, 0, 0, 6]) + + line = track.album + if fqo.type == 0: + line = track.title + + if not line: + line = clean_string(track.filename) + + line2y = yy + 14 * gui.scale + + artist_line = track.artist + if fqo.type == 1 and track.album_artist: + artist_line = track.album_artist + + if fqo.type == 0 and not artist_line: + line2y -= 7 * gui.scale + + ddt.text( + (rect[0] + (40 * gui.scale), yy - 1 * gui.scale), artist_line, text_colour1, 210, + max_w=rect[2] - 60 * gui.scale, bg=bg) + + ddt.text( + (rect[0] + (40 * gui.scale), line2y), line, text_colour2, 211, + max_w=rect[2] - 60 * gui.scale, bg=bg) + + if draw_album_indicator: + if fqo.type == 1: + if fqo.album_stage == 0: + ddt.rect((rect[0] + rect[2] - 5 * gui.scale, rect[1], 5 * gui.scale, rect[3]), [220, 130, 20, 255]) + else: + ddt.rect((rect[0] + rect[2] - 5 * gui.scale, rect[1], 5 * gui.scale, rect[3]), [140, 220, 20, 255]) + + if fqo.auto_stop: + xx = rect[0] + rect[2] - 9 * gui.scale + if fqo.type == 1: + xx -= 11 * gui.scale + ddt.rect((xx, rect[1] + 5 * gui.scale, 7 * gui.scale, 7 * gui.scale), [230, 190, 0, 255]) + + def draw(self, x: int, y: int, w: int, h: int): + + yy = y + + yy += round(4 * gui.scale) + + sep_colour = alpha_blend([255, 255, 255, 11], colours.queue_background) + + if y > gui.panelY + 10 * gui.scale: # Draw fancy light mode border + gui.queue_frame_draw = y + # else: + # if not colours.lm: + # ddt.rect((x, y, w, 3 * gui.scale), colours.queue_background, True) + + yy += round(3 * gui.scale) + + box_rect = (x, yy - 6 * gui.scale, w, h) + ddt.rect(box_rect, colours.queue_background) + ddt.text_background_colour = colours.queue_background + + if coll(box_rect) and quick_drag and not pctl.force_queue: + ddt.rect(box_rect, [255, 255, 255, 2]) + ddt.text_background_colour = alpha_blend([255, 255, 255, 2], ddt.text_background_colour) + + # if y < gui.panelY * 2: + # ddt.rect((x, y - 3 * gui.scale, w, 30 * gui.scale), colours.queue_background, True) + + if h > 40 * gui.scale: + if not pctl.force_queue: + if quick_drag: + text = _("Add to Queue") + else: + text = _("Queue") + ddt.text((x + (w // 2), y + 15 * gui.scale, 2), text, alpha_mod(colours.index_text, 200), 212) + + qb_right_click = 0 + + if coll(box_rect): + # Update scroll position + self.scroll_position += mouse_wheel * -1 + self.scroll_position = max(self.scroll_position, 0) + + if right_click: + qb_right_click = 1 + + # text_colour = [255, 255, 255, 91] + text_colour = rgb_add_hls(colours.queue_background, 0, 0.3, -0.15) + if test_lumi(colours.queue_background) < 0.2: + text_colour = [0, 0, 0, 200] + + line = _("Up Next:") + if pctl.force_queue: + # line = "Queue" + ddt.text((x + (10 * gui.scale), yy + 2 * gui.scale), line, text_colour, 211) + + yy += 7 * gui.scale + + if len(pctl.force_queue) < 3: + self.scroll_position = 0 + + # Draw square dots to indicate view has been scrolled down + if self.scroll_position > 0: + ds = 3 * gui.scale + gp = 4 * gui.scale + + ddt.rect((x + int(w / 2), yy, ds, ds), [230, 190, 0, 255]) + ddt.rect((x + int(w / 2), yy + gp, ds, ds), [230, 190, 0, 255]) + ddt.rect((x + int(w / 2), yy + gp + gp, ds, ds), [230, 190, 0, 255]) + + # Draw pause icon + if pctl.pause_queue: + ddt.rect((x + w - 24 * gui.scale, yy + 2 * gui.scale, 3 * gui.scale, 9 * gui.scale), [230, 190, 0, 255]) + ddt.rect((x + w - 19 * gui.scale, yy + 2 * gui.scale, 3 * gui.scale, 9 * gui.scale), [230, 190, 0, 255]) + + yy += 6 * gui.scale + + yy += 10 * gui.scale + + i = 0 + + # Get new copy of queue if not dragging + if not self.dragging: + self.fq = copy.deepcopy(pctl.force_queue) + else: + # gui.update += 1 + gui.update_on_drag = True + + # End drag if mouse not in correct state for it + if not mouse_down and not mouse_up: + self.dragging = None + + if not queue_menu.active: + self.right_click_id = None + + fq = self.fq + + list_top = yy + + i = self.scroll_position + + # Limit scroll distance + if i > len(fq): + self.scroll_position = len(fq) + i = self.scroll_position + + showed_indicator = False + list_extends = False + x1 = x + 13 * gui.scale # highlight position + w1 = w - 28 * gui.scale - 10 * gui.scale + + while i < len(fq) + 1: + + # Stop drawing if past window + if yy > window_size[1] - gui.panelBY - gui.panelY - (50 * gui.scale): + list_extends = True + break + + # Calculate drag collision box. Special case for first and last which extend out in y direction + h_rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h + 3 * gui.scale) + if i == len(fq): + h_rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h + 3 * gui.scale + 1000 * gui.scale) + if i == 0: + h_rect = ( + 0, yy - 1000 * gui.scale, w - 28 * gui.scale + 10000, self.tab_h + 3 * gui.scale + 1000 * gui.scale) + + if self.dragging is not None and coll(h_rect) and mouse_up: + + ob = None + for u in reversed(range(len(pctl.force_queue))): + + if pctl.force_queue[u].uuid_int == self.dragging: + ob = pctl.force_queue[u] + pctl.force_queue[u] = None + break + + else: + self.dragging = None + + if self.dragging: + pctl.force_queue.insert(i, ob) + self.dragging = None + + for u in reversed(range(len(pctl.force_queue))): + if pctl.force_queue[u] is None: + del pctl.force_queue[u] + gui.pl_update += 1 + continue + + # Reset album in flag if not first item + if pctl.force_queue[u].album_stage == 1: + if u != 0: + pctl.force_queue[u].album_stage = 0 + + inp.mouse_click = False + self.draw(x, y, w, h) + return + + if i > len(fq) - 1: + break + + track = pctl.get_track(fq[i].track_id) + + rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h) + + if inp.mouse_click and coll(rect): + + self.dragging = fq[i].uuid_int + self.drag_start_y = mouse_position[1] + self.drag_start_top = yy + + if d_click_timer.get() < 1: + + if self.d_click_ref == fq[i].uuid_int: + + pl = id_to_pl(fq[i].uuid_int) + if pl is not None: + switch_playlist(pl) + + pctl.show_current(playing=False, highlight=True, index=fq[i].track_id) + self.d_click_ref = None + # else: + self.d_click_ref = fq[i].uuid_int + + d_click_timer.set() + + if self.dragging and coll(h_rect): + yy += self.tab_h + yy += 4 * gui.scale + + if qb_right_click and coll(rect): + self.right_click_id = fq[i].uuid_int + qb_right_click = 2 + + if middle_click and coll(rect): + pctl.force_queue.remove(fq[i]) + gui.pl_update += 1 + + if fq[i].uuid_int == self.dragging: + # ddt.rect_r(rect, [22, 22, 22, 255], True) + pass + else: + + db = False + if fq[i].uuid_int == self.right_click_id: + db = True + + self.draw_card(x, y, w, h, yy, track, fq[i], db) + + # Drag tracks from main playlist and insert ------------ + if quick_drag: + + if x < mouse_position[0] < x + w: + + y1 = yy - 4 * gui.scale + y2 = y1 + h1 = self.tab_h // 2 + if i == 0: + # Extend up if first element + y1 -= 5 * gui.scale + h1 += 10 * gui.scale + + insert_position = None + + if y1 < mouse_position[1] < y1 + h1: + ddt.rect((x1, yy - 2 * gui.scale, w1, 2 * gui.scale), colours.queue_drag_indicator_colour) + showed_indicator = True + + if mouse_up: + insert_position = i + + elif y2 < mouse_position[1] < y2 + self.tab_h + 5 * gui.scale: + ddt.rect( + (x1, yy + self.tab_h + 2 * gui.scale, w1, 2 * gui.scale), + colours.queue_drag_indicator_colour) + showed_indicator = True + + if mouse_up: + insert_position = i + 1 + + if insert_position is not None: + self.drop_tracks_insert(insert_position) + + # ----------------------------------------- + yy += self.tab_h + yy += 4 * gui.scale + + i += 1 + + # Show drag marker if mouse holding below list + if quick_drag and not list_extends and not showed_indicator and fq and mouse_position[ + 1] > yy - 4 * gui.scale and coll(box_rect): + yy -= self.tab_h + yy -= 4 * gui.scale + ddt.rect((x1, yy + self.tab_h + 2 * gui.scale, w1, 2 * gui.scale), colours.queue_drag_indicator_colour) + yy += self.tab_h + yy += 4 * gui.scale + + yy += 15 * gui.scale + if fq: + ddt.rect((x, yy, w, 3 * gui.scale), sep_colour) + yy += 11 * gui.scale + + # Calculate total queue duration + duration = 0 + tracks = 0 + + for item in fq: + if item.type == 0: + duration += pctl.get_track(item.track_id).length + tracks += 1 + else: + pl = id_to_pl(item.playlist_id) + if pl is not None: + playlist = pctl.multi_playlist[pl].playlist_ids + i = item.position + + album_parent_path = pctl.get_track(item.track_id).parent_folder_path + + playing_track = pctl.playing_object() + + if pl == pctl.active_playlist_playing \ + and item.album_stage \ + and playing_track and playing_track.parent_folder_path == album_parent_path: + i = pctl.playlist_playing_position + 1 + + if item.track_id not in playlist: + continue + if i > len(playlist) - 1: + continue + if playlist[i] != item.track_id: + i = playlist.index(item.track_id) + + while i < len(playlist): + if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: + break + + duration += pctl.get_track(playlist[i]).length + tracks += 1 + i += 1 + + # Show total duration text "n Tracks [0:00:00]" + if tracks and fq: + if tracks < 2: + line = _("{N} Track").format(N=str(tracks)) + " [" + get_hms_time(duration) + "]" + ddt.text((x + 12 * gui.scale, yy), line, text_colour, 11.5, bg=colours.queue_background) + else: + line = _("{N} Tracks").format(N=str(tracks)) + " [" + get_hms_time(duration) + "]" + ddt.text((x + 12 * gui.scale, yy), line, text_colour, 11.5, bg=colours.queue_background) + + + + if self.dragging: + + fqo = None + for item in fq: + if item.uuid_int == self.dragging: + fqo = item + break + else: + self.dragging = False + + if self.dragging: + yyy = self.drag_start_top + (mouse_position[1] - self.drag_start_y) + yyy = max(yyy, list_top) + track = pctl.get_track(fqo.track_id) + self.draw_card(x, y, w, h, yyy, track, fqo, draw_back=True) + + # Drag and drop tracks from main playlist into queue + if quick_drag and mouse_up and coll(box_rect) and shift_selection: + self.drop_tracks_insert(len(fq)) + + # Right click context menu in blank space + if qb_right_click: + if qb_right_click == 1: + self.right_click_id = None + queue_menu.activate(position=mouse_position) + +class MetaBox: + + def l_panel(self, x, y, w, h, track, top_border=True): + + if not track: + return + + border_colour = [255, 255, 255, 30] + line1_colour = [255, 255, 255, 235] + line2_colour = [255, 255, 255, 200] + if test_lumi(colours.gallery_background) < 0.55: + border_colour = [0, 0, 0, 30] + line1_colour = [0, 0, 0, 200] + line2_colour = [0, 0, 0, 230] + + rect = (x, y, w, h) + + ddt.rect(rect, colours.gallery_background) + if top_border: + ddt.rect((x, y, w, round(1 * gui.scale)), border_colour) + else: + ddt.rect((x, y + h - round(1 * gui.scale), w, round(1 * gui.scale)), border_colour) + + ddt.text_background_colour = colours.gallery_background + + insert = round(9 * gui.scale) + border = round(2 * gui.scale) + + compact_mode = False + if w < h * 1.9: + compact_mode = True + + art_rect = [x + insert - 2 * gui.scale, y + insert, h - insert * 2 + 1 * gui.scale, + h - insert * 2 + 1 * gui.scale] + + if compact_mode: + art_rect[0] = x + round(w / 2 - art_rect[2] / 2) - round(1 * gui.scale) # - border + + border_rect = ( + art_rect[0] - border, art_rect[1] - border, art_rect[2] + (border * 2), art_rect[3] + (border * 2)) + + if (inp.mouse_click or right_click) and is_level_zero(False): + if coll(border_rect): + if inp.mouse_click: + album_art_gen.cycle_offset(target_track) + if right_click: + picture_menu.activate(in_reference=target_track) + elif coll(rect): + if inp.mouse_click: + pctl.show_current() + if right_click: + showcase_menu.activate(track) + + ddt.rect(border_rect, border_colour) + ddt.rect(art_rect, colours.gallery_background) + album_art_gen.display(track, (art_rect[0], art_rect[1]), (art_rect[2], art_rect[3])) + + fields.add(border_rect) + if coll(border_rect) and is_level_zero(True): + showc = album_art_gen.get_info(target_track) + art_metadata_overlay( + art_rect[0] + art_rect[2] + 2 * gui.scale, art_rect[1] + art_rect[3] + 12 * gui.scale, showc) + + if not compact_mode: + text_x = border_rect[0] + border_rect[2] + round(10 * gui.scale) + max_w = w - (border_rect[2] + 28 * gui.scale) + yy = y + round(15 * gui.scale) + + ddt.text((text_x, yy), track.title, line1_colour, 316, max_w=max_w) + yy += round(20 * gui.scale) + ddt.text((text_x, yy), track.artist, line2_colour, 14, max_w=max_w) + yy += round(30 * gui.scale) + ddt.text((text_x, yy), track.album, line2_colour, 14, max_w=max_w) + yy += round(20 * gui.scale) + ddt.text((text_x, yy), track.date, line2_colour, 14, max_w=max_w) + + gui.showed_title = True + + def lyrics(self, x, y, w, h, track: TrackClass): + + ddt.rect((x, y, w, h), colours.side_panel_background) + ddt.text_background_colour = colours.side_panel_background + + if not track: + return + + # Test for show lyric menu on right ckick + if coll((x + 10, y, w - 10, h)): + if right_click: # and 3 > pctl.playing_state > 0: + gui.force_showcase_index = -1 + showcase_menu.activate(track) + + # Test for scroll wheel input + if mouse_wheel != 0 and coll((x + 10, y, w - 10, h)): + lyrics_ren_mini.lyrics_position += mouse_wheel * 30 * gui.scale + if lyrics_ren_mini.lyrics_position > 0: + lyrics_ren_mini.lyrics_position = 0 + lyric_side_top_pulse.pulse() + + gui.update += 1 + + tw, th = ddt.get_text_wh(track.lyrics + "\n", 15, w - 50 * gui.scale, True) + + oth = th + + th -= h + th += 25 * gui.scale # Empty space buffer at end + + if lyrics_ren_mini.lyrics_position * -1 > th: + lyrics_ren_mini.lyrics_position = th * -1 + if oth > h: + lyric_side_bottom_pulse.pulse() + + scroll_w = 15 * gui.scale + if gui.maximized: + scroll_w = 17 * gui.scale + + lyrics_ren_mini.lyrics_position = mini_lyrics_scroll.draw( + x + w - 17 * gui.scale, y, scroll_w, h, + lyrics_ren_mini.lyrics_position * -1, th, + jump_distance=160 * gui.scale) * -1 + + margin = 10 * gui.scale + if colours.lm: + margin += 1 * gui.scale + + lyrics_ren_mini.render( + pctl.track_queue[pctl.queue_step], x + margin, + y + lyrics_ren_mini.lyrics_position + 13 * gui.scale, + w - 50 * gui.scale, + None, 0) + + ddt.rect((x, y + h - 1, w, 1), colours.side_panel_background) + + lyric_side_top_pulse.render(x, y, w - round(17 * gui.scale), 16 * gui.scale) + lyric_side_bottom_pulse.render(x, y + h, w - round(17 * gui.scale), 15 * gui.scale, bottom=True) + + def draw(self, x, y, w, h, track=None): + + ddt.rect((x, y, w, h), colours.side_panel_background) + + if not track: + return + + # Test for show lyric menu on right ckick + if coll((x + 10, y, w - 10, h)): + if right_click: # and 3 > pctl.playing_state > 0: + gui.force_showcase_index = -1 + showcase_menu.activate(track) + + if pctl.playing_state == 0: + if not prefs.meta_persists_stop and not prefs.meta_shows_selected and not prefs.meta_shows_selected_always: + return + + if h < 15: + return + + # Check for lyrics if auto setting + test_auto_lyrics(track) + + # # Draw lyrics if avaliable + # if prefs.show_lyrics_side and pctl.track_queue \ + # and track.lyrics != "" and h > 45 * gui.scale and w > 200 * gui.scale: + # + # self.lyrics(x, y, w, h, track) + + # Draw standard metadata + if len(pctl.track_queue) > 0: + + if pctl.playing_state == 0: + if not prefs.meta_persists_stop and not prefs.meta_shows_selected and not prefs.meta_shows_selected_always: + return + + ddt.text_background_colour = colours.side_panel_background + + if coll((x + 10, y, w - 10, h)): + # Click area to jump to current track + if inp.mouse_click: + pctl.show_current() + gui.update += 1 + + title = "" + album = "" + artist = "" + ext = "" + date = "" + genre = "" + + margin = x + 10 * gui.scale + if colours.lm: + margin += 2 * gui.scale + + text_width = w - 25 * gui.scale + tr = None + + # if pctl.playing_state < 3: + + if pctl.playing_state == 0 and prefs.meta_persists_stop: + tr = pctl.master_library[pctl.track_queue[pctl.queue_step]] + if pctl.playing_state == 0 and prefs.meta_shows_selected: + + if -1 < pctl.selected_in_playlist < len(pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids): + tr = pctl.get_track(pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids[pctl.selected_in_playlist]) + + if prefs.meta_shows_selected_always and pctl.playing_state != 3: + if -1 < pctl.selected_in_playlist < len(pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids): + tr = pctl.get_track(pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids[pctl.selected_in_playlist]) + + if tr is None: + tr = pctl.playing_object() + if tr is None: + return + + title = tr.title + album = tr.album + artist = tr.artist + ext = tr.file_ext + if ext == "JELY": + ext = "Jellyfin" + if "container" in tr.misc: + ext = tr.misc.get("container", "") + " | Jellyfin" + if tr.lyrics: + ext += "," + date = tr.date + genre = tr.genre + + if not title and not artist: + title = pctl.tag_meta + + if h > 58 * gui.scale: + + block_y = y + 7 * gui.scale + + if not prefs.show_side_art: + block_y += 3 * gui.scale + + if title != "": + ddt.text( + (margin, block_y + 2 * gui.scale), title, colours.side_bar_line1, fonts.side_panel_line1, + max_w=text_width) + if artist != "": + ddt.text( + (margin, block_y + 23 * gui.scale), artist, colours.side_bar_line2, fonts.side_panel_line2, + max_w=text_width) + + gui.showed_title = True + + if h > 140 * gui.scale: + + block_y = y + 80 * gui.scale + if artist != "": + ddt.text( + (margin, block_y), album, colours.side_bar_line2, + fonts.side_panel_line2, max_w=text_width) + + if not genre == date == "": + line = date + if genre != "": + if line != "": + line += " | " + line += genre + + ddt.text( + (margin, block_y + 20 * gui.scale), line, colours.side_bar_line2, + fonts.side_panel_line2, max_w=text_width) + + if ext != "": + if ext == "SPTY": + ext = "Spotify" + if ext == "RADIO": + ext = radiobox.playing_title + sp = ddt.text( + (margin, block_y + 40 * gui.scale), ext, colours.side_bar_line2, + fonts.side_panel_line2, max_w=text_width) + + if tr and tr.lyrics: + if draw_internel_link( + margin + sp + 6 * gui.scale, block_y + 40 * gui.scale, "Lyrics", colours.side_bar_line2, fonts.side_panel_line2): + prefs.show_lyrics_showcase = True + enter_showcase_view(track_id=tr.index) + +class PictureRender: + + def __init__(self): + self.show = False + self.path = "" + + self.image_data = None + self.texture = None + self.sdl_rect = None + self.size = (0, 0) + + def load(self, path, box_size=None): + + if not os.path.isfile(path): + logging.warning("NO PICTURE FILE TO LOAD") + return + + g = io.BytesIO() + g.seek(0) + + im = Image.open(path) + if box_size is not None: + im.thumbnail(box_size, Image.Resampling.LANCZOS) + + im.save(g, "BMP") + g.seek(0) + self.image_data = g + logging.info("Save BMP to memory") + self.size = im.size[0], im.size[1] + + def draw(self, x, y): + + if self.show is False: + return + + if self.image_data is not None: + if self.texture is not None: + SDL_DestroyTexture(self.texture) + + # Convert raw image to sdl texture + #logging.info("Create Texture") + wop = rw_from_object(self.image_data) + s_image = IMG_Load_RW(wop, 0) + self.texture = SDL_CreateTextureFromSurface(renderer, s_image) + SDL_FreeSurface(s_image) + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + SDL_QueryTexture(self.texture, None, None, tex_w, tex_h) + self.sdl_rect = SDL_Rect(round(x), round(y)) + self.sdl_rect.w = int(tex_w.contents.value) + self.sdl_rect.h = int(tex_h.contents.value) + self.image_data = None + + if self.texture is not None: + self.sdl_rect.x = round(x) + self.sdl_rect.y = round(y) + SDL_RenderCopy(renderer, self.texture, None, self.sdl_rect) + style_overlay.hole_punches.append(self.sdl_rect) + +class PictureRender: + + def __init__(self): + self.show = False + self.path = "" + + self.image_data = None + self.texture = None + self.sdl_rect = None + self.size = (0, 0) + + def load(self, path, box_size=None): + + if not os.path.isfile(path): + logging.warning("NO PICTURE FILE TO LOAD") + return + + g = io.BytesIO() + g.seek(0) + + im = Image.open(path) + if box_size is not None: + im.thumbnail(box_size, Image.Resampling.LANCZOS) + + im.save(g, "BMP") + g.seek(0) + self.image_data = g + logging.info("Save BMP to memory") + self.size = im.size[0], im.size[1] + + def draw(self, x, y): + + if self.show is False: + return + + if self.image_data is not None: + if self.texture is not None: + SDL_DestroyTexture(self.texture) + + # Convert raw image to sdl texture + #logging.info("Create Texture") + wop = rw_from_object(self.image_data) + s_image = IMG_Load_RW(wop, 0) + self.texture = SDL_CreateTextureFromSurface(renderer, s_image) + SDL_FreeSurface(s_image) + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + SDL_QueryTexture(self.texture, None, None, tex_w, tex_h) + self.sdl_rect = SDL_Rect(round(x), round(y)) + self.sdl_rect.w = int(tex_w.contents.value) + self.sdl_rect.h = int(tex_h.contents.value) + self.image_data = None + + if self.texture is not None: + self.sdl_rect.x = round(x) + self.sdl_rect.y = round(y) + SDL_RenderCopy(renderer, self.texture, None, self.sdl_rect) + style_overlay.hole_punches.append(self.sdl_rect) + +class RadioThumbGen: + def __init__(self): + self.cache = {} + self.requests = [] + self.size = 100 + + def loader(self): + + while self.requests: + item = self.requests[0] + del self.requests[0] + station = item[0] + size = item[1] + key = (station["title"], size) + src = None + filename = filename_safe(station["title"]) + + cache_path = os.path.join(r_cache_dir, filename + ".jpg") + if os.path.isfile(cache_path): + src = open(cache_path, "rb") + else: + cache_path = os.path.join(r_cache_dir, filename + ".png") + if os.path.isfile(cache_path): + src = open(cache_path, "rb") + else: + cache_path = os.path.join(r_cache_dir, filename) + if os.path.isfile(cache_path): + src = open(cache_path, "rb") + + if src: + pass + #logging.info("found cached") + elif station.get("icon") and station["icon"] not in prefs.radio_thumb_bans: + try: + r = requests.get(station.get("icon"), headers={"User-Agent": t_agent}, timeout=5, stream=True) + if r.status_code != 200 or int(r.headers.get("Content-Length", 0)) > 2000000: + raise Exception("Error get radio thumb") + except Exception: + logging.exception("error get radio thumb") + self.cache[key] = [0] + if station.get("icon") and station.get("icon") not in prefs.radio_thumb_bans: + prefs.radio_thumb_bans.append(station.get("icon")) + continue + src = io.BytesIO() + length = 0 + for chunk in r.iter_content(1024): + src.write(chunk) + length += len(chunk) + if length > 2000000: + scr = None + if src is None: + self.cache[key] = [0] + if station.get("icon") and station.get("icon") not in prefs.radio_thumb_bans: + prefs.radio_thumb_bans.append(station.get("icon")) + continue + src.seek(0) + with open(cache_path, "wb") as f: + f.write(src.read()) + src.seek(0) + else: + # logging.info("no icon") + self.cache[key] = [0] + continue + + try: + im = Image.open(src) + if im.mode != "RGBA": + im = im.convert("RGBA") + except Exception: + logging.exception("malform get radio thumb") + self.cache[key] = [0] + if station.get("icon") and station.get("icon") not in prefs.radio_thumb_bans: + prefs.radio_thumb_bans.append(station.get("icon")) + continue + if src is not None: + src.close() + + im = im.resize((size, size), Image.Resampling.LANCZOS) + g = io.BytesIO() + g.seek(0) + im.save(g, "PNG") + g.seek(0) + wop = rw_from_object(g) + s_image = IMG_Load_RW(wop, 0) + self.cache[key] = [2, None, None, s_image] + gui.update += 1 + + def draw(self, station, x, y, w): + if not station.get("title"): + return 0 + key = (station["title"], w) + + r = self.cache.get(key) + if r is None: + if len(self.requests) < 3: + self.requests.append((station, w)) + tauon.thread_manager.ready("radio-thumb") + return 0 + if r[0] == 2: + texture = SDL_CreateTextureFromSurface(renderer, r[3]) + SDL_FreeSurface(r[3]) + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + SDL_QueryTexture(texture, None, None, tex_w, tex_h) + sdl_rect = SDL_Rect(0, 0) + sdl_rect.w = int(tex_w.contents.value) + sdl_rect.h = int(tex_h.contents.value) + r[2] = texture + r[1] = sdl_rect + r[0] = 1 + if r[0] == 1: + r[1].x = round(x) + r[1].y = round(y) + SDL_RenderCopy(renderer, r[2], None, r[1]) + return 1 + return 0 + +class RadioThumbGen: + def __init__(self): + self.cache = {} + self.requests = [] + self.size = 100 + + def loader(self): + + while self.requests: + item = self.requests[0] + del self.requests[0] + station = item[0] + size = item[1] + key = (station["title"], size) + src = None + filename = filename_safe(station["title"]) + + cache_path = os.path.join(r_cache_dir, filename + ".jpg") + if os.path.isfile(cache_path): + src = open(cache_path, "rb") + else: + cache_path = os.path.join(r_cache_dir, filename + ".png") + if os.path.isfile(cache_path): + src = open(cache_path, "rb") + else: + cache_path = os.path.join(r_cache_dir, filename) + if os.path.isfile(cache_path): + src = open(cache_path, "rb") + + if src: + pass + #logging.info("found cached") + elif station.get("icon") and station["icon"] not in prefs.radio_thumb_bans: + try: + r = requests.get(station.get("icon"), headers={"User-Agent": t_agent}, timeout=5, stream=True) + if r.status_code != 200 or int(r.headers.get("Content-Length", 0)) > 2000000: + raise Exception("Error get radio thumb") + except Exception: + logging.exception("error get radio thumb") + self.cache[key] = [0] + if station.get("icon") and station.get("icon") not in prefs.radio_thumb_bans: + prefs.radio_thumb_bans.append(station.get("icon")) + continue + src = io.BytesIO() + length = 0 + for chunk in r.iter_content(1024): + src.write(chunk) + length += len(chunk) + if length > 2000000: + scr = None + if src is None: + self.cache[key] = [0] + if station.get("icon") and station.get("icon") not in prefs.radio_thumb_bans: + prefs.radio_thumb_bans.append(station.get("icon")) + continue + src.seek(0) + with open(cache_path, "wb") as f: + f.write(src.read()) + src.seek(0) + else: + # logging.info("no icon") + self.cache[key] = [0] + continue + + try: + im = Image.open(src) + if im.mode != "RGBA": + im = im.convert("RGBA") + except Exception: + logging.exception("malform get radio thumb") + self.cache[key] = [0] + if station.get("icon") and station.get("icon") not in prefs.radio_thumb_bans: + prefs.radio_thumb_bans.append(station.get("icon")) + continue + if src is not None: + src.close() + + im = im.resize((size, size), Image.Resampling.LANCZOS) + g = io.BytesIO() + g.seek(0) + im.save(g, "PNG") + g.seek(0) + wop = rw_from_object(g) + s_image = IMG_Load_RW(wop, 0) + self.cache[key] = [2, None, None, s_image] + gui.update += 1 + + def draw(self, station, x, y, w): + if not station.get("title"): + return 0 + key = (station["title"], w) + + r = self.cache.get(key) + if r is None: + if len(self.requests) < 3: + self.requests.append((station, w)) + tauon.thread_manager.ready("radio-thumb") + return 0 + if r[0] == 2: + texture = SDL_CreateTextureFromSurface(renderer, r[3]) + SDL_FreeSurface(r[3]) + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + SDL_QueryTexture(texture, None, None, tex_w, tex_h) + sdl_rect = SDL_Rect(0, 0) + sdl_rect.w = int(tex_w.contents.value) + sdl_rect.h = int(tex_h.contents.value) + r[2] = texture + r[1] = sdl_rect + r[0] = 1 + if r[0] == 1: + r[1].x = round(x) + r[1].y = round(y) + SDL_RenderCopy(renderer, r[2], None, r[1]) + return 1 + return 0 + +class Showcase: + + def __init__(self): + + self.lastfm_artist = None + self.artist_mode = False + + def render(self): + + global right_click + + box = int(window_size[1] * 0.4 + 120 * gui.scale) + box = min(window_size[0] // 2, box) + + hide_art = False + if window_size[0] < 900 * gui.scale: + hide_art = True + + x = int(window_size[0] * 0.15) + y = int((window_size[1] / 2) - (box / 2)) - 10 * gui.scale + + if hide_art: + box = 45 * gui.scale + elif window_size[1] / window_size[0] > 0.7: + x = int(window_size[0] * 0.07) + + bbg = rgb_add_hls(colours.playlist_panel_background, 0, 0.05, 0) # [255, 255, 255, 18] + bfg = rgb_add_hls(colours.playlist_panel_background, 0, 0.09, 0) # [255, 255, 255, 30] + bft = colours.grey(235) + bbt = colours.grey(200) + + t1 = colours.grey(250) + + gui.vis_4_colour = None + light_mode = False + if colours.lm: + bbg = colours.vis_colour + bfg = alpha_blend([255, 255, 255, 60], colours.vis_colour) + bft = colours.grey(250) + bbt = colours.grey(245) + elif prefs.art_bg and prefs.bg_showcase_only: + bbg = [255, 255, 255, 18] + bfg = [255, 255, 255, 30] + bft = [255, 255, 255, 250] + bbt = [255, 255, 255, 200] + + if test_lumi(colours.playlist_panel_background) < 0.7: + light_mode = True + t1 = colours.grey(30) + gui.vis_4_colour = [40, 40, 40, 255] + + ddt.rect((0, gui.panelY, window_size[0], window_size[1] - gui.panelY), colours.playlist_panel_background) + + if prefs.bg_showcase_only and prefs.art_bg: + style_overlay.display() + + # Draw textured background + if not light_mode and not colours.lm and prefs.showcase_overlay_texture: + rect = SDL_Rect() + rect.x = 0 + rect.y = 0 + rect.w = 300 + rect.h = 300 + + xx = 0 + yy = 0 + while yy < window_size[1]: + xx = 0 + while xx < window_size[0]: + rect.x = xx + rect.y = yy + SDL_RenderCopy(renderer, overlay_texture_texture, None, rect) + xx += 300 + yy += 300 + + if prefs.bg_showcase_only and prefs.art_bg: + ddt.alpha_bg = True + ddt.force_gray = True + + # if not prefs.shuffle_lock: + # if draw.button(_("Return"), 25 * gui.scale, window_size[1] - gui.panelBY - 40 * gui.scale, + # text_highlight_colour=bft, text_colour=bbt, backgound_colour=bbg, + # background_highlight_colour=bfg): + # gui.switch_showcase_off = True + # gui.update += 1 + # gui.update_layout() + + # ddt.force_gray = True + + if pctl.playing_state == 3 and not radiobox.dummy_track.title: + + if not pctl.tag_meta: + y = int(window_size[1] / 2) - 60 - gui.scale + ddt.text((window_size[0] // 2, y, 2), pctl.url, colours.side_bar_line2, 317) + else: + w = window_size[0] - (x + box) - 30 * gui.scale + x = int((window_size[0]) / 2) + + y = int(window_size[1] / 2) - 60 - gui.scale + ddt.text((x, y, 2), pctl.tag_meta, colours.side_bar_line1, 216, w) + + else: + + if len(pctl.track_queue) < 1: + ddt.alpha_bg = False + return + + # if draw.button("Return", 20, gui.panelY + 5, bg=colours.grey(30)): + # pass + + if prefs.bg_showcase_only and prefs.art_bg: + ddt.alpha_bg = True + ddt.force_gray = True + + if gui.force_showcase_index >= 0: + if draw.button( + _("Playing"), 25 * gui.scale, gui.panelY + 20 * gui.scale, text_highlight_colour=bft, + text_colour=bbt, background_colour=bbg, background_highlight_colour=bfg): + gui.force_showcase_index = -1 + ddt.force_gray = False + + if gui.force_showcase_index >= 0: + index = gui.force_showcase_index + track = pctl.master_library[index] + else: + + if pctl.playing_state == 3: + track = radiobox.dummy_track + else: + index = pctl.track_queue[pctl.queue_step] + track = pctl.master_library[index] + + if not hide_art: + + # Draw frame around art box + # drop_shadow.render(x + 5 * gui.scale, y + 5 * gui.scale, box + 10 * gui.scale, box + 10 * gui.scale) + ddt.rect( + (x - round(2 * gui.scale), y - round(2 * gui.scale), box + round(4 * gui.scale), + box + round(4 * gui.scale)), [60, 60, 60, 135]) + ddt.rect((x, y, box, box), colours.playlist_panel_background) + rect = SDL_Rect(round(x), round(y), round(box), round(box)) + style_overlay.hole_punches.append(rect) + + # Draw album art in box + album_art_gen.display(track, (x, y), (box, box)) + + # Click art to cycle + if coll((x, y, box, box)): + if inp.mouse_click is True: + album_art_gen.cycle_offset(track) + if right_click: + picture_menu.activate(in_reference=track) + right_click = False + + # Check for lyrics if auto setting + test_auto_lyrics(track) + + gui.draw_vis4_top = False + + if gui.panelY < mouse_position[1] < window_size[1] - gui.panelBY: + if mouse_wheel != 0: + lyrics_ren.lyrics_position += mouse_wheel * 35 * gui.scale + if right_click: + # track = pctl.playing_object() + if track != None: + showcase_menu.activate(track) + + gcx = x + box + int(window_size[0] * 0.15) + 10 * gui.scale + gcx -= 100 * gui.scale + + timed_ready = False + if True and prefs.show_lyrics_showcase: + timed_ready = timed_lyrics_ren.generate(track) + + if timed_ready and track.lyrics: + + # if not prefs.guitar_chords or guitar_chords.test_ready_status(track) != 1: + # + # line = _("Prefer synced") + # if prefs.prefer_synced_lyrics: + # line = _("Prefer static") + # if draw.button(line, 25 * gui.scale, window_size[1] - gui.panelBY - 70 * gui.scale, + # text_highlight_colour=bft, text_colour=bbt, background_colour=bbg, + # background_highlight_colour=bfg): + # prefs.prefer_synced_lyrics ^= True + + timed_ready = prefs.prefer_synced_lyrics + + #if prefs.guitar_chords and track.title and prefs.show_lyrics_showcase and guitar_chords.render(track, gcx, y): + # if not guitar_chords.auto_scroll: + # if draw.button( + # _("Auto-Scroll"), 25 * gui.scale, window_size[1] - gui.panelBY - 70 * gui.scale, + # text_highlight_colour=bft, text_colour=bbt, background_colour=bbg, + # background_highlight_colour=bfg): + # guitar_chords.auto_scroll = True + + if True and prefs.show_lyrics_showcase and timed_ready: + w = window_size[0] - (x + box) - round(30 * gui.scale) + timed_lyrics_ren.render(track.index, gcx, y, w=w) + + elif track.lyrics == "" or not prefs.show_lyrics_showcase: + + w = window_size[0] - (x + box) - round(30 * gui.scale) + x = int(x + box + (window_size[0] - x - box) / 2) + + if hide_art: + x = window_size[0] // 2 + + # x = int((window_size[0]) / 2) + y = int(window_size[1] / 2) - round(60 * gui.scale) + + if prefs.showcase_vis and prefs.backend == 1: + y -= round(30 * gui.scale) + + if track.artist == "" and track.title == "": + + ddt.text((x, y, 2), clean_string(track.filename), t1, 216, w) + + else: + + ddt.text((x, y, 2), track.artist, t1, 20, w) + + y += round(48 * gui.scale) + + if window_size[0] < 700 * gui.scale: + if len(track.title) < 30: + ddt.text((x, y, 2), track.title, t1, 220, w) + elif len(track.title) < 40: + ddt.text((x, y, 2), track.title, t1, 217, w) + else: + ddt.text((x, y, 2), track.title, t1, 213, w) + + elif len(track.title) < 35: + ddt.text((x, y, 2), track.title, t1, 220, w) + elif len(track.title) < 50: + ddt.text((x, y, 2), track.title, t1, 219, w) + else: + ddt.text((x, y, 2), track.title, t1, 216, w) + + gui.spec4_rec.x = x - (gui.spec4_rec.w // 2) + gui.spec4_rec.y = y + round(50 * gui.scale) + + if prefs.showcase_vis and window_size[1] > 369 and not search_over.active and not ( + tauon.spot_ctl.coasting or tauon.spot_ctl.playing): + + if gui.message_box or not is_level_zero(include_menus=True): + self.render_vis() + else: + gui.draw_vis4_top = True + + else: + x += box + int(window_size[0] * 0.15) + 10 * gui.scale + x -= 100 * gui.scale + w = window_size[0] - x - 30 * gui.scale + + if key_up_press and not (key_ctrl_down or key_shift_down or key_shiftr_down): + lyrics_ren.lyrics_position += 35 * gui.scale + if key_down_press and not (key_ctrl_down or key_shift_down or key_shiftr_down): + lyrics_ren.lyrics_position -= 35 * gui.scale + + lyrics_ren.test_update(track) + tw, th = ddt.get_text_wh(lyrics_ren.text + "\n", 17, w, True) + + lyrics_ren.lyrics_position = max(lyrics_ren.lyrics_position, th * -1 + 100 * gui.scale) + lyrics_ren.lyrics_position = min(lyrics_ren.lyrics_position, 70 * gui.scale) + + lyrics_ren.render( + x, + y + lyrics_ren.lyrics_position, + w, + int(window_size[1] - 100 * gui.scale), + 0) + ddt.alpha_bg = False + ddt.force_gray = False + + def render_vis(self, top=False): + + SDL_SetRenderTarget(renderer, gui.spec4_tex) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_RenderClear(renderer) + + bx = 0 + by = 50 * gui.scale + + if gui.vis_4_colour is not None: + SDL_SetRenderDrawColor( + renderer, gui.vis_4_colour[0], gui.vis_4_colour[1], gui.vis_4_colour[2], gui.vis_4_colour[3]) + + if (pctl.playing_time < 0.5 and (pctl.playing_state == 1 or pctl.playing_state == 3)) or ( + pctl.playing_state == 0 and gui.spec4_array.count(0) != len(gui.spec4_array)): + gui.update = 2 + gui.level_update = True + + for i in range(len(gui.spec4_array)): + gui.spec4_array[i] -= 0.1 + gui.spec4_array[i] = max(gui.spec4_array[i], 0) + + if not top and (pctl.playing_state == 1 or pctl.playing_state == 3): + gui.update = 2 + + slide = 0.7 + for i, bar in enumerate(gui.spec4_array): + + # We wont draw higher bars that may not move + if i > 40: + break + + # Scale input amplitude to pixel distance (Applying a slight exponentional) + dis = (2 + math.pow(bar / (2 + slide), 1.5)) + slide -= 0.03 # Set a slight bias for higher bars + + # Define colour for bar + if gui.vis_4_colour is None: + set_colour( + hsl_to_rgb( + 0.7 + min(0.15, (bar / 150)) + pctl.total_playtime / 300, min(0.9, 0.7 + (dis / 300)), + min(0.9, 0.7 + (dis / 600)))) + + # Define bar size and draw + gui.bar4.x = int(bx) + gui.bar4.y = round(by - dis * gui.scale) + gui.bar4.w = round(2 * gui.scale) + gui.bar4.h = round(dis * 2 * gui.scale) + + SDL_RenderFillRect(renderer, gui.bar4) + + # Set distance between bars + bx += 8 * gui.scale + + if top: + SDL_SetRenderTarget(renderer, None) + else: + SDL_SetRenderTarget(renderer, gui.main_texture) + + # SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + SDL_RenderCopy(renderer, gui.spec4_tex, None, gui.spec4_rec) + +class ColourPulse2: + """Animates colour between two colours""" + def __init__(self): + + self.timer = Timer() + self.in_timer = Timer() + self.out_timer = Timer() + self.out_timer.start = 0 + self.active = False + + def get(self, hit, on, off, low_hls, high_hls): + + if on: + return high_hls + # rgb = colorsys.hls_to_rgb(high_hls[0], high_hls[1], high_hls[2]) + # return [int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255), 255] + if off: + return low_hls + # rgb = colorsys.hls_to_rgb(low_hls[0], low_hls[1], low_hls[2]) + # return [int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255), 70] + + ani_time = 0.15 + + if hit is True and self.active is False: + self.active = True + self.in_timer.set() + + out_time = self.out_timer.get() + if out_time < ani_time: + self.in_timer.force_set(ani_time - out_time) + + elif hit is False and self.active is True: + self.active = False + self.out_timer.set() + + in_time = self.in_timer.get() + if in_time < ani_time: + self.out_timer.force_set(ani_time - in_time) + + pro = 0.5 + if self.active: + time = self.in_timer.get() + if time <= 0: + pro = 0 + elif time >= ani_time: + pro = 1 + else: + pro = time / ani_time + gui.update = 2 + else: + time = self.out_timer.get() + if time <= 0: + pro = 1 + elif time >= ani_time: + pro = 0 + else: + pro = 1 - (time / ani_time) + gui.update = 2 + + return colour_slide(low_hls, high_hls, pro, 1) + + +class ViewBox: + + def __init__(self, reload=False): + self.x = 0 + self.y = gui.panelY + self.w = 52 * gui.scale + self.h = 260 * gui.scale # 257 + self.active = False + + self.border = 3 * gui.scale + + self.tracks_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "tracks.png", True) + self.side_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "tracks+side.png", True) + self.gallery1_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "gallery1.png", True) + self.gallery2_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "gallery2.png", True) + self.combo_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "combo.png", True) + self.lyrics_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "lyrics.png", True) + self.gallery2_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "gallery2.png", True) + self.radio_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "radio.png", True) + self.col_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "col.png", True) + # self.artist_img = asset_loader(scaled_asset_directory, loaded_asset_dc, "artist.png", True) + + # _ .15 0 + self.tracks_colour = ColourPulse2() # (0.5) # .5 .6 .75 + self.side_colour = ColourPulse2() # (0.55) # .55 .6 .75 + self.gallery1_colour = ColourPulse2() # (0.6) # .6 .6 .75 + self.radio_colour = ColourPulse2() # (0.6) # .6 .6 .75 + # self.combo_colour = ColourPulse(0.75) + self.lyrics_colour = ColourPulse2() # (0.7) + # self.gallery2_colour = ColourPulse(0.65) + self.col_colour = ColourPulse2() # (0.14) + self.artist_colour = ColourPulse2() # (0.2) + + self.on_colour = [255, 190, 50, 255] + self.over_colour = [255, 190, 50, 255] + self.off_colour = colours.grey(40) + + if not reload: + gui.combo_was_album = False + + def activate(self, x): + self.x = x + self.active = True + self.clicked = False + + self.tracks_colour.out_timer.force_set(10) + self.side_colour.out_timer.force_set(10) + self.gallery1_colour.out_timer.force_set(10) + self.radio_colour.out_timer.force_set(10) + # self.combo_colour.out_timer.force_set(10) + self.lyrics_colour.out_timer.force_set(10) + # self.gallery2_colour.out_timer.force_set(10) + self.col_colour.out_timer.force_set(10) + self.artist_colour.out_timer.force_set(10) + + self.tracks_colour.active = False + self.side_colour.active = False + self.gallery1_colour.active = False + self.radio_colour.active = False + # self.combo_colour.active = False + self.lyrics_colour.active = False + # self.gallery2_colour.active = False + self.col_colour.active = False + self.artist_colour.active = False + + self.col_force_off = False + + # gui.level_2_click = False + gui.update = 2 + + def button(self, x, y, asset, test, colour_get=None, name="Unknown", animate=True, low=0, high=0): + + on = test() + rect = [x - 8 * gui.scale, + y - 8 * gui.scale, + asset.w + 16 * gui.scale, + asset.h + 16 * gui.scale] + fields.add(rect) + + if on: + colour = self.on_colour + + else: + colour = self.off_colour + + fun = None + col = False + if coll(rect): + + tool_tip.test(x + asset.w + 10 * gui.scale, y - 15 * gui.scale, name) + + col = True + if gui.level_2_click: + fun = test + if colour_get is None: + colour = self.over_colour + + colour = colour_get.get(col, on, not on and not animate, low, high) + + # if "+" in name: + # + # colour = cctest.get(col, on, [0, 0.2, 0.0], [0, 0.8, 0.8]) + + # if not on and not animate: + # colour = self.off_colour + + asset.render(x, y, colour) + + return fun + + def tracks(self, hit=False): + + if hit is False: + return album_mode is False and \ + gui.combo_mode is False and \ + gui.rsp is False + + if not (album_mode is False and \ + gui.combo_mode is False and \ + gui.rsp is False): + if x_menu.active: + x_menu.close_next_frame = True + + view_tracks() + + def side(self, hit=False): + + if hit is False: + return album_mode is False and \ + gui.combo_mode is False and \ + gui.rsp is True + if not (album_mode is False and \ + gui.combo_mode is False and \ + gui.rsp is True): + if x_menu.active: + x_menu.close_next_frame = True + + view_standard_meta() + + def gallery1(self, hit: bool = False) -> bool | None: + + if hit is False: + return album_mode is True # and gui.show_playlist is True + + if album_mode and not gui.combo_mode: + gui.hide_tracklist_in_gallery ^= True + gui.rspw = gui.pref_gallery_w + gui.update_layout() + # x_menu.active = False + x_menu.close_next_frame = True + # Menu.active = False + return None + + if x_menu.active: + x_menu.close_next_frame = True + + force_album_view() + + def radio(self, hit=False): + + if hit is False: + return gui.radio_view + + if not gui.radio_view: + enter_radio_view() + else: + exit_combo(restore=True) + + if x_menu.active: + x_menu.close_next_frame = True + + def lyrics(self, hit=False): + + if hit is False: + return gui.showcase_mode + + if not gui.showcase_mode: + if gui.radio_view: + gui.was_radio = True + enter_showcase_view() + + elif gui.was_radio: + enter_radio_view() + else: + exit_combo(restore=True) + if x_menu.active: + x_menu.close_next_frame = True + + def col(self, hit=False): + + if hit is False: + return gui.set_mode + + if not gui.set_mode: + if gui.combo_mode: + exit_combo() + + if album_mode and gui.plw < 550 * gui.scale: + toggle_album_mode() + + toggle_library_mode() + + def artist_info(self, hit=False): + + if hit is False: + return gui.artist_info_panel + + gui.artist_info_panel ^= True + gui.update_layout() + + def render(self): + + if prefs.shuffle_lock: + self.active = False + self.clicked = False + return + + if not self.active: + return + + # rect = [self.x, self.y, self.w, self.h] + # if x_menu.clicked or inp.mouse_click: + if self.clicked: + gui.level_2_click = True + self.clicked = False + + x = self.x - 40 * gui.scale + + vr = [x, gui.panelY, self.w, self.h] + # vr = [x, gui.panelY, 52 * gui.scale, 220 * gui.scale] + + border_colour = colours.menu_tab # colours.grey(30) + if colours.lm: + ddt.rect((vr[0], vr[1], vr[2] + round(4 * gui.scale), vr[3]), border_colour) + else: + ddt.rect( + (vr[0] - round(4 * gui.scale), vr[1], vr[2] + round(8 * gui.scale), + vr[3] + round(4 * gui.scale)), border_colour) + ddt.rect(vr, colours.menu_background) + + x += 7 * gui.scale + y = gui.panelY + 14 * gui.scale + + func = None + + # low = (0, .15, 0) + # low = (0, .40, 0) + # low = rgb_to_hls(*alpha_blend(colours.menu_icons, colours.menu_background)[:3]) # fix me + low = alpha_blend(colours.menu_icons, colours.menu_background) + + # if colours.lm: + # low = (0, 0.5, 0) + + # ---- + #logging.info(hls_to_rgb(.55, .6, .75)) + high = [76, 183, 229, 255] # (.55, .6, .75) + if colours.lm: + # high = (.55, .75, .75) + high = [63, 63, 63, 255] + + test = self.button(x, y, self.side_img, self.side, self.side_colour, _("Tracks + Art"), low=low, high=high) + if test is not None: + func = test + + # ---- + + y += 40 * gui.scale + + high = [76, 137, 229, 255] # (.6, .6, .75) + if colours.lm: + # high = (.6, .80, .85) + high = [63, 63, 63, 255] + + if gui.hide_tracklist_in_gallery: + test = self.button( + x - round(1 * gui.scale), y, self.gallery2_img, self.gallery1, self.gallery1_colour, + _("Gallery"), low=low, high=high) + else: + test = self.button( + x, y, self.gallery1_img, self.gallery1, self.gallery1_colour, _("Gallery"), low=low, high=high) + if test is not None: + func = test + + # --- + + y += 40 * gui.scale + + high = [76, 229, 229, 255] + if colours.lm: + # high = (.5, .7, .65) + high = [63, 63, 63, 255] + + test = self.button( + x + 3 * gui.scale, y, self.tracks_img, self.tracks, self.tracks_colour, _("Tracks only"), + low=low, high=high) + if test is not None: + func = test + + # --- + + y += 45 * gui.scale + + high = [107, 76, 229, 255] + if colours.lm: + # high = (.7, .75, .75) + high = [63, 63, 63, 255] + + test = self.button( + x + 4 * gui.scale, y, self.lyrics_img, self.lyrics, self.lyrics_colour, + _("Showcase + Lyrics"), low=low, high=high) + if test is not None: + func = test + + # -- + + y += 40 * gui.scale + + high = [92, 86, 255, 255] + if colours.lm: + # high = (.7, .75, .75) + high = [63, 63, 63, 255] + + test = self.button( + x + 3 * gui.scale, y, self.radio_img, self.radio, self.radio_colour, _("Radio"), low=low, high=high) + if test is not None: + func = test + + # -- + + y += 45 * gui.scale + + high = [229, 205, 76, 255] + if colours.lm: + # high = (.9, .75, .65) + high = [63, 63, 63, 255] + + test = self.button( + x + 5 * gui.scale, y, self.col_img, self.col, self.col_colour, _("Toggle columns"), False, low=low, high=high) + if test is not None: + func = test + + # -- + + # y += 41 * gui.scale + # + # high = [198, 229, 76, 255] + # if colours.lm: + # #high = (.2, .6, .75) + # high = [63, 63, 63, 255] + # + # if gui.scale == 1.25: + # x-= 1 + # + # test = self.button(x + 2 * gui.scale, y, self.artist_img, self.artist_info, self.artist_colour, _("Toggle artist info"), False, low=low, high=high) + # if test is not None: + # func = test + + if func is not None: + func(True) + + if gui.level_2_click and coll(vr): + x_menu.clicked = False + + gui.level_2_click = False + if not x_menu.active: + self.active = False + +class DLMon: + + def __init__(self): + + self.ticker = Timer() + self.ticker.force_set(8) + + self.watching = {} + self.ready = set() + self.done = set() + + def scan(self): + + if len(self.watching) == 0: + if self.ticker.get() < 10: + return + elif self.ticker.get() < 2: + return + + self.ticker.set() + + for downloads in download_directories: + + for item in os.listdir(downloads): + + path = os.path.join(downloads, item) + + if path in self.done: + continue + + if path in self.ready and not os.path.exists(path): + del self.ready[path] + continue + + if path in self.watching and not os.path.exists(path): + del self.watching[path] + continue + + # stamp = os.stat(path)[stat.ST_MTIME] + try: + stamp = os.path.getmtime(path) + except Exception: + logging.exception(f"Failed to scan item at {path}") + self.done.add(path) + continue + + min_age = (time.time() - stamp) / 60 + ext = os.path.splitext(path)[1][1:].lower() + + if msys and "TauonMusicBox" in path: + continue + + if min_age < 240 and os.path.isfile(path) and ext in Archive_Formats: + size = os.path.getsize(path) + #logging.info("Check: " + path) + if path in self.watching: + # Check if size is stable, then scan for audio files + #logging.info("watching...") + if size == self.watching[path] and size != 0: + #logging.info("scan") + del self.watching[path] + + # Check if folder to extract to exists + split = os.path.splitext(path) + target_dir = split[0] + if prefs.extract_to_music and music_directory is not None: + target_dir = os.path.join(str(music_directory), os.path.basename(target_dir)) + + if os.path.exists(target_dir): + pass + #logging.info("Target folder for archive already exists") + + elif archive_file_scan(path, DA_Formats, launch_prefix) >= 0.4: + self.ready.add(path) + gui.update += 1 + #logging.info("Archive detected as music") + else: + pass + #logging.info("Archive rejected as music") + self.done.add(path) + else: + #logging.info("update.") + self.watching[path] = size + else: + self.watching[path] = size + #logging.info("add.") + + elif min_age < 60 \ + and os.path.isdir(path) \ + and path not in quick_import_done \ + and "encode-output" not in path: + try: + size = get_folder_size(path) + except FileNotFoundError: + logging.warning(f"Failed to find watched folder {path}, deleting from watchlist") + if path in self.watching: + del self.watching[path] + continue + except Exception: + logging.exception("Unknown error getting folder size") + if path in self.watching: + # Check if size is stable, then scan for audio files + if size == self.watching[path]: + del self.watching[path] + if folder_file_scan(path, DA_Formats) > 0.5: + + # Check if folder not already imported + imported = False + for pl in pctl.multi_playlist: + for i in pl.playlist_ids: + if path.replace("\\", "/") == pctl.master_library[i].fullpath[:len(path)]: + imported = True + if imported: + break + if imported: + break + else: + self.ready.add(path) + gui.update += 1 + self.done.add(path) + else: + self.watching[path] = size + else: + self.watching[path] = size + else: + self.done.add(path) + + if len(self.ready) > 0: + temp = set() + #logging.info(quick_import_done) + #logging.info(self.ready) + for item in self.ready: + if item not in quick_import_done: + if os.path.exists(path): + temp.add(item) + # else: + # logging.info("FILE IMPORTED") + self.ready = temp + + if len(self.watching) > 0: + gui.update += 1 + +class Fader: + + def __init__(self): + + self.total_timer = Timer() + self.timer = Timer() + self.ani_duration = 0.3 + self.state = 0 # 0 = Want off, 1 = Want fade on + self.a = 0 # The fade progress (0-1) + + def render(self): + + if self.total_timer.get() > self.ani_duration: + self.a = self.state + elif self.state == 0: + t = self.timer.hit() + self.a -= t / self.ani_duration + self.a = max(0, self.a) + elif self.state == 1: + t = self.timer.hit() + self.a += t / self.ani_duration + self.a = min(1, self.a) + + rect = [0, 0, window_size[0], window_size[1]] + ddt.rect(rect, [0, 0, 0, int(110 * self.a)]) + + if not (self.a == 0 or self.a == 1): + gui.update += 1 + + def rise(self): + + self.state = 1 + self.timer.hit() + self.total_timer.set() + + def fall(self): + + self.state = 0 + self.timer.hit() + self.total_timer.set() + +class EdgePulse: + + def __init__(self): + + self.timer = Timer() + self.timer.force_set(10) + self.ani_duration = 0.5 + + def render(self, x, y, w, h, r=200, g=120, b=0) -> bool: + r = colours.pluse_colour[0] + g = colours.pluse_colour[1] + b = colours.pluse_colour[2] + time = self.timer.get() + if time < self.ani_duration: + alpha = 255 - int(255 * (time / self.ani_duration)) + ddt.rect((x, y, w, h), [r, g, b, alpha]) + gui.update = 2 + return True + return False + + def pulse(self): + self.timer.set() + + +class EdgePulse2: + + def __init__(self): + + self.timer = Timer() + self.timer.force_set(10) + self.ani_duration = 0.22 + + def render(self, x, y, w, h, bottom=False) -> bool | None: + + time = self.timer.get() + if time < self.ani_duration: + + if bottom: + if mouse_wheel > 0: + self.timer.force_set(10) + return None + elif mouse_wheel < 0: + self.timer.force_set(10) + return None + + alpha = 30 - int(25 * (time / self.ani_duration)) + h_off = (h // 5) * (time / self.ani_duration) * 4 + + if colours.lm: + colour = (0, 0, 0, alpha) + else: + colour = (255, 255, 255, alpha) + + if not bottom: + ddt.rect((x, y, w, h - h_off), colour) + else: + ddt.rect((x, y - (h - h_off), w, h - h_off), colour) + gui.update = 2 + return True + return False + + def pulse(self): + self.timer.set() + +class Undo: + + def __init__(self): + + self.e = [] + + def undo(self): + + if not self.e: + show_message(_("There are no more steps to undo.")) + return + + job = self.e.pop() + + if job[0] == "playlist": + pctl.multi_playlist.append(job[1]) + switch_playlist(len(pctl.multi_playlist) - 1) + elif job[0] == "tracks": + + uid = job[1] + li = job[2] + + for i, playlist in enumerate(pctl.multi_playlist): + if playlist.uuid_int == uid: + pl = playlist.playlist_ids + switch_playlist(i) + break + else: + logging.info("No matching playlist ID to restore tracks to") + return + + for i, ref in reversed(li): + + if i > len(pl): + logging.error("restore track error - playlist not correct length") + continue + pl.insert(i, ref) + + if not pctl.playlist_view_position < i < pctl.playlist_view_position + gui.playlist_view_length: + pctl.playlist_view_position = i + logging.debug("Position changed by undo") + elif job[0] == "ptt": + j, fr, fr_s, fr_scr, so, to_s, to_scr = job + star_store.insert(fr.index, fr_s) + star_store.insert(to.index, to_s) + to.lfm_scrobbles = to_scr + fr.lfm_scrobbles = fr_scr + + gui.pl_update = 1 + + def bk_playlist(self, pl_index: int) -> None: + + self.e.append(("playlist", pctl.multi_playlist[pl_index])) + + def bk_tracks(self, pl_index: int, indis) -> None: + + uid = pctl.multi_playlist[pl_index].uuid_int + self.e.append(("tracks", uid, indis)) + + def bk_playtime_transfer(self, fr, fr_s, fr_scr, so, to_s, to_scr) -> None: + self.e.append(("ptt", fr, fr_s, fr_scr, so, to_s, to_scr)) + +class Directories: + def __init__(self, *, + install_directory: Path, + svg_directory: Path, + asset_directory: Path, + scaled_asset_directory: Path, + locale_directory: Path, + user_directory: Path, + config_directory: Path, + cache_directory: Path, + home_directory: Path, + music_directory: Path, + download_directory: Path, + ) -> None: + + self.install_directory = install_directory + self.svg_directory = svg_directory + self.asset_directory = asset_directory + self.scaled_asset_directory = scaled_asset_directory + self.locale_directory = locale_directory + self.user_directory = user_directory + self.config_directory = config_directory + self.cache_directory = cache_directory + self.home_directory = home_directory + self.music_directory = music_directory + self.download_directory = download_directory