From 455399d5f32361bc10202fbf3d6161860a3b668e Mon Sep 17 00:00:00 2001 From: Martin Rys Date: Sat, 11 Jan 2025 17:18:35 +0100 Subject: [PATCH 01/13] Large refactor WIP Somewhat arrange to current upstream state Small Prefs fix up Bootstrap: Use dataclass t_main reworks Move classes on top of main More reworking GuiVar: Remove duplicated macOS detection More rework More rework, create class Formats, remove global formats Fix colon convention Remove empty rework file More refactoring Add more typing More rework Remove old comment Refactor Refactor fixups Moar refactor Refactor class Tauon Fix up asset_loader() Moar refactor More reworks Remove doubled ctypes import More refactor Further refactor and add accidentally deleted classes back Premove touchups Add Bag to Tauon and continue refactoring pctl reworks Move some things around and rename devance func to "previous" further reworks Further reworks Further reworks More refactoring, convert Prefs to a dataclass, to be merged with Bag Small refactor Small refactor + adapt some upstream changes fix lastfm url Start using classes for Radio playlists and stations (#1422) * Radio changes WIP * Fix ups to get it running * Add migrations * Convert more code * Fix broken buttons * Do not wipe website URL if stream URL hasn't changed * Handle radio websocket better * Fix indent * Add the default station fallback URL back * Fix up adding radio and m3u radio * Fix if * Fix comma Fix merge issues Start up more refactors Further reworks and global eliminations More reworks More reworks, just kill me already Theme refactor Shoot me noooow, this is a call for heeeelp MORE REWORKS Probably enough for today More input refactors Fix get_track_in_playlist() Stash some more reworks Further refactors Simplify Menu Moar Reeefactor More refactor --- src/tauon/__main__.py | 58 +- src/tauon/t_modules/t_bootstrap.py | 34 +- src/tauon/t_modules/t_db_migrate.py | 30 +- src/tauon/t_modules/t_extra.py | 22 +- src/tauon/t_modules/t_main.py | 74543 +++++++++++++------------- src/tauon/t_modules/t_prefs.py | 644 +- src/tauon/t_modules/t_stream.py | 8 +- 7 files changed, 37263 insertions(+), 38076 deletions(-) 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..01417be42 100644 --- a/src/tauon/t_modules/t_bootstrap.py +++ b/src/tauon/t_modules/t_bootstrap.py @@ -1,44 +1,42 @@ from __future__ import annotations -#from dataclasses import dataclass +from dataclasses import dataclass from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Callable from io import TextIOWrapper from pathlib import Path - from typing import Any + from sdl2 import render, video from tauon.__main__ import LogHistoryHandler -#@dataclass +@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 + t_window: video.LP_SDL_Window # SDL_CreateWindow() return type + renderer: render.LP_SDL_Renderer # 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 + 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") + 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" + 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() diff --git a/src/tauon/t_modules/t_db_migrate.py b/src/tauon/t_modules/t_db_migrate.py index 6945bef7b..dfbf2fc39 100644 --- a/src/tauon/t_modules/t_db_migrate.py +++ b/src/tauon/t_modules/t_db_migrate.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from tauon.t_modules.t_extra import TauonPlaylist, TauonQueueItem +from tauon.t_modules.t_extra import RadioPlaylist, RadioStation, TauonPlaylist, TauonQueueItem if TYPE_CHECKING: from tauon.t_modules.t_main import GuiVar, Prefs, StarStore, TrackClass @@ -28,7 +28,7 @@ def database_migrate( gui: GuiVar, gen_codes: dict[int, str], prefs: Prefs, - radio_playlists: list[TauonPlaylist], + radio_playlists: list[dict[str, int | str | list[dict[str, str]]]] | list[RadioPlaylist], p_force_queue: list | list[TauonQueueItem], theme: int, ) -> tuple[ @@ -40,7 +40,7 @@ def database_migrate( Prefs, GuiVar, dict[int, str], - list[TauonPlaylist]]: + list[RadioPlaylist]]: """Migrate database to a newer version if we're behind Returns all the objects that could've been possibly changed: @@ -543,4 +543,28 @@ def database_migrate( multi_playlist = new_multi_playlist p_force_queue = new_queue + if db_version <= 69: + logging.info("Updating database to version 69") + new_radio_playlists: list[RadioPlaylist] = [] + for playlist in radio_playlists: + stations: list[RadioStation] = [] + + for station in playlist["items"]: + stations.append( + RadioStation( + title=station["title"], + stream_url=station["stream_url"], + country=station.get("country", ""), + website_url=station.get("website_url", ""), + icon=station.get("icon", ""), + stream_url_fallback=station.get("stream_url_unresolved", ""))) + new_radio_playlists.append( + RadioPlaylist( + uid=playlist["uid"], + name=playlist["name"], + scroll=playlist.get("scroll", 0), + stations=stations)) + radio_playlists = new_radio_playlists + + return master_library, multi_playlist, star_store, p_force_queue, theme, prefs, gui, gen_codes, radio_playlists diff --git a/src/tauon/t_modules/t_extra.py b/src/tauon/t_modules/t_extra.py index 35bd2f3ca..3b4037cf5 100644 --- a/src/tauon/t_modules/t_extra.py +++ b/src/tauon/t_modules/t_extra.py @@ -35,7 +35,7 @@ import time import urllib.parse import zipfile -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import TYPE_CHECKING from gi.repository import GLib @@ -45,6 +45,21 @@ from tauon.t_modules.t_main import TrackClass +@dataclass +class RadioStation: + title: str + stream_url: str + country: str = "" + website_url: str = "" + icon: str = "" + stream_url_fallback: str = "" + +@dataclass +class RadioPlaylist: + name: str + uid: int + scroll: int = 0 + stations: list[RadioStation] = field(default_factory=list) @dataclass class TauonQueueItem: @@ -176,8 +191,11 @@ def rm_16(line: str) -> str: return line -def get_display_time(seconds: str) -> str: +def get_display_time(seconds: float) -> str: """Returns a string from seconds to a compact time format, e.g 2h:23""" + if math.isinf(seconds) or math.isnan(seconds): + logging.error("Infinite/NaN time passed to get_display_time()!") + return "??:??" result = divmod(int(seconds), 60) if result[0] > 99: result = divmod(result[0], 60) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index f740706bf..d22f11735 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -6,6 +6,10 @@ learning python, as a result this code can be quite messy. No doubt I have written some things terribly wrong or inefficiently in places. I would highly recommend not using this project as an example on how to code cleanly or correctly. + +TODO + +Verify the rework actually uses copies where copies should be used! """ # Copyright © 2015-2024, Taiko2k captain(dot)gxj(at)gmail.com @@ -66,6 +70,7 @@ 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 dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING @@ -77,6 +82,7 @@ import mutagen.oggvorbis import requests from bs4 import BeautifulSoup +from dataclasses import dataclass from PIL import Image, ImageDraw, ImageEnhance, ImageFilter from sdl2 import ( SDL_BLENDMODE_BLEND, @@ -261,7 +267,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 @@ -273,6 +278,8 @@ TauonQueueItem, TestTimer, Timer, + RadioPlaylist, + RadioStation, alpha_blend, alpha_mod, archive_file_scan, @@ -338,511 +345,35 @@ 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 if TYPE_CHECKING: from ctypes import CDLL from io import BufferedReader, BytesIO from pylast import Artist, LibreFMNetwork from PIL.ImageFile import ImageFile - -# 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()) - -# TLS setup (needed for frozen installs) -def get_cert_path() -> str: - if pyinstaller_mode: - return os.path.join(sys._MEIPASS, 'certifi', 'cacert.pem') - # Running as script - return certifi.where() - - -def setup_ssl() -> ssl.SSLContext: - # Set the SSL certificate path environment variable - cert_path = get_cert_path() - 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()) - return ssl_context - -ssl_context = setup_ssl() - - - -# Detect what desktop environment we are in to enable specific features -desktop = os.environ.get("XDG_CURRENT_DESKTOP") -# de_notify_support = desktop == 'GNOME' or desktop == 'KDE' -de_notify_support = False -draw_min_button = True -draw_max_button = True -left_window_control = False -xdpi = 0 - -detect_macstyle = False -gtk_settings: Settings | None = None -mac_close = (253, 70, 70, 255) -mac_maximize = (254, 176, 36, 255) -mac_minimize = (42, 189, 49, 255) -try: - # TODO(Martin): Bump to 4.0 - https://github.com/Taiko2k/Tauon/issues/1316 - gi.require_version("Gtk", "3.0") - from gi.repository import Gtk - - gtk_settings = Gtk.Settings().get_default() - xdpi = gtk_settings.get_property("gtk-xft-dpi") / 1024 - if "minimize" not in str(gtk_settings.get_property("gtk-decoration-layout")): - draw_min_button = False - if "maximize" not in str(gtk_settings.get_property("gtk-decoration-layout")): - draw_max_button = False - if "close" in str(gtk_settings.get_property("gtk-decoration-layout")).split(":")[0]: - left_window_control = True - gtk_theme = str(gtk_settings.get_property("gtk-theme-name")).lower() - #logging.info(f"GTK theme is: {gtk_theme}") - for k, v in mac_styles.items(): - if k in gtk_theme: - detect_macstyle = True - if v is not None: - mac_close = v[0] - mac_maximize = v[1] - mac_minimize = v[2] - -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") - - -# 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: - try: - if flatpak_mode: - complete = subprocess.run( - shlex.split("flatpak-spawn --host which " + target), stdout=subprocess.PIPE, - stderr=subprocess.PIPE, check=True) - r = complete.stdout.decode() - return "bin/" + target in r - return shutil.which(target) - except Exception: - logging.exception("Failed to run flatpak-spawn") - 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) + from tauon.t_modules.t_bootstrap import Holder 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: + def __init__(self, *, bag: Bag, 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 + self.scaled_asset_directory: Path = bag.dirs.scaled_asset_directory raw_image = IMG_Load(self.path.encode()) - self.sdl_texture = SDL_CreateTextureFromSurface(renderer, raw_image) + self.sdl_texture = SDL_CreateTextureFromSurface(bag.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) + SDL_SetTextureAlphaMod(self.sdl_texture, bag.prefs.custom_bg_opacity) self.rect = SDL_Rect(0, 0, p_w.contents.value, p_h.contents.value) SDL_FreeSurface(raw_image) @@ -853,26 +384,26 @@ 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) + self.__init__(scaled_asset_directory=self.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 = ""): + def __init__(self, *, bag: Bag, path: str, reload: bool = False, scale_name: str = ""): + self.bag = bag if not reload: self.assets.append(self) self.path = path self.scale_name = scale_name - self.scaled_asset_directory: Path = scaled_asset_directory + self.scaled_asset_directory: Path = self.bag.dirs.scaled_asset_directory raw_image = IMG_Load(path.encode()) - self.sdl_texture = SDL_CreateTextureFromSurface(renderer, raw_image) + self.sdl_texture = SDL_CreateTextureFromSurface(self.bag.renderer, raw_image) self.colour = [255, 255, 255, 255] p_w = pointer(c_int(0)) p_h = pointer(c_int(0)) @@ -886,7 +417,7 @@ 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) + self.__init__(scaled_asset_directory=self.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: @@ -895,142 +426,11 @@ def render(self, x: int, y: int, colour) -> None: 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] = {} - - -def asset_loader( - scaled_asset_directory: Path, loaded_asset_dc: dict[str, WhiteModImageAsset | LoadImageAsset], name: str, mod: bool = False, -) -> WhiteModImageAsset | LoadImageAsset: - if name in loaded_asset_dc: - return loaded_asset_dc[name] - - target = str(scaled_asset_directory / name) - if mod: - item = WhiteModImageAsset(scaled_asset_directory=scaled_asset_directory, path=target, scale_name=name) - else: - item = LoadImageAsset(scaled_asset_directory=scaled_asset_directory, path=target, scale_name=name) - 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") - -# logging.info(arch) -# ----------------------------------------------------------- -# Detect locale for translations - -try: - py_locale.setlocale(py_locale.LC_ALL, "") -except Exception: - logging.exception("SET LOCALE ERROR") - -# ------------------------------------------------ - -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 - - - -def no_padding() -> int: - """This will remove all padding""" - return 0 - -wayland = True -if os.environ.get("SDL_VIDEODRIVER") != "wayland": - wayland = False - os.environ["GDK_BACKEND"] = "x11" - - -# Setting various timers - -message_box_min_timer = Timer() -cursor_blink_timer = Timer() -animate_monitor_timer = Timer() -min_render_timer = Timer() -check_file_timer = Timer() -vis_rate_timer = Timer() -vis_decay_timer = Timer() -scroll_timer = Timer() -perf_timer = Timer() -quick_d_timer = Timer() -core_timer = Timer() -sleep_timer = Timer() -gallery_select_animate_timer = Timer() -gallery_select_animate_timer.force_set(10) -search_clear_timer = Timer() -gall_pl_switch_timer = Timer() -gall_pl_switch_timer.force_set(999) -d_click_timer = Timer() -d_click_timer.force_set(10) -lyrics_check_timer = Timer() -scroll_hide_timer = Timer(100) -scroll_gallery_hide_timer = Timer(100) -get_lfm_wait_timer = Timer(10) -lyrics_fetch_timer = Timer(10) -gallery_load_delay = Timer(10) -queue_add_timer = Timer(100) -toast_love_timer = Timer(100) -toast_mode_timer = Timer(100) -scrobble_warning_timer = Timer(1000) -sync_file_timer = Timer(1000) -sync_file_update_timer = Timer(1000) -sync_get_device_click_timer = Timer(100) - -f_store = FunctionStore() - -after_scan = [] - -search_string_cache = {} -search_dia_string_cache = {} - -vis_update = False - - -# GUI Variables ------------------------------------------------------------------------------------------- - -# Variables now go in the gui, pctl, input and prefs class instances. The following just haven't been moved yet + SDL_RenderCopy(self.bag.renderer, self.sdl_texture, None, self.rect) class DConsole: """GUI console with logs""" + def __init__(self) -> None: self.show: bool = False @@ -1038,303 +438,6 @@ def toggle(self) -> None: """Toggle the GUI console with logs on and off""" self.show ^= True -console = DConsole() - -spot_cache_saved_albums = [] - -resize_mode = False - -side_panel_text_align = 0 - -album_mode = False -spec_smoothing = True - -# gui.offset_extra = 0 - -old_album_pos = -55 - -album_dex = [] -album_artist_dict = {} -row_len = 5 -last_row = 0 -album_v_gap = 66 -album_h_gap = 30 -album_v_slide_value = 50 - -album_mode_art_size = int(200 * scale) - -time_last_save = 0 - -b_info_y = int(window_size[1] * 0.7) # For future possible panel below playlist - -volume_store = 50 # Used to save the previous volume when muted - -# row_alt = False - -to_get = 0 # Used to store temporary import count display -to_got = 0 - -editline = "" -# gui.rsp = True -quick_drag = False - -# Playlist Panel -pl_view_offset = 0 -pl_rect = (2, 12, 10, 10) - -theme = 7 -scroll_enable = True -scroll_timer = Timer() -scroll_timer.set() -scroll_opacity = 0 -break_enable = True - -source = None - -album_playlist_width = 430 - -update_title = False - -playlist_hold_position = 0 -playlist_hold = False -selection_stage = 0 - -selected_in_playlist = -1 - -shift_selection = [] - -gen_codes: dict[int, str] = {} -# Control Variables-------------------------------------------------------------------------- - -mouse_down = False -right_down = False -click_location = [200, 200] -last_click_location = [0, 0] -mouse_position = [0, 0] -mouse_up_position = [0, 0] - -k_input = True -key_shift_down = False -drag_mode = False -side_drag = False -clicked = False - -# Player Variables---------------------------------------------------------------------------- - -format_colours = { # These are the colours used for the label icon in UI 'track info box' - "MP3": [255, 130, 80, 255], # Burnt orange - "FLAC": [156, 249, 79, 255], # Bright lime green - "M4A": [81, 220, 225, 255], # Soft cyan - "AIFF": [81, 220, 225, 255], # Soft cyan - "OGG": [244, 244, 78, 255], # Light yellow - "OGA": [244, 244, 78, 255], # Light yellow - "WMA": [213, 79, 247, 255], # Magenta - "APE": [247, 79, 79, 255], # Deep pink - "TTA": [94, 78, 244, 255], # Purple - "OPUS": [247, 79, 146, 255], # Pink - "AAC": [79, 247, 168, 255], # Teal - "WV": [229, 23, 18, 255], # Deep red - "PLEX": [229, 160, 13, 255], # Orange-brown - "KOEL": [111, 98, 190, 255], # Lavender - "TAU": [111, 98, 190, 255], # Lavender - "SUB": [235, 140, 20, 255], # Golden yellow - "SPTY": [30, 215, 96, 255], # Bright green - "TIDAL": [0, 0, 0, 255], # Black - "JELY": [190, 100, 210, 255], # Fuchsia - "XM": [50, 50, 50, 255], # Grey - "MOD": [50, 50, 50, 255], # Grey - "S3M": [50, 50, 50, 255], # Grey - "IT": [50, 50, 50, 255], # Grey - "MPTM": [50, 50, 50, 255], # Grey - "AY": [237, 212, 255, 255], # Pastel purple - "GBS": [255, 165, 0, 255], # Vibrant orange - "GYM": [0, 191, 255, 255], # Bright blue - "HES": [176, 224, 230, 255], # Light blue-green - "KSS": [255, 255, 153, 255], # Bright yellow - "NSF": [255, 140, 0, 255], # Deep orange - "NSFE": [255, 140, 0, 255], # Deep orange - "SAP": [152, 255, 152, 255], # Light green - "SPC": [255, 128, 0, 255], # Bright orange - "VGM": [0, 128, 255, 255], # Deep blue - "VGZ": [0, 128, 255, 255], # Deep blue -} - -# These will be the extensions of files to be added when importing -VID_Formats = {"mp4", "webm"} - -MOD_Formats = {"xm", "mod", "s3m", "it", "mptm", "umx", "okt", "mtm", "669", "far", "wow", "dmf", "med", "mt2", "ult"} - -GME_Formats = {"ay", "gbs", "gym", "hes", "kss", "nsf", "nsfe", "sap", "spc", "vgm", "vgz"} - -DA_Formats = { - "mp3", "wav", "opus", "flac", "ape", "aiff", - "m4a", "ogg", "oga", "aac", "tta", "wv", "wma", -} | MOD_Formats | GME_Formats - -Archive_Formats = {"zip"} - -if whicher("unrar"): - Archive_Formats.add("rar") - -if whicher("7z"): - Archive_Formats.add("7z") - -cargo = [] - -# --------------------------------------------------------------------- -# Player variables - -# pl_follow = False - -# List of encodings to check for with the fix mojibake function -encodings = ["cp932", "utf-8", "big5hkscs", "gbk"] # These seem to be the most common for Japanese - -track_box = False - -transcode_list: list[list[int]] = [] -transcode_state = "" - -taskbar_progress = True -track_queue: list[int] = [] - -playing_in_queue = 0 -draw_sep_hl = False - -# ------------------------------------------------------------------------------- -# Playlist Variables -playlist_view_position = 0 -playlist_playing = -1 - -loading_in_progress = False - -core_use = 0 -dl_use = 0 - -random_mode = False -repeat_mode = False - - - - -def uid_gen() -> int: - return random.randrange(1, 100000000) - - -notify_change = lambda: None - - -def pl_gen( - title: str = "Default", - playing: int = 0, - playlist_ids: list[int] | None = None, - position: int = 0, - hide_title: bool = False, - selected: int = 0, - parent: str = "", - hidden: bool = False, -) -> TauonPlaylist: - """Generate a TauonPlaylist - - Creates a default playlist when called without parameters - """ - if playlist_ids == None: - playlist_ids = [] - - notify_change() - -# return copy.deepcopy([title, playing, playlist, position, hide_title, selected, uid_gen(), [], hidden, False, parent, False]) - return TauonPlaylist(title=title, playing=playing, playlist_ids=playlist_ids, position=position, hide_title=hide_title, selected=selected, uuid_int=uid_gen(), last_folder=[], hidden=hidden, locked=False, parent_playlist_id=parent, persist_time_positioning=False) - -multi_playlist: list[TauonPlaylist] = [pl_gen()] - - -def queue_item_gen(track_id: int, position: int, pl_id: int, type: int = 0, album_stage: int = 0) -> TauonQueueItem: - # type; 0 is track, 1 is album - auto_stop = False - -# return [track_id, position, pl_id, type, album_stage, uid_gen(), auto_stop] - return TauonQueueItem(track_id=track_id, position=position, playlist_id=pl_id, type=type, album_stage=album_stage, uuid_int=uid_gen(), auto_stop=auto_stop) - - -default_playlist: list[int] = multi_playlist[0].playlist_ids -playlist_active: int = 0 - -quick_search_mode = False -search_index = 0 - -# ---------------------------------------- -# Playlist right click menu - -r_menu_index = 0 -r_menu_position = 0 - -# Library and loader Variables-------------------------------------------------------- -master_library: dict[int, TrackClass] = {} - -cue_list = [] - -LC_None = 0 -LC_Done = 1 -LC_Folder = 2 -LC_File = 3 - -loaderCommand = LC_None -loaderCommandReady = False - -master_count = 0 - -load_orders = [] - -volume = 75 - -folder_image_offsets: dict[str, int] = {} -db_version: float = 0.0 -latest_db_version: float = 69 - -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, -) - - -def open_uri(uri:str) -> None: - logging.info("OPEN URI") - load_order = LoadClass() - - for w in range(len(pctl.multi_playlist)): - if pctl.multi_playlist[w].title == "Default": - load_order.playlist = pctl.multi_playlist[w].uuid_int - break - else: - logging.warning("'Default' playlist not found, generating a new one!") - pctl.multi_playlist.append(pl_gen()) - load_order.playlist = pctl.multi_playlist[len(pctl.multi_playlist) - 1].uuid_int - switch_playlist(len(pctl.multi_playlist) - 1) - - load_order.target = str(urllib.parse.unquote(uri)).replace("file:///", "/").replace("\r", "") - - if gui.auto_play_import is False: - load_order.play = True - gui.auto_play_import = True - - load_orders.append(copy.deepcopy(load_order)) - gui.update += 1 - - class GuiVar: """Use to hold any variables for use in relation to UI""" @@ -1346,7 +449,7 @@ def show_message(self, line1: str, line2: str = "", line3: str = "", mode: str = show_message(line1, line2, line3, mode=mode) def delay_frame(self, t): - gui.frame_callback_list.append(TestTimer(t)) + self.frame_callback_list.append(TestTimer(t)) def destroy_textures(self): SDL_DestroyTexture(self.spec4_tex) @@ -1381,7 +484,7 @@ def rescale(self): 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.scroll_hide_box = (0, self.panelY, 28, self.bag.window_size[1] - self.panelBY - self.panelY) self.spec2_y = int(round(22 * self.scale)) self.spec2_w = int(round(140 * self.scale)) @@ -1404,13 +507,13 @@ def rescale(self): 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.bag.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.bag.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.bag.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) + self.bag.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 @@ -1418,9 +521,11 @@ def rescale(self): self.window_control_hit_area_w = 100 * self.scale self.window_control_hit_area_h = 30 * self.scale - def __init__(self): + def __init__(self, bag: Bag, tracklist_texture_rect: SDL_Rect, tracklist_texture, album_v_slide_value: int, console: DConsole, main_texture_overlay_temp, main_texture, max_window_tex): + self.inp = Input(gui=self) - self.scale = prefs.ui_scale + self.bag = bag + self.scale = self.bag.prefs.ui_scale self.window_id = 0 self.update = 2 # UPDATE @@ -1430,6 +535,7 @@ def __init__(self): self.lowered = False self.request_raise = False self.maximized = False + self.side_drag = False self.message_box = False self.message_text = "" @@ -1481,7 +587,7 @@ def __init__(self): self.universal_y_text_offset = 0 self.star_text_y_offset = 0 - if system == "Windows": + if self.bag.system == "Windows": self.star_text_y_offset = -2 self.set_bar = True @@ -1505,7 +611,7 @@ def __init__(self): 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_rect: SDL_Rect = tracklist_texture_rect self.tracklist_texture = tracklist_texture self.trunk_end = "..." # "…" @@ -1562,7 +668,7 @@ def __init__(self): self.web_running = False self.rsp = True - if phone: + if self.bag.phone: self.rsp = False self.rspw = round(300 * self.scale) self.lsp = False @@ -1671,9 +777,9 @@ def __init__(self): self.main_texture = main_texture self.main_texture_overlay_temp = main_texture_overlay_temp - self.preview_artist = "" + self.preview_artist: str = "" self.preview_artist_location = (0, 0) - self.preview_artist_loading = "" + self.preview_artist_loading: str = "" self.mouse_left_window = False self.rendered_playlist_position = 0 @@ -1691,8 +797,8 @@ def __init__(self): 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_down_icon = asset_loader(self.bag, self.bag.loaded_asset_dc, "sort-down.png", True) + self.column_sort_up_icon = asset_loader(self.bag, self.bag.loaded_asset_dc, "sort-up.png", True) self.column_sort_ani_direction = 1 self.column_sort_ani_x = 0 @@ -1726,7 +832,7 @@ def __init__(self): self.backend_reloading = False - self.spot_info_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "spot-info.png", True) + self.spot_info_icon = asset_loader(self.bag, self.bag.loaded_asset_dc, "spot-info.png", True) self.tray_active = False self.buffering = False self.buffering_text = "" @@ -1736,11 +842,9 @@ def __init__(self): 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.macstyle = self.bag.prefs.macstyle self.radio_view = False - self.window_size = window_size + self.window_size = self.bag.window_size self.box_over = False self.suggest_clean_db = False self.style_worker_timer = Timer() @@ -1759,70 +863,26 @@ def __init__(self): # self.text_input_active = False self.center_blur_pixel = (0, 0, 0) - -gui = GuiVar() - - -def toast(text: str) -> None: - gui.mode_toast_text = text - toast_mode_timer.set() - gui.frame_callback_list.append(TestTimer(1.5)) - - -def set_artist_preview(path, artist, x, y): - m = min(round(500 * gui.scale), window_size[1] - (gui.panelY + gui.panelBY + 50 * gui.scale)) - artist_preview_render.load(path, box_size=(m, m)) - artist_preview_render.show = True - ah = artist_preview_render.size[1] - ay = round(y) - (ah // 2) - if ay < gui.panelY + 20 * gui.scale: - ay = gui.panelY + round(20 * gui.scale) - if ay + ah > window_size[1] - (gui.panelBY + 5 * gui.scale): - ay = window_size[1] - (gui.panelBY + ah + round(5 * gui.scale)) - gui.preview_artist = artist - gui.preview_artist_location = (x + 15 * gui.scale, ay) - - -def get_artist_preview(artist, x, y): - # show_message(_("Loading artist image...")) - - gui.preview_artist_loading = artist - artist_info_box.get_data(artist, force_dl=True) - path = artist_info_box.get_data(artist, get_img_path=True) - if not path: - show_message(_("No artist image found.")) - if not prefs.enable_fanart_artist and not verify_discogs(): - show_message(_("No artist image found."), _("No providers are enabled in settings!"), mode="warning") - gui.preview_artist_loading = "" - return - set_artist_preview(path, artist, x, y) - gui.message_box = False - gui.preview_artist_loading = "" - - -def set_drag_source(): - gui.drag_source_position = tuple(click_location) - gui.drag_source_position_persist = tuple(click_location) - - -# Functions for reading and setting play counts class StarStore: - def __init__(self) -> None: + def __init__(self, tauon: Tauon) -> None: + self.tauon = tauon + self.after_scan = tauon.after_scan + self.pctl = self.tauon.pctl self.db = {} def key(self, track_id: int) -> tuple[str, str, str]: - track_object = pctl.master_library[track_id] + track_object = self.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): + def add(self, index: int, value: int) -> None: """Increments the play time""" - track_object = pctl.master_library[index] + track_object = self.pctl.master_library[index] - if after_scan: - if track_object in after_scan: + if self.after_scan: + if track_object in self.after_scan: return key = track_object.artist, track_object.title, track_object.filename @@ -1855,12 +915,12 @@ def set_rating(self, index: int, value: int, write: bool = False) -> None: self.db[key] = self.new_object() self.db[key][2] = value - tr = pctl.get_track(index) + tr = self.pctl.get_track(index) if tr.file_ext == "SUB": self.db[key][2] = math.ceil(value / 2) * 2 - shooter(subsonic.set_rating, (tr, value)) + shooter(self.tauon.subsonic.set_rating, (tr, value)) - if prefs.write_ratings and write: + if self.bag.prefs.write_ratings and write: logging.info("Writing rating..") assert value <= 10 assert value >= 0 @@ -1960,14 +1020,11 @@ def merge(self, index: int, object): if cha not in self.db[key][1]: self.db[key][1] += cha - -star_store = StarStore() - - class AlbumStarStore: - def __init__(self) -> None: + def __init__(self, subsonic: SubsonicService) -> None: self.db = {} + self.subsonic: SubsonicService = subsonic def get_key(self, track_object: TrackClass) -> str: artist = track_object.album_artist @@ -1982,7 +1039,7 @@ 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) + self.subsonic.set_album_rating(track_object, rating) def set_rating_artist_title(self, artist: str, album: str, rating): self.db[artist + ":" + album] = rating @@ -1990,10 +1047,6 @@ def set_rating_artist_title(self, artist: str, album: str, 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)""" @@ -2009,57 +1062,70 @@ def __init__(self): # 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 + def __init__(self, gui: GuiVar) -> None: + self.gui = gui + self.mouse_click: bool = False + self.right_click: bool = False + self.level_2_enter: bool = False + self.backspace_press: int = 0 + self.mouse_wheel: int = 0 + self.mouse_down: bool = False + self.mouse_up: bool = False + self.right_down: bool = False + self.click_location = [200, 200] + self.last_click_location = [0, 0] + self.mouse_position = [0, 0] + self.mouse_up_position = [0, 0] + self.drag_mode: bool = False + self.quick_drag: bool = False + self.clicked: bool = False + + self.k_input: bool = True + self.key_return_press: bool = False + self.key_tab_press: bool = False + self.key_shift_down: bool = False + self.key_shiftr_down: bool = False + self.key_ctrl_down: bool = False + self.key_rctrl_down: bool = False + self.key_meta: bool = False + self.key_ralt: bool = False + self.key_lalt: bool = False self.media_key = "" def m_key_play(self) -> None: self.media_key = "Play" - gui.update += 1 + self.gui.update += 1 def m_key_pause(self) -> None: self.media_key = "Pause" - gui.update += 1 + self.gui.update += 1 def m_key_stop(self) -> None: self.media_key = "Stop" - gui.update += 1 + self.gui.update += 1 def m_key_next(self) -> None: self.media_key = "Next" - gui.update += 1 + self.gui.update += 1 def m_key_previous(self) -> None: self.media_key = "Previous" - gui.update += 1 - - -inp = Input() - + self.gui.update += 1 class KeyMap: - def __init__(self): - + def __init__(self, bag: Bag): + self.bag = bag self.hits = [] # The keys hit this frame self.maps = {} # Loaded from input.txt def load(self): - path = config_directory / "input.txt" + path = self.bag.dirs.config_directory / "input.txt" with path.open(encoding="utf_8") as f: content = f.read().splitlines() for p in content: @@ -2075,7 +1141,7 @@ def load(self): if items[1] in ("MB4", "MB5"): key = items[1] else: - if prefs.use_scancodes: + if self.bag.prefs.use_scancodes: key = SDL_GetScancodeFromName(items[1].encode()) else: key = SDL_GetKeyFromName(items[1].encode()) @@ -2105,102 +1171,15 @@ def test(self, 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 + ctrl = (inp.key_ctrl_down or inp.key_rctrl_down) * 1 + shift = (inp.key_shift_down or inp.key_shiftr_down) * 10 + alt = (inp.key_lalt or inp.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 @@ -2390,10 +1369,10 @@ def post_config(self): self.time_sub = rgb_add_hls(self.bottom_panel_colour, 0, 0.29, 0) - if test_lumi(colours.bottom_panel_colour) < 0.2: + 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(colours.bottom_panel_colour) < 0.8: + elif test_lumi(self.bottom_panel_colour) < 0.8: self.time_sub = [255, 255, 255, 135] # self.time_sub = self.mode_button_off @@ -2470,57 +1449,6 @@ def light_mode(self): # 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 get_themes(deco: bool = False): - themes = [] # full, name - decos = {} - direcs = [str(install_directory / "theme")] - if user_directory != install_directory: - direcs.append(str(user_directory / "theme")) - - def scan_folders(folders: list[str]) -> None: - for folder in folders: - if not os.path.isdir(folder): - continue - paths = [os.path.join(folder, f) for f in os.listdir(folder)] - for path in paths: - if os.path.islink(path): - path = os.readlink(path) - if os.path.isfile(path): - if path[-7:] == ".ttheme": - themes.append((path, os.path.basename(path).split(".")[0])) - elif path[-6:] == ".tdeco": - decos[os.path.basename(path).split(".")[0]] = path - elif os.path.isdir(path): - scan_folders([path]) - - scan_folders(direcs) - themes.sort() - if deco: - return decos - return themes - - -# This is legacy. New settings are added straight to the save list (need to overhaul) -view_prefs = { - "split-line": True, - "update-title": False, - "star-lines": False, - "side-panel": True, - "dim-art": False, - "pl-follow": False, - "scroll-enable": True, -} - - class TrackClass: """This is the fundamental object/data structure of a track""" @@ -2567,20 +1495,6 @@ def __init__(self) -> None: 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] == "/": - direc = direc[-w:] - return direc - return None - -def set_path(nt: TrackClass, path: str) -> None: - nt.fullpath = path.replace("\\", "/") - nt.filename = os.path.basename(path) - nt.parent_folder_path = os.path.dirname(path.replace("\\", "/")) - 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)""" @@ -2595,38877 +1509,36292 @@ def __init__(self) -> None: self.play: bool = False self.force_scan: bool = False -# url_saves = [] -rename_files_previous = "" -rename_folder_previous = "" -p_force_queue: list[TauonQueueItem] = [] +class GetSDLInput: -reload_state = None + 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 show_message(line1: str, line2: str ="", line3: str = "", mode: str = "info") -> None: - gui.message_box = True - gui.message_text = line1 - gui.message_mode = mode - gui.message_subtext = line2 - gui.message_subtext2 = line3 - message_box_min_timer.set() - match mode: - case "done" | "confirm": - logging.debug("Message: " + line1 + line2 + line3) - case "info": - logging.info("Message: " + line1 + line2 + line3) - case "warning": - logging.warning("Message: " + line1 + line2 + line3) - case "error": - logging.error("Message: " + line1 + line2 + line3) - case _: - logging.error(f"Unknown mode '{mode}' for message: " + line1 + line2 + line3) - gui.update = 1 + 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 -# ----------------------------------------------------- -# 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") +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), + ] -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!") +class PlayerCtl: + """Main class that controls playback (play, pause, stepping, playlists, queue etc). Sends commands to backend.""" -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") - -perf_timer.set() - -radio_playlists = [{"uid": uid_gen(), "name": "Default", "items": []}] - -primary_stations: list[dict[str, str]] = [] -station = { - "title": "SomaFM Groove Salad", - "stream_url": "https://ice3.somafm.com/groovesalad-128-mp3", - "country": "USA", - "website_url": "https://somafm.com/groovesalad", - "icon": "https://somafm.com/logos/120/groovesalad120.png", -} -primary_stations.append(station) -station = { - "title": "SomaFM PopTron", - "stream_url": "https://ice3.somafm.com/poptron-128-mp3", - "country": "USA", - "website_url": "https://somafm.com/poptron/", - "icon": "https://somafm.com/logos/120/poptron120.jpg", -} -primary_stations.append(station) -station = { - "title": "SomaFM Vaporwaves", - "stream_url": "https://ice4.somafm.com/vaporwaves-128-mp3", - "country": "USA", - "website_url": "https://somafm.com/vaporwaves", - "icon": "https://somafm.com/img3/vaporwaves400.png", -} -primary_stations.append(station) - -station = { - "title": "DKFM Shoegaze Radio", - "stream_url": "https://kathy.torontocast.com:2005/stream", - "country": "Canada", - "website_url": "https://decayfm.com", - "icon": "https://cdn-profiles.tunein.com/s193842/images/logod.png", -} -primary_stations.append(station) - -for item in primary_stations: - radio_playlists[0]["items"].append(item) - -radio_playlist_viewing = 0 - -pump = True - - -def pumper(): - if macos: - return - while pump: - time.sleep(0.005) - SDL_PumpEvents() + # C-PC + def __init__(self, tauon: Tauon): + self.tauon = tauon + self.gui = self.tauon.gui + self.bag = self.tauon.bag + self.smtc: bool = self.tauon.bag.smtc + self.radiobox = self.tauon.radiobox + self.running: bool = True + self.prefs: Prefs = self.bag.prefs + self.lfm_scrobbler = LastScrob(tauon=self.tauon, pctl=self) + self.install_directory: Path = self.bag.dirs.install_directory + self.loading_in_progress: bool = False -shoot_pump = threading.Thread(target=pumper) -shoot_pump.daemon = True -shoot_pump.start() + # Database -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") + self.master_count: int = self.bag.master_count + self.total_playtime: float = 0 + self.master_library: dict[int, TrackClass] = self.bag.master_library + # Lets clients know when to invalidate cache + self.db_inc = random.randint(0, 10000) + # self.star_library = star_library + self.LoadClass = LoadClass -core_timer.set() -logging.info(f"Database loaded in {round(perf_timer.get(), 3)} seconds.") + self.gen_codes: dict[int, str] = self.bag.gen_codes -perf_timer.set() -keys = set(master_library.keys()) -for pl in multi_playlist: - if db_version > 68 or db_version == 0: - keys -= set(pl.playlist_ids) - else: - keys -= set(pl[2]) -if len(keys) > 5000: - gui.suggest_clean_db = True -# logging.info(f"Database scanned in {round(perf_timer.get(), 3)} seconds.") + self.shuffle_pools = {} + self.after_import_flag = False + self.quick_add_target = None -pump = False -shoot_pump.join() + self.album_mbid_release_cache = {} + self.album_mbid_release_group_cache = {} + self.mbid_image_url_cache = {} -# temporary -if window_size is None: - window_size = window_default_size - gui.rspw = 200 + # Misc player control + self.url: str = "" + # self.save_urls = url_saves + self.tag_meta: str = "" + self.found_tags = {} + self.encoder_pause = 0 -def track_number_process(line: str) -> str: - line = str(line).split("/", 1)[0].lstrip("0") - if prefs.dd_index and len(line) == 1: - return "0" + line - return line + # Playback + self.track_queue: list[int] = self.bag.track_queue + self.default_playlist: list[int] = [] + self.queue_step: int = self.bag.playing_in_queue + self.playing_time = 0 + self.playlist_playing_position: int = self.bag.playlist_playing # track in playlist that is playing + if self.playlist_playing_position is None: + self.playlist_playing_position = -1 + self.playlist_view_position: int = self.bag.playlist_view_position + self.selected_in_playlist: int = self.bag.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 = self.prefs.random_mode + self.repeat_mode = self.prefs.repeat_mode + self.album_repeat_mode = self.prefs.album_repeat_mode + self.album_shuffle_mode = self.prefs.album_shuffle_mode + # self.album_shuffle_pool = [] + # self.album_shuffle_id = "" + self.last_playing_time = 0 + self.multi_playlist: list[TauonPlaylist] = self.bag.multi_playlist + self.active_playlist_viewing: int = self.bag.playlist_active # the playlist index that is being viewed # TODO(Martin): Rename playlist_active and active_playlist? + self.active_playlist_playing: int = self.bag.playlist_active # the playlist index that is playing from + self.force_queue: list[TauonQueueItem] = self.bag.p_force_queue + self.pause_queue: bool = False + self.left_time = 0 + self.left_index = 0 + self.player_volume: float = self.bag.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 -def advance_theme() -> None: - global theme + self.record_stream = False + self.record_title = "" - theme += 1 - gui.reload_theme = True + # Bass + self.bass_devices = [] + self.set_device = 0 -def get_theme_number(name: str) -> int: - if name == "Mindaro": - return 0 - themes = get_themes() - for i, theme in enumerate(themes): - if theme[1] == name: - return i + 1 - return 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 -def get_theme_name(number: int) -> str: - if number == 0: - return "Mindaro" - number -= 1 - themes = get_themes() - logging.info((number, themes)) - if len(themes) > number: - return themes[number][1] - return "" + self.windows_progress = None -# Run upgrades if we're behind the current DB standard -if db_version > 0 and db_version < latest_db_version: - logging.warning(f"Current DB version {db_version} was lower than latest {latest_db_version}, running migrations!") - try: - master_library, multi_playlist, star_store, p_force_queue, theme, prefs, gui, gen_codes, radio_playlists = database_migrate( - db_version=db_version, - master_library=master_library, - install_mode=install_mode, - multi_playlist=multi_playlist, - star_store=star_store, - install_directory=install_directory, - a_cache_dir=a_cache_dir, - cache_directory=cache_directory, - config_directory=config_directory, - user_directory=user_directory, - gui=gui, - gen_codes=gen_codes, - prefs=prefs, - radio_playlists=radio_playlists, - theme=theme, - p_force_queue=p_force_queue, - ) - except ValueError: - logging.exception("That should not happen") - except Exception: - logging.exception("Unknown error running database migration!") + self.finish_transition = False + # self.queue_target = 0 + self.start_time_target = 0 -playing_in_queue = min(playing_in_queue, len(track_queue) - 1) + self.decode_time = 0 + self.download_time = 0 -shoot = threading.Thread(target=keymaps.load) -shoot.daemon = True -shoot.start() + self.radio_meta_on = "" -# Loading Config ----------------- + self.radio_scrobble_trip = True + self.radio_scrobble_timer = Timer() -download_directories: list[str] = [] + self.radio_image_bin = None + self.radio_rate_timer = Timer(2) + self.radio_poll_timer = Timer(2) -if download_directory.is_dir(): - download_directories.append(str(download_directory)) + self.volume_update_timer = Timer() + self.wake_past_time = 0 -if music_directory is not None and music_directory.is_dir(): - download_directories.append(str(music_directory)) + self.regen_in_progress = False + self.notify_in_progress = False -cf = Config() + self.radio_playlists: list[RadioPlaylist] = self.bag.radio_playlists + self.radio_playlist_viewing: int = self.bag.radio_playlist_viewing + self.tag_history = {} + self.commit: int | None = None + self.spot_playing = False -def save_prefs(): - cf.update_value("sync-bypass-transcode", prefs.bypass_transcode) - cf.update_value("sync-bypass-low-bitrate", prefs.smart_bypass) - cf.update_value("radio-record-codec", prefs.radio_record_codec) + self.buffering_percent = 0 - cf.update_value("plex-username", prefs.plex_username) - cf.update_value("plex-password", prefs.plex_password) - cf.update_value("plex-servername", prefs.plex_servername) + def id_to_pl(self, id: int): + for i, item in enumerate(self.multi_playlist): + if item.uuid_int == id: + return i + return None - cf.update_value("subsonic-username", prefs.subsonic_user) - cf.update_value("subsonic-password", prefs.subsonic_password) - cf.update_value("subsonic-password-plain", prefs.subsonic_password_plain) - cf.update_value("subsonic-server-url", prefs.subsonic_server) + def pl_to_id(self, pl: int) -> int: + return self.multi_playlist[pl].uuid_int - cf.update_value("jelly-username", prefs.jelly_username) - cf.update_value("jelly-password", prefs.jelly_password) - cf.update_value("jelly-server-url", prefs.jelly_server_url) + def notify_change(self) -> None: + self.db_inc += 1 + self.tauon.bg_save() - cf.update_value("koel-username", prefs.koel_username) - cf.update_value("koel-password", prefs.koel_password) - cf.update_value("koel-server-url", prefs.koel_server_url) - cf.update_value("stream-bitrate", prefs.network_stream_bitrate) + def update_tag_history(self) -> None: + if self.prefs.auto_rec: + self.tag_history[self.radiobox.song_key] = { + "title": self.radiobox.dummy_track.title, + "artist": self.radiobox.dummy_track.artist, + "album": self.radiobox.dummy_track.album, + # "image": self.radio_image_bin + } - cf.update_value("display-language", prefs.ui_lang) - # cf.update_value("decode-search", prefs.diacritic_search) + def radio_progress(self) -> None: + if self.radiobox.loaded_url and "radio.plaza.one" in self.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) - # cf.update_value("use-log-volume-scale", prefs.log_vol) - # cf.update_value("audio-backend", prefs.backend) - cf.update_value("use-pipewire", prefs.pipewire) - cf.update_value("seek-interval", prefs.seek_interval) - cf.update_value("pause-fade-time", prefs.pause_fade_time) - cf.update_value("cross-fade-time", prefs.cross_fade_time) - cf.update_value("device-buffer-ms", prefs.device_buffer) - cf.update_value("output-samplerate", prefs.samplerate) - cf.update_value("resample-quality", prefs.resample) - cf.update_value("avoid_resampling", prefs.avoid_resampling) - # cf.update_value("fast-scrubbing", prefs.pa_fast_seek) - cf.update_value("precache-local-files", prefs.precache) - cf.update_value("cache-use-tmp", prefs.tmp_cache) - cf.update_value("cache-limit", prefs.cache_limit) - cf.update_value("always-ffmpeg", prefs.always_ffmpeg) - cf.update_value("volume-curve", prefs.volume_power) - # cf.update_value("force-mono", prefs.mono) - # cf.update_value("disconnect-device-pause", prefs.dc_device_setting) - # cf.update_value("use-short-buffering", prefs.short_buffer) + 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"] - # cf.update_value("gst-output", prefs.gst_output) - # cf.update_value("gst-use-custom-output", prefs.gst_use_custom_output) + 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 - cf.update_value("separate-multi-genre", prefs.sep_genre_multi) + self.radiobox.dummy_track.art_url_key = "" + self.radiobox.dummy_track.title = "" + self.radiobox.dummy_track.date = "" + self.radiobox.dummy_track.artist = "" + self.radiobox.dummy_track.album = "" + self.radiobox.dummy_track.lyrics = "" + self.radiobox.dummy_track.date = "" - cf.update_value("tag-editor-name", prefs.tag_editor_name) - cf.update_value("tag-editor-target", prefs.tag_editor_target) + tags = self.found_tags + if "title" in tags: + self.radiobox.dummy_track.title = tags["title"] + if "artist" in tags: + self.radiobox.dummy_track.artist = tags["artist"] + if "year" in tags: + self.radiobox.dummy_track.date = tags["year"] + if "album" in tags: + self.radiobox.dummy_track.album = tags["album"] - cf.update_value("playback-follow-cursor", prefs.playback_follow_cursor) - cf.update_value("spotify-prefer-web", prefs.launch_spotify_web) - cf.update_value("spotify-allow-local", prefs.launch_spotify_local) - cf.update_value("back-restarts", prefs.back_restarts) - cf.update_value("end-queue-stop", prefs.stop_end_queue) - cf.update_value("block-suspend", prefs.block_suspend) - cf.update_value("allow-video-formats", prefs.allow_video_formats) + 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("-") + self.radiobox.dummy_track.title = title.strip() + self.radiobox.dummy_track.artist = artist.strip() - cf.update_value("ui-scale", prefs.scale_want) - cf.update_value("auto-scale", prefs.x_scale) - cf.update_value("tracklist-y-text-offset", prefs.tracklist_y_text_offset) - cf.update_value("theme-name", prefs.theme_name) - cf.update_value("mac-style", prefs.macstyle) - cf.update_value("allow-art-zoom", prefs.zoom_art) + if self.tag_meta: + self.radiobox.song_key = self.tag_meta + else: + self.radiobox.song_key = self.radiobox.dummy_track.artist + " - " + self.radiobox.dummy_track.title - cf.update_value("scroll-gallery-by-row", prefs.gallery_row_scroll) - cf.update_value("prefs.gallery_scroll_wheel_px", prefs.gallery_row_scroll) - cf.update_value("scroll-spectrogram", prefs.spec2_scroll) - cf.update_value("mascot-opacity", prefs.custom_bg_opacity) - cf.update_value("synced-lyrics-time-offset", prefs.sync_lyrics_time_offset) + self.update_tag_history() + if self.radiobox.loaded_url not in self.radiobox.websocket_source_urls: + self.radio_image_bin = None + logging.info("NEXT RADIO TRACK") - cf.update_value("artist-list-prefers-album-artist", prefs.artist_list_prefer_album_artist) - cf.update_value("side-panel-info-persists", prefs.meta_persists_stop) - cf.update_value("side-panel-info-selected", prefs.meta_shows_selected) - cf.update_value("side-panel-info-selected-always", prefs.meta_shows_selected_always) - cf.update_value("mini-mode-avoid-notifications", prefs.stop_notifications_mini_mode) - cf.update_value("hide-queue-when-empty", prefs.hide_queue) - # cf.update_value("show-playlist-list", prefs.show_playlist_list) - cf.update_value("enable-art-header-bar", prefs.art_in_top_panel) - cf.update_value("always-art-header-bar", prefs.always_art_header) - # cf.update_value("prefer-center-bg", prefs.center_bg) - cf.update_value("showcase-texture-background", prefs.showcase_overlay_texture) - cf.update_value("side-panel-style", prefs.side_panel_layout) - cf.update_value("side-lyrics-art", prefs.show_side_lyrics_art_panel) - cf.update_value("side-lyrics-art-on-top", prefs.lyric_metadata_panel_top) - cf.update_value("absolute-track-indices", prefs.use_absolute_track_index) - cf.update_value("auto-hide-bottom-title", prefs.hide_bottom_title) - cf.update_value("auto-show-playing", prefs.auto_goto_playing) - cf.update_value("notify-include-album", prefs.notify_include_album) - cf.update_value("show-rating-hint", prefs.rating_playtime_stars) - cf.update_value("drag-tab-to-unpin", prefs.drag_to_unpin) + try: + get_radio_art() + except Exception: + logging.exception("Get art error") - cf.update_value("gallery-thin-borders", prefs.thin_gallery_borders) - cf.update_value("increase-row-spacing", prefs.increase_gallery_row_spacing) - cf.update_value("gallery-center-text", prefs.center_gallery_text) + self.notify_update(mpris=False) + if self.mpris: + self.mpris.update(force=True) - cf.update_value("use-custom-fonts", prefs.use_custom_fonts) - cf.update_value("font-main-standard", prefs.linux_font) - cf.update_value("font-main-medium", prefs.linux_font_semibold) - cf.update_value("font-main-bold", prefs.linux_font_bold) - cf.update_value("font-main-condensed", prefs.linux_font_condensed) - cf.update_value("font-main-condensed-bold", prefs.linux_font_condensed_bold) + self.lfm_scrobbler.listen_track(self.radiobox.dummy_track) + self.lfm_scrobbler.start_queue() - cf.update_value("force-subpixel-text", prefs.force_subpixel_text) + if self.radio_scrobble_trip is False and self.radio_scrobble_timer.get() > 45: + self.radio_scrobble_trip = True + self.lfm_scrobbler.scrob_full_track(copy.deepcopy(self.radiobox.dummy_track)) - cf.update_value("double-digit-indices", prefs.dd_index) - cf.update_value("column-album-artist-fallsback", prefs.column_aa_fallback_artist) - cf.update_value("left-aligned-album-artist-title", prefs.left_align_album_artist_title) - cf.update_value("import-auto-sort", prefs.auto_sort) + 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") - cf.update_value("encode-output-dir", prefs.custom_encoder_output) - cf.update_value("sync-device-music-dir", prefs.sync_target) - cf.update_value("add_download_directory", prefs.download_dir1) + def notify_update_fire(self) -> None: + if self.mpris is not None: + self.mpris.update() + if self.tauon.update_play_lock is not None: + self.tauon.update_play_lock() + # if self.tray_update is not None: + # self.tray_update() + self.notify_in_progress = False - cf.update_value("use-system-tray", prefs.use_tray) - cf.update_value("use-gamepad", prefs.use_gamepad) - cf.update_value("enable-remote-interface", prefs.enable_remote) + def notify_update(self, mpris: bool = True) -> None: + self.tauon.tray_releases += 1 + if self.tauon.tray_lock.locked(): + try: + self.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") - cf.update_value("enable-mpris", prefs.enable_mpris) - cf.update_value("hide-maximize-button", prefs.force_hide_max_button) - cf.update_value("restore-window-position", prefs.save_window_position) - cf.update_value("mini-mode-always-on-top", prefs.mini_mode_on_top) - cf.update_value("resume-playback-on-restart", prefs.reload_play_state) - cf.update_value("resume-playback-on-wake", prefs.resume_play_wake) - cf.update_value("auto-dl-artist-data", prefs.auto_dl_artist_data) + if mpris and self.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 = self.tauon.thumb_tracks.path(tr) + except Exception: + logging.exception("Failed to set image_path from thumb_tracks.path") - cf.update_value("fanart.tv-cover", prefs.enable_fanart_cover) - cf.update_value("fanart.tv-artist", prefs.enable_fanart_artist) - cf.update_value("fanart.tv-background", prefs.enable_fanart_bg) - cf.update_value("auto-update-playlists", prefs.always_auto_update_playlists) - cf.update_value("write-ratings-to-tag", prefs.write_ratings) - cf.update_value("enable-spotify", prefs.spot_mode) - cf.update_value("enable-discord-rpc", prefs.discord_enable) - cf.update_value("auto-search-lyrics", prefs.auto_lyrics) - cf.update_value("shortcuts-ignore-keymap", prefs.use_scancodes) - cf.update_value("alpha_key_activate_search", prefs.search_on_letter) + if image_path is None: + image_path = "" - cf.update_value("discogs-personal-access-token", prefs.discogs_pat) - cf.update_value("listenbrainz-token", prefs.lb_token) - cf.update_value("custom-listenbrainz-url", prefs.listenbrainz_url) + image_path = image_path.replace("/", "\\") + #logging.info(image_path) - cf.update_value("maloja-key", prefs.maloja_key) - cf.update_value("maloja-url", prefs.maloja_url) - cf.update_value("maloja-enable", prefs.maloja_enable) + 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)) - cf.update_value("tau-url", prefs.sat_url) - cf.update_value("lastfm-pull-love", prefs.lastfm_pull_love) + 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 self.prefs.art_bg or (self.gui.mode == 3 and self.prefs.mini_mode_mode == 5): + self.tauon.thread_manager.ready("style") - cf.update_value("broadcast-page-port", prefs.metadata_page_port) - cf.update_value("show-current-on-transition", prefs.show_current_on_transition) + def get_url(self, track_object: TrackClass) -> tuple[str | None, dict | None] | None: + if track_object.file_ext == "TIDAL": + return self.tauon.tidal.resolve_stream(track_object), None + if track_object.file_ext == "PLEX": + return self.tauon.plex.resolve_stream(track_object.url_key), None - cf.update_value("chart-columns", prefs.chart_columns) - cf.update_value("chart-rows", prefs.chart_rows) - cf.update_value("chart-uses-text", prefs.chart_text) - cf.update_value("chart-font", prefs.chart_font) - cf.update_value("chart-sorts-top-played", prefs.topchart_sorts_played) + if track_object.file_ext == "JELY": + return self.tauon.jellyfin.resolve_stream(track_object.url_key) - if config_directory.is_dir(): - cf.dump(str(config_directory / "tauon.conf")) - else: - logging.error("Missing config directory") + if track_object.file_ext == "KOEL": + return self.tauon.koel.resolve_stream(track_object.url_key) + if track_object.file_ext == "SUB": + return self.tauon.subsonic.resolve_stream(track_object.url_key) -def load_prefs(): - cf.reset() - cf.load(str(config_directory / "tauon.conf")) + if track_object.file_ext == "TAU": + return self.tauon.tau.resolve_stream(track_object.url_key), None - cf.add_comment("Tauon Music Box configuration file") - cf.br() - cf.add_comment( - "This file will be regenerated while app is running. Formatting and additional comments will be lost.") - cf.add_comment("Tip: Use TOML syntax highlighting") + return None, None - cf.br() - cf.add_text("[audio]") + def playing_playlist(self) -> list[int] | None: + return self.multi_playlist[self.active_playlist_playing].playlist_ids - # prefs.backend = cf.sync_add("int", "audio-backend", prefs.backend, "4: Built in backend (Phazor), 2: GStreamer") - prefs.pipewire = cf.sync_add( - "bool", "use-pipewire", prefs.pipewire, - "Experimental setting to use Pipewire native only.") + def playing_ready(self) -> bool: + return len(self.track_queue) > 0 - prefs.seek_interval = cf.sync_add( - "int", "seek-interval", prefs.seek_interval, - "In s. Interval to seek when using keyboard shortcut. Default is 15.") - # prefs.pause_fade_time = cf.sync_add("int", "pause-fade-time", prefs.pause_fade_time, "In milliseconds. Default is 400. (GStreamer Only)") + def selected_ready(self) -> bool: + return pctl.default_playlist and self.selected_in_playlist < len(pctl.default_playlist) - prefs.pause_fade_time = max(prefs.pause_fade_time, 100) - prefs.pause_fade_time = min(prefs.pause_fade_time, 5000) + def render_playlist(self) -> None: + if taskbar_progress and self.msys and self.windows_progress: + self.windows_progress.update(True) + self.gui.pl_update = 1 - prefs.cross_fade_time = cf.sync_add( - "int", "cross-fade-time", prefs.cross_fade_time, - "In ms. Min: 200, Max: 2000, Default: 700. Applies to track change crossfades. End of track is always gapless.") + def show_selected(self) -> int: + if self.gui.playlist_view_length < 1: + return 0 - prefs.device_buffer = cf.sync_add("int", "device-buffer-ms", prefs.device_buffer, "Default: 80") - #prefs.samplerate = cf.sync_add( - # "int", "output-samplerate", prefs.samplerate, - # "In hz. Default: 48000, alt: 44100. (restart app to apply change)") - prefs.avoid_resampling = cf.sync_add( - "bool", "avoid_resampling", prefs.avoid_resampling, - "Only implemented for FLAC, MP3, OGG, OPUS") - prefs.resample = cf.sync_add( - "int", "resample-quality", prefs.resample, - "0=best, 1=medium, 2=fast, 3=fastest. Default: 1. (applies on restart)") - if prefs.resample < 0 or prefs.resample > 4: - prefs.resample = 1 - # prefs.pa_fast_seek = cf.sync_add("bool", "fast-scrubbing", prefs.pa_fast_seek, "Seek without a delay but may cause audible popping") - prefs.cache_limit = cf.sync_add( - "int", "cache-limit", prefs.cache_limit, - "Limit size of network audio file cache. In MB.") - prefs.tmp_cache = cf.sync_add( - "bool", "cache-use-tmp", prefs.tmp_cache, - "Use /tmp for cache. When enabled, above setting overridden to a small value. (applies on restart)") - prefs.precache = cf.sync_add( - "bool", "precache-local-files", prefs.precache, - "Cache files from local sources too. (Useful for mounted network drives)") - prefs.always_ffmpeg = cf.sync_add( - "bool", "always-ffmpeg", prefs.always_ffmpeg, - "Prefer decoding using FFMPEG. Fixes stuttering on Raspberry Pi OS.") - prefs.volume_power = cf.sync_add( - "int", "volume-curve", prefs.volume_power, - "1=Linear volume control. Values above one give greater control bias over lower volume range. Default: 2") + global shift_selection - # prefs.mono = cf.sync_add("bool", "force-mono", prefs.mono, "This is a placeholder setting and currently has no effect.") - # prefs.dc_device_setting = cf.sync_add("string", "disconnect-device-pause", prefs.dc_device_setting, "Can be \"on\" or \"off\". BASS only. When off, connection to device will he held open.") - # prefs.short_buffer = cf.sync_add("bool", "use-short-buffering", prefs.short_buffer, "BASS only.") + 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((self.gui.playlist_view_length / 3) * 2) + int(self.gui.playlist_view_length / 6)) + logging.debug("Position changed show selected (a)") + elif abs(self.playlist_view_position - i) > self.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 > self.gui.playlist_view_length * 1 and i + (self.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(self.gui.playlist_view_length / 3) * 2) + logging.debug("Position changed show selected (d)") + break + self.render_playlist() + return 0 - # cf.br() - # cf.add_text("[audio (gstreamer only)]") - # - # prefs.gst_output = cf.sync_add("string", "gst-output", prefs.gst_output, "GStreamer output pipeline specification. Only used with GStreamer backend.") - # prefs.gst_use_custom_output = cf.sync_add("bool", "gst-use-custom-output", prefs.gst_use_custom_output, "Set this to true to apply any manual edits of the above string.") + def get_track(self, track_index: int) -> TrackClass: + """Get track object by track_index""" + return self.master_library[track_index] - if prefs.dc_device_setting == "on": - prefs.dc_device = True - elif prefs.dc_device_setting == "off": - prefs.dc_device = False + 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_ids + 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 - cf.br() - cf.add_text("[locale]") - prefs.ui_lang = cf.sync_add( - "string", "display-language", prefs.ui_lang, "Override display language to use if " - "available. E.g. \"en\", \"ja\", \"zh_CH\". " - "Default: \"auto\"") - # prefs.diacritic_search = cf.sync_add("bool", "decode-search", prefs.diacritic_search, "Allow searching of diacritics etc using ascii in search functions. (Disablng may speed up search)") - cf.br() - cf.add_text("[search]") - prefs.sep_genre_multi = cf.sync_add( - "bool", "separate-multi-genre", prefs.sep_genre_multi, - "If true, the standard genre result will exclude results from multi-value tags. These will be included in a separate result.") + def show_object(self) -> None: + """The track to show in the metadata side panel""" + target_track = None - cf.br() - cf.add_text("[tag-editor]") - if system == "Windows" or msys: - prefs.tag_editor_name = cf.sync_add("string", "tag-editor-name", "Picard", "Name to display in UI.") - prefs.tag_editor_target = cf.sync_add( - "string", "tag-editor-target", - "C:\\Program Files (x86)\\MusicBrainz Picard\\picard.exe", - "The path of the exe to run.") - else: - prefs.tag_editor_name = cf.sync_add("string", "tag-editor-name", "Picard", "Name to display in UI.") - prefs.tag_editor_target = cf.sync_add( - "string", "tag-editor-target", "picard", - "The name of the binary to call.") + if self.playing_state == 3: + return self.radiobox.dummy_track - cf.br() - cf.add_text("[playback]") - prefs.playback_follow_cursor = cf.sync_add( - "bool", "playback-follow-cursor", prefs.playback_follow_cursor, - "When advancing, always play the track that is selected.") - prefs.launch_spotify_web = cf.sync_add( - "bool", "spotify-prefer-web", prefs.launch_spotify_web, - "Launch the web client rather than attempting to launch the desktop client.") - prefs.launch_spotify_local = cf.sync_add( - "bool", "spotify-allow-local", prefs.launch_spotify_local, - "Play Spotify audio through Tauon.") - prefs.back_restarts = cf.sync_add( - "bool", "back-restarts", prefs.back_restarts, - "Pressing the back button restarts playing track on first press.") - prefs.stop_end_queue = cf.sync_add( - "bool", "end-queue-stop", prefs.stop_end_queue, - "Queue will always enable auto-stop on last track") - prefs.block_suspend = cf.sync_add( - "bool", "block-suspend", prefs.block_suspend, - "Prevent system suspend during playback") - prefs.allow_video_formats = cf.sync_add( - "bool", "allow-video-formats", prefs.allow_video_formats, - "Allow the import of MP4 and WEBM formats") - if prefs.allow_video_formats: - for item in VID_Formats: - if item not in DA_Formats: - DA_Formats.add(item) + if 3 > self.playing_state > 0: + target_track = self.playing_object() - cf.br() - cf.add_text("[HiDPI]") - prefs.scale_want = cf.sync_add( - "float", "ui-scale", prefs.scale_want, - "UI scale factor. Default is 1.0, try increase if using a HiDPI display.") - prefs.x_scale = cf.sync_add("bool", "auto-scale", prefs.x_scale, "Automatically choose above setting") - prefs.tracklist_y_text_offset = cf.sync_add( - "int", "tracklist-y-text-offset", prefs.tracklist_y_text_offset, - "If you're using a UI scale, you may need to tweak this.") + elif self.playing_state == 0 and self.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]) - cf.br() - cf.add_text("[ui]") + elif self.playing_state == 0 and self.prefs.meta_persists_stop: + target_track = self.master_library[self.track_queue[self.queue_step]] - prefs.theme_name = cf.sync_add("string", "theme-name", prefs.theme_name) - macstyle = cf.sync_add("bool", "mac-style", prefs.macstyle, "Use macOS style window buttons") - prefs.zoom_art = cf.sync_add("bool", "allow-art-zoom", prefs.zoom_art) - prefs.gallery_row_scroll = cf.sync_add("bool", "scroll-gallery-by-row", True) - prefs.gallery_scroll_wheel_px = cf.sync_add( - "int", "scroll-gallery-distance", 90, - "Only has effect if scroll-gallery-by-row is false.") - prefs.spec2_scroll = cf.sync_add("bool", "scroll-spectrogram", prefs.spec2_scroll) - prefs.custom_bg_opacity = cf.sync_add("int", "mascot-opacity", prefs.custom_bg_opacity) - if prefs.custom_bg_opacity < 0 or prefs.custom_bg_opacity > 100: - prefs.custom_bg_opacity = 40 - logging.warning("Invalid value for mascot-opacity") + if self.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]) - prefs.sync_lyrics_time_offset = cf.sync_add( - "int", "synced-lyrics-time-offset", prefs.sync_lyrics_time_offset, - "In milliseconds. May be negative.") - prefs.artist_list_prefer_album_artist = cf.sync_add( - "bool", "artist-list-prefers-album-artist", - prefs.artist_list_prefer_album_artist, - "May require restart for change to take effect.") - prefs.meta_persists_stop = cf.sync_add( - "bool", "side-panel-info-persists", prefs.meta_persists_stop, - "Show album art and metadata of last played track when stopped.") - prefs.meta_shows_selected = cf.sync_add( - "bool", "side-panel-info-selected", prefs.meta_shows_selected, - "Show album art and metadata of selected track when stopped. (overides above setting)") - prefs.meta_shows_selected_always = cf.sync_add( - "bool", "side-panel-info-selected-always", - prefs.meta_shows_selected_always, - "Show album art and metadata of selected track at all times. (overides the above 2 settings)") - prefs.stop_notifications_mini_mode = cf.sync_add( - "bool", "mini-mode-avoid-notifications", - prefs.stop_notifications_mini_mode, - "Avoid sending track change notifications when in Mini Mode") - prefs.hide_queue = cf.sync_add("bool", "hide-queue-when-empty", prefs.hide_queue) - # prefs.show_playlist_list = cf.sync_add("bool", "show-playlist-list", prefs.show_playlist_list) + return target_track - prefs.show_current_on_transition = cf.sync_add( - "bool", "show-current-on-transition", - prefs.show_current_on_transition, - "Always jump to new playing track even with natural transition (broken setting, is always enabled") - prefs.art_in_top_panel = cf.sync_add( - "bool", "enable-art-header-bar", prefs.art_in_top_panel, - "Show art in top panel when window is narrow") - prefs.always_art_header = cf.sync_add( - "bool", "always-art-header-bar", prefs.always_art_header, - "Show art in top panel at any size. (Requires enable-art-header-bar)") + def playing_object(self) -> TrackClass | None: - # prefs.center_bg = cf.sync_add("bool", "prefer-center-bg", prefs.center_bg, "Always center art for the background art function") - prefs.showcase_overlay_texture = cf.sync_add( - "bool", "showcase-texture-background", prefs.showcase_overlay_texture, - "Draw pattern over background art") - prefs.side_panel_layout = cf.sync_add("int", "side-panel-style", prefs.side_panel_layout, "0:default, 1:centered") - prefs.show_side_lyrics_art_panel = cf.sync_add("bool", "side-lyrics-art", prefs.show_side_lyrics_art_panel) - prefs.lyric_metadata_panel_top = cf.sync_add("bool", "side-lyrics-art-on-top", prefs.lyric_metadata_panel_top) - prefs.use_absolute_track_index = cf.sync_add( - "bool", "absolute-track-indices", prefs.use_absolute_track_index, - "For playlists with titles disabled only") - prefs.hide_bottom_title = cf.sync_add( - "bool", "auto-hide-bottom-title", prefs.hide_bottom_title, - "Hide title in bottom panel when already shown in side panel") - prefs.auto_goto_playing = cf.sync_add( - "bool", "auto-show-playing", prefs.auto_goto_playing, - "Show playing track in current playlist on track and playlist change even if not the playing playlist") + if self.playing_state == 3: + return self.radiobox.dummy_track - prefs.notify_include_album = cf.sync_add( - "bool", "notify-include-album", prefs.notify_include_album, - "Include album name in track change notifications") - prefs.rating_playtime_stars = cf.sync_add( - "bool", "show-rating-hint", prefs.rating_playtime_stars, - "Indicate playtime in rating stars") + if len(self.track_queue) > 0: + return self.master_library[self.track_queue[self.queue_step]] + return None - prefs.drag_to_unpin = cf.sync_add( - "bool", "drag-tab-to-unpin", prefs.drag_to_unpin, - "Dragging a tab off the top-panel un-pins it") + def title_text(self) -> str: + line = "" + track = self.playing_object() + if track: + title = track.title + artist = track.artist - cf.br() - cf.add_text("[gallery]") - prefs.thin_gallery_borders = cf.sync_add("bool", "gallery-thin-borders", prefs.thin_gallery_borders) - prefs.increase_gallery_row_spacing = cf.sync_add("bool", "increase-row-spacing", prefs.increase_gallery_row_spacing) - prefs.center_gallery_text = cf.sync_add("bool", "gallery-center-text", prefs.center_gallery_text) + if not title: + line = clean_string(track.filename) + else: + if artist != "": + line += artist + if title != "": + if line != "": + line += " - " + line += title - # show-current-on-transition", prefs.show_current_on_transition) - if system != "windows": - cf.br() - cf.add_text("[fonts]") - cf.add_comment("Changes will require app restart.") - prefs.use_custom_fonts = cf.sync_add( - "bool", "use-custom-fonts", prefs.use_custom_fonts, - "Setting to false will reset below settings to default on restart") - if prefs.use_custom_fonts: - prefs.linux_font = cf.sync_add( - "string", "font-main-standard", prefs.linux_font, - "Suggested alternate: Liberation Sans") - prefs.linux_font_semibold = cf.sync_add("string", "font-main-medium", prefs.linux_font_semibold) - prefs.linux_font_bold = cf.sync_add("string", "font-main-bold", prefs.linux_font_bold) - prefs.linux_font_condensed = cf.sync_add("string", "font-main-condensed", prefs.linux_font_condensed) - prefs.linux_font_condensed_bold = cf.sync_add("string", "font-main-condensed-bold", prefs.linux_font_condensed_bold) + if self.playing_state == 3 and not title and not artist: + return self.tag_meta - else: - cf.sync_add("string", "font-main-standard", prefs.linux_font, "Suggested alternate: Liberation Sans") - cf.sync_add("string", "font-main-medium", prefs.linux_font_semibold) - cf.sync_add("string", "font-main-bold", prefs.linux_font_bold) - cf.sync_add("string", "font-main-condensed", prefs.linux_font_condensed) - cf.sync_add("string", "font-main-condensed-bold", prefs.linux_font_condensed_bold) + return line - # prefs.force_subpixel_text = cf.sync_add("bool", "force-subpixel-text", prefs.force_subpixel_text, "(Subpixel rendering defaults to off with Flatpak)") + def show(self) -> int | None: + global shift_selection - cf.br() - cf.add_text("[tracklist]") - prefs.dd_index = cf.sync_add("bool", "double-digit-indices", prefs.dd_index) - prefs.column_aa_fallback_artist = cf.sync_add( - "bool", "column-album-artist-fallsback", - prefs.column_aa_fallback_artist, - "'Album artist' column shows 'artist' if otherwise blank.") - prefs.left_align_album_artist_title = cf.sync_add( - "bool", "left-aligned-album-artist-title", - prefs.left_align_album_artist_title, - "Show 'Album artist' in the folder/album title. Uses colour 'column-album-artist' from theme file") - prefs.auto_sort = cf.sync_add( - "bool", "import-auto-sort", prefs.auto_sort, - "This setting is deprecated and will be removed in a future version") + if not self.track_queue: + return 0 + return None - cf.br() - cf.add_text("[transcode]") - prefs.bypass_transcode = cf.sync_add( - "bool", "sync-bypass-transcode", prefs.bypass_transcode, - "Don't transcode files with sync function") - prefs.smart_bypass = cf.sync_add("bool", "sync-bypass-low-bitrate", prefs.smart_bypass, - "Skip transcode of <=128kbs folders") - prefs.radio_record_codec = cf.sync_add("string", "radio-record-codec", prefs.radio_record_codec, - "Can be OPUS, OGG, FLAC, or MP3. Default: OPUS") + 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: - cf.br() - cf.add_text("[directories]") - cf.add_comment("Use full paths") - prefs.sync_target = cf.sync_add("string", "sync-device-music-dir", prefs.sync_target) - prefs.custom_encoder_output = cf.sync_add( - "string", "encode-output-dir", prefs.custom_encoder_output, - "E.g. \"/home/example/music/output\". If left blank, encode-output in home music dir will be used.") - if prefs.custom_encoder_output: - prefs.encoder_output = prefs.custom_encoder_output - prefs.download_dir1 = cf.sync_add( - "string", "add_download_directory", prefs.download_dir1, - "Add another folder to monitor in addition to home downloads and music.") - if prefs.download_dir1 and prefs.download_dir1 not in download_directories: - if os.path.isdir(prefs.download_dir1): - download_directories.append(prefs.download_dir1) - else: - logging.warning("Invalid download directory in config") + # 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") - cf.br() - cf.add_text("[app]") - prefs.enable_remote = cf.sync_add( - "bool", "enable-remote-interface", prefs.enable_remote, - "For use with Tauon Music Remote for Android") - prefs.use_gamepad = cf.sync_add("bool", "use-gamepad", prefs.use_gamepad, "Use game controller for UI control, restart on change.") - prefs.use_tray = cf.sync_add("bool", "use-system-tray", prefs.use_tray) - prefs.force_hide_max_button = cf.sync_add("bool", "hide-maximize-button", prefs.force_hide_max_button) - prefs.save_window_position = cf.sync_add( - "bool", "restore-window-position", prefs.save_window_position, - "Save and restore the last window position on desktop on open") - prefs.mini_mode_on_top = cf.sync_add("bool", "mini-mode-always-on-top", prefs.mini_mode_on_top) - prefs.enable_mpris = cf.sync_add("bool", "enable-mpris", prefs.enable_mpris) - prefs.reload_play_state = cf.sync_add("bool", "resume-playback-on-restart", prefs.reload_play_state) - prefs.resume_play_wake = cf.sync_add("bool", "resume-playback-on-wake", prefs.resume_play_wake) - prefs.auto_dl_artist_data = cf.sync_add( - "bool", "auto-dl-artist-data", prefs.auto_dl_artist_data, - "Enable automatic downloading of thumbnails in artist list") - prefs.enable_fanart_cover = cf.sync_add("bool", "fanart.tv-cover", prefs.enable_fanart_cover) - prefs.enable_fanart_artist = cf.sync_add("bool", "fanart.tv-artist", prefs.enable_fanart_artist) - prefs.enable_fanart_bg = cf.sync_add("bool", "fanart.tv-background", prefs.enable_fanart_bg) - prefs.always_auto_update_playlists = cf.sync_add( - "bool", "auto-update-playlists", - prefs.always_auto_update_playlists, - "Automatically update generator playlists") - prefs.write_ratings = cf.sync_add( - "bool", "write-ratings-to-tag", prefs.write_ratings, - "This writes FMPS_Rating tags on disk. Only writing to MP3, OGG and FLAC files is currently supported.") - prefs.spot_mode = cf.sync_add("bool", "enable-spotify", prefs.spot_mode, "Enable Spotify specific features") - prefs.discord_enable = cf.sync_add( - "bool", "enable-discord-rpc", prefs.discord_enable, - "Show track info in running Discord application") - prefs.auto_lyrics = cf.sync_add( - "bool", "auto-search-lyrics", prefs.auto_lyrics, - "Automatically search internet for lyrics when display is wanted") + global shift_selection - prefs.use_scancodes = cf.sync_add( - "bool", "shortcuts-ignore-keymap", prefs.use_scancodes, - "When enabled, shortcuts will map to the physical keyboard layout") - prefs.search_on_letter = cf.sync_add("bool", "alpha_key_activate_search", prefs.search_on_letter, - "When enabled, pressing single letter keyboard key will activate the global search") + if self.tauon.spot_ctl.coasting: + sptr = self.tauon.dummy_track.misc.get("spotify-track-url") + if sptr: + for p in pctl.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 - cf.br() - cf.add_text("[tokens]") - temp = cf.sync_add( - "string", "discogs-personal-access-token", prefs.discogs_pat, - "Used for sourcing of artist thumbnails.") - if not temp: - prefs.discogs_pat = "" - elif len(temp) != 40: - logging.warning("Invalid discogs token in config") - else: - prefs.discogs_pat = temp + if not self.track_queue: + return 0 - prefs.listenbrainz_url = cf.sync_add( - "string", "custom-listenbrainz-url", prefs.listenbrainz_url, - "Specify a custom Listenbrainz compatible api url. E.g. \"https://example.tld/apis/listenbrainz/\" Default: Blank") - prefs.lb_token = cf.sync_add("string", "listenbrainz-token", prefs.lb_token) + track_index = self.track_queue[self.queue_step] + if index is not None: + track_index = index - cf.br() - cf.add_text("[tauon_satellite]") - prefs.sat_url = cf.sync_add("string", "tau-url", prefs.sat_url, "Exclude the port") + # 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) - cf.br() - cf.add_text("[lastfm]") - prefs.lastfm_pull_love = cf.sync_add( - "bool", "lastfm-pull-love", prefs.lastfm_pull_love, - "Overwrite local love status on scrobble") + if self.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: - cf.br() - cf.add_text("[maloja_account]") - prefs.maloja_url = cf.sync_add( - "string", "maloja-url", prefs.maloja_url, - "A Maloja server URL, e.g. http://localhost:32400") - prefs.maloja_key = cf.sync_add("string", "maloja-key", prefs.maloja_key, "One of your Maloja API keys") - prefs.maloja_enable = cf.sync_add("bool", "maloja-enable", prefs.maloja_enable) - - cf.br() - cf.add_text("[plex_account]") - prefs.plex_username = cf.sync_add( - "string", "plex-username", prefs.plex_username, - "Probably the email address you used to make your PLEX account.") - prefs.plex_password = cf.sync_add( - "string", "plex-password", prefs.plex_password, - "The password associated with your PLEX account.") - prefs.plex_servername = cf.sync_add( - "string", "plex-servername", prefs.plex_servername, - "Probably your servers hostname.") + 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 - cf.br() - cf.add_text("[subsonic_account]") - prefs.subsonic_user = cf.sync_add("string", "subsonic-username", prefs.subsonic_user) - prefs.subsonic_password = cf.sync_add("string", "subsonic-password", prefs.subsonic_password) - prefs.subsonic_password_plain = cf.sync_add("bool", "subsonic-password-plain", prefs.subsonic_password_plain) - prefs.subsonic_server = cf.sync_add("string", "subsonic-server-url", prefs.subsonic_server) + if select: + self.selected_in_playlist = i - cf.br() - cf.add_text("[koel_account]") - prefs.koel_username = cf.sync_add("string", "koel-username", prefs.koel_username, "E.g. admin@example.com") - prefs.koel_password = cf.sync_add("string", "koel-password", prefs.koel_password, "The default is admin") - prefs.koel_server_url = cf.sync_add( - "string", "koel-server-url", prefs.koel_server_url, - "The URL or IP:Port where the Koel server is hosted. E.g. http://localhost:8050 or https://localhost:8060") - prefs.koel_server_url = prefs.koel_server_url.rstrip("/") + if playing: + # Make the found track the playing track + self.playlist_playing_position = i + self.active_playlist_playing = self.active_playlist_viewing - cf.br() - cf.add_text("[jellyfin_account]") - prefs.jelly_username = cf.sync_add("string", "jelly-username", prefs.jelly_username, "") - prefs.jelly_password = cf.sync_add("string", "jelly-password", prefs.jelly_password, "") - prefs.jelly_server_url = cf.sync_add( - "string", "jelly-server-url", prefs.jelly_server_url, - "The IP:Port where the jellyfin server is hosted.") - prefs.jelly_server_url = prefs.jelly_server_url.rstrip("/") + vl = self.gui.playlist_view_length + if self.multi_playlist[self.active_playlist_viewing].uuid_int == self.gui.playlist_current_visible_tracks_id: + vl = self.gui.playlist_current_visible_tracks - cf.br() - cf.add_text("[network]") - prefs.network_stream_bitrate = cf.sync_add( - "int", "stream-bitrate", prefs.network_stream_bitrate, - "Optional bitrate koel/subsonic should transcode to (Server may need to be configured for this). Set to 0 to disable transcoding.") + if not ( + quiet and self.playing_object().length < 15): # or (abs(self.playlist_view_position - i) < vl - 1)): - cf.br() - cf.add_text("[listenalong]") - prefs.metadata_page_port = cf.sync_add( - "int", "broadcast-page-port", prefs.metadata_page_port, - "Change applies on app restart or setting re-enable") + # Align to album if in view range (and folder titles are active) + ap = get_album_info(i)[1][0] - cf.br() - cf.add_text("[chart]") - prefs.chart_columns = cf.sync_add("int", "chart-columns", prefs.chart_columns) - prefs.chart_rows = cf.sync_add("int", "chart-rows", prefs.chart_rows) - prefs.chart_text = cf.sync_add("bool", "chart-uses-text", prefs.chart_text) - prefs.topchart_sorts_played = cf.sync_add("bool", "chart-sorts-top-played", prefs.topchart_sorts_played) - prefs.chart_font = cf.sync_add( - "string", "chart-font", prefs.chart_font, - "Format is fontname + size. Default is Monospace 10") + 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 --- -load_prefs() -save_prefs() + elif i == self.playlist_view_position - 1 and self.playlist_view_position > 1: + self.playlist_view_position -= 1 -# 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() + # 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 -# Auto detect lang -lang: list[str] | None = None -if prefs.ui_lang != "auto" or prefs.ui_lang == "": - # Force set lang - lang = [prefs.ui_lang] + # 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(( + self.gui.playlist_view_length / 3) * 2) + int(self.gui.playlist_view_length / 6)) -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 + # 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 > self.gui.playlist_view_length and i + (self.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(self.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 - logging.info(f"Translation file for '{lang}' loaded") -elif lang: - logging.error(f"No translation file available for '{lang}'") + 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") -sss = SDL_SysWMinfo() -SDL_GetWindowWMInfo(t_window, sss) + if select: + shift_selection = [] -if prefs.use_gamepad: - SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) + self.render_playlist() -smtc = False + if prefs.album_mode and not quiet: + if highlight: + self.gui.gallery_animate_highlight_on = goto_album(self.selected_in_playlist) + gallery_select_animate_timer.set() + else: + goto_album(self.selected_in_playlist) -if msys and win_ver >= 10: + if self.prefs.left_panel_mode == "artist list" and self.gui.lsp and not quiet: + artist_list_box.locate_artist(self.playing_object()) - #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() + if folder_list and self.prefs.left_panel_mode == "folder view" and self.gui.lsp and not quiet and not tree_view_box.lock_pl: + tree_view_box.show_track(self.playing_object()) - 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!") + 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 -def auto_scale() -> None: + self.set_volume() - old = prefs.scale_want + def set_volume(self, notify: bool = True) -> None: + if (self.tauon.spot_ctl.coasting or self.tauon.spot_ctl.playing) and not self.tauon.spot_ctl.local and self.tauon.inp.mouse_down: + # Rate limit network volume change + t = self.volume_update_timer.get() + if t < 0.3: + return - if prefs.x_scale: - if sss.subsystem in (SDL_SYSWM_WAYLAND, SDL_SYSWM_COCOA, SDL_SYSWM_UNKNOWN): - prefs.scale_want = window_size[0] / logical_size[0] - if old != prefs.scale_want: - logging.info("Applying scale based on buffer size") - elif sss.subsystem == SDL_SYSWM_X11: - if xdpi > 40: - prefs.scale_want = xdpi / 96 - if old != prefs.scale_want: - logging.info("Applying scale based on xft setting") + self.volume_update_timer.set() + self.playerCommand = "volume" + self.playerCommandReady = True + if notify: + self.notify_update() - prefs.scale_want = round(round(prefs.scale_want / 0.05) * 0.05, 2) + def revert(self) -> None: + if self.queue_step == 0: + return - if prefs.scale_want == 0.95: - prefs.scale_want = 1.0 - if prefs.scale_want == 1.05: - prefs.scale_want = 1.0 - if prefs.scale_want == 1.95: - prefs.scale_want = 2.0 - if prefs.scale_want == 2.05: - prefs.scale_want = 2.0 + 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 old != prefs.scale_want: - logging.info(f"Using UI scale: {prefs.scale_want}") + if not len(self.track_queue) > self.queue_step >= 0: + logging.error("There is no previous track?") + return - if prefs.scale_want < 0.5: - prefs.scale_want = 1.0 + 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 window_size[0] < (560 * prefs.scale_want) * 0.9 or window_size[1] < (330 * prefs.scale_want) * 0.9: - logging.info("Window overscale!") - show_message(_("Detected unsuitable UI scaling."), _("Scaling setting reset to 1x")) - prefs.scale_want = 1.0 + if self.tauon.stream_proxy.download_running: + self.tauon.stream_proxy.stop() -auto_scale() + 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 -def scale_assets(scale_want: int, force: bool = False) -> None: - global scaled_asset_directory - if scale_want != 1: - scaled_asset_directory = user_directory / "scaled-icons" - if not scaled_asset_directory.exists() or len(os.listdir(str(svg_directory))) != len( - os.listdir(str(scaled_asset_directory))): - logging.info("Force rerender icons") - force = True - else: - scaled_asset_directory = asset_directory + if id not in self.shuffle_pools: + self.update_shuffle_pool(pl.uuid_int) - if scale_want != prefs.ui_scale or force: + 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 scale_want != 1: - if scaled_asset_directory.is_dir() and scaled_asset_directory != asset_directory: - shutil.rmtree(str(scaled_asset_directory)) - from tauon.t_modules.t_svgout import render_icons + if track_id in pool: + pool.remove(track_id) - if scaled_asset_directory != asset_directory: - logging.info("Rendering icons...") - render_icons(str(svg_directory), str(scaled_asset_directory), scale_want) + def play_target_rr(self) -> None: + self.tauon.thread_manager.ready_playback() + self.playing_length = self.master_library[self.track_queue[self.queue_step]].length - logging.info("Done rendering icons") + 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 - diff_ratio = scale_want / prefs.ui_scale - prefs.ui_scale = scale_want - prefs.playlist_row_height = round(22 * prefs.ui_scale) + 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 self.prefs.use_jump_crossfade: + self.playerSubCommand = "now" + self.playerCommandReady = True + self.playing_state = 1 + self.radiobox.loaded_station = None - # Save user values - column_backup = gui.pl_st - rspw = gui.pref_rspw - grspw = gui.pref_gallery_w + if self.tauon.stream_proxy.download_running: + self.tauon.stream_proxy.stop() - gui.destroy_textures() - gui.rescale() + if update_title: + update_title_do() - # Scale saved values - gui.pl_st = column_backup - for item in gui.pl_st: - item[1] *= diff_ratio - gui.pref_rspw = rspw * diff_ratio - gui.pref_gallery_w = grspw * diff_ratio - 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!") + self.deduct_shuffle(self.target_object.index) -if prefs.prefer_side is False: - gui.rsp = False + def play_target(self, gapless: bool = False, jump: bool = False) -> None: + self.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 + self.radiobox.loaded_station = None -def get_global_mouse(): - i_y = pointer(c_int(0)) - i_x = pointer(c_int(0)) - SDL_GetGlobalMouseState(i_x, i_y) - return i_x.contents.value, i_y.contents.value + if self.tauon.stream_proxy and self.tauon.stream_proxy.download_running: + self.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 -def get_window_position(): - i_y = pointer(c_int(0)) - i_x = pointer(c_int(0)) - SDL_GetWindowPosition(t_window, i_x, i_y) - return i_x.contents.value, i_y.contents.value + self.playerCommand = "open" + if jump: # and not prefs.use_jump_crossfade: + self.playerSubCommand = "now" + self.playerCommandReady = True -# Access functions from libopenmpt for scanning tracker files -class MOD(Structure): - _fields_ = [("ctl", c_char_p), ("value", c_char_p)] + 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() -mpt = None -try: - p = ctypes.util.find_library("libopenmpt") - if p: - mpt = ctypes.cdll.LoadLibrary(p) - elif msys: - mpt = ctypes.cdll.LoadLibrary("libopenmpt-0.dll") - else: - mpt = ctypes.cdll.LoadLibrary("libopenmpt.so") + if self.lfm_scrobbler.a_sc: + self.lfm_scrobbler.a_sc = False + self.a_time = 0 - mpt.openmpt_module_create_from_memory.restype = c_void_p - mpt.openmpt_module_get_metadata.restype = c_char_p - mpt.openmpt_module_get_duration_seconds.restype = c_double -except Exception: - logging.exception("Failed to load libopenmpt!") + self.lfm_scrobbler.start_queue() + if (prefs.album_mode or not self.gui.rsp) and (self.gui.theme_name == "Carbon" or self.prefs.colour_from_image): + target = self.playing_object() + if target and self.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) -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), - ] + def jump(self, index: int, pl_position: int = None, jump: bool = True) -> None: + self.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] -gme = None -p = None -try: - p = ctypes.util.find_library("libgme") - if p: - gme = ctypes.cdll.LoadLibrary(p) - elif msys: - gme = ctypes.cdll.LoadLibrary("libgme-0.dll") - else: - gme = ctypes.cdll.LoadLibrary("libgme.so") + if len(self.track_queue) > 0: + self.left_time = self.playing_time + self.left_index = self.track_queue[self.queue_step] - gme.gme_free_info.argtypes = [ctypes.POINTER(GMETrackInfo)] - gme.gme_track_info.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.POINTER(GMETrackInfo)), ctypes.c_int] - gme.gme_track_info.restype = ctypes.c_char_p - gme.gme_open_file.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p), ctypes.c_int] - gme.gme_open_file.restype = ctypes.c_char_p + 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 -except Exception: - logging.exception("Cannot find libgme") + global playlist_hold + self.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) -def use_id3(tags: ID3, nt: TrackClass): - def natural_get(tag: ID3, track: TrackClass, frame: str, attr: str) -> str | None: - frames = tag.getall(frame) - if frames and frames[0].text: - if track is None: - return str(frames[0].text[0]) - setattr(track, attr, str(frames[0].text[0])) - elif track is None: - return "" - else: - setattr(track, attr, "") + if pl_position is not None: + self.playlist_playing_position = pl_position - tag = tags + self.gui.pl_update = 1 - natural_get(tags, nt, "TIT2", "title") - natural_get(tags, nt, "TPE1", "artist") - natural_get(tags, nt, "TPE2", "album_artist") - natural_get(tags, nt, "TCON", "genre") # content type - natural_get(tags, nt, "TALB", "album") - natural_get(tags, nt, "TDRC", "date") - natural_get(tags, nt, "TCOM", "composer") - natural_get(tags, nt, "COMM", "comment") + def back(self) -> None: + if self.playing_state < 3 and self.prefs.back_restarts and self.playing_time > 6: + self.seek_time(0) + self.render_playlist() + return - process_odat(nt, natural_get(tags, None, "TDOR", None)) + if self.tauon.spot_ctl.coasting: + self.tauon.spot_ctl.control("previous") + self.tauon.spot_ctl.update_timer.set() + self.playing_time = -2 + self.decode_time = -2 + return - frames = tag.getall("POPM") - rating = 0 - if frames: - for frame in frames: - if frame.rating: - rating = frame.rating - nt.misc["POPM"] = frame.rating + if len(self.track_queue) > 0: + self.left_time = self.playing_time + self.left_index = self.track_queue[self.queue_step] - if len(nt.comment) > 4 and nt.comment[2] == "+": - nt.comment = "" - if nt.comment[0:3] == "000": - nt.comment = "" + self.gui.update_spec = 0 + # Move up + if self.random_mode is False and len(self.playing_playlist()) > self.playlist_playing_position > 0: - frames = tag.getall("USLT") - if frames: - nt.lyrics = frames[0].text - if 0 < len(nt.lyrics) < 150: - if "unavailable" in nt.lyrics or ".com" in nt.lyrics or "www." in nt.lyrics: - nt.lyrics = "" + if len(self.track_queue) > 0 and self.playing_playlist()[self.playlist_playing_position] != \ + self.track_queue[ + self.queue_step]: - frames = tag.getall("TPE1") - if frames: - d = [] - for frame in frames: - for t in frame.text: - d.append(t) - if len(d) > 1: - nt.misc["artists"] = d - nt.artist = "; ".join(d) + 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 - frames = tag.getall("TCON") - if frames: - d = [] - for frame in frames: - for t in frame.text: - d.append(t) - if len(d) > 1: - nt.misc["genres"] = d - nt.genre = " / ".join(d) + 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) - track_no = natural_get(tags, None, "TRCK", None) - nt.track_total = "" - nt.track_number = "" - if track_no and track_no != "null": - if "/" in track_no: - a, b = track_no.split("/") - nt.track_number = a - nt.track_total = b + elif self.random_mode is True and self.queue_step > 0: + self.queue_step -= 1 + self.play_target(jump=True) else: - nt.track_number = track_no + logging.info("BACK: NO CASE!") + self.show_current() - disc = natural_get(tags, None, "TPOS", None) # set ? or ?/? - nt.disc_total = "" - nt.disc_number = "" - if disc: - if "/" in disc: - a, b = disc.split("/") - nt.disc_number = a - nt.disc_total = b - else: - nt.disc_number = disc + if self.active_playlist_viewing == self.active_playlist_playing: + self.show_current(False, True) - tx = tags.getall("UFID") - if tx: - for item in tx: - if item.owner == "http://musicbrainz.org": - nt.misc["musicbrainz_recordingid"] = item.data.decode() + if prefs.album_mode: + goto_album(self.playlist_playing_position) + if self.gui.combo_mode and self.active_playlist_viewing == self.active_playlist_playing: + self.show_current() - tx = tags.getall("TSOP") - if tx: - nt.misc["artist_sort"] = tx[0].text[0] + self.render_playlist() + self.notify_update() + notify_song() + self.lfm_scrobbler.start_queue() + self.gui.pl_update += 1 - tx = tags.getall("TXXX") - if tx: - for item in tx: - if item.desc == "MusicBrainz Release Track Id": - nt.misc["musicbrainz_trackid"] = item.text[0] - if item.desc == "MusicBrainz Album Id": - nt.misc["musicbrainz_albumid"] = item.text[0] - if item.desc == "MusicBrainz Release Group Id": - nt.misc["musicbrainz_releasegroupid"] = item.text[0] - if item.desc == "MusicBrainz Artist Id": - artist_id_list: list[str] = [] - for uuid in item.text: - split_uuids = uuid.split("/") # UUIDs can be split by a special character - for split_uuid in split_uuids: - artist_id_list.append(split_uuid) - nt.misc["musicbrainz_artistids"] = artist_id_list + 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 self.tauon.thread_manager.player_lock.locked(): try: - desc = item.desc.lower() - if desc == "replaygain_track_gain": - nt.misc["replaygain_track_gain"] = float(item.text[0].strip(" dB")) - if desc == "replaygain_track_peak": - nt.misc["replaygain_track_peak"] = float(item.text[0]) - if desc == "replaygain_album_gain": - nt.misc["replaygain_album_gain"] = float(item.text[0].strip(" dB")) - if desc == "replaygain_album_peak": - nt.misc["replaygain_album_peak"] = float(item.text[0]) + self.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("Tag Scan: Read Replay Gain MP3 error") - logging.debug(nt.fullpath) + logging.exception("Unknown exception trying to release player_lock") - if item.desc == "FMPS_RATING": - nt.misc["FMPS_Rating"] = float(item.text[0]) + 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() + self.gui.update_spec = 0 + # gui.update_level = True # Allows visualiser to enter decay sequence + self.gui.update = True + if update_title: + update_title_do() # Update title bar text -def scan_ffprobe(nt: TrackClass): - startupinfo = None - if system == "Windows" or msys: - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - try: - result = subprocess.run( - [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format=duration", "-of", - "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) - nt.length = float(result.stdout.decode()) - except Exception: - logging.exception("FFPROBE couldn't supply a duration") - try: - result = subprocess.run( - [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format_tags=title", "-of", - "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) - nt.title = str(result.stdout.decode()) - except Exception: - logging.exception("FFPROBE couldn't supply a title") - try: - result = subprocess.run( - [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format_tags=artist", "-of", - "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) - nt.artist = str(result.stdout.decode()) - except Exception: - logging.exception("FFPROBE couldn't supply a artist") - try: - result = subprocess.run( - [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format_tags=album", "-of", - "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) - nt.album = str(result.stdout.decode()) - except Exception: - logging.exception("FFPROBE couldn't supply a album") - try: - result = subprocess.run( - [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format_tags=date", "-of", - "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) - nt.date = str(result.stdout.decode()) - except Exception: - logging.exception("FFPROBE couldn't supply a date") - try: - result = subprocess.run( - [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format_tags=track", "-of", - "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) - nt.track_number = str(result.stdout.decode()) - except Exception: - logging.exception("FFPROBE couldn't supply a track") + if self.tauon.stream_proxy and self.tauon.stream_proxy.download_running: + self.tauon.stream_proxy.stop() + if block: + loop = 0 + sleep_timeout(lambda: self.playerSubCommand != "stopped", 2) + if self.tauon.stream_proxy.download_running: + sleep_timeout(lambda: tauon.stream_proxy.download_running, 2) + if self.tauon.spot_ctl.playing or self.tauon.spot_ctl.coasting: + logging.info("Spotify stop") + self.tauon.spot_ctl.control("stop") -def tag_scan(nt: TrackClass) -> TrackClass | None: - """This function takes a track object and scans metadata for it. (Filepath needs to be set)""" - if nt.is_embed_cue: - return nt - if nt.is_network or not nt.fullpath: - return None - try: - try: - nt.modified_time = os.path.getmtime(nt.fullpath) - nt.found = True - except FileNotFoundError: - logging.error("File not found when executing getmtime!") - nt.found = False - return nt - except Exception: - logging.exception("Unknown error executing getmtime!") - nt.found = False - return nt + self.notify_update() + self.lfm_scrobbler.start_queue() + return previous_state - nt.misc.clear() + def pause(self) -> None: - nt.file_ext = os.path.splitext(os.path.basename(nt.fullpath))[1][1:].upper() + if self.tauon.spotc and self.tauon.spotc.running and self.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 nt.file_ext.lower() in GME_Formats and gme: + if self.playing_state == 3: + if self.tauon.spot_ctl.coasting: + if self.tauon.spot_ctl.paused: + self.tauon.spot_ctl.control("resume") + else: + self.tauon.spot_ctl.control("pause") + return - emu = ctypes.c_void_p() - track_info = ctypes.POINTER(GMETrackInfo)() - err = gme.gme_open_file(nt.fullpath.encode("utf-8"), ctypes.byref(emu), -1) - #logging.error(err) - if not err: - n = nt.subtrack - err = gme.gme_track_info(emu, byref(track_info), n) - #logging.error(err) - if not err: - nt.length = track_info.contents.play_length / 1000 - nt.title = track_info.contents.song.decode("utf-8") - nt.artist = track_info.contents.author.decode("utf-8") - nt.album = track_info.contents.game.decode("utf-8") - nt.comment = track_info.contents.comment.decode("utf-8") - gme.gme_free_info(track_info) - gme.gme_delete(emu) + if self.tauon.spot_ctl.playing: + if self.playing_state == 2: + self.tauon.spot_ctl.control("resume") + self.playing_state = 1 + elif self.playing_state == 1: + self.tauon.spot_ctl.control("pause") + self.playing_state = 2 + self.render_playlist() + return - filepath = nt.fullpath # this is the full file path - filename = nt.filename # this is the name of the file + 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() - # Get the directory of the file - dir_path = os.path.dirname(filepath) + self.playerCommandReady = True - # Loop through all files in the directory to find any matching M3U - for file in os.listdir(dir_path): - if file.endswith(".m3u"): - with open(os.path.join(dir_path, file), encoding="utf-8", errors="replace") as f: - content = f.read() - if "�" in content: # Check for replacement marker - with open(os.path.join(dir_path, file), encoding="windows-1252") as b: - content = b.read() - if "::" in content: - a, b = content.split("::") - if a == filename: - s = re.split(r"(? None: + if self.playing_state == 1: + self.playerCommand = "pauseon" + self.playing_state = 2 - mpt.openmpt_module_destroy(byref(MOD1)) - del MOD1 + self.playerCommandReady = True + self.render_playlist() + self.notify_update() - elif nt.file_ext == "FLAC": - with Flac(nt.fullpath) as audio: - audio.read() + def play_pause(self) -> None: + if self.playing_state == 3: + self.stop() + elif self.playing_state > 0: + self.pause() + else: + self.play() - nt.length = audio.length - nt.title = audio.title - nt.artist = audio.artist - nt.album = audio.album - nt.composer = audio.composer - nt.date = audio.date - nt.samplerate = audio.sample_rate - nt.bit_depth = audio.bit_depth - nt.size = os.path.getsize(nt.fullpath) - nt.track_number = audio.track_number - nt.genre = audio.genre - nt.album_artist = audio.album_artist - nt.disc_number = audio.disc_number - nt.lyrics = audio.lyrics - if nt.length: - nt.bitrate = int(nt.size / nt.length * 8 / 1024) - nt.track_total = audio.track_total - nt.disc_total = audio.disc_total - nt.comment = audio.comment - nt.cue_sheet = audio.cue_sheet - nt.misc = audio.misc + def seek_decimal(self, decimal: int) -> None: + # if self.commit: + # return + if self.playing_state in (1, 2) or (self.playing_state == 3 and self.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 - elif nt.file_ext == "WAV": - with Wav(nt.fullpath) as audio: - try: - audio.read() + if self.msys and taskbar_progress and self.windows_progress: + self.windows_progress.update(True) - nt.samplerate = audio.sample_rate - nt.length = audio.length - nt.title = audio.title - nt.artist = audio.artist - nt.album = audio.album - nt.track_number = audio.track_number + if self.mpris is not None: + self.mpris.seek_do(self.playing_time) - except Exception: - logging.exception("Failed saving WAV file as a Track, will try again differently") - audio = mutagen.File(nt.fullpath) - nt.samplerate = audio.info.sample_rate - nt.bitrate = audio.info.bitrate // 1000 - nt.length = audio.info.length - nt.size = os.path.getsize(nt.fullpath) - audio = mutagen.File(nt.fullpath) - if audio.tags and type(audio.tags) == mutagen.wave._WaveID3: - use_id3(audio.tags, nt) + def seek_time(self, new: float) -> None: + # if self.commit: + # return + if self.playing_state in (1, 2) or (self.playing_state == 3 and self.tauon.spot_ctl.coasting): - elif nt.file_ext == "OPUS" or nt.file_ext == "OGG" or nt.file_ext == "OGA": + if new > self.playing_length - 0.5: + self.advance() + return - #logging.info("get opus") - with Opus(nt.fullpath) as audio: - audio.read() + if new < 0.4: + new = 0 - #logging.info(audio.title) + self.new_time = new + self.playing_time = new - nt.length = audio.length - nt.title = audio.title - nt.artist = audio.artist - nt.album = audio.album - nt.composer = audio.composer - nt.date = audio.date - nt.samplerate = audio.sample_rate - nt.size = os.path.getsize(nt.fullpath) - nt.track_number = audio.track_number - nt.genre = audio.genre - nt.album_artist = audio.album_artist - nt.bitrate = audio.bit_rate - nt.lyrics = audio.lyrics - nt.disc_number = audio.disc_number - nt.track_total = audio.track_total - nt.disc_total = audio.disc_total - nt.comment = audio.comment - nt.misc = audio.misc - if nt.bitrate == 0 and nt.length > 0: - nt.bitrate = int(nt.size / nt.length * 8 / 1024) + self.playerCommand = "seek" + self.playerCommandReady = True - elif nt.file_ext == "APE": - with mutagen.File(nt.fullpath) as audio: - nt.length = audio.info.length - nt.bit_depth = audio.info.bits_per_sample - nt.samplerate = audio.info.sample_rate - nt.size = os.path.getsize(nt.fullpath) - if nt.length > 0: - nt.bitrate = int(nt.size / nt.length * 8 / 1024) + if self.mpris is not None: + self.mpris.seek_do(self.playing_time) - # # def getter(audio, key, type): - # # if - # t = audio.tags - # logging.info(t.keys()) - # nt.size = os.path.getsize(nt.fullpath) - # nt.title = str(t.get("title", "")) - # nt.album = str(t.get("album", "")) - # nt.date = str(t.get("year", "")) - # nt.disc_number = str(t.get("discnumber", "")) - # nt.comment = str(t.get("comment", "")) - # nt.artist = str(t.get("artist", "")) - # nt.composer = str(t.get("composer", "")) - # nt.composer = str(t.get("composer", "")) + def play(self) -> None: - with Ape(nt.fullpath) as audio: - audio.read() + if self.tauon.spot_ctl.playing: + if self.playing_state == 2: + self.play_pause() + return - # logging.info(audio.title) + # Unpause if paused + if self.playing_state == 2: + self.playerCommand = "pauseoff" + self.playerCommandReady = True + self.playing_state = 1 + self.notify_update() - # nt.length = audio.length - nt.title = audio.title - nt.artist = audio.artist - nt.album = audio.album - nt.date = audio.date - nt.composer = audio.composer - # nt.bit_depth = audio.bit_depth - nt.track_number = audio.track_number - nt.genre = audio.genre - nt.album_artist = audio.album_artist - nt.disc_number = audio.disc_number - nt.lyrics = audio.lyrics - nt.track_total = audio.track_total - nt.disc_total = audio.disc_total - nt.comment = audio.comment - nt.misc = audio.misc + # If stopped + elif self.playing_state == 0: - elif nt.file_ext == "WV" or nt.file_ext == "TTA": + if self.radiobox.loaded_station: + self.radiobox.start(self.radiobox.loaded_station) + return - with Ape(nt.fullpath) as audio: - audio.read() + # 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 - # logging.info(audio.title) + self.play_target() - nt.length = audio.length - nt.title = audio.title - nt.artist = audio.artist - nt.album = audio.album - nt.date = audio.date - nt.composer = audio.composer - nt.samplerate = audio.sample_rate - nt.bit_depth = audio.bit_depth - nt.size = os.path.getsize(nt.fullpath) - nt.track_number = audio.track_number - nt.genre = audio.genre - nt.album_artist = audio.album_artist - nt.disc_number = audio.disc_number - nt.lyrics = audio.lyrics - if nt.length > 0: - nt.bitrate = int(nt.size / nt.length * 8 / 1024) - nt.track_total = audio.track_total - nt.disc_total = audio.disc_total - nt.comment = audio.comment - nt.misc = audio.misc + # If the queue is not empty, play? + elif len(self.track_queue) > 0: + self.play_target() - else: - # Use MUTAGEN - try: - if nt.file_ext.lower() in VID_Formats: - scan_ffprobe(nt) - return nt + self.render_playlist() - try: - audio = mutagen.File(nt.fullpath) - except Exception: - logging.exception("Mutagen scan failed, falling back to FFPROBE") - scan_ffprobe(nt) - return nt + def spot_test_progress(self) -> None: + if self.playing_state in (1, 2) and self.tauon.spot_ctl.playing: + th = 5 # the rate to poll the spotify API + if self.playing_time > self.playing_length: + th = 1 + if not self.tauon.spot_ctl.paused: + if self.tauon.spot_ctl.start_timer.get() < 0.5: + self.tauon.spot_ctl.progress_timer.set() + return + add_time = self.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() + self.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 self.tauon.spot_ctl.update_timer.get() > th: + self.tauon.spot_ctl.update_timer.set() + shooter(self.tauon.spot_ctl.monitor) + else: + self.test_progress() - nt.samplerate = audio.info.sample_rate - nt.bitrate = audio.info.bitrate // 1000 - nt.length = audio.info.length - nt.size = os.path.getsize(nt.fullpath) + elif self.playing_state == 3 and self.tauon.spot_ctl.coasting: + th = 7 + if self.playing_time > self.playing_length or self.playing_time < 2.5: + th = 1 + if self.tauon.spot_ctl.update_timer.get() < th: + if not self.tauon.spot_ctl.paused: + self.playing_time += self.tauon.spot_ctl.progress_timer.get() + self.decode_time = self.playing_time + self.tauon.spot_ctl.progress_timer.set() - if not nt.length: - try: - startupinfo = None - if system == "Windows" or msys: - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - result = subprocess.run([tauon.get_ffprobe(), "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) - nt.length = float(result.stdout.decode()) - except Exception: - logging.exception("FFPROBE couldn't supply a duration") + else: + self.tauon.spot_ctl.update_timer.set() + self.tauon.spot_ctl.update() - if type(audio.tags) == mutagen.mp4.MP4Tags: - tags = audio.tags + 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 in_get(key, tags): - if key in tags: - return tags[key][0] - return "" + def test_progress(self) -> None: + # Fuzzy reload lastfm for rescrobble + if self.lfm_scrobbler.a_sc and self.playing_time < 1: + self.lfm_scrobbler.a_sc = False + self.a_time = 0 - nt.title = in_get("\xa9nam", tags) - nt.album = in_get("\xa9alb", tags) - nt.artist = in_get("\xa9ART", tags) - nt.album_artist = in_get("aART", tags) - nt.composer = in_get("\xa9wrt", tags) - nt.date = in_get("\xa9day", tags) - nt.comment = in_get("\xa9cmt", tags) - nt.genre = in_get("\xa9gen", tags) - if "\xa9lyr" in tags: - nt.lyrics = in_get("\xa9lyr", tags) - nt.track_total = "" - nt.track_number = "" - t = in_get("trkn", tags) - if t: - nt.track_number = str(t[0]) - if t[1]: - nt.track_total = str(t[1]) + # 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 - nt.disc_total = "" - nt.disc_number = "" - t = in_get("disk", tags) - if t: - nt.disc_number = str(t[0]) - if t[1]: - nt.disc_total = str(t[1]) + gap_extra = 2 # 2 - if "----:com.apple.iTunes:MusicBrainz Track Id" in tags: - nt.misc["musicbrainz_recordingid"] = in_get( - "----:com.apple.iTunes:MusicBrainz Track Id", - tags).decode() - if "----:com.apple.iTunes:MusicBrainz Release Track Id" in tags: - nt.misc["musicbrainz_trackid"] = in_get( - "----:com.apple.iTunes:MusicBrainz Release Track Id", - tags).decode() - if "----:com.apple.iTunes:MusicBrainz Album Id" in tags: - nt.misc["musicbrainz_albumid"] = in_get( - "----:com.apple.iTunes:MusicBrainz Album Id", - tags).decode() - if "----:com.apple.iTunes:MusicBrainz Release Group Id" in tags: - nt.misc["musicbrainz_releasegroupid"] = in_get( - "----:com.apple.iTunes:MusicBrainz Release Group Id", - tags).decode() - if "----:com.apple.iTunes:MusicBrainz Artist Id" in tags: - nt.misc["musicbrainz_artistids"] = [x.decode() for x in - tags.get("----:com.apple.iTunes:MusicBrainz Artist Id")] + if self.tauon.spot_ctl.playing or self.tauon.chrome_mode: + gap_extra = 3 + if self.msys and taskbar_progress and self.windows_progress: + self.windows_progress.update(True) - elif type(audio.tags) == mutagen.id3.ID3: - use_id3(audio.tags, nt) + 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 - except Exception: - logging.exception("Failed loading file through Mutagen") - raise - - - # Parse any multiple artists into list - artists = nt.artist.split(";") - if len(artists) > 1: - for a in artists: - a = a.strip() - if a: - if "artists" not in nt.misc: - nt.misc["artists"] = [] - if a not in nt.misc["artists"]: - nt.misc["artists"].append(a) + 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 self.tauon.spot_ctl.playing and self.tauon.spot_ctl.start_timer.get() < 3: + return - except Exception: - try: - if Exception is UnicodeDecodeError: - logging.exception("Unicode decode error on file:", nt.fullpath, "\n") - else: - logging.exception("Error: Tag read failed on file:", nt.fullpath, "\n") - except Exception: - logging.exception("Error printing error. Non utf8 not allowed:", nt.fullpath.encode("utf-8", "surrogateescape").decode("utf-8", "replace"), "\n") - return nt + # Allow some time for backend to provide a length + if self.playing_time < 6 and self.playing_length == 0: + return + if not self.tauon.spot_ctl.playing and self.a_time < 2: + return - return nt + self.decode_time = 0 + pp = self.playing_playlist() -def get_radio_art() -> None: - if radiobox.loaded_url in radiobox.websocket_source_urls: - return - if "ggdrasil" in radiobox.playing_title: - time.sleep(3) - url = "https://yggdrasilradio.net/data.php?" - response = requests.get(url, timeout=10) - if response.status_code == 200: - lines = response.content.decode().split("|") - if len(lines) > 11 and lines[11]: - art_id = lines[11].strip().strip("*") - art_url = "https://yggdrasilradio.net/images/albumart/" + art_id - art_response = requests.get(art_url, timeout=10) - 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" - pctl.update_tag_history() + 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) + self.gui.update += 2 + self.auto_stop = False - elif "gensokyoradio.net" in radiobox.loaded_url: + 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) - response = requests.get("https://gensokyoradio.net/api/station/playing/", timeout=10) + 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? - if response.status_code == 200: - d = json.loads(response.text) - song_info = d.get("SONGINFO") - if song_info: - radiobox.dummy_track.artist = song_info.get("ARTIST", "") - radiobox.dummy_track.title = song_info.get("TITLE", "") - radiobox.dummy_track.album = song_info.get("ALBUM", "") + ti = self.get_track(pp[self.playlist_playing_position]) - misc = d.get("MISC") - if misc: - art = misc.get("ALBUMART") - if art: - art_url = "https://gensokyoradio.net/images/albums/500/" + art - art_response = requests.get(art_url, timeout=10) - 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" - pctl.update_tag_history() + i = self.playlist_playing_position - elif "radio.plaza.one" in radiobox.loaded_url: - time.sleep(3) - logging.info("Fetching plaza art") - response = requests.get("https://api.plaza.one/status", timeout=10) - if response.status_code == 200: - d = json.loads(response.text) - if "song" in d: - tr = d["song"]["length"] - d["song"]["position"] - tr += 1 - tr = max(tr, 10) - pctl.radio_poll_timer.force_set(tr * -1) + # 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 - if "artist" in d["song"]: - radiobox.dummy_track.artist = d["song"]["artist"] - if "title" in d["song"]: - radiobox.dummy_track.title = d["song"]["title"] - if "album" in d["song"]: - radiobox.dummy_track.album = d["song"]["album"] - if "artwork_src" in d["song"]: - art_url = d["song"]["artwork_src"] - art_response = requests.get(art_url, timeout=10) - 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" - pctl.update_tag_history() + # 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) - # Failure - elif pctl.radio_image_bin: - pctl.radio_image_bin.close() - pctl.radio_image_bin = None + self.selected_in_playlist = i + shift_selection = [i] - gui.clear_image_cache_next += 1 + self.jump(pp[i], i, jump=False) -class PlayerCtl: - """Main class that controls playback (play, pause, stepping, playlists, queue etc). Sends commands to backend.""" + elif self.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(pctl.default_playlist): - # C-PC - def __init__(self): + logging.info("Repeat follow cursor") - self.running: bool = True - self.prefs: Prefs = prefs - self.install_directory: Path = install_directory + self.playing_time = 0 + self.decode_time = 0 + self.active_playlist_playing = self.active_playlist_viewing + self.playlist_playing_position = self.selected_in_playlist - # Database + self.track_queue.append(pctl.default_playlist[self.selected_in_playlist]) + self.queue_step = len(self.track_queue) - 1 + self.play_target(jump=False) + self.render_playlist() + self.lfm_scrobbler.start_queue() - 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 + 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.gen_codes = gen_codes + #self.render_playlist() + self.lfm_scrobbler.start_queue() - self.shuffle_pools = {} - self.after_import_flag = False - self.quick_add_target = None + # Reload lastfm for rescrobble + if self.lfm_scrobbler.a_sc: + self.lfm_scrobbler.a_sc = False + self.a_time = 0 - self.album_mbid_release_cache = {} - self.album_mbid_release_group_cache = {} - self.mbid_image_url_cache = {} + 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: - # Misc player control + # not (self.force_queue and not self.pause_queue) and \ - self.url: str = "" - # self.save_urls = url_saves - self.tag_meta: str = "" - self.found_tags = {} - self.encoder_pause = 0 + # We can shave it closer + if not self.playing_time + 0.1 >= self.playing_length: + return - # Playback + 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 + self.lfm_scrobbler.start_queue() - 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.gui.update += 1 + self.gui.pl_update = 1 - self.record_stream = False - self.record_title = "" + if update_title: + update_title_do() + self.notify_update() + else: + # self.advance(quiet=True, end=True) - # Bass + id = self.advance(quiet=True, end=True, dry=True) + if id is not None and not self.tauon.spot_ctl.playing: + #logging.info("Commit") + self.start_commit(id) + return - self.bass_devices = [] - self.set_device = 0 + self.advance(quiet=True, end=True) + self.playing_time = 0 + self.decode_time = 0 - self.gst_devices = [] # Display names - self.gst_outputs = {} # Display name : (sink, device) + 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 -# 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 + 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 self.tauon.spot_ctl.coasting: + self.tauon.spot_ctl.control("next") + self.tauon.spot_ctl.update_timer.set() + self.playing_time = -2 + self.decode_time = -2 + return None - self.playing_time_int = 0 # playing time but with no decimel + # Temporary Workaround for UI block causing unwanted dragging + if not dry: + quick_d_timer.set() - self.windows_progress = None + if self.prefs.show_current_on_transition: + quiet = False - self.finish_transition = False - # self.queue_target = 0 - self.start_time_target = 0 + # Trim the history if it gets too long + while len(self.track_queue) > 250: + self.queue_step -= 1 + del self.track_queue[0] - self.decode_time = 0 - self.download_time = 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] - self.radio_meta_on = "" + # 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') - self.radio_scrobble_trip = True - self.radio_scrobble_timer = Timer() + if not dry: + self.playing_time = 0 + self.decode_time = 0 + self.playing_length = 100 + self.gui.update_spec = 0 - self.radio_image_bin = None - self.radio_rate_timer = Timer(2) - self.radio_poll_timer = Timer(2) + old = self.queue_step + end_of_playlist = False - self.volume_update_timer = Timer() - self.wake_past_time = 0 + # Force queue (middle click on track) + if len(self.force_queue) > 0 and not self.pause_queue: - self.regen_in_progress = False - self.notify_in_progress = False + q = self.force_queue[0] + target_index = q.track_id - self.radio_playlists = radio_playlists - self.radio_playlist_viewing = radio_playlist_viewing - self.tag_history = {} + 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) - self.commit: int | None = None - self.spot_playing = False + if not dry: + pl = id_to_pl(q.playlist_id) + if pl is not None: + self.active_playlist_playing = pl - self.buffering_percent = 0 + if target_index not in self.playing_playlist(): + del self.force_queue[0] + self.advance() + return None - def notify_change(self) -> None: - self.db_inc += 1 - tauon.bg_save() + if dry: + return target_index - 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 - } + 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) - 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) + # Set the flag that we have entered the album + self.force_queue[0].album_stage = 1 - 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"] + # This code is mirrored below ------- + ok_continue = True - 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 + # 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 - 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 = "" + # 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 - 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() + elif q.album_stage == 1: + # We have previously started playing this album - if self.tag_meta: - radiobox.song_key = self.tag_meta - else: - radiobox.song_key = radiobox.dummy_track.artist + " - " + radiobox.dummy_track.title + # Check to see if we still are: + ok_continue = True - self.update_tag_history() - if radiobox.loaded_url not in radiobox.websocket_source_urls: - self.radio_image_bin = None - logging.info("NEXT RADIO TRACK") + 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 - try: - get_radio_art() - except Exception: - logging.exception("Get art error") + pl = self.multi_playlist[self.active_playlist_playing].playlist_ids - self.notify_update(mpris=False) - if self.mpris: - self.mpris.update(force=True) + # Check next song is in album + if ok_continue: - lfm_scrobbler.listen_track(radiobox.dummy_track) - lfm_scrobbler.start_queue() + # 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 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)) + if dry: + return None - 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") + del self.force_queue[0] + self.advance() + return None - 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") + # 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 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 ok_continue: + # We seem to be still in the album. Step down one and play + if not dry: + self.playlist_playing_position += 1 - if image_path is None: - image_path = "" + 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 - image_path = image_path.replace("/", "\\") - #logging.info(image_path) + 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) - 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 not ok_continue: + # It seems this item has expired, remove it and call advance again + if dry: + return None - 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") + logging.info("Remove expired album from queue") + del self.force_queue[0] - 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 q.auto_stop: + self.auto_stop = True + if self.prefs.stop_end_queue and not self.force_queue: + self.auto_stop = True - if track_object.file_ext == "JELY": - return jellyfin.resolve_stream(track_object.url_key) + if queue_box.scroll_position > 0: + queue_box.scroll_position -= 1 - if track_object.file_ext == "KOEL": - return koel.resolve_stream(track_object.url_key) + # self.advance() + # return - if track_object.file_ext == "SUB": - return subsonic.resolve_stream(track_object.url_key) + 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 track_object.file_ext == "TAU": - return tau.resolve_stream(track_object.url_key), None + if target_index not in self.playing_playlist(): + if dry: + return None + del self.force_queue[0] + self.advance() + return None - return None, None + if dry: + return target_index - def playing_playlist(self) -> list[int] | None: - return self.multi_playlist[self.active_playlist_playing].playlist_ids + 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 self.prefs.stop_end_queue and not self.force_queue: + self.auto_stop = True + if queue_box.scroll_position > 0: + queue_box.scroll_position -= 1 - def playing_ready(self) -> bool: - return len(self.track_queue) > 0 + # Stop if playlist is empty + elif len(self.playing_playlist()) == 0: + if dry: + return None + self.stop() + return 0 - def selected_ready(self) -> bool: - return default_playlist and self.selected_in_playlist < len(default_playlist) + # Playback follow cursor + elif self.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(pctl.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 + if dry: + return pctl.default_playlist[self.selected_in_playlist] - def show_selected(self) -> int: - if gui.playlist_view_length < 1: - return 0 + self.active_playlist_playing = self.active_playlist_viewing + self.playlist_playing_position = self.selected_in_playlist - global shift_selection + self.track_queue.append(pctl.default_playlist[self.selected_in_playlist]) + self.queue_step = len(self.track_queue) - 1 + if play: + self.play_target(jump=not end) - for i in range(len(self.multi_playlist[self.active_playlist_viewing].playlist_ids)): + # 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 self.prefs.album_shuffle_lock_mode): + # self.queue_step += 1 + new_step = self.queue_step + 1 - if i == self.selected_in_playlist: + if new_step == len(self.track_queue): - 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 + 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]] - self.render_playlist() + if ti.index not in pp: + if dry: + return None + logging.info("No tracks to repeat!") + return 0 - return 0 + matches = [] + for i, p in enumerate(pp): - def get_track(self, track_index: int) -> TrackClass: - """Get track object by track_index""" - return self.master_library[track_index] + if self.get_track(p).parent_folder_path == ti.parent_folder_path: + matches.append((i, p)) - 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 + if matches: + # Avoid a repeat of same track + if len(matches) > 1 and (k, ti.index) in matches: + matches.remove((k, ti.index)) - def show_object(self) -> None: - """The track to show in the metadata side panel""" - target_track = None + i, p = random.choice(matches) # not used - if self.playing_state == 3: - return radiobox.dummy_track + if self.prefs.true_shuffle: + id = ti.parent_folder_path + while True: + if id in self.shuffle_pools: + pool = self.shuffle_pools[id] - if 3 > self.playing_state > 0: - target_track = self.playing_object() + if not pool: + del self.shuffle_pools[id] # Trigger a refill + continue - 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]) + ref = pool.pop() + if dry: + pool.append(ref) + return ref[1] + # ref = random.choice(pool) + # pool.remove(ref) - elif self.playing_state == 0 and prefs.meta_persists_stop: - target_track = self.master_library[self.track_queue[self.queue_step]] + if ref[1] not in pp: # Check track still in the live playlist + logging.info("Track not in pool") + continue - 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]) + i, p = ref # Find position of reference in playlist + break - return target_track + # Refill the pool + random.shuffle(matches) + self.shuffle_pools[id] = matches + logging.info("Refill folder shuffle pool") - def playing_object(self) -> TrackClass | None: + self.playlist_playing_position = i + self.track_queue.append(p) + else: + # Normal select from playlist + if self.prefs.true_shuffle: + # True shuffle avoids repeats by using a pool + pl = self.multi_playlist[self.active_playlist_playing] + id = pl.uuid_int - if self.playing_state == 3: - return radiobox.dummy_track + while True: - if len(self.track_queue) > 0: - return self.master_library[self.track_queue[self.queue_step]] - return None + if id in self.shuffle_pools: + pool = self.shuffle_pools[id] + if not pool: + del self.shuffle_pools[id] # Trigger a refill + continue - def title_text(self) -> str: - line = "" - track = self.playing_object() - if track: - title = track.title - artist = track.artist + ref = pool.pop() + if dry: + pool.append(ref) + return ref + # ref = random.choice(pool) + # pool.remove(ref) - if not title: - line = clean_string(track.filename) - else: - if artist != "": - line += artist - if title != "": - if line != "": - line += " - " - line += title + if ref not in pl.playlist_ids: # Check track still in the live playlist + continue - if self.playing_state == 3 and not title and not artist: - return self.tag_meta + random_jump = pl.playlist_ids.index(ref) # Find position of reference in playlist + break - return line + # Refill the pool + self.update_shuffle_pool(pl.uuid_int) + else: + random_jump = random.randrange(len(self.playing_playlist())) # not used - def show(self) -> int | None: - global shift_selection + self.playlist_playing_position = random_jump + self.track_queue.append(self.playing_playlist()[random_jump]) - if not self.track_queue: - return 0 - return None + 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 - 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: + if rr: + if dry: + return None + self.play_target_rr() + elif play: + self.play_target(jump=not end) - # 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 not random mode, Step down 1 on the playlist + elif self.random_mode is False and len(self.playing_playlist()) > 0: - if tauon.spot_ctl.coasting: - sptr = tauon.dummy_track.misc.get("spotify-track-url") - if sptr: + # Stop at end of playlist + if self.playlist_playing_position == len(self.playing_playlist()) - 1: + if dry: + return None + if self.prefs.end_setting == "stop": + self.playing_state = 0 + self.playerCommand = "runstop" + self.playerCommandReady = True + end_of_playlist = True - 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 + elif self.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 self.prefs.end_setting != "cycle": + self.playing_state = 0 + self.playerCommand = "runstop" + self.playerCommandReady = True + end_of_playlist = True - if not self.track_queue: - return 0 + else: + p = self.active_playlist_playing + for i in range(len(self.multi_playlist)): - track_index = self.track_queue[self.queue_step] - if index is not None: - track_index = index + k = (p + i + 1) % len(self.multi_playlist) - # 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) + # Skip a playlist if empty + if not (self.multi_playlist[k].playlist_ids): + continue - if gui.playlist_view_length < 1: - return 0 + # Skip a playlist if hidden + if self.multi_playlist[k].hidden and self.prefs.tabs_on_top: + continue - 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: + # 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 - 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 + else: + # Restart current if no other eligible playlist found + self.playlist_playing_position = -1 + self.advance(end=end, force=True, play=play) - if select: - self.selected_in_playlist = i + return None - if playing: - # Make the found track the playing track - self.playlist_playing_position = i - self.active_playlist_playing = self.active_playlist_viewing + elif self.prefs.end_setting == "repeat": + self.playlist_playing_position = -1 + self.advance(end=end, force=True, play=play) + return None - 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 + self.gui.update += 3 - if not ( - quiet and self.playing_object().length < 15): # or (abs(self.playlist_view_position - i) < vl - 1)): + else: + if self.playlist_playing_position > len(self.playing_playlist()) - 1: + if dry: + return None + self.playlist_playing_position = 0 - # Align to album if in view range (and folder titles are active) - ap = get_album_info(i)[1][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 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 len(self.playing_playlist()) == self.playlist_playing_position + 1: + return None - # Move to a random offset --- + 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]) - elif i == self.playlist_view_position - 1 and self.playlist_view_position > 1: - self.playlist_view_position -= 1 + # 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) - # 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 + elif self.random_mode and (self.album_shuffle_mode or self.prefs.album_shuffle_lock_mode): + # Album shuffle mode + logging.info("Album shuffle mode") + po = self.playing_object() + redraw = False - # 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)) + # 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 self.prefs.album_shuffle_lock_mode and not end: + redraw = 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 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) - break + random.shuffle(albums) - 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 + 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!") - self.playlist_view_position = max(self.playlist_view_position, 0) + if dry: + return None - # if self.playlist_view_position > len(self.multi_playlist[self.active_playlist_viewing].playlist_ids) - 1: - # logging.info("Run Over") + if self.active_playlist_viewing == self.active_playlist_playing: + self.show_current(quiet=quiet) + elif self.prefs.auto_goto_playing: + self.show_current(quiet=quiet, this_only=True, playing=False, highlight=True, no_switch=True) - if select: - shift_selection = [] + # if prefs.album_mode: + # goto_album(self.playlist_playing) 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 self.tauon.spot_ctl.playing and end_of_playlist: + self.tauon.spot_ctl.control("stop") - if prefs.left_panel_mode == "artist list" and gui.lsp and not quiet: - artist_list_box.locate_artist(self.playing_object()) + self.notify_update() + self.lfm_scrobbler.start_queue() + if play: + notify_song(end_of_playlist, delay=1.3) + return None - 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()) + def reset_missing_flags(self) -> None: + for value in self.master_library.values(): + value.found = True + self.gui.pl_update += 1 - return 0 +class LastFMapi: + API_SECRET = "6e433964d3ff5e817b7724d16a9cf0cc" + connected = False + API_KEY = "bfdaf6357f1dddd494e5bee1afe38254" + scanning_username = "" - 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 + network = None + lastfm_network = None + tries = 0 - self.set_volume() + scanning_friends = False + scanning_loves = False + scanning_scrobbles = False - def set_volume(self, notify: bool = True) -> None: + def __init__(self, tauon: Tauon) -> None: + self.tauon = tauon + self.gui = self.tauon.gui + self.pctl = self.tauon.pctl + self.prefs = self.tauon.prefs + self.sg = None + self.url = 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 + def get_network(self) -> LibreFMNetwork: + if self.prefs.use_libre_fm: + return pylast.LibreFMNetwork + return pylast.LastFMNetwork - self.volume_update_timer.set() - self.playerCommand = "volume" - self.playerCommandReady = True - if notify: - self.notify_update() + 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" - def revert(self) -> None: + if self.network is None: + self.no_user_connect() - if self.queue_step == 0: - return + self.sg = pylast.SessionKeyGenerator(self.network) + self.url = self.sg.get_web_auth_url() + logging.info(str(self.url)) + copy_to_clipboard(self.url) + show_message(_("Web auth page opened"), _("Once authorised click the 'done' button."), mode="arrow") + webbrowser.open(self.url, new=2, autoraise=True) - 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 + def auth2(self) -> None: - if not len(self.track_queue) > self.queue_step >= 0: - logging.error("There is no previous track?") + # This is step 2 where the user clicks "Done" + + if self.sg is None: + show_message(_("You need to log in first")) 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 + 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) + self.prefs.last_fm_token = session_key + self.network = self.get_network()(api_key=self.API_KEY, api_secret= + self.API_SECRET, session_key=self.prefs.last_fm_token) + # user = self.network.get_authenticated_user() + # username = user.get_name() + self.prefs.last_fm_username = username - if id not in self.shuffle_pools: - self.update_shuffle_pool(pl.uuid_int) + 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") - 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 not toggle_lfm_auto(mode=1): + toggle_lfm_auto() - if track_id in pool: - pool.remove(track_id) + def auth3(self) -> None: + """This is used for 'logout'""" + self.prefs.last_fm_token = None + self.prefs.last_fm_username = "" + show_message(_("Logout will complete on app restart.")) + def connect(self, m_notify: bool = True) -> bool | None: - 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 not last_fm_enable: + return False - 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 + if self.connected is True: + if m_notify: + show_message(_("Already connected to Last.fm")) + return True - 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 self.prefs.last_fm_token is None: + show_message(_("No Last.Fm account registered"), _("Authorise an account in settings"), mode="info") + return None - if tauon.stream_proxy.download_running: - tauon.stream_proxy.stop() + logging.info("Attempting to connect to Last.fm network") - if update_title: - update_title_do() + try: - self.deduct_shuffle(self.target_object.index) + self.network = self.get_network()( + api_key=self.API_KEY, api_secret=self.API_SECRET, session_key=self.prefs.last_fm_token) # , username=lfm_username, password_hash=lfm_hash) - def play_target(self, gapless: bool = False, jump: bool = False) -> None: + self.connected = True + if m_notify: + show_message(_("Connection to Last.fm was successful."), mode="done") - tauon.thread_manager.ready_playback() + logging.info("Connection to lastfm appears successful") + return True - #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 + 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 tauon.stream_proxy and tauon.stream_proxy.download_running: - tauon.stream_proxy.stop() + def toggle(self) -> None: + self.prefs.scrobble_hold ^= True - 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 + def details_ready(self) -> bool: + if self.prefs.last_fm_token: + return True + return False - self.playerCommand = "open" - if jump: # and not prefs.use_jump_crossfade: - self.playerSubCommand = "now" + 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 - self.playerCommandReady = 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 - self.playing_state = 1 - self.update_change() - self.deduct_shuffle(target.index) + 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 - def update_change(self) -> None: - if update_title: - update_title_do() - self.notify_update() - hit_discord() - self.render_playlist() + 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 - if lfm_scrobbler.a_sc: - lfm_scrobbler.a_sc = False - self.a_time = 0 + def get_all_scrobbles_estimate_time(self) -> float | None: - lfm_scrobbler.start_queue() + if not self.connected: + self.connect(False) + if not self.connected or not self.prefs.last_fm_username: + return None - 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 + user = pylast.User(self.prefs.last_fm_username, self.network) + total = user.get_playcount() - album_art_gen.display(target, (0, 0), (50, 50), theme_only=True) + if total: + return 0.04364 * total + return 0 - def jump(self, index: int, pl_position: int = None, jump: bool = True) -> None: - lfm_scrobbler.start_queue() - self.auto_stop = False + def get_all_scrobbles(self) -> None: - 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 not self.connected: + self.connect(False) + if not self.connected or not self.prefs.last_fm_username: + return - if len(self.track_queue) > 0: - self.left_time = self.playing_time - self.left_index = self.track_queue[self.queue_step] + try: + self.scanning_scrobbles = True + self.network.enable_rate_limit() + user = pylast.User(self.prefs.last_fm_username, self.network) + # username = user.get_name() + perf_timer.set() + tracks = user.get_recent_tracks(None) - 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 + counts = {} - 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) + # 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 - if pl_position is not None: - self.playlist_playing_position = pl_position + touched = [] - gui.pl_update = 1 + # Add counts to matching tracks + for key, value in counts.items(): + artist, title = key + artist = artist.lower() + title = title.lower() - 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() + for track in self.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?") + self.gui.pl_update += 1 + self.scanning_scrobbles = False + show_message(_("Scanning failed. Try again?"), mode="error") 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 + logging.info(perf_timer.get()) + self.gui.pl_update += 1 + self.scanning_scrobbles = False + self.tauon.bg_save() + show_message(_("Scanning scrobbles complete"), mode="done") - if len(self.track_queue) > 0: - self.left_time = self.playing_time - self.left_index = self.track_queue[self.queue_step] + def artist_info(self, artist: str): - gui.update_spec = 0 - # Move up - if self.random_mode is False and len(self.playing_playlist()) > self.playlist_playing_position > 0: + if self.lastfm_network is None: + if self.last_fm_only_connect() is False: + return False, "", "" - if len(self.track_queue) > 0 and self.playing_playlist()[self.playlist_playing_position] != \ - self.track_queue[ - self.queue_step]: + 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() - 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 + return True, bio, "", mbid, url + except Exception: + logging.exception("last.fm get artist info failed") - 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) + return False, "", "", "", "" - 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() + def artist_mbid(self, artist: str): - if self.active_playlist_viewing == self.active_playlist_playing: - self.show_current(False, True) + if self.lastfm_network is None: + if self.last_fm_only_connect() is False: + return "" - 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() + 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") - self.render_playlist() - self.notify_update() - notify_song() - lfm_scrobbler.start_queue() - gui.pl_update += 1 + return "" - def stop(self, block: bool = False, run : bool = False) -> None: + 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 - self.playerCommand = "stop" - if run: - self.playerCommand = "runstop" - if block: - self.playerSubCommand = "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 - self.playerCommandReady = True + remote_loved = track.get_userloved() - 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 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 - 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() + if remote_loved is None: + logging.error("Error getting loved status") + return - 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 + local_loved = love(set=False, track_id=track_object.index, notify=False, sync=False) - if tauon.stream_proxy and tauon.stream_proxy.download_running: - tauon.stream_proxy.stop() + 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") - 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) + 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 tauon.spot_ctl.playing or tauon.spot_ctl.coasting: - logging.info("Spotify stop") - tauon.spot_ctl.control("stop") + if timestamp is None: + timestamp = int(time.time()) - self.notify_update() - lfm_scrobbler.start_queue() - return previous_state + # lastfm_user = self.network.get_user(self.username) - def pause(self) -> None: + title = track_object.title + album = track_object.album + artist = get_artist_strip_feat(track_object) + album_artist = track_object.album_artist - 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 + logging.info("Submitting scrobble...") - if self.playing_state == 3: - if tauon.spot_ctl.coasting: - if tauon.spot_ctl.paused: - tauon.spot_ctl.control("resume") + # 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: - tauon.spot_ctl.control("pause") - return + self.network.scrobble(artist=artist, title=title, timestamp=timestamp) + # logging.info('Scrobbled') - 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 + # Pull loved status - 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.sync_pull_love(track_object) - self.playerCommandReady = True - self.render_playlist() - self.notify_update() + else: + logging.warning("Not sent, incomplete metadata") - def pause_only(self) -> None: - if self.playing_state == 1: - self.playerCommand = "pauseon" - self.playing_state = 2 + except Exception as e: + logging.exception("Failed to Scrobble!") + if "retry" in str(e): + logging.warning("Retrying in a couple seconds...") + time.sleep(7) - self.playerCommandReady = True - self.render_playlist() - self.notify_update() + try: + self.network.scrobble(artist=artist, title=title, timestamp=timestamp) + # logging.info('Scrobbled') + return True + except Exception: + logging.exception("Failed to retry!") - def play_pause(self) -> None: - if self.playing_state == 3: - self.stop() - elif self.playing_state > 0: - self.pause() - else: - self.play() + # show_message(_("Error: Could not scrobble. ", str(e), mode='warning') + logging.error("Error connecting to last.fm") + scrobble_warning_timer.set() + self.gui.update += 1 + self.gui.delay_frame(5) - 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 + return False + return True - if msys and taskbar_progress and self.windows_progress: - self.windows_progress.update(True) + def get_bio(self, artist: str) -> str: - if self.mpris is not None: - self.mpris.seek_do(self.playing_time) + if self.lastfm_network is None: + if self.last_fm_only_connect() is False: + return "" - 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): + 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 "" - if new > self.playing_length - 0.5: - self.advance() - return + def love(self, artist: str, title: str): - if new < 0.4: - new = 0 + if not self.connected and self.prefs.auto_lfm: + self.connect(False) + self.prefs.scrobble_hold = True + if self.connected and artist != "" and title != "": + track = self.network.get_track(artist, title) + track.love() - self.new_time = new - self.playing_time = new + def unlove(self, artist: str, title: str): + if not last_fm_enable: + return + if not self.connected and self.prefs.auto_lfm: + self.connect(False) + self.prefs.scrobble_hold = True + if self.connected and artist != "" and title != "": + track = self.network.get_track(artist, title) + track.love() + track.unlove() - self.playerCommand = "seek" - self.playerCommandReady = True + def clear_friends_love(self) -> None: - if self.mpris is not None: - self.mpris.seek_do(self.playing_time) + count = 0 + for index, tr in self.pctl.master_library.items(): + count += len(tr.lfm_friend_likes) + tr.lfm_friend_likes.clear() - def play(self) -> None: + show_message(_("Removed {N} loves.").format(N=count)) - if tauon.spot_ctl.playing: - if self.playing_state == 2: - self.play_pause() + def get_friends_love(self): + if not last_fm_enable: return + self.scanning_friends = True - # 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: + try: + username = prefs.last_fm_username + logging.info(f"Username is {username}") - if radiobox.loaded_station: - radiobox.start(radiobox.loaded_station) + if not username: + self.scanning_friends = False + show_message(_("There was an error, try re-log in")) 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 + if self.network is None: + self.no_user_connect() - self.play_target() + 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) - # If the queue is not empty, play? - elif len(self.track_queue) > 0: - self.play_target() + try: + loves = friend.get_loved_tracks(limit=None) + except Exception: + logging.exception("Failed to get_loved_tracks!") - self.render_playlist() + for track in loves: + title = track.track.title.casefold() + artist = track.track.artist.name.casefold() + for index, tr in pctl.master_library.items(): - 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() + 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) - 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() + except Exception: + logging.exception("There was an error getting friends loves") + show_message(_("There was an error getting friends loves"), "", mode="warning") - else: - tauon.spot_ctl.update_timer.set() - tauon.spot_ctl.update() + self.scanning_friends = False - 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 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 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 + if not username: + show_message(_("No username found"), mode="error") + return - # 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 + if len(username) > 25: + logging.error("Aborted due to long username") + return - gap_extra = 2 # 2 + self.scanning_loves = True - if tauon.spot_ctl.playing or tauon.chrome_mode: - gap_extra = 3 + logging.info("Connect for friend scan") - if msys and taskbar_progress and self.windows_progress: - self.windows_progress.update(True) + try: + if self.network is None: + self.no_user_connect() - if self.commit is not None: - return + self.network.enable_rate_limit() + logging.info("Get user...") + lastfm_user = self.network.get_user(username) + tracks = lastfm_user.get_loved_tracks(limit=None) - 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 + matches = 0 + updated = 0 - if self.playing_state == 1 and self.decode_time + gap_extra >= self.playing_length and self.decode_time > 0.2: + for track in tracks: + title = track.track.title.casefold() + artist = track.track.artist.name.casefold() - # Allow some time for spotify playing time to update? - if tauon.spot_ctl.playing and tauon.spot_ctl.start_timer.get() < 3: - return + 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" - # Allow some time for backend to provide a length - if self.playing_time < 6 and self.playing_length == 0: + star_store.insert(index, star) + + self.scanning_loves = False + if len(tracks) == 0: + show_message(_("User has no loved tracks.")) return - if not tauon.spot_ctl.playing and self.a_time < 2: + 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 - 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 + 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 - 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) + # 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 - elif self.repeat_mode is True: +class ListenBrainz: - if self.album_repeat_mode: + def __init__(self, prefs: Prefs): + self.prefs = prefs + self.enable = prefs.enable_lb + # self.url = "https://api.listenbrainz.org/1/submit-listens" - if self.playlist_playing_position > len(pp) - 1: - self.playlist_playing_position = 0 # Hack fix, race condition bug? + def url(self): + url = self.prefs.listenbrainz_url + if not url: + url = "https://api.listenbrainz.org/" + if not url.endswith("/"): + url += "/" + return url + "1/submit-listens" - ti = self.get_track(pp[self.playlist_playing_position]) + def listen_full(self, track_object: TrackClass, time) -> bool: - i = self.playlist_playing_position + if self.enable is False: + return True + if self.prefs.scrobble_hold is True: + return True + if self.prefs.lb_token is None: + show_message(_("ListenBrainz is enabled but there is no token."), _("How did this even happen."), mode="error") - # 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 + title = track_object.title + album = track_object.album + artist = get_artist_strip_feat(track_object) - # 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) + if title == "" or artist == "": + return True - self.selected_in_playlist = i - shift_selection = [i] + data = {"listen_type": "single", "payload": []} + metadata = {"track_name": title, "artist_name": artist} - self.jump(pp[i], i, jump=False) + additional = {} - 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): + # MusicBrainz Artist IDs + if "musicbrainz_artistids" in track_object.misc: + additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] - logging.info("Repeat follow cursor") + # MusicBrainz Release ID + if "musicbrainz_albumid" in track_object.misc: + additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] - self.playing_time = 0 - self.decode_time = 0 - self.active_playlist_playing = self.active_playlist_viewing - self.playlist_playing_position = self.selected_in_playlist + # MusicBrainz Recording ID + if "musicbrainz_recordingid" in track_object.misc: + additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] - 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() + # MusicBrainz Track ID + if "musicbrainz_trackid" in track_object.misc: + additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] - 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 + if additional: + metadata["additional_info"] = additional - #self.render_playlist() - lfm_scrobbler.start_queue() + # logging.info(additional) + data["payload"].append({"track_metadata": metadata}) + data["payload"][0]["listened_at"] = time - # Reload lastfm for rescrobble - if lfm_scrobbler.a_sc: - lfm_scrobbler.a_sc = False - self.a_time = 0 + 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 - 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: + def listen_playing(self, track_object: TrackClass) -> None: + if self.enable is False: + return + if self.prefs.scrobble_hold is True: + return + if self.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) - # not (self.force_queue and not self.pause_queue) and \ + if title == "" or artist == "": + return - # We can shave it closer - if not self.playing_time + 0.1 >= self.playing_length: - return + data = {"listen_type": "playing_now", "payload": []} + metadata = {"track_name": title, "artist_name": artist} - 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() + additional = {} - gui.update += 1 - gui.pl_update = 1 + # MusicBrainz Artist IDs + if "musicbrainz_artistids" in track_object.misc: + additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] - if update_title: - update_title_do() - self.notify_update() - else: - # self.advance(quiet=True, end=True) + # MusicBrainz Release ID + if "musicbrainz_albumid" in track_object.misc: + additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] - 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 + # MusicBrainz Recording ID + if "musicbrainz_recordingid" in track_object.misc: + additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] - self.advance(quiet=True, end=True) - self.playing_time = 0 - self.decode_time = 0 + # MusicBrainz Track ID + if "musicbrainz_trackid" in track_object.misc: + additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] - 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 + if track_object.track_number: + try: + additional["tracknumber"] = str(int(track_object.track_number)) + except Exception: + logging.exception("Error trying to get track_number") - 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 + if track_object.length: + additional["duration"] = str(int(track_object.length)) - # Temporary Workaround for UI block causing unwanted dragging - if not dry: - quick_d_timer.set() + additional["media_player"] = t_title + additional["submission_client"] = t_title + additional["media_player_version"] = str(n_version) - if prefs.show_current_on_transition: - quiet = False + metadata["additional_info"] = additional + data["payload"].append({"track_metadata": metadata}) + # data["payload"][0]["listened_at"] = int(time.time()) - # Trim the history if it gets too long - while len(self.track_queue) > 250: - self.queue_step -= 1 - del self.track_queue[0] + r = requests.post(self.url(), headers={"Authorization": "Token " + self.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()) - # 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] + def paste_key(self): - # 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') + text = copy_from_clipboard() + if text == "": + show_message(_("There is no text in the clipboard"), mode="error") + return - if not dry: - self.playing_time = 0 - self.decode_time = 0 - self.playing_length = 100 - gui.update_spec = 0 + if self.prefs.listenbrainz_url: + self.prefs.lb_token = text + return - old = self.queue_step - end_of_playlist = False + if len(text) == 36 and text[8] == "-": + self.prefs.lb_token = text + else: + show_message(_("That is not a valid token."), mode="error") - # Force queue (middle click on track) - if len(self.force_queue) > 0 and not self.pause_queue: + def clear_key(self): + self.prefs.lb_token = "" + save_prefs() + self.enable = False - q = self.force_queue[0] - target_index = q.track_id +class LastScrob: - if q.type == 1: - # This is an album type + def __init__(self, tauon: Tauon, pctl: PlayerCtl) -> None: + self.gui = tauon.gui + self.pctl = pctl + self.prefs = tauon.prefs + self.a_index = -1 + self.a_sc = False + self.a_pt = False + self.queue = [] + self.running = False - 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) + def start_queue(self): + self.running = True + mini_t = threading.Thread(target=self.process_queue) + mini_t.daemon = True + mini_t.start() - if not dry: + def process_queue(self): + time.sleep(0.4) - pl = id_to_pl(q.playlist_id) - if pl is not None: - self.active_playlist_playing = pl + while self.queue: + try: + tr = self.queue.pop() - if target_index not in self.playing_playlist(): - del self.force_queue[0] - self.advance() - return None + self.gui.pl_update = 1 + logging.info("Submit Scrobble " + tr[0].artist + " - " + tr[0].title) - if dry: - return target_index + success = True - 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 tr[2] == "lfm" and self.prefs.auto_lfm and (tauon.lastfm.connected or tauon.lastfm.details_ready()): + success = tauon.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 = tauon.subsonic.listen(tr[0], submit=True) + elif tr[2] == "koel": + success = koel.listen(tr[0], submit=True) - # Set the flag that we have entered the album - self.force_queue[0].album_stage = 1 + if not success: + logging.info("Re-queue scrobble") + self.queue.append(tr) + time.sleep(10) + break - # This code is mirrored below ------- - ok_continue = True + except Exception: + logging.exception("SCROBBLE QUEUE ERROR") - # 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 + if not self.queue: + scrobble_warning_timer.force_set(1000) - # 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 + self.running = False - # ----------- + def update(self, add_time): + if self.pctl.queue_step > len(self.pctl.track_queue) - 1: + logging.info("Queue step error 1") + return + if self.a_index != self.pctl.track_queue[self.pctl.queue_step]: + self.pctl.a_time = 0 + self.pctl.b_time = 0 + self.a_index = self.pctl.track_queue[self.pctl.queue_step] + self.a_pt = False + self.a_sc = False + if self.pctl.playing_time == 0 and self.a_sc is True: + logging.info("Reset scrobble timer") + self.pctl.a_time = 0 + self.pctl.b_time = 0 + self.a_pt = False + self.a_sc = False - elif q.album_stage == 1: - # We have previously started playing this album + if self.pctl.a_time > 6 and self.a_pt is False and self.pctl.master_library[self.a_index].length > 30: + self.a_pt = True + self.listen_track(self.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() - # Check to see if we still are: - ok_continue = True + if self.pctl.a_time > 6 and self.a_pt: + self.pctl.b_time += add_time + if self.pctl.b_time > 20: + self.pctl.b_time = 0 + self.listen_track(self.pctl.master_library[self.a_index]) - 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 + send_full = False + if self.pctl.master_library[self.a_index].length > 30 and self.pctl.a_time > self.pctl.master_library[self.a_index].length \ + * 0.50 and self.a_sc is False: + self.a_sc = True + send_full = True - pl = self.multi_playlist[self.active_playlist_playing].playlist_ids + if self.a_sc is False and self.pctl.master_library[self.a_index].length > 30 and self.pctl.a_time > 240: + self.a_sc = True + send_full = True - # Check next song is in album - if ok_continue: + if send_full: + self.scrob_full_track(self.pctl.master_library[self.a_index]) - # 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): + def listen_track(self, track_object: TrackClass): + # logging.info("LISTEN") - if dry: - return None + if track_object.is_network: + if track_object.file_ext == "SUB": + tauon.subsonic.listen(track_object, submit=False) - del self.force_queue[0] - self.advance() - return None + if not self.prefs.scrobble_hold: + if self.prefs.auto_lfm and (tauon.lastfm.connected or tauon.lastfm.details_ready()): + mini_t = threading.Thread(target=tauon.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() - # 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 + def scrob_full_track(self, track_object: TrackClass): + # logging.info("SCROBBLE") + track_object.lfm_scrobbles += 1 + gui.pl_update += 1 - # 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 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 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 not prefs.scrobble_hold: + if prefs.auto_lfm and (tauon.lastfm.connected or tauon.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")) - 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) +class Strings: - if not ok_continue: - # It seems this item has expired, remove it and call advance again + 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") - if dry: - return None + self.day = _("day") + self.days = _("days") - logging.info("Remove expired album from queue") - del self.force_queue[0] + self.scan_chrome = _("Scanning for Chromecasts...") + self.cast_to = _("Cast to: %s") + self.no_chromecasts = _("No Chromecast devices found") + self.stop_cast = _("End Cast") - if q.auto_stop: - self.auto_stop = True - if prefs.stop_end_queue and not self.force_queue: - self.auto_stop = True + self.web_server_stopped = _("Web server stopped.") - if queue_box.scroll_position > 0: - queue_box.scroll_position -= 1 + 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") - # self.advance() - # return +class Chunker: - 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 + def __init__(self): + self.master_count = 0 + self.chunks = {} + self.header = None + self.headers = [] + self.h2 = None - if target_index not in self.playing_playlist(): - if dry: - return None - del self.force_queue[0] - self.advance() - return None + self.clients = {} - if dry: - return target_index +class MenuIcon: - 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 + 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 - # Stop if playlist is empty - elif len(self.playing_playlist()) == 0: - if dry: - return None - self.stop() - return 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 - # 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): +class ThreadManager: + def __init__(self, tauon: Tauon): + self.prefs = tauon.bag.prefs + self.tauon = tauon + 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: threading.Lock = threading.Lock() - if dry: - return default_playlist[self.selected_in_playlist] + self.d: dict = {} - self.active_playlist_playing = self.active_playlist_viewing - self.playlist_playing_position = self.selected_in_playlist + 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 - 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) + def ready_playback(self) -> None: + if self.playback is None or not self.playback.is_alive(): + if self.prefs.backend == 4: + self.playback = threading.Thread(target=player4, args=[self.tauon]) + # elif self.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() - # 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 + def check_playback_running(self) -> bool: + if self.playback is None: + return False + return self.playback.is_alive() - if new_step == len(self.track_queue): +class Menu: + """Right click context menu generator""" - 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]] + switch = 0 + count = switch + 1 + instances: list[Menu] = [] + active = False - if ti.index not in pp: - if dry: - return None - logging.info("No tracks to repeat!") - return 0 + def rescale(self): + self.vertical_size = round(self.base_v_size * self.tauon.gui.scale) + self.h = self.vertical_size + self.w = self.request_width * self.tauon.gui.scale + if self.tauon.gui.scale == 2: + self.w += 15 - matches = [] - for i, p in enumerate(pp): + 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() - if self.get_track(p).parent_folder_path == ti.parent_folder_path: - matches.append((i, p)) + 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(tauon.bag, tauon.bag.loaded_asset_dc, "sub.png", True)) - if matches: - # Avoid a repeat of same track - if len(matches) > 1 and (k, ti.index) in matches: - matches.remove((k, ti.index)) + self.id = Menu.count + self.break_height = round(4 * tauon.gui.scale) - i, p = random.choice(matches) # not used + Menu.count += 1 - if prefs.true_shuffle: + self.sub_number = 0 + self.sub_active = -1 + self.sub_y_postion = 0 + Menu.instances.append(self) - id = ti.parent_folder_path + @staticmethod + def deco(_=_): + return [colours.menu_text, colours.menu_background, None] - while True: - if id in self.shuffle_pools: + def click(self) -> None: + self.clicked = True + # cheap hack to prevent scroll bar from being activated when closing menu + inp.click_location = [0, 0] - pool = self.shuffle_pools[id] + 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) - if not pool: - del self.shuffle_pools[id] # Trigger a refill - continue + def br(self) -> None: + self.items.append(None) - ref = pool.pop() - if dry: - pool.append(ref) - return ref[1] - # ref = random.choice(pool) - # pool.remove(ref) + 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([]) - if ref[1] not in pp: # Check track still in the live playlist - logging.info("Track not in pool") - continue + 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) - i, p = ref # Find position of reference in playlist - break + def test_item_active(self, item): + if item.show_test is not None: + if item.show_test(1) is False: + return False + return True - # Refill the pool - random.shuffle(matches) - self.shuffle_pools[id] = matches - logging.info("Refill folder shuffle pool") + 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() - self.playlist_playing_position = i - self.track_queue.append(p) + def render_icon(self, x, y, icon, selected, fx): - else: - # Normal select from playlist + if colours.lm: + selected = True - if prefs.true_shuffle: - # True shuffle avoids repeats by using a pool + if icon is not None: - pl = self.multi_playlist[self.active_playlist_playing] - id = pl.uuid_int + x += icon.xoff * gui.scale + y += icon.yoff * gui.scale - while True: + colour = None - if id in self.shuffle_pools: + if icon.base_asset is None: + # Colourise mode - pool = self.shuffle_pools[id] + if icon.colour_callback is not None: # and icon.colour_callback() is not None: + colour = icon.colour_callback() - if not pool: - del self.shuffle_pools[id] # Trigger a refill - continue + elif selected and fx[0] != colours.menu_text_disabled: + colour = icon.colour - ref = pool.pop() - if dry: - pool.append(ref) - return ref - # ref = random.choice(pool) - # pool.remove(ref) + 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 ref not in pl.playlist_ids: # Check track still in the live playlist - continue + if colour is None: + # colour = [145, 145, 145, 70] + colour = colours.menu_icons # [255, 255, 255, 35] + # colour = [50, 50, 50, 255] - random_jump = pl.playlist_ids.index(ref) # Find position of reference in playlist - break + icon.asset.render(x, y, colour) - # Refill the pool - self.update_shuffle_pool(pl.uuid_int) + 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: - random_jump = random.randrange(len(self.playing_playlist())) # not used + icon.base_asset.render(x, y) + elif selected: + icon.asset.render(x, y) + else: + icon.base_asset.render(x, y) - self.playlist_playing_position = random_jump - self.track_queue.append(self.playing_playlist()[random_jump]) + def render(self): + if self.active: - 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 Menu.switch != self.id: + self.active = False - if rr: - if dry: - return None - self.play_target_rr() - elif play: - self.play_target(jump=not end) + for menu in Menu.instances: + if menu.active: + break + else: + Menu.active = False + return - # If not random mode, Step down 1 on the playlist - elif self.random_mode is False and len(self.playing_playlist()) > 0: + # ytoff = 3 + y_run = round(self.pos[1]) + to_call = None - # 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 + # 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) - elif prefs.end_setting in ("advance", "cycle"): + x_run = self.pos[0] - # 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 + 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) - p = self.active_playlist_playing - for i in range(len(self.multi_playlist)): + rect = (x_run, y_run, self.w, self.break_height - 1) + if tauon.coll(rect): + self.clicked = False - k = (p + i + 1) % len(self.multi_playlist) + ddt.rect_a((x_run, y_run), (self.w, self.break_height), colours.menu_background) - # Skip a playlist if empty - if not (self.multi_playlist[k].playlist_ids): - continue + ddt.rect_a((x_run, y_run + 2 * gui.scale), (self.w, 2 * gui.scale), break_colour) - # Skip a playlist if hidden - if self.multi_playlist[k].hidden and prefs.tabs_on_top: - continue + # Draw tab + ddt.rect_a((x_run, y_run), (4 * gui.scale, self.break_height), colours.menu_tab) + y_run += self.break_height - # 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 + continue - else: - # Restart current if no other eligible playlist found - self.playlist_playing_position = -1 - self.advance(end=end, force=True, play=play) + 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 - return None + # 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() - elif prefs.end_setting == "repeat": - self.playlist_playing_position = -1 - self.advance(end=end, force=True, play=play) - return None + if fx[2] is not None: + label = fx[2] + else: + label = self.items[i].title - gui.update += 3 + # Show text as disabled if disable_test() passes + if self.is_item_disabled(self.items[i]): + fx[0] = colours.menu_text_disabled - else: - if self.playlist_playing_position > len(self.playing_playlist()) - 1: - if dry: - return None - self.playlist_playing_position = 0 + # Draw item background, black by default + ddt.rect_a((x_run, y_run), (self.w, self.h), fx[1]) + bg = fx[1] - 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") + # Detect if mouse is over this item + selected = False + rect = (x_run, y_run, self.w, self.h - 1) + tauon.fields.add(rect) - if len(self.playing_playlist()) == self.playlist_playing_position + 1: - return None + if coll_point(inp.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) - 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]) + # Call menu items callback if clicked + if self.clicked: - # 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) + 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 + self.tauon.inp.mouse_down = False - elif self.random_mode and (self.album_shuffle_mode or prefs.album_shuffle_lock_mode): + else: + self.clicked = False + self.sub_active = self.items[i].sub_menu_number + self.sub_y_postion = y_run - # Album shuffle mode - logging.info("Album shuffle mode") + # Draw tab + ddt.rect_a((x_run, y_run), (4 * gui.scale, self.h), colours.menu_tab) - po = self.playing_object() + # 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) - redraw = False + if self.show_icons: + x += 25 * gui.scale - # Checks - if po is not None and len(self.playing_playlist()) > 0: + # Draw arrow icon for sub menu + if self.items[i].is_sub_menu is True: - # 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 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 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) + 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) - else: + # 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) - 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) + # 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) - random.shuffle(albums) + # Render the items hint + if self.items[i].hint != None: - 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() + 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) - else: - logging.error("ADVANCE ERROR - NO CASE!") + # 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) - if dry: - return None + y_run += self.h - 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 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] - # if album_mode: - # goto_album(self.playlist_playing) + # 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: - self.render_playlist() + # 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 tauon.spot_ctl.playing and end_of_playlist: - tauon.spot_ctl.control("stop") + 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 - self.notify_update() - lfm_scrobbler.start_queue() - if play: - notify_song(end_of_playlist, delay=1.3) - return None + fx = self.deco() - def reset_missing_flags(self) -> None: - for value in self.master_library.values(): - value.found = True - gui.pl_update += 1 + minY = window_size[1] - self.h * len(self.subs[self.sub_active]) - 15 * gui.scale + sub_pos[1] = min(sub_pos[1], minY) -pctl = PlayerCtl() + xoff = 0 + for i in self.subs[self.sub_active]: + if i.icon is not None: + xoff = 24 * gui.scale + break -notify_change = pctl.notify_change + 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 -def auto_name_pl(target_pl: int) -> None: - if not pctl.multi_playlist[target_pl].playlist_ids: - return + # 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() - albums = [] - artists = [] - parents = [] + # Item background + ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (sub_w, self.h), fx[1]) - track = None + # Detect if mouse is over this item + rect = (sub_pos[0], sub_pos[1] + w * self.h, sub_w, self.h - 1) + tauon.fields.add(rect) + this_select = False + bg = colours.menu_background + if coll_point(inp.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 - 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) + # Call Callback + if self.clicked and not self.is_item_disabled(self.subs[self.sub_active][w]): - nt = "" - artist = "" + # 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 track: - artist = track.artist - if track.album_artist: - artist = track.album_artist + # If callback just need ref + elif self.subs[self.sub_active][w].pass_ref: + self.subs[self.sub_active][w].func(self.reference) - if track and albums and albums[0] and albums.count(albums[0]) == len(albums): - nt = artist + " - " + track.album + else: + self.subs[self.sub_active][w].func() - elif track and artists and artists[0] and artists.count(artists[0]) == len(artists): - nt = artists[0] + if fx[2] is not None: + label = fx[2] + else: + label = self.subs[self.sub_active][w].title - else: - nt = os.path.basename(commonprefix(parents)) + # 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 - pctl.multi_playlist[target_pl].title = nt + # 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) -def get_object(index: int) -> TrackClass: - return pctl.master_library[index] + # 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)) -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) + # 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() -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]) + if self.clicked or key_esc_press or self.close_next_frame: + self.close_next_frame = False + self.active = False + self.clicked = False + inp.last_click_location[0] = 0 + inp.last_click_location[1] = 0 -def g_open_encode_out(a, b, c) -> None: - open_encode_out() + 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): -if system == "Linux" and not macos and not msys: + Menu.active = True - 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 position != None: + self.pos = [position[0], position[1]] + else: + self.pos = [copy.deepcopy(inp.mouse_position[0]), copy.deepcopy(inp.mouse_position[1])] - except Exception: - logging.exception("Failed init notifications") + self.reference = in_reference + Menu.switch = self.id + self.sub_active = -1 - 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) + # 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 -def notify_song_fire(notification, delay, id) -> None: - time.sleep(delay) - notification.show() - if id is None: - return + # Flip menu up if would intersect with bottom of window + if self.pos[1] + full_h > window_size[1]: + self.pos[1] -= shown_h - time.sleep(8) - if id == gui.notify_main_id: - notification.close() + # 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 -def notify_song(notify_of_end: bool = False, delay: float = 0.0) -> None: - if not de_notify_support: - return +class GallClass: + def __init__(self, tauon: Tauon, size: int = 250, save_out: bool = True) -> None: + self.gui = tauon.gui + self.prefs = tauon.prefs + self.search_over = tauon.search_over + 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 notify_of_end and prefs.end_setting != "stop": - return + def get_file_source(self, track_object: TrackClass): + global album_art_gen + sources = album_art_gen.get_sources(track_object) - 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 len(sources) == 0: + return False, 0 - track = pctl.playing_object() + offset = album_art_gen.get_offset(track_object.fullpath, sources) + return sources[offset], offset - 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 + def worker_render(self) -> None: + self.lock.acquire() + # time.sleep(0.1) - 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 self.search_over.active: + while QuickThumbnail.queue: + img = QuickThumbnail.queue.pop(0) + response = urllib.request.urlopen(img.url, context=tls_context) + source_image = io.BytesIO(response.read()) + img.read_and_thumbnail(source_image, img.size, img.size) + source_image.close() + self.gui.update += 1 - top_line = track.title + while len(self.queue) > 0: + source_image = None - if prefs.notify_include_album: - bottom_line = (track.artist + " | " + track.album).strip("| ") - else: - bottom_line = track.artist + if self.gui.halt_image_rendering: + self.queue.clear() + break - if not track.title: - a, t = filename_to_metadata(clean_string(track.filename)) - if not track.artist: - bottom_line = a - top_line = t + self.i += 1 - gui.notify_main_id = uid_gen() - id = gui.notify_main_id + try: + # key = self.queue[0] + key = self.queue.pop(0) + except Exception: + logging.exception("thumb queue empty") + break - if notify_of_end: - bottom_line = "Tauon Music Box" - top_line = (_("End of playlist")) - id = None + if key not in self.gall: + order = [1, None, None, None] + self.gall[key] = order + else: + order = self.gall[key] - song_notification.update(top_line, bottom_line, i_path) + size = key[1] - shoot_dl = threading.Thread(target=notify_song_fire, args=([song_notification, delay, id])) - shoot_dl.daemon = True - shoot_dl.start() + 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 self.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 -# Last.FM ----------------------------------------------------------------- -class LastFMapi: - API_SECRET = "6e433964d3ff5e817b7724d16a9cf0cc" - connected = False - API_KEY = "bfdaf6357f1dddd494e5bee1afe38254" - scanning_username = "" + if slow_load: - network = None - lastfm_network = None - tries = 0 + source, c_offset = self.get_file_source(key[0]) - scanning_friends = False - scanning_loves = False - scanning_scrobbles = False + if source is False: + order[0] = 0 + self.gall[key] = order + # del self.queue[0] + continue - def __init__(self) -> None: - self.sg = None - self.url = None + img_name = str(key[2]) + "-" + str(size) + "-" + str(key[0].index) + "-" + str(c_offset) - def get_network(self) -> LibreFMNetwork: - if prefs.use_libre_fm: - return pylast.LibreFMNetwork - return pylast.LastFMNetwork + # gall_render_last_timer.set() - 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.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 - if self.network is None: - self.no_user_connect() + # 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) - 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) + g = io.BytesIO() + g.seek(0) - def auth2(self) -> None: + if cache_load: + g.write(source_image.read()) - # This is step 2 where the user clicks "Done" + 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 self.sg is None: - show_message(_("You need to log in first")) - return + im.save(g, "BMP") - 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 + if not error and self.save_out and self.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) - 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") + g.seek(0) - if not toggle_lfm_auto(mode=1): - toggle_lfm_auto() + # source_image.close() - 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.")) + order = [2, g, None, None] + self.gall[key] = order - def connect(self, m_notify: bool = True) -> bool | None: + self.gui.update += 1 + if source_image: + source_image.close() + source_image = None + # del self.queue[0] - if not last_fm_enable: - return False + time.sleep(0.001) - if self.connected is True: - if m_notify: - show_message(_("Already connected to Last.fm")) + except Exception: + logging.exception("Image load failed on track: " + key[0].fullpath) + order = [0, None, None, None] + self.gall[key] = order + self.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 - if prefs.last_fm_token is None: - show_message(_("No Last.Fm account registered"), _("Authorise an account in settings"), mode="info") + def render(self, track: TrackClass, location, size=None, force_offset=None) -> bool | None: + if gallery_load_delay.get() < 0.5: return None - logging.info("Attempting to connect to Last.fm network") + x = round(location[0]) + y = round(location[1]) - try: + # time.sleep(0.1) + if size is None: + size = self.size - 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) + size = round(size) - self.connected = True - if m_notify: - show_message(_("Connection to Last.fm was successful."), mode="done") + # 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 - logging.info("Connection to lastfm appears successful") - return True + if force_offset is not None: + offset = force_offset - 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 + key = (track, size, offset) - def toggle(self) -> None: - prefs.scrobble_hold ^= True + if key in self.gall: + #logging.info("old") - def details_ready(self) -> bool: - if prefs.last_fm_token: - return True - return False + order = self.gall[key] - 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 + if order[0] == 0: + # broken + return False - 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 + if order[0] == 1: + # not done yet + 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 + if order[0] == 2: + # finish processing - 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 + 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) - 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 + order[0] = 3 + order[1].close() + order[1] = None + order[2] = c + order[3] = dst + self.gall[(track, size, offset)] = order - user = pylast.User(prefs.last_fm_username, self.network) - total = user.get_playcount() + if order[0] == 3: + # ready - if total: - return 0.04364 * total - return 0 + 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]) - def get_all_scrobbles(self) -> None: + if (track, size, offset) in self.key_list: + self.key_list.remove((track, size, offset)) + self.key_list.append((track, size, offset)) - if not self.connected: - self.connect(False) - if not self.connected or not prefs.last_fm_username: - return + # 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] - 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) + return True - counts = {} + 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 - # 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 +class ThumbTracks: + def __init__(self) -> None: + pass - touched = [] + def path(self, track: TrackClass) -> str: + source, offset = tauon.gall_ren.get_file_source(track) - # Add counts to matching tracks - for key, value in counts.items(): - artist, title = key - artist = artist.lower() - title = title.lower() + if source is False: # No art + return None - 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 + image_name = track.album + track.parent_folder_path + str(offset) + image_name = hashlib.md5(image_name.encode("utf-8", "replace")).hexdigest() - logging.info(perf_timer.get()) - gui.pl_update += 1 - self.scanning_scrobbles = False - tauon.bg_save() - show_message(_("Scanning scrobbles complete"), mode="done") + t_path = os.path.join(e_cache_dir, image_name + ".jpg") - def artist_info(self, artist: str): + if os.path.isfile(t_path): + return t_path - if self.lastfm_network is None: - if self.last_fm_only_connect() is False: - return False, "", "" + source_image = album_art_gen.get_source_raw(0, 0, track, subsource=source) - 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() + 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 - return True, bio, "", mbid, url - except Exception: - logging.exception("last.fm get artist info failed") +class Tauon: + """Root class for everything Tauon""" + def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): + self.bag = bag + self.t_window = holder.t_window + 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.draw_border: bool = holder.draw_border + self.desktop: str | None = bag.desktop + self.device = socket.gethostname() + self.move_jobs: list = [] + self.to_scan: list = [] + self.after_scan: list[TrackClass] = [] + self.move_in_progress: bool = False + self.worker2_lock = threading.Lock() + #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.gui: GuiVar = gui + self.prefs: Prefs = bag.prefs + self.fields = Fields() + self.artist_list_box = ArtistList(tauon=self) + self.search_over = SearchOverlay(tauon=self) + self.radiobox = RadioBox(tauon=self) + self.pctl: PlayerCtl = PlayerCtl(tauon=self) + self.deco = Deco(tauon=self) + self.lfm_scrobbler: LastScrob = self.pctl.lfm_scrobbler + self.star_store: StarStore = StarStore(tauon=self) + self.bottom_bar1 = BottomBarType1(tauon=self) + self.top_panel = TopPanel(tauon=self) + self.playlist_box = PlaylistBox(tauon=self) + self.cache_directory: Path = bag.dirs.cache_directory + self.user_directory: Path | None = bag.dirs.user_directory + self.music_directory: Path | None = bag.dirs.music_directory + self.locale_directory: Path = bag.dirs.locale_directory + self.transcode_list: list[list[int]] = [] + self.transcode_state: str = "" + # TODO(Martin): Rework this LC_* stuff, maybe use a simple object instead? + self.LC_None = 0 + self.LC_Done = 1 + self.LC_Folder = 2 + self.LC_File = 3 + self.loaderCommand: int = self.LC_None + self.loaderCommandReady: bool = False + self.cm_clean_db: bool = False + self.worker_save_state: bool = False + self.launch_prefix: str = bag.launch_prefix + self.whicher = whicher + self.load_orders: list[LoadClass] = bag.load_orders + self.switch_playlist = None + self.open_uri = open_uri + self.love = love + self.album_mode: bool = False + self.snap_mode: bool = bag.snap_mode + self.console = bag.console + self.msys = bag.msys + self.TrackClass = TrackClass + self.pl_gen = pl_gen + self.gall_ren = GallClass(tauon=self, size=bag.album_mode_art_size) + self.QuickThumbnail = QuickThumbnail + self.thumb_tracks = ThumbTracks() + self.chunker = Chunker() + self.thread_manager: ThreadManager | None = None # Avoid NameError + self.thread_manager: ThreadManager = ThreadManager(tauon=self) + self.stream_proxy = None + self.stream_proxy = StreamEnc(self) + self.level_train: list[list[float]] = [] + self.radio_server = None + self.mod_formats = bag.formats.MOD_Formats + self.listen_alongers = {} + self.encode_folder_name = encode_folder_name + self.encode_track_name = encode_track_name + # Create top menu + self.x_menu = Menu(self, 190, show_icons=True) + self.set_menu = Menu(self, 150) + self.field_menu = Menu(self, 140) + self.dl_menu = Menu(self, 90) + + self.cancel_menu = Menu(self, 100) + self.extra_menu = Menu(self, 175, show_icons=True) + self.shuffle_menu = Menu(self, 120) + self.repeat_menu = Menu(self, 120) - return False, "", "", "", "" + self.tray_lock = threading.Lock() + self.tray_releases = 0 - def artist_mbid(self, artist: str): + self.play_lock = None + self.update_play_lock = None + self.sleep_lock = None + self.shutdown_lock = None + self.quick_close = False - if self.lastfm_network is None: - if self.last_fm_only_connect() is False: - return "" + self.copied_track = None + self.macos = bag.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 = bag.formats.GME_Formats + + self.chrome: Chrome | None = None + self.chrome_menu: Menu | None = None try: - if artist != "": - l_artist = pylast.Artist( - artist.replace("/", "").replace("\\", "").replace(" & ", " and ").replace("&", " "), - self.lastfm_network) - mbid = l_artist.get_mbid() - return mbid + from tauon.t_modules.t_chrome import Chrome + self.chrome = Chrome(self) + 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("last.fm get artist mbid info failed") + logging.exception("Unknown error trying to import Chrome(pychromecast), chromecast support will be disabled.") + finally: + logging.debug("Found Chrome(pychromecast) for chromecast support") - return "" + self.spot_ctl: SpotCtl = SpotCtl(self) + self.tidal: Tidal = Tidal(self) + self.plex = PlexService() + self.jellyfin = Jellyfin(self) + self.subsonic = SubsonicService(bag) + self.koel = KoelService() + self.tau = TauService() + self.lastfm = LastFMapi(tauon=self) - 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 + self.tls_context = bag.tls_context - 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 coll(self, r: list[int]) -> bool: + inp = self.gui.inp + return r[0] < inp.mouse_position[0] <= r[0] + r[2] and r[1] <= inp.mouse_position[1] <= r[1] + r[3] - remote_loved = track.get_userloved() + 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 - 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 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 - if remote_loved is None: - logging.error("Error getting loved status") - return + 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)) - local_loved = love(set=False, track_id=track_object.index, notify=False, sync=False) + except Exception as e: + logging.exception("Download failed") + show_message(_("Download failed"), str(e), mode="error") - 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") + 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()) - 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) + 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()) - if timestamp is None: - timestamp = int(time.time()) + exe.close() + show_message(_("FFMPEG fetch complete"), mode="done") - # lastfm_user = self.network.get_user(self.username) + shooter(go) - title = track_object.title - album = track_object.album - artist = get_artist_strip_feat(track_object) - album_artist = track_object.album_artist + def set_tray_icons(self, force: bool = False): - logging.info("Submitting scrobble...") + indicator_icon_play = str(self.pctl.install_directory / "assets/svg/tray-indicator-play.svg") + indicator_icon_pause = str(self.pctl.install_directory / "assets/svg/tray-indicator-pause.svg") + indicator_icon_default = str(self.pctl.install_directory / "assets/svg/tray-indicator-default.svg") - # 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') + if self.prefs.tray_theme == "gray": + indicator_icon_play = str(self.pctl.install_directory / "assets/svg/tray-indicator-play-g1.svg") + indicator_icon_pause = str(self.pctl.install_directory / "assets/svg/tray-indicator-pause-g1.svg") + indicator_icon_default = str(self.pctl.install_directory / "assets/svg/tray-indicator-default-g1.svg") - # Pull loved status + 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)) - self.sync_pull_love(track_object) + 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") - else: - logging.warning("Not sent, incomplete metadata") + def get_tray_icon(self, name: str) -> str: + return str(self.cache_directory / "icon-export" / f"{name}.svg") - 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 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 - try: - self.network.scrobble(artist=artist, title=title, timestamp=timestamp) - # logging.info('Scrobbled') - return True - except Exception: - logging.exception("Failed to retry!") + 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 - # 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 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 - return False - return True + def bg_save(self) -> None: + self.worker_save_state = True + tauon.thread_manager.ready("worker") - def get_bio(self, artist: str) -> str: + def exit(self, reason: str) -> None: + logging.info("Shutting down. Reason: " + reason) + pctl.running = False + self.wake() - if self.lastfm_network is None: - if self.last_fm_only_connect() is False: - return "" + def min_to_tray(self) -> None: + SDL_HideWindow(t_window) + gui.mouse_unknown = True - 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 raise_window(self) -> None: + SDL_ShowWindow(t_window) + SDL_RaiseWindow(t_window) + SDL_RestoreWindow(t_window) + gui.lowered = False + gui.update += 1 - def love(self, artist: str, title: str): + def focus_window(self) -> None: + SDL_RaiseWindow(t_window) - 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 get_playing_playlist_id(self) -> int: + return pl_to_id(pctl.active_playlist_playing) - 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 wake(self) -> None: + SDL_PushEvent(ctypes.byref(self.dummy_event)) - def clear_friends_love(self) -> None: +class PlexService: - count = 0 - for index, tr in pctl.master_library.items(): - count += len(tr.lfm_friend_likes) - tr.lfm_friend_likes.clear() + def __init__(self): + self.connected = False + self.resource = None + self.scanning = False - show_message(_("Removed {N} loves.").format(N=count)) + def connect(self): - def get_friends_love(self): - if not last_fm_enable: + 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 - 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() + 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 - 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: + 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 - try: - loves = friend.get_loved_tracks(limit=None) - except Exception: - logging.exception("Failed to get_loved_tracks!") + # from plexapi.server import PlexServer + # baseurl = 'http://localhost:32400' + # token = '' - for track in loves: - title = track.track.title.casefold() - artist = track.track.artist.name.casefold() - for index, tr in pctl.master_library.items(): + # self.resource = PlexServer(baseurl, token) - 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) + self.connected = True - except Exception: - logging.exception("There was an error getting friends loves") - show_message(_("There was an error getting friends loves"), "", mode="warning") + def resolve_stream(self, location): + logging.info("Get plex stream") + if not self.connected: + self.connect() - self.scanning_friends = False + # return self.resource.url(location, True) + return self.resource.library.fetchItem(location).getStreamURL() - 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 resolve_thumbnail(self, location): - if not username: - show_message(_("No username found"), mode="error") - return + if not self.connected: + self.connect() + if self.connected: + return self.resource.url(location, True) + return None - if len(username) > 25: - logging.error("Aborted due to long username") - return + def get_albums(self, return_list=False): - self.scanning_loves = True + gui.update += 1 + self.scanning = True - logging.info("Connect for friend scan") + if not self.connected: + self.connect() - try: - if self.network is None: - self.no_user_connect() + if not self.connected: + self.scanning = False + return [] - self.network.enable_rate_limit() - logging.info("Get user...") - lastfm_user = self.network.get_user(username) - tracks = lastfm_user.get_loved_tracks(limit=None) + playlist = [] - matches = 0 - updated = 0 + 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 - for track in tracks: - title = track.track.title.casefold() - artist = track.track.artist.name.casefold() + albums = self.resource.library.section("Music").albums() + gui.to_got = 0 - 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" + for album in albums: + year = album.year + album_artist = album.parentTitle + album_title = album.title - star_store.insert(index, star) + parent = (album_artist + " - " + album_title).strip("- ") - 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 + for track in album.tracks(): - 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 + if not track.duration: + logging.warning("Skipping track with invalid duration - " + track.title + " - " + track.grandparentTitle) + continue - # logging.info('Updating Now Playing') + id = pctl.master_count + replace_existing = False - title = track_object.title - album = track_object.album - artist = get_artist_strip_feat(track_object) + e = existing.get(track.key) + if e is not None: + id = e + replace_existing = True - 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 + 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] -def get_backend_time(path): - pctl.time_to_get = path + nt.is_network = True - pctl.playerCommand = "time" - pctl.playerCommandReady = True + if track.thumb: + nt.art_url_key = track.thumb - while pctl.playerCommand != "done": - time.sleep(0.005) + nt.url_key = track.key + nt.date = str(year) - return pctl.time_to_get + pctl.master_library[id] = nt + if not replace_existing: + pctl.master_count += 1 -lastfm = LastFMapi() + playlist.append(nt.index) + gui.to_got += 1 + gui.update += 1 + gui.pl_update += 1 -class ListenBrainz: + self.scanning = False - def __init__(self): + if return_list: + return playlist - self.enable = prefs.enable_lb - # self.url = "https://api.listenbrainz.org/1/submit-listens" + 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 url(self): - url = prefs.listenbrainz_url - if not url: - url = "https://api.listenbrainz.org/" - if not url.endswith("/"): - url += "/" - return url + "1/submit-listens" +class SubsonicService: - def listen_full(self, track_object: TrackClass, time) -> bool: + def __init__(self, bag: Bag): + self.scanning = False + self.playlists = bag.prefs.subsonic_playlists - 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") + def r(self, point, p=None, binary: bool = False, get_url: bool = False): + salt = secrets.token_hex(8) + server = prefs.subsonic_server.rstrip("/") + "/" - title = track_object.title - album = track_object.album - artist = get_artist_strip_feat(track_object) + params = { + "u": prefs.subsonic_user, + "v": "1.13.0", + "c": t_title, + "f": "json", + } - if title == "" or artist == "": - return True + if prefs.subsonic_password_plain: + params["p"] = prefs.subsonic_password + else: + params["t"] = hashlib.md5((prefs.subsonic_password + salt).encode()).hexdigest() + params["s"] = salt - data = {"listen_type": "single", "payload": []} - metadata = {"track_name": title, "artist_name": artist} + if p: + params.update(p) - additional = {} + point = "rest/" + point - # MusicBrainz Artist IDs - if "musicbrainz_artistids" in track_object.misc: - additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] + url = server + point - # MusicBrainz Release ID - if "musicbrainz_albumid" in track_object.misc: - additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] + if get_url: + return url, params - # MusicBrainz Recording ID - if "musicbrainz_recordingid" in track_object.misc: - additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] + response = requests.get(url, params=params, timeout=10) - # MusicBrainz Track ID - if "musicbrainz_trackid" in track_object.misc: - additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] + if binary: + return response.content - if additional: - metadata["additional_info"] = additional + d = json.loads(response.text) + # logging.info(d) - # logging.info(additional) - data["payload"].append({"track_metadata": metadata}) - data["payload"][0]["listened_at"] = time + if d["subsonic-response"]["status"] != "ok": + show_message(_("Subsonic Error: ") + response.text, mode="warning") + logging.error("Subsonic Error: " + response.text) - 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 + return d - 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) + def get_cover(self, track_object: TrackClass): + response = self.r("getCoverArt", p={"id": track_object.art_url_key}, binary=True) + return io.BytesIO(response) - if title == "" or artist == "": - return + def resolve_stream(self, key): - data = {"listen_type": "playing_now", "payload": []} - metadata = {"track_name": title, "artist_name": artist} + p = {"id": key} + if prefs.network_stream_bitrate > 0: + p["maxBitRate"] = prefs.network_stream_bitrate - additional = {} + return self.r("stream", p={"id": key}, get_url=True) + # logging.info(response.content) - # MusicBrainz Artist IDs - if "musicbrainz_artistids" in track_object.misc: - additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] + def listen(self, track_object: TrackClass, submit: bool = False): - # MusicBrainz Release ID - if "musicbrainz_albumid" in track_object.misc: - additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] + 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 - # MusicBrainz Recording ID - if "musicbrainz_recordingid" in track_object.misc: - additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] + def set_rating(self, track_object: TrackClass, rating): - # MusicBrainz Track ID - if "musicbrainz_trackid" in track_object.misc: - additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] + 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 - if track_object.track_number: + def set_album_rating(self, track_object: TrackClass, rating): + id = track_object.misc.get("subsonic-folder-id") + if id is not None: try: - additional["tracknumber"] = str(int(track_object.track_number)) + a = self.r("setRating", p={"id": id, "rating": math.ceil(rating / 2)}) except Exception: - logging.exception("Error trying to get track_number") - - if track_object.length: - additional["duration"] = str(int(track_object.length)) + logging.exception("Error connect for set rating on airsonic") + return True - additional["media_player"] = t_title - additional["submission_client"] = t_title - additional["media_player_version"] = str(n_version) + def get_music3(self, return_list: bool = False): - metadata["additional_info"] = additional - data["payload"].append({"track_metadata": metadata}) - # data["payload"][0]["listened_at"] = int(time.time()) + self.scanning = True + gui.to_got = 0 - 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()) + existing = {} - def paste_key(self): + for track_id, track in pctl.master_library.items(): + if track.is_network and track.file_ext == "SUB": + existing[track.url_key] = track_id - text = copy_from_clipboard() - if text == "": - show_message(_("There is no text in the clipboard"), mode="error") - return + 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 [] - if prefs.listenbrainz_url: - prefs.lb_token = text - return + b = a["subsonic-response"]["indexes"]["index"] - if len(text) == 36 and text[8] == "-": - prefs.lb_token = text - else: - show_message(_("That is not a valid token."), mode="error") + folders = [] - def clear_key(self): + for letter in b: + artists = letter["artist"] + for artist in artists: + folders.append(( + artist["id"], + artist["name"], + )) - prefs.lb_token = "" - save_prefs() - self.enable = False + playlist = [] + songsets = [] + for i in range(len(folders)): + songsets.append([]) + statuses = [0] * len(folders) + dupes = [] -lb = ListenBrainz() + 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 -def get_love(track_object: TrackClass) -> bool: - star = star_store.full_get(track_object.index) - if star is None: - return False + 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 "L" in star[1]: - return True - return False + items = d["subsonic-response"]["directory"]["child"] + gui.update = 2 -def get_love_index(index: int) -> bool: - star = star_store.full_get(index) - if star is None: - return False + for item in items: - if "L" in star[1]: - return True - return False + if item["isDir"]: -def get_love_timestamp_index(index: int): - star = star_store.full_get(index) - if star is None: - return 0 - return star[3] + 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)) -def love(set=True, track_id=None, no_delay=False, notify=False, sync=True): - if len(pctl.track_queue) < 1: - return False + getsongs(index, item["id"], item["title"], inner=True, parent=item) + continue - if track_id is not None and track_id < 0: - return False + gui.to_got += 1 + song = item + nt = TrackClass() - if track_id is None: - track_id = pctl.track_queue[pctl.queue_step] + if parent and "artist" in parent: + nt.album_artist = parent["artist"] - loved = False - star = star_store.full_get(track_id) + 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 star is not None: - if "L" in star[1]: - loved = True + 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 - if set is False: - return loved + rating = 0 + if "userRating" in song: + rating = int(song["userRating"]) - # 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 + songsets[index].append((nt, name, song["id"], rating)) - if star is None: - star = star_store.new_object() + if inner: + return + statuses[index] = 2 - loved ^= True + i = -1 + for id, name in folders: + i += 1 + while statuses.count(1) > 3: + time.sleep(0.1) - 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) - - delay = 0.3 - if no_delay or not sync or not lastfm.details_ready(): - delay = 0 + statuses[i] = 1 + t = threading.Thread(target=getsongs, args=([i, id, name])) + t.daemon = True + t.start() - star[3] = time.time() + while statuses.count(2) != len(statuses): + time.sleep(0.1) - 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") + for sset in songsets: + for nt, name, song_id, rating in sset: - if pctl.master_library[track_id].file_ext == "JELY": - jellyfin.favorite(pctl.master_library[track_id]) + id = pctl.master_count - 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) + replace_existing = False + ex = existing.get(song_id) + if ex is not None: + id = ex + replace_existing = True - gui.pl_update = 2 - gui.update += 1 - if sync and pctl.mpris is not None: - pctl.mpris.update(force=True) + nt.index = id + pctl.master_library[id] = nt + if not replace_existing: + pctl.master_count += 1 + playlist.append(nt.index) -def maloja_get_scrobble_counts(): - if lastfm.scanning_scrobbles is True or not prefs.maloja_url: - return + if star_store.get_rating(nt.index) == 0 and rating == 0: + pass + else: + star_store.set_rating(nt.index, rating * 2) - 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) + self.scanning = False + if return_list: + return playlist - 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 + 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) - try: - data = json.loads(r.text) - l = data["list"] + # 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) - counts = {} +class STray: - 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 + def __init__(self) -> None: + self.active = False - touched = [] + def up(self, systray: SysTrayIcon): + SDL_ShowWindow(t_window) + SDL_RaiseWindow(t_window) + SDL_RestoreWindow(t_window) + gui.lowered = False - 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") + def down(self) -> None: + if self.active: + SDL_HideWindow(t_window) - except Exception: - logging.exception("There was an error parsing the data") - show_message(_("There was an error parsing the data"), mode="warning") + def advance(self, systray: SysTrayIcon) -> None: + pctl.advance() - gui.pl_update += 1 - lastfm.scanning_scrobbles = False - tauon.bg_save() + def back(self, systray: SysTrayIcon) -> None: + pctl.back() + def pause(self, systray: SysTrayIcon) -> None: + pctl.play_pause() -def maloja_scrobble(track: TrackClass, timestamp: int = int(time.time())) -> bool | None: - url = prefs.maloja_url + def track_stop(self, systray: SysTrayIcon) -> None: + pctl.stop() - if not track.artist or not track.title: - return None + def on_quit_callback(self, systray: SysTrayIcon) -> None: + tauon.exit("Exit called from tray.") - if not url.endswith("/newscrobble"): - if not url.endswith("/"): - url += "/" - url += "apis/mlj_1/newscrobble" + 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 - d = {} - d["artists"] = [track.artist] # let Maloja parse/fix artists - d["title"] = track.title + def stop(self) -> None: + self.systray.shutdown() + self.active = False - 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 +class GStats: + def __init__(self): - 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 + self.last_db = 0 + self.last_pl = 0 + self.artist_list = [] + self.album_list = [] + self.genre_list = [] + self.genre_dict = {} + def update(self, playlist): -class LastScrob: + pt = 0 - def __init__(self): + if pctl.master_count != self.last_db or self.last_pl != playlist: + self.last_db = pctl.master_count + self.last_pl = playlist - self.a_index = -1 - self.a_sc = False - self.a_pt = False - self.queue = [] - self.running = False + artists = {} - def start_queue(self): + for index in pctl.multi_playlist[playlist].playlist_ids: + artist = pctl.master_library[index].artist - self.running = True - mini_t = threading.Thread(target=self.process_queue) - mini_t.daemon = True - mini_t.start() + if artist == "": + artist = "" - def process_queue(self): + pt = int(star_store.get(index)) + if pt < 30: + continue - time.sleep(0.4) + if artist in artists: + artists[artist] += pt + else: + artists[artist] = pt - while self.queue: + art_list = artists.items() - try: - tr = self.queue.pop() + sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) - gui.pl_update = 1 - logging.info("Submit Scrobble " + tr[0].artist + " - " + tr[0].title) + self.artist_list = copy.deepcopy(sorted_list) - success = True + genres = {} + genre_dict = {} - 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) + for index in pctl.multi_playlist[playlist].playlist_ids: + genre_r = pctl.master_library[index].genre - if not success: - logging.info("Re-queue scrobble") - self.queue.append(tr) - time.sleep(10) - break + pt = int(star_store.get(index)) - except Exception: - logging.exception("SCROBBLE QUEUE ERROR") + 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] - if not self.queue: - scrobble_warning_timer.force_set(1000) + pt = int(pt / len(gn)) - self.running = False + for genre in gn: - def update(self, add_time): + if genre.lower() in {"", "other", "unknown", "misc"}: + genre = "" + 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" - if pctl.queue_step > len(pctl.track_queue) - 1: - logging.info("Queue step error 1") - return + genre = genre.title() - 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 len(genre) == 3 and genre[2] == "m": + genre = genre.upper() - 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 genre in genres: - 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]) + genres[genre] += pt + else: + genres[genre] = pt - 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 genre in genre_dict: + genre_dict[genre].append(index) + else: + genre_dict[genre] = [index] - 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 + art_list = genres.items() + sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) - if send_full: - self.scrob_full_track(pctl.master_library[self.a_index]) + self.genre_list = copy.deepcopy(sorted_list) + self.genre_dict = genre_dict - def listen_track(self, track_object: TrackClass): - # logging.info("LISTEN") + # logging.info('\n-----------------------\n') - if track_object.is_network: - if track_object.file_ext == "SUB": - subsonic.listen(track_object, submit=False) + g_albums = {} - 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() + for index in pctl.multi_playlist[playlist].playlist_ids: + album = pctl.master_library[index].album - if lb.enable: - mini_t = threading.Thread(target=lb.listen_playing, args=([track_object])) - mini_t.daemon = True - mini_t.start() + if album == "": + album = "" - def scrob_full_track(self, track_object: TrackClass): - # logging.info("SCROBBLE") - track_object.lfm_scrobbles += 1 - gui.pl_update += 1 + pt = int(star_store.get(index)) - 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 pt < 30: + continue - 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")) + if album in g_albums: + g_albums[album] += pt + else: + g_albums[album] = pt + art_list = g_albums.items() -lfm_scrobbler = LastScrob() + sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) -QuickThumbnail.renderer = renderer + self.album_list = copy.deepcopy(sorted_list) +class Drawing: -class Strings: + 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 __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") + if w is None: + w = ddt.get_text_w(text, font) + 18 * gui.scale + if h is None: + h = 22 * gui.scale - self.day = _("day") - self.days = _("days") + rect = (x, y, w, h) + tauon.fields.add(rect) - self.scan_chrome = _("Scanning for Chromecasts...") - self.cast_to = _("Cast to: %s") - self.no_chromecasts = _("No Chromecast devices found") - self.stop_cast = _("End Cast") + 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 - self.web_server_stopped = _("Web server stopped.") + click = False - 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") + if press is None: + press = inp.mouse_click + if tauon.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 -strings = Strings() + 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 id_to_pl(id: int): - for i, item in enumerate(pctl.multi_playlist): - if item.uuid_int == id: - return i - return None + 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 -def pl_to_id(pl: int) -> int: - return pctl.multi_playlist[pl].uuid_int + 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)) -class Chunker: + g = io.BytesIO() + g.seek(0) + im.save(g, "PNG") + g.seek(0) - def __init__(self): - self.master_count = 0 - self.chunks = {} - self.header = None - self.headers = [] - self.h2 = None + wop = rw_from_object(g) + s_image = IMG_Load_RW(wop, 0) + c = SDL_CreateTextureFromSurface(renderer, s_image) + SDL_SetTextureAlphaMod(c, self.opacity) - self.clients = {} + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + SDL_QueryTexture(c, None, None, tex_w, tex_h) -class MenuIcon: + dst = SDL_Rect(0, 0) + dst.w = int(tex_w.contents.value) + dst.h = int(tex_h.contents.value) - 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 + SDL_FreeSurface(s_image) + g.close() + im.close() -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 + 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) -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] + 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 encode_folder_name(track_object: TrackClass) -> str: - folder_name = track_object.artist + " - " + track_object.album + def __init__(self): + self.index = -1 + self.text = "" - if folder_name == " - ": - folder_name = track_object.parent_folder_name + self.lyrics_position = 0 - folder_name = filename_safe(folder_name).strip() + def generate(self, index, w): + self.text = pctl.master_library[index].lyrics + self.lyrics_position = 0 - if not folder_name: - folder_name = str(track_object.index) + 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) - 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) + colour = colours.side_bar_line1 - return folder_name + # if inp.key_ctrl_down: + # if inp.mouse_wheel < 0: + # prefs.lyrics_font_size += 1 + # if inp.mouse_wheel > 0: + # prefs.lyrics_font_size -= 1 -class ThreadManager: + 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.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.index = -1 + self.text = "" - self.d: dict = {} + self.lyrics_position = 0 - 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 test_update(self, track_object: TrackClass): - 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() + 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 check_playback_running(self) -> bool: - if self.playback is None: - return False - return self.playback.is_alive() + def render(self, x, y, w, h, p): -class Menu: - """Right click context menu generator""" + colour = colours.lyrics + if test_lumi(colours.gallery_background) < 0.5: + colour = colours.grey(40) - switch = 0 - count = switch + 1 - instances: list[Menu] = [] - active = False + ddt.text((x, y, 4, w), self.text, colour, 17, w, colours.playlist_panel_background) - 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 +class TimedLyricsToStatic: - def __init__(self, width: int, show_icons: bool = False) -> None: + def __init__(self): + self.cache_key = None + self.cache_lyrics = "" - 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() + 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) - 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)) + if data is None: + self.cache_lyrics = "" + self.cache_key = track + return "" + text = "" - self.id = Menu.count - self.break_height = round(4 * gui.scale) + for line in data: + if len(line) < 10: + continue - Menu.count += 1 + if line[0] != "[" or line[9] != "]" or ":" not in line or "." not in line: + continue - self.sub_number = 0 - self.sub_active = -1 - self.sub_y_postion = 0 - Menu.instances.append(self) + text += line.split("]")[-1].rstrip("\n") + "\n" - @staticmethod - def deco(_=_): - return [colours.menu_text, colours.menu_background, None] + self.cache_lyrics = text + self.cache_key = track + return text - 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] +class TimedLyricsRen: - 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 __init__(self): - def br(self) -> None: - self.items.append(None) + self.index = -1 - 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([]) + self.scanned = {} + self.ready = False + self.data = [] - 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) + self.scroll_position = 0 - 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 generate(self, track: TrackClass) -> bool | None: - 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() + if self.index == track.index: + return self.ready - def render_icon(self, x, y, icon, selected, fx): + self.ready = False + self.index = track.index + self.scroll_position = 0 + self.data.clear() - if colours.lm: - selected = True + data = find_synced_lyric_data(track) + if data is None: + return None - if icon is not None: + for line in data: + if len(line) < 10: + continue - x += icon.xoff * gui.scale - y += icon.yoff * gui.scale + if line[0] != "[" or "]" not in line or ":" not in line or "." not in line: + continue - colour = None + try: - if icon.base_asset is None: - # Colourise mode + text = line.split("]")[-1].rstrip("\n") + t = line - if icon.colour_callback is not None: # and icon.colour_callback() is not None: - colour = icon.colour_callback() + while t[0] == "[" and t[9] == "]" and ":" in t and "." in t: - elif selected and fx[0] != colours.menu_text_disabled: - colour = icon.colour + a = t.lstrip("[") + t = t.split("]")[1] + "]" - 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 + a = a.split("]")[0] + mm, b = a.split(":") + ss, ms = b.split(".") - if colour is None: - # colour = [145, 145, 145, 70] - colour = colours.menu_icons # [255, 255, 255, 35] - # colour = [50, 50, 50, 255] + s = int(mm) * 60 + int(ss) + if len(ms) == 2: + s += int(ms) / 100 + elif len(ms) == 3: + s += int(ms) / 1000 - icon.asset.render(x, y, colour) + self.data.append((s, text)) - 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 len(t) < 10: + break + except Exception: + logging.exception("Failed generating timed lyrics") + continue - # 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) + self.data = sorted(self.data, key=lambda x: x[0]) + # logging.info(self.data) - def render(self): - if self.active: + self.ready = True + return True - if Menu.switch != self.id: - self.active = False + def render(self, index: int, x: int, y: int, side_panel: bool = False, w: int = 0, h: int = 0) -> bool | None: - for menu in Menu.instances: - if menu.active: - break - else: - Menu.active = False + if index != self.index: + self.ready = False + self.generate(pctl.master_library[index]) - return + if right_click and x and y and tauon.coll((x, y, w, h)): + showcase_menu.activate(pctl.master_library[index]) - # ytoff = 3 - y_run = round(self.pos[1]) - to_call = None + if not self.ready: + return False - # 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) + if inp.mouse_wheel and (pctl.playing_state != 1 or pctl.track_queue[pctl.queue_step] != index): + if side_panel: + if tauon.coll((x, y, w, h)): + self.scroll_position += int(inp.mouse_wheel * 30 * gui.scale) + else: + self.scroll_position += int(inp.mouse_wheel * 30 * gui.scale) - x_run = self.pos[0] + line_active = -1 + last = -1 - for i in range(len(self.items)): - #logging.info(self.items[i]) + highlight = True - # Draw menu break - if self.items[i] is 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) - 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) + test_time = get_real_time() - rect = (x_run, y_run, self.w, self.break_height - 1) - if coll(rect): - self.clicked = False + if pctl.track_queue[pctl.queue_step] == index: - ddt.rect_a((x_run, y_run), (self.w, self.break_height), colours.menu_background) + for i, line in enumerate(self.data): + if line[0] < test_time: + last = i - ddt.rect_a((x_run, y_run + 2 * gui.scale), (self.w, 2 * gui.scale), break_colour) + if line[0] > test_time: + pctl.wake_past_time = line[0] + line_active = last + break + else: + line_active = len(self.data) - 1 - # Draw tab - ddt.rect_a((x_run, y_run), (4 * gui.scale, self.break_height), colours.menu_tab) - y_run += self.break_height + if pctl.playing_state == 1: + self.scroll_position = (max(0, line_active)) * spacing * -1 - continue + yy = y + self.scroll_position - 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 + for i, line in enumerate(self.data): - # 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 0 < yy < window_size[1]: - if fx[2] is not None: - label = fx[2] - else: - label = self.items[i].title + colour = colours.lyrics + if test_lumi(colours.gallery_background) < 0.5: + colour = colours.grey(40) - # Show text as disabled if disable_test() passes - if self.is_item_disabled(self.items[i]): - fx[0] = colours.menu_text_disabled + if i == line_active and highlight: + colour = [255, 210, 50, 255] + if colours.lm: + colour = [180, 130, 210, 255] - # Draw item background, black by default - ddt.rect_a((x_run, y_run), (self.w, self.h), fx[1]) - bg = fx[1] + 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 - # Detect if mouse is over this item - selected = False - rect = (x_run, y_run, self.w, self.h - 1) - fields.add(rect) +class TextBox2: + cursor = True - 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) + def __init__(self) -> None: - # Call menu items callback if clicked - if self.clicked: + self.text: str = "" + self.cursor_position = 0 + self.selection = 0 + self.offset = 0 + self.down_lock = False + self.paste_text = "" - 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 + def paste(self) -> None: - else: - self.clicked = False - self.sub_active = self.items[i].sub_menu_number - self.sub_y_postion = y_run + if SDL_HasClipboardText(): + clip = SDL_GetClipboardText().decode("utf-8") + self.paste_text = clip - # Draw tab - ddt.rect_a((x_run, y_run), (4 * gui.scale, self.h), colours.menu_tab) + def copy(self) -> None: - # 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) + text = self.get_selection() + if not text: + text = self.text + if text != "": + SDL_SetClipboardText(text.encode("utf-8")) - if self.show_icons: - x += 25 * gui.scale + def set_text(self, text: str) -> None: - # Draw arrow icon for sub menu - if self.items[i].is_sub_menu is True: + self.text = text + if self.cursor_position > len(text): + self.cursor_position = 0 + self.selection = 0 + else: + self.selection = self.cursor_position - 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) + def clear(self) -> None: + self.text = "" + #self.cursor_position = 0 + self.selection = self.cursor_position - 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) + def highlight_all(self) -> None: - # 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) + self.selection = len(self.text) + self.cursor_position = 0 - # 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) + 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 - # Render the items hint - if self.items[i].hint != None: + 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] - 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) + 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):] - # 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) + else: + return "" - y_run += self.h + def draw( + self, x, y, colour, active=True, secret=False, font=13, width=0, click=False, selection_height=18, big=False): - 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] + # 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 - # 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: + SDL_SetRenderTarget(renderer, text_box_canvas) + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - # 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 + text_box_canvas_rect.x = 0 + text_box_canvas_rect.y = 0 + SDL_RenderFillRect(renderer, text_box_canvas_rect) - 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 + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) - fx = self.deco() + selection_height *= gui.scale - minY = window_size[1] - self.h * len(self.subs[self.sub_active]) - 15 * gui.scale - sub_pos[1] = min(sub_pos[1], minY) + if click is False: + click = inp.mouse_click + if inp.mouse_down: + gui.update = 2 # TODO, more elegant fix - xoff = 0 - for i in self.subs[self.sub_active]: - if i.icon is not None: - xoff = 24 * gui.scale - break + 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) - for w in range(len(self.subs[self.sub_active])): + tauon.fields.add(rect) - 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 + # Activate Menu + if tauon.coll(rect): + if right_click or level_2_right_click: + field_menu.activate(self) - # 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 width > 0 and active: - # Item background - ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (sub_w, self.h), fx[1]) + if click and field_menu.active: + # field_menu.click() + click = False - # 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 + # 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:] - # Call Callback - if self.clicked and not self.is_item_disabled(self.subs[self.sub_active][w]): + 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] - # 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) + def g2(): + if len(self.text) == 0 or self.cursor_position == 0: + return None + return self.text[len(self.text) - self.cursor_position] - # If callback just need ref - elif self.subs[self.sub_active][w].pass_ref: - self.subs[self.sub_active][w].func(self.reference) + 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 - else: - self.subs[self.sub_active][w].func() + # Ctrl + Backspace to delete word + if inp.backspace_press and (inp.key_ctrl_down or inp.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() - if fx[2] is not None: - label = fx[2] - else: - label = self.subs[self.sub_active][w].title + # Ctrl + left to move cursor back a word + elif (inp.key_ctrl_down or inp.key_rctrl_down) and key_left_press: + while g() == " ": + self.cursor_position += 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + while g() != None and g() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": + self.cursor_position += 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + if g() == " ": + self.cursor_position -= 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + break - # 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 + # Ctrl + right to move cursor forward a word + elif (inp.key_ctrl_down or inp.key_rctrl_down) and key_right_press: + while g2() == " ": + self.cursor_position -= 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + while g2() != None and g2() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": + self.cursor_position -= 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + if g2() == " ": + self.cursor_position += 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + break - # 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) + # 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() - # 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) + # Left and right arrow keys to move cursor + if key_right_press: + if self.cursor_position > 0: + self.cursor_position -= 1 + if not inp.key_shift_down and not inp.key_shiftr_down: + self.selection = self.cursor_position - # Draw tab - ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (4 * gui.scale, self.h), colours.menu_tab) + if key_left_press: + if self.cursor_position < len(self.text): + self.cursor_position += 1 + if not inp.key_shift_down and not inp.key_shiftr_down: + self.selection = self.cursor_position - # Render the menu outline - # ddt.rect_a(sub_pos, (sub_w, self.h * len(self.subs[self.sub_active])), colours.grey(40)) + if self.paste_text: + if "http://" in self.text and "http://" in self.paste_text: + self.text = "" - # Process Click Actions - if to_call is not None: + self.paste_text = self.paste_text.rstrip(" ").lstrip(" ") + self.paste_text = self.paste_text.replace("\n", " ").replace("\r", "") - 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() + 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 = "" - if self.clicked or key_esc_press or self.close_next_frame: - self.close_next_frame = False - self.active = False - self.clicked = False + # Paste via ctrl-v + if inp.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:] - last_click_location[0] = 0 - last_click_location[1] = 0 + if inp.key_ctrl_down and key_c_press: + self.copy() - for menu in Menu.instances: - if menu.active: - break - else: - Menu.active = False + if inp.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() - # Render the menu outline - # ddt.rect_a(self.pos, (self.w, self.h * len(self.items)), colours.grey(40)) + if inp.key_ctrl_down and key_a_press: + self.cursor_position = 0 + self.selection = len(self.text) - def activate(self, in_reference=0, position=None): + # ddt.rect(rect, [255, 50, 50, 80], True) + if tauon.coll(rect) and not field_menu.active: + gui.cursor_want = 2 - Menu.active = True + # 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 position != None: - self.pos = [position[0], position[1]] - else: - self.pos = [copy.deepcopy(mouse_position[0]), copy.deepcopy(mouse_position[1])] + if key_home_press: + self.cursor_position = len(self.text) + if not inp.key_shift_down and not inp.key_shiftr_down: + self.selection = self.cursor_position + if key_end_press: + self.cursor_position = 0 + if not inp.key_shift_down and not inp.key_shiftr_down: + self.selection = self.cursor_position - self.reference = in_reference - Menu.switch = self.id - self.sub_active = -1 + 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 - # 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) + if cursor_x < self.offset: + self.offset = cursor_x - round(15 * 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 + self.offset = max(self.offset, 0) + else: + self.offset = 0 - # Flip menu up if would intersect with bottom of window - if self.pos[1] + full_h > window_size[1]: - self.pos[1] -= shown_h + x -= self.offset - # Prevent moving outside top of window - if self.pos[1] < gui.panelY: - self.pos[1] = gui.panelY - self.pos[0] += 5 * gui.scale + if tauon.coll(select_rect): # tauon.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 inp.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) - self.active = True + if x + pre - 0 <= inp.mouse_position[0] <= x + post + 0: + diff = post - pre + if inp.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 -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 inp.mouse_up: + self.down_lock = False + if self.down_lock: + pre = 0 + post = 0 + text = self.text + if secret: + text = "●" * len(self.text) + if inp.mouse_position[0] < x + 1: + self.selection = len(text) + else: - def get_file_source(self, track_object: TrackClass): + for i in range(len(text)): + post = ddt.get_text_w(text[0:i + 1], font) + # pre_half = int((post - pre) / 2) - global album_art_gen + if x + pre - 0 <= inp.mouse_position[0] <= x + post + 0: + diff = post - pre - sources = album_art_gen.get_sources(track_object) + if inp.mouse_position[0] >= x + pre + int(diff / 2): + self.selection = len(text) - i - 1 - if len(sources) == 0: - return False, 0 + else: + self.selection = len(text) - i - offset = album_art_gen.get_offset(track_object.fullpath, sources) - return sources[offset], offset + break + pre = post - def worker_render(self): + else: + self.selection = 0 - self.lock.acquire() - # time.sleep(0.1) + text = self.text[0: len(self.text) - self.cursor_position] + if secret: + text = "●" * len(text) + a = ddt.get_text_w(text, font) - 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 + text = self.text[0: len(self.text) - self.selection] + if secret: + text = "●" * len(text) + b = ddt.get_text_w(text, font) - while len(self.queue) > 0: + top = y + if big: + top -= 12 * gui.scale - source_image = None + ddt.rect([a, 0, b - a, selection_height], [40, 120, 180, 255]) - if gui.halt_image_rendering: - self.queue.clear() - break + 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) - self.i += 1 + text = self.text[0: len(self.text) - self.cursor_position] + if secret: + text = "●" * len(text) + space = ddt.get_text_w(text, font) - try: - # key = self.queue[0] - key = self.queue.pop(0) - except Exception: - logging.exception("thumb queue empty") - break + if TextBox.cursor and self.selection == self.cursor_position: + # ddt.line(x + space, y + 2, x + space, y + 15, colour) - if key not in self.gall: - order = [1, None, None, None] - self.gall[key] = order - else: - order = self.gall[key] + ddt.rect((0 + space, 0 + 2, 1 * gui.scale, 14 * gui.scale), colour) - size = key[1] + if click: + self.selection = self.cursor_position - slow_load = False - cache_load = False + 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 tauon.coll(rect) and not field_menu.active: + gui.cursor_want = 2 - try: + 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 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 + 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) - if slow_load: + animate_monitor_timer.set() - source, c_offset = self.get_file_source(key[0]) + text_box_canvas_hide_rect.x = 0 + text_box_canvas_hide_rect.y = 0 - if source is False: - order[0] = 0 - self.gall[key] = order - # del self.queue[0] - continue + # if self.offset: + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) - img_name = str(key[2]) + "-" + str(size) + "-" + str(key[0].index) + "-" + str(c_offset) + text_box_canvas_hide_rect.w = round(self.offset) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_RenderFillRect(renderer, text_box_canvas_hide_rect) - # gall_render_last_timer.set() + 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) - 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 + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + SDL_SetRenderTarget(renderer, gui.main_texture) - # 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) + 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) - g = io.BytesIO() - g.seek(0) +class TextBox: + cursor = True - if cache_load: - g.write(source_image.read()) + def __init__(self) -> None: - 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 + self.text = "" + self.cursor_position = 0 + self.selection = 0 + self.down_lock = False - im.save(g, "BMP") + def paste(self) -> None: - 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 SDL_HasClipboardText(): + clip = SDL_GetClipboardText().decode("utf-8") - g.seek(0) + if "http://" in self.text and "http://" in clip: + self.text = "" - # source_image.close() + clip = clip.rstrip(" ").lstrip(" ") + clip = clip.replace("\n", " ").replace("\r", "") - order = [2, g, None, None] - self.gall[key] = order + self.eliminate_selection() + self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( + self.text) - self.cursor_position:] - gui.update += 1 - if source_image: - source_image.close() - source_image = None - # del self.queue[0] + def copy(self) -> None: - time.sleep(0.001) + text = self.get_selection() + if not text: + text = self.text + if text != "": + SDL_SetClipboardText(text.encode("utf-8")) - 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] + def set_text(self, text): - if size < 150: - random.shuffle(self.queue) + self.text = text + self.cursor_position = 0 + self.selection = 0 - if self.i > 0: - self.i = 0 - return True - return False + def clear(self) -> None: + self.text = "" - def render(self, track: TrackClass, location, size=None, force_offset=None) -> bool | None: - if gallery_load_delay.get() < 0.5: - return None + def highlight_all(self) -> None: - x = round(location[0]) - y = round(location[1]) + self.selection = len(self.text) + self.cursor_position = 0 - # time.sleep(0.1) - if size is None: - size = self.size + def highlight_none(self) -> None: + self.selection = 0 + self.cursor_position = 0 - size = round(size) + 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 - # 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 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] - if force_offset is not None: - offset = force_offset + 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):] - key = (track, size, offset) + else: + return "" - if key in self.gall: - #logging.info("old") + 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): - order = self.gall[key] + # 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 - if order[0] == 0: - # broken - return False + selection_height *= gui.scale - if order[0] == 1: - # not done yet - return False + if click is False: + click = inp.mouse_click - if order[0] == 2: - # finish processing + if width > 0 and active: - 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) + 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 tauon.coll(rect): + if right_click or level_2_right_click: + field_menu.activate(self) - order[0] = 3 - order[1].close() - order[1] = None - order[2] = c - order[3] = dst - self.gall[(track, size, offset)] = order + if click and field_menu.active: + # field_menu.click() + click = False - if order[0] == 3: - # ready + # 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:] - 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]) + 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] - if (track, size, offset) in self.key_list: - self.key_list.remove((track, size, offset)) - self.key_list.append((track, size, offset)) + def g2(): + if len(self.text) == 0 or self.cursor_position == 0: + return None + return self.text[len(self.text) - self.cursor_position] - # 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] + 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 - return True + # Ctrl + Backspace to delete word + if inp.backspace_press and (inp.key_ctrl_down or inp.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() - 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 + # Ctrl + left to move cursor back a word + elif (inp.key_ctrl_down or inp.key_rctrl_down) and key_left_press: + while g() == " ": + self.cursor_position += 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + while g() != None and g() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": + self.cursor_position += 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + if g() == " ": + self.cursor_position -= 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + break -class ThumbTracks: - def __init__(self) -> None: - pass + # Ctrl + right to move cursor forward a word + elif (inp.key_ctrl_down or inp.key_rctrl_down) and key_right_press: + while g2() == " ": + self.cursor_position -= 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + while g2() != None and g2() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": + self.cursor_position -= 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + if g2() == " ": + self.cursor_position += 1 + if not inp.key_shift_down: + self.selection = self.cursor_position + break - def path(self, track: TrackClass) -> str: - source, offset = tauon.gall_ren.get_file_source(track) + # 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() - if source is False: # No art - return None + # Left and right arrow keys to move cursor + if key_right_press: + if self.cursor_position > 0: + self.cursor_position -= 1 + if not inp.key_shift_down and not inp.key_shiftr_down: + self.selection = self.cursor_position - image_name = track.album + track.parent_folder_path + str(offset) - image_name = hashlib.md5(image_name.encode("utf-8", "replace")).hexdigest() + if key_left_press: + if self.cursor_position < len(self.text): + self.cursor_position += 1 + if not inp.key_shift_down and not inp.key_shiftr_down: + self.selection = self.cursor_position - t_path = os.path.join(e_cache_dir, image_name + ".jpg") + # Paste via ctrl-v + if inp.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 os.path.isfile(t_path): - return t_path + if inp.key_ctrl_down and key_c_press: + self.copy() - source_image = album_art_gen.get_source_raw(0, 0, track, subsource=source) + if inp.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() - 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 + if inp.key_ctrl_down and key_a_press: + self.cursor_position = 0 + self.selection = len(self.text) -class Tauon: - """Root class for everything Tauon""" - def __init__(self): + # ddt.rect_r(rect, [255, 50, 50, 80], True) + if tauon.coll(rect) and not field_menu.active: + gui.cursor_want = 2 - 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 + tauon.fields.add(rect) - self.tray_lock = threading.Lock() - self.tray_releases = 0 + # 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 - self.play_lock = None - self.update_play_lock = None - self.sleep_lock = None - self.shutdown_lock = None - self.quick_close = False + if key_home_press: + self.cursor_position = len(self.text) + if not inp.key_shift_down and not inp.key_shiftr_down: + self.selection = self.cursor_position + if key_end_press: + self.cursor_position = 0 + if not inp.key_shift_down and not inp.key_shiftr_down: + self.selection = self.cursor_position - self.copied_track = None - self.macos = macos - self.aud: CDLL | None = None + if tauon.coll(select_rect): + # ddt.rect_r((x - 15, y, width + 16, 19), [50, 255, 50, 50], True) + if click: + pre = 0 + post = 0 + if inp.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) - self.recorded_songs = [] + if x + pre - 0 <= inp.mouse_position[0] <= x + post + 0: + diff = post - pre + if inp.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 - self.chrome_mode = False - self.web_running = False - self.web_thread = None - self.remote_limited = True - self.enable_librespot = shutil.which("librespot") + if inp.mouse_up: + self.down_lock = False + if self.down_lock: + pre = 0 + post = 0 + if inp.mouse_position[0] < x + 1: -# 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.selection = len(self.text) + else: - self.gme_formats = GME_Formats + for i in range(len(self.text)): + post = ddt.get_text_w(self.text[0:i + 1], font) + # pre_half = int((post - pre) / 2) - self.spot_ctl: SpotCtl = SpotCtl(self) - self.tidal: Tidal = Tidal(self) - self.chrome: Chrome | None = None - self.chrome_menu: Menu | None = None + if x + pre - 0 <= inp.mouse_position[0] <= x + post + 0: + diff = post - pre - self.ssl_context = ssl_context + if inp.mouse_position[0] >= x + pre + int(diff / 2): + self.selection = len(self.text) - i - 1 - def start_remote(self) -> None: + else: + self.selection = len(self.text) - i - 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 + break + pre = post - 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 + else: + self.selection = 0 - 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)) + 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) - except Exception as e: - logging.exception("Download failed") - show_message(_("Download failed"), str(e), mode="error") + b = ddt.get_text_w(self.text[0: len(self.text) - self.selection], font) - 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()) + # rint((a, b)) - 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()) + top = y + if big: + top -= 12 * gui.scale - exe.close() - show_message(_("FFMPEG fetch complete"), mode="done") + ddt.rect([x + a, top, b - a, selection_height], [40, 120, 180, 255]) - shooter(go) + 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) - def set_tray_icons(self, force: bool = False): + space = ddt.get_text_w(self.text[0: len(self.text) - self.cursor_position], font) - 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 TextBox.cursor and self.selection == self.cursor_position: + # ddt.line(x + space, y + 2, x + space, y + 15, colour) - 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 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) - 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 click: + self.selection = self.cursor_position - if not user_icon_dir.is_dir(): - os.makedirs(user_icon_dir) + else: + if active: + self.text += input_text + if input_text != "": + self.cursor = True - 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") + while inp.backspace_press and len(self.text) > 0: + self.text = self.text[:-1] + inp.backspace_press -= 1 - def get_tray_icon(self, name: str) -> str: - return str(self.cache_directory / "icon-export" / f"{name}.svg") + if inp.key_ctrl_down and key_v_press: + self.paste() - 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 + if secret: + space = ddt.text((x, y), "●" * len(self.text), colour, font) + else: + space = ddt.text((x, y), self.text, colour, font) - 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 + 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) - def focus_window(self) -> None: - SDL_RaiseWindow(t_window) + 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]) - def get_playing_playlist_id(self) -> int: - return pl_to_id(pctl.active_playlist_playing) + 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) - def wake(self) -> None: - SDL_PushEvent(ctypes.byref(self.dummy_event)) + 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 = "" -tauon = Tauon() +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 -def signal_handler(signum, frame): - signal.signal(signum, signal.SIG_IGN) # ignore additional signals - tauon.exit(reason="SIGINT recieved") + self.blur_texture = None + self.blur_rect = None + self.loaded_bg_type = 0 -signal.signal(signal.SIGINT, signal_handler) + self.download_in_progress = False + self.downloaded_image = None + self.downloaded_track = None -deco = Deco(tauon) -deco.get_themes = get_themes -deco.renderer = renderer + self.base64cache = (0, 0, "") + self.processing64on = None -if prefs.backend != 4: - prefs.backend = 4 + self.bin_cached = (None, None, None) # track, subsource, bin -chrome = None + self.embed_cached = (None, None) -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 async_download_image(self, track: TrackClass, subsource: list[tuple[int, str]]) -> None: -tauon.chrome = chrome + 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 -class PlexService: + def get_info(self, track_object: TrackClass) -> list[tuple[str, int, int, int, str]]: - def __init__(self): - self.connected = False - self.resource = None - self.scanning = False + sources = self.get_sources(track_object) + if len(sources) == 0: + return None - def connect(self): + offset = self.get_offset(track_object.fullpath, sources) - 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 + o_size = (0, 0) + format = "ERROR" - 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 + 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 - 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 + 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 - # from plexapi.server import PlexServer - # baseurl = 'http://localhost:32400' - # token = '' + return [sources[offset][0], len(sources), offset, o_size, format] - # self.resource = PlexServer(baseurl, token) + def get_sources(self, tr: TrackClass) -> list[tuple[int, str]]: - self.connected = True + filepath = tr.fullpath + ext = tr.file_ext - def resolve_stream(self, location): - logging.info("Get plex stream") - if not self.connected: - self.connect() + # Check if source list already exists, if not, make it + if tr.index in self.source_cache: + return self.source_cache[tr.index] - # return self.resource.url(location, True) - return self.resource.library.fetchItem(location).getStreamURL() + source_list: list[tuple[int, str]] = [] # istag, - def resolve_thumbnail(self, location): + # Source type the is first element in list + # 0 = File + # 1 = Embedded in tag + # 2 = Network location - if not self.connected: - self.connect() - if self.connected: - return self.resource.url(location, True) - return None + 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 [] - def get_albums(self, return_list=False): + # 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") - gui.update += 1 - self.scanning = True + if not tr.is_network: - if not self.connected: - self.connect() + 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] - if not self.connected: - self.scanning = False - return [] + 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]) - playlist = [] + for i in range(len(dirs_in_dir)): + subdirec = os.path.join(direc, dirs_in_dir[i]) + items_in_dir2 = os.listdir(subdirec) - 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 + 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]) - albums = self.resource.library.section("Music").albums() - gui.to_got = 0 + self.source_cache[tr.index] = source_list - for album in albums: - year = album.year - album_artist = album.parentTitle - album_title = album.title + return source_list - parent = (album_artist + " - " + album_title).strip("- ") + 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 - for track in album.tracks(): + def fast_display(self, index, location, box, source: list[tuple[int, str]], offset) -> int: + """Renders cached image only by given size for faster performance""" - if not track.duration: - logging.warning("Skipping track with invalid duration - " + track.title + " - " + track.grandparentTitle) - continue + found_unit = None + max_h = 0 - id = pctl.master_count - replace_existing = False + 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 - e = existing.get(track.key) - if e is not None: - id = e - replace_existing = True + if found_unit == None: + return 1 - title = track.title - track_artist = track.grandparentTitle - duration = track.duration / 1000 + unit = found_unit - 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] + temp_dest.x = round(location[0]) + temp_dest.y = round(location[1]) - nt.is_network = True + temp_dest.w = unit.original_size[0] # round(box[0]) + temp_dest.h = unit.original_size[1] # round(box[1]) - if track.thumb: - nt.art_url_key = track.thumb + bh = round(box[1]) + bw = round(box[0]) - nt.url_key = track.key - nt.date = str(year) + if prefs.zoom_art: + temp_dest.w, temp_dest.h = fit_box((unit.original_size[0], unit.original_size[1]), box) + else: - pctl.master_library[id] = nt + # 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 not replace_existing: - pctl.master_count += 1 + if temp_dest.h > bh: + temp_dest.h = bh + temp_dest.w = int(temp_dest.h * (unit.original_size[0] / unit.original_size[1])) - playlist.append(nt.index) + # 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] - gui.to_got += 1 - gui.update += 1 - gui.pl_update += 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 - self.scanning = False + # render the image + SDL_RenderCopy(renderer, unit.texture, None, temp_dest) + style_overlay.hole_punches.append(temp_dest) - if return_list: - return playlist + gui.art_drawn_rect = (temp_dest.x, temp_dest.y, temp_dest.w, temp_dest.h) - 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) + return 0 + def open_external(self, track_object: TrackClass) -> int: -plex = PlexService() -tauon.plex = plex + index = track_object.index -jellyfin = Jellyfin(tauon) -tauon.jellyfin = jellyfin + source = self.get_sources(track_object) + if len(source) == 0: + return 0 + offset = self.get_offset(track_object.fullpath, source) -class SubsonicService: + 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 - def __init__(self): - self.scanning = False - self.playlists = prefs.subsonic_playlists + source_image = io.BytesIO(pic) + im = Image.open(source_image) + source_image.close() - def r(self, point, p=None, binary: bool = False, get_url: bool = False): - salt = secrets.token_hex(8) - server = prefs.subsonic_server.rstrip("/") + "/" + 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) - params = { - "u": prefs.subsonic_user, - "v": "1.13.0", - "c": t_title, - "f": "json", - } + if len(pic) > 30: + with open(target, "wb") as w: + w.write(pic) - if prefs.subsonic_password_plain: - params["p"] = prefs.subsonic_password else: - params["t"] = hashlib.md5((prefs.subsonic_password + salt).encode()).hexdigest() - params["s"] = salt + target = source[offset][1] - if p: - params.update(p) + if system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", target]) + else: + subprocess.call(["xdg-open", target]) - point = "rest/" + point + return 0 - url = server + point + def cycle_offset(self, track_object: TrackClass, reverse: bool = False) -> int: - if get_url: - return url, params + 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: - response = requests.get(url, params=params, timeout=10) + if reverse: + folder_image_offsets[parent_folder] -= 1 + else: + folder_image_offsets[parent_folder] += 1 - if binary: - return response.content + folder_image_offsets[parent_folder] %= len(sources) + return 0 - d = json.loads(response.text) - # logging.info(d) + def cycle_offset_reverse(self, track_object: TrackClass) -> None: + self.cycle_offset(track_object, True) - if d["subsonic-response"]["status"] != "ok": - show_message(_("Subsonic Error: ") + response.text, mode="warning") - logging.error("Subsonic Error: " + response.text) + def get_offset(self, filepath: str, source: list[tuple[int, str]]) -> int: - return d + # Check if folder offset already exsts, if not, make it + parent_folder = os.path.dirname(filepath) - def get_cover(self, track_object: TrackClass): - response = self.r("getCoverArt", p={"id": track_object.art_url_key}, binary=True) - return io.BytesIO(response) + if parent_folder in folder_image_offsets: - def resolve_stream(self, key): + # 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 - p = {"id": key} - if prefs.network_stream_bitrate > 0: - p["maxBitRate"] = prefs.network_stream_bitrate + return folder_image_offsets[parent_folder] - return self.r("stream", p={"id": key}, get_url=True) - # logging.info(response.content) + def get_embed(self, track: TrackClass): - def listen(self, track_object: TrackClass, submit: bool = False): + # cached = self.embed_cached + # if cached[0] == track: + # #logging.info("used cached") + # return cached[1] - 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 + filepath = track.fullpath - def set_rating(self, track_object: TrackClass, rating): + # Use cached file if present + if prefs.precache and tauon.cachement: + path = tauon.cachement.get_file_cached_only(track) + if path: + filepath = path - 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 + pic = None - def set_album_rating(self, track_object: TrackClass, rating): - id = track_object.misc.get("subsonic-folder-id") - if id is not None: + if track.file_ext == "MP3": try: - a = self.r("setRating", p={"id": id, "rating": math.ceil(rating / 2)}) + tag = mutagen.id3.ID3(filepath) + frame = tag.getall("APIC") + if frame: + pic = frame[0].data except Exception: - logging.exception("Error connect for set rating on airsonic") - return True + logging.exception(f"Failed to get tags on file: {filepath}") - def get_music3(self, return_list: bool = False): + if pic is not None and len(pic) < 30: + pic = None - self.scanning = True - gui.to_got = 0 + 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 - existing = {} + elif track.file_ext == "APE": + with Ape(filepath) as tag: + tag.read() + if tag.has_picture and len(tag.picture) > 30: + pic = tag.picture - for track_id, track in pctl.master_library.items(): - if track.is_network and track.file_ext == "SUB": - existing[track.url_key] = track_id + 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 - 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 [] + 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 - b = a["subsonic-response"]["indexes"]["index"] + # self.embed_cached = (track, pic) + return pic - folders = [] + def get_source_raw(self, offset: int, sources: list[tuple[int, str]] | int, track: TrackClass, subsource: list[tuple[int, str]] | None = None): - for letter in b: - artists = letter["artist"] - for artist in artists: - folders.append(( - artist["id"], - artist["name"], - )) - - playlist = [] + source_image = None - songsets = [] - for i in range(len(folders)): - songsets.append([]) - statuses = [0] * len(folders) - dupes = [] + if subsource is None: + subsource = sources[offset] - def getsongs(index, folder_id, name: str, inner: bool = False, parent=None): + 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: - d = self.r("getMusicDirectory", p={"id": folder_id}) - if "child" not in d["subsonic-response"]["directory"]: - if not inner: - statuses[index] = 2 - return + 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=tls_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 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") + logging.exception("Failed to get source") - items = d["subsonic-response"]["directory"]["child"] + else: + source_image = open(subsource[1], "rb") - gui.update = 2 + return source_image - for item in items: + def get_base64(self, track: TrackClass, size): - if item["isDir"]: + # 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 - 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)) + cahced = self.base64cache + if track == cahced[0] and size == cahced[1]: + return cahced[2] - getsongs(index, item["id"], item["title"], inner=True, parent=item) - continue + self.processing64on = track - gui.to_got += 1 - song = item - nt = TrackClass() + filepath = track.fullpath + sources = self.get_sources(track) - if parent and "artist" in parent: - nt.album_artist = parent["artist"] + if len(sources) == 0: + self.processing64on = None + return False - 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"] + offset = self.get_offset(filepath, sources) - 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 + # Get source IO + source_image = self.get_source_raw(offset, sources, track) - rating = 0 - if "userRating" in song: - rating = int(song["userRating"]) + if source_image is None: + self.processing64on = None + return "" - songsets[index].append((nt, name, song["id"], rating)) + 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()) - if inner: - return - statuses[index] = 2 + self.base64cache = (track, size, sss) + self.processing64on = None + return sss - i = -1 - for id, name in folders: - i += 1 - while statuses.count(1) > 3: - time.sleep(0.1) + 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 - statuses[i] = 1 - t = threading.Thread(target=getsongs, args=([i, id, name])) - t.daemon = True - t.start() + # 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") - while statuses.count(2) != len(statuses): - time.sleep(0.1) + # 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") - for sset in songsets: - for nt, name, song_id, rating in sset: + # Check we've not already attempted a search for this artist + if artist in prefs.failed_background_artists: + return None - id = pctl.master_count + # 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 - replace_existing = False - ex = existing.get(song_id) - if ex is not None: - id = ex - replace_existing = True + # Search fanart.tv for background + try: - nt.index = id - pctl.master_library[id] = nt - if not replace_existing: - pctl.master_count += 1 + r = requests.get( + "https://webservice.fanart.tv/v3/music/" \ + + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) - playlist.append(nt.index) + artlink = r.json()["artistbackground"][0]["url"] - if star_store.get_rating(nt.index) == 0 and rating == 0: - pass - else: - star_store.set_rating(nt.index, rating * 2) + response = urllib.request.urlopen(artlink, context=tls_context) + info = response.info() - self.scanning = False - if return_list: - return playlist + assert info.get_content_maintype() == "image" - 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) + t = io.BytesIO() + t.seek(0) + t.write(response.read()) + t.seek(0, 2) + l = t.tell() + t.seek(0) - # 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) + 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 -subsonic = SubsonicService() + 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") -class KoelService: + prefs.failed_background_artists.append(artist) + return None - def __init__(self) -> None: - self.connected: bool = False - self.resource = None - self.scanning: bool = False - self.server: str = "" + def get_blur_im(self, track: TrackClass) -> BytesIO | bool | None: - self.token: str = "" + 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 - def connect(self) -> None: + if source_image is None: + filepath = track.fullpath + sources = self.get_sources(track) - 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 len(sources) == 0: + return False - if self.token: - self.connected = True - logging.info("Already authorised") - return + offset = self.get_offset(filepath, sources) - password = prefs.koel_password - username = prefs.koel_username - server = prefs.koel_server_url - self.server = server + source_image = self.get_source_raw(offset, sources, track) - target = server + "/api/me" + if source_image is None: + return None - headers = { - "Accept": "application/json", - "Content-Type": "application/json", - } - body = { - "email": username, - "password": password, - } + im = Image.open(source_image) - 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 + ox_size = im.size[0] + oy_size = im.size[1] - 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 + format = im.format + if im.format == "JPEG": + format = "JPG" - else: - logging.info("AUTH ERROR") + #logging.info(im.size) + if im.mode != "RGB": + im = im.convert("RGB") - else: - error = "" - j = r.json() - if "message" in j: - error = j["message"] + ratio = window_size[0] / ox_size + ratio += 0.2 - gui.show_message(_("Could not establish connection/authorisation"), error, mode="error") + 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) - def resolve_stream(self, id: str) -> tuple[str, dict[str, str]]: + im = im.resize((new_x, new_y)) - if not self.connected: - self.connect() + 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 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 (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) - # 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" + gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 4 * 3)) - #params = {"token": self.token, } + im = im.filter(ImageFilter.GaussianBlur(blur)) - #target = f"{self.server}/api/download/songs" - #params["songs"] = [id,] - logging.info(target) - logging.info(urllib.parse.urlencode(params)) - return target, params + gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 2)) - 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", - } + g = io.BytesIO() + g.seek(0) - 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") + a_channel = Image.new("L", im.size, 255) # 'L' 8-bit pixels, black and white + im.putalpha(a_channel) - def get_albums(self, return_list: bool = False) -> list[int] | None: + im.save(g, "PNG") + g.seek(0) - gui.update += 1 - self.scanning = True + # source_image.close() - if not self.connected: - self.connect() + return g - if not self.connected: - self.scanning = False - return [] + def save_thumb(self, track_object: TrackClass, size: tuple[int, int], save_path: str, png=False, zoom=False): - playlist = [] + filepath = track_object.fullpath + sources = self.get_sources(track_object) - target = self.server + "/api/data" - headers = { - "Authorization": "Bearer " + self.token, - "Accept": "application/json", - "Content-Type": "application/json", - } + if len(sources) == 0: + logging.error("Error thumbnailing; no source images found") + return False - r = requests.get(target, headers=headers, timeout=10) - data = r.json() + offset = self.get_offset(filepath, sources) + source_image = self.get_source_raw(offset, sources, track_object) - 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"] + im = Image.open(source_image) + if im.mode != "RGB": + im = im.convert("RGB") - 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"] + 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, + )) - existing = {} + im = im.resize(size, Image.Resampling.LANCZOS) - 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 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 song in songs: + if png: + im.save(save_path + ".png", "PNG") + else: + im.save(save_path + ".jpg", "JPEG") - id = pctl.master_count - replace_existing = False + def display(self, track: TrackClass, location, box, fast: bool = False, theme_only: bool = False) -> int | None: + index = track.index + filepath = track.fullpath - e = existing.get(song["id"]) - if e is not None: - id = e - replace_existing = True + 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 - nt = TrackClass() + source = self.get_sources(track) - 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"]) + if len(source) == 0: + return 1 - 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 + offset = self.get_offset(filepath, source) - nt.art_url_key = covers.get(song["album_id"], "") - nt.url_key = song["id"] + 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 - nt.is_network = True - nt.file_ext = "KOEL" + if fast: + return self.fast_display(track, location, box, source, offset) - pctl.master_library[id] = nt + # 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 - if not replace_existing: - pctl.master_count += 1 + 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]) - playlist.append(nt.index) + 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 - self.scanning = False + elif self.download_in_progress: + return 0 - if return_list: - return playlist + 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() - 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) + # 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 -koel = KoelService() -tauon.koel = koel + assert self.downloaded_image + source_image = self.downloaded_image -class TauService: - def __init__(self) -> None: - self.processing = False + except Exception: + logging.exception("IMAGE NETWORK LOAD ERROR") + raise - def resolve_stream(self, key: str) -> str: - return "http://" + prefs.sat_url + ":7814/api1/file/" + key + else: + # source_image = open(source[offset][1], 'rb') + source_image = self.get_source_raw(0, 0, track, source[offset]) - def resolve_picture(self, key: str) -> str: - return "http://" + prefs.sat_url + ":7814/api1/pic/medium/" + key + # Generate + g = io.BytesIO() + g.seek(0) + im = Image.open(source_image) + o_size = im.size - 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 + format = im.format - def get_playlist(self, playlist_name: str | None = None, return_list: bool = False) -> list[int] | None: + try: + if im.format == "JPEG": + format = "JPG" - p = self.get("playlists") + 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 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 [] + if not theme_only: - id = None - name = "" - for pp in p["playlists"]: - if pp["name"].lower() == playlist_name.lower(): - id = pp["id"] - name = pp["name"] + 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) - if id is None: - show_message(_("Playlist not found on target"), mode="error") - self.processing = False - return [] + # Processing for "Carbon" theme + if track == pctl.playing_object() and gui.theme_name == "Carbon" and track.parent_folder_path != colours.last_album: - try: - t = self.get("tracklist/" + id) - except Exception: - logging.exception("error getting tracklist") - return [] - at = t["tracks"] + # 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] - exist = {} - for k, v in pctl.master_library.items(): - if v.is_network and v.file_ext == "TAU": - exist[v.url_key] = k + # 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 - playlist = [] - for item in at: - replace_existing = True + h_colour = rgb_to_hls(colour[0], colour[1], colour[2]) - tid = item["id"] - id = exist.get(str(tid)) - if id is None: - id = pctl.master_count - replace_existing = False + l = .51 + s = .44 - 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) + 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 - 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) + colours.bottom_panel_colour = hls_to_rgb(hh, l, s) + colours.last_album = track.parent_folder_path - nt.url_key = str(tid) - nt.art_url_key = str(tid) + # 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 - nt.is_network = True - nt.file_ext = "TAU" - pctl.master_library[id] = nt + colours = copy.deepcopy(colours) - if not replace_existing: - pctl.master_count += 1 - playlist.append(nt.index) + 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) - if return_list: - self.processing = False - return playlist + min_colour_varience = 75 - 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 + 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 -tau = TauService() -tauon.tau = tau + 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: -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 + black = [25, 25, 25, 255] + white = [220, 220, 220, 255] - return None + 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 -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() + colours.artist_text = choice + colours.artist_playing = choice -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 + # Check title text colour + if contrast_ratio(colours.title_text, colours.playlist_panel_background) < 1.9: - jellyfin.scanning = True - shoot_dl = threading.Thread(target=jellyfin.ingest_library) - shoot_dl.daemon = True - shoot_dl.start() + 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) -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 + choice = black + if con_w > con_b: + choice = white - shoot_dl = threading.Thread(target=plex.get_albums) - shoot_dl.daemon = True - shoot_dl.start() + 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] -def sub_get_album_thread() -> None: - # if prefs.backend != 1: - # show_message("This feature is currently only available with the BASS backend") - # return + colours.album_text = colours.title_text + colours.album_playing = colours.title_playing - pref_box.close() - save_prefs() - if subsonic.scanning: - inp.mouse_click = False - show_message(_("Already scanning!")) - return - subsonic.scanning = True + gui.pl_update = 1 - shoot_dl = threading.Thread(target=subsonic.get_music3) - shoot_dl.daemon = True - shoot_dl.start() + 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) -def koel_get_album_thread() -> None: - # if prefs.backend != 1: - # show_message("This feature is currently only available with the BASS backend") - # return + gui.temp_themes[track.album] = copy.deepcopy(colours) + colours = gui.temp_themes[track.album] + gui.theme_temp_current = track.album - pref_box.close() - save_prefs() - if koel.scanning: - inp.mouse_click = False - show_message(_("Already scanning!")) - return - koel.scanning = True + if theme_only: + source_image.close() + g.close() + return None - shoot_dl = threading.Thread(target=koel.get_albums) - shoot_dl.daemon = True - shoot_dl.start() + wop = rw_from_object(g) + s_image = IMG_Load_RW(wop, 0) + #logging.error(IMG_GetError()) + c = SDL_CreateTextureFromSurface(renderer, s_image) -if system == "Windows" or msys: - from lynxtray import SysTrayIcon + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + SDL_QueryTexture(c, None, None, tex_w, tex_h) -class STray: + dst = SDL_Rect(round(location[0]), round(location[1])) + dst.w = int(tex_w.contents.value) + dst.h = int(tex_h.contents.value) - def __init__(self) -> None: - self.active = False + # Clean uo + SDL_FreeSurface(s_image) + source_image.close() + g.close() + # if close: + # source_image.close() - def up(self, systray: SysTrayIcon): - SDL_ShowWindow(t_window) - SDL_RaiseWindow(t_window) - SDL_RestoreWindow(t_window) - gui.lowered = 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 down(self) -> None: - if self.active: - SDL_HideWindow(t_window) + self.current_wu = unit + self.image_cache.append(unit) - def advance(self, systray: SysTrayIcon) -> None: - pctl.advance() + self.render(unit, location) - def back(self, systray: SysTrayIcon) -> None: - pctl.back() + 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] - def pause(self, systray: SysTrayIcon) -> None: - pctl.play_pause() + # temp fix + global move_on_title + global playlist_hold + inp.quick_drag = False + move_on_title = False + playlist_hold = False - def track_stop(self, systray: SysTrayIcon) -> None: - pctl.stop() + except Exception: + logging.exception("Image load error") + logging.error("-- Associated track: " + track.fullpath) - def on_quit_callback(self, systray: SysTrayIcon) -> None: - tauon.exit("Exit called from tray.") + self.current_wu = None + try: + del self.source_cache[index][offset] + except Exception: + logging.exception(" -- Error, no source cache?") - 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 + return 1 - def stop(self) -> None: - self.systray.shutdown() - self.active = False + return 0 + def render(self, unit, location) -> None: -tray = STray() + rect = unit.rect -if system == "Linux" and not macos and not msys: + gui.art_aspect_ratio = unit.actual_size[0] / unit.actual_size[1] - gnome = Gnome(tauon) + 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]) - try: - gnomeThread = threading.Thread(target=gnome.main) - gnomeThread.daemon = True - gnomeThread.start() - except Exception: - logging.exception("Could not start Dbus thread") + style_overlay.hole_punches.append(rect) -if (system == "Windows" or msys): + SDL_RenderCopy(renderer, unit.texture, None, rect) - tray.start() + gui.art_drawn_rect = (rect.x, rect.y, rect.w, rect.h) - if win_ver < 10: - logging.warning("Unsupported Windows version older than W10, hooking media keys the old way without SMTC!") - import keyboard + def clear_cache(self) -> None: - def key_callback(event): + for unit in self.image_cache: + SDL_DestroyTexture(unit.texture) - 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() + self.image_cache.clear() + self.source_cache.clear() + self.current_wu = None + self.downloaded_track = None - keyboard.hook_key(-179, key_callback) - keyboard.hook_key(-178, key_callback) - keyboard.hook_key(-177, key_callback) - keyboard.hook_key(-176, key_callback) + 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 GStats: - def __init__(self): +class StyleOverlay: - self.last_db = 0 - self.last_pl = 0 - self.artist_list = [] - self.album_list = [] - self.genre_list = [] - self.genre_dict = {} + def __init__(self): - def update(self, playlist): + self.min_on_timer = Timer() + self.fade_on_timer = Timer(0) + self.fade_off_timer = Timer() - pt = 0 + self.stage = 0 - if pctl.master_count != self.last_db or self.last_pl != playlist: - self.last_db = pctl.master_count - self.last_pl = playlist + self.im = None - artists = {} + self.a_texture = None + self.a_rect = None - for index in pctl.multi_playlist[playlist].playlist_ids: - artist = pctl.master_library[index].artist + self.b_texture = None + self.b_rect = None - if artist == "": - artist = "" + self.a_type = 0 + self.b_type = 0 - pt = int(star_store.get(index)) - if pt < 30: - continue + self.window_size = None + self.parent_path = None - if artist in artists: - artists[artist] += pt - else: - artists[artist] = pt + self.hole_punches = [] + self.hole_refills = [] - art_list = artists.items() + self.go_to_sleep = False - sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) + self.current_track_album = "none" + self.current_track_id = -1 - self.artist_list = copy.deepcopy(sorted_list) + def worker(self) -> None: - genres = {} - genre_dict = {} + if self.stage == 0: - for index in pctl.multi_playlist[playlist].playlist_ids: - genre_r = pctl.master_library[index].genre + if (gui.mode == 3 and prefs.mini_mode_mode == 5): + pass + elif prefs.bg_showcase_only and not gui.combo_mode: + return - pt = int(star_store.get(index)) + if pctl.playing_ready() and self.min_on_timer.get() > 0: - 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] + track = pctl.playing_object() - pt = int(pt / len(gn)) + 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 - for genre in gn: + try: + self.im = album_art_gen.get_blur_im(track) + except Exception: + logging.exception("Blur blackground error") + raise + #logging.debug(track.fullpath) - if genre.lower() in {"", "other", "unknown", "misc"}: - genre = "" - 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" + 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 - genre = genre.title() + self.stage = 1 + gui.update += 1 + return - if len(genre) == 3 and genre[2] == "m": - genre = genre.upper() + def flush(self): - if genre in genres: + 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 - genres[genre] += pt - else: - genres[genre] = pt + def display(self) -> None: - if genre in genre_dict: - genre_dict[genre].append(index) - else: - genre_dict[genre] = [index] + if self.min_on_timer.get() < 0: + return - art_list = genres.items() - sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) + if self.stage == 1: - self.genre_list = copy.deepcopy(sorted_list) - self.genre_dict = genre_dict + wop = rw_from_object(self.im) + s_image = IMG_Load_RW(wop, 0) - # logging.info('\n-----------------------\n') + c = SDL_CreateTextureFromSurface(renderer, s_image) - g_albums = {} + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) - for index in pctl.multi_playlist[playlist].playlist_ids: - album = pctl.master_library[index].album + SDL_QueryTexture(c, None, None, tex_w, tex_h) - if album == "": - album = "" + dst = SDL_Rect(round(-40, 0)) + dst.w = int(tex_w.contents.value) + dst.h = int(tex_h.contents.value) - pt = int(star_store.get(index)) + # Clean uo + SDL_FreeSurface(s_image) + self.im.close() - if pt < 30: - continue + # SDL_SetTextureAlphaMod(c, 10) + self.fade_on_timer.set() - if album in g_albums: - g_albums[album] += pt - else: - g_albums[album] = pt + 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 - art_list = g_albums.items() + self.a_texture = c + self.a_rect = dst + self.a_type = album_art_gen.loaded_bg_type - sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) + self.stage = 2 + self.radio_meta = None - self.album_list = copy.deepcopy(sorted_list) + gui.update += 1 + if self.stage == 2: + track = pctl.playing_object() -stats_gen = GStats() + 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 -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") + if gui.mode == 3 and prefs.mini_mode_mode == 5: + pass + elif prefs.bg_showcase_only: + if not gui.combo_mode: 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") + 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 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 self.a_texture is not None: + if self.window_size != window_size: + self.flush() - mouse_down = False - inp.mouse_click = False - drag_mode = False + 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 -def do_minimize_button(): + if t < 0.4: - 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) + SDL_RenderCopy(renderer, self.b_texture, None, self.b_rect) - mouse_down = False - inp.mouse_click = False - drag_mode = False + else: + SDL_DestroyTexture(self.b_texture) + self.b_texture = None + self.b_rect = None + if self.a_texture is not None: -mac_circle = asset_loader(scaled_asset_directory, loaded_asset_dc, "macstyle.png", True) + 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 -def draw_window_tools(): - global mouse_down - global drag_mode + else: + fade = 255 - # 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 + if self.go_to_sleep: + t = self.fade_off_timer.get() + gui.update += 1 - 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 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 - h = round(28 * gui.scale) - y = round(1 * gui.scale) - if macstyle: - y = round(9 * gui.scale) + 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) - 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 + # 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 - xx = 0 - l = prefs.left_window_control - r = not l - focused = window_is_focused() + SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) - # Close - if r: - xx = window_size[0] - x_width - xx -= round(2 * gui.scale) + SDL_SetTextureAlphaMod(self.a_texture, fade) + SDL_RenderCopy(renderer, self.a_texture, None, self.a_rect) - 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) + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) - # 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) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + for rect in self.hole_punches: + SDL_RenderFillRect(renderer, rect) - 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 + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) - # maximize + SDL_SetRenderTarget(renderer, gui.main_texture) + opacity = prefs.art_bg_opacity + if prefs.mini_mode_mode == 5 and gui.mode == 3: + opacity = 255 - 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) + SDL_SetTextureAlphaMod(gui.main_texture_overlay_temp, opacity) + SDL_RenderCopy(renderer, gui.main_texture_overlay_temp, None, None) - 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() + SDL_SetRenderTarget(renderer, gui.main_texture) 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) + SDL_SetRenderTarget(renderer, gui.main_texture) - # minimize +class ToolTip: - if draw_min_button: + def __init__(self, bag: Bag, gui: GuiVar) -> None: + self.bag = bag + self.gui = gui + self.text = "" + self.h = 24 * self.gui.scale + self.w = 62 * self.gui.scale + self.x = 0 + self.y = 0 + self.timer = Timer() + self.trigger = 1.1 + self.font = 13 + self.called = False + self.a = 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) + def test(self, x, y, text): - 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() + if self.text != text or x != self.x or y != self.y: + self.text = text + # self.timer.set() + self.a = False - else: - if r: - xx -= mi_width - if l: - xx += last_width + self.x = x + self.y = y + self.w = ddt.get_text_w(text, self.font) + 20 * self.gui.scale - 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) + self.called = True - # restore - - if gui.mode == 3: - - # 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 - - 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 self.a is False: + self.timer.set() + gui.frame_callback_list.append(TestTimer(self.trigger)) + self.a = True + def render(self) -> None: -def draw_window_border(): - corner_icon.render(window_size[0] - corner_icon.w, window_size[1] - corner_icon.h, colours.corner_icon) + if self.called is True: - corner_rect = (window_size[0] - 20 * gui.scale, window_size[1] - 20 * gui.scale, 20, 20) - fields.add(corner_rect) + if self.timer.get() > self.trigger: - right_rect = (window_size[0] - 3 * gui.scale, 20 * gui.scale, 10, window_size[1] - 40 * gui.scale) - fields.add(right_rect) + 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 - # top_rect = (20 * gui.scale, 0, window_size[0] - 40 * gui.scale, 2 * gui.scale) - # fields.add(top_rect) + self.called = False - left_rect = (0, 10 * gui.scale, 4 * gui.scale, window_size[1] - 50 * gui.scale) - fields.add(left_rect) +class ToolTip3: - bottom_rect = (20 * gui.scale, window_size[1] - 4, window_size[0] - 40 * gui.scale, 7 * gui.scale) - fields.add(bottom_rect) + def __init__(self, bag: Bag, gui: GuiVar) -> None: + self.gui = gui + 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) - 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 + def set(self, x, y, text, font, rect): - colour = colours.window_frame + 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() - 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 point_proximity_test(self.click_exclude_point, inp.mouse_position, 20 * gui.scale): + self.timer.set() + return + if inp.mouse_click: + self.click_exclude_point = copy.copy(inp.mouse_position) + self.timer.set() + return -# ------------------------------------------------------------------------------------------- -# initiate SDL2 --------------------------------------------------------------------C-IS----- + 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 -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 render(self): -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 not self.show: + return -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 not point_proximity_test(self.click_exclude_point, inp.mouse_position, 20 * gui.scale): + self.click_exclude_point = (0, 0) - 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) + if not tauon.coll( + self.rect) or inp.mouse_click or gui.level_2_click or self.pl_position != pctl.playlist_view_position: + self.show = False - except Exception: - logging.exception("Error loading xcursor") + gui.frame_callback_list.append(TestTimer(0.02)) + if self.timer.get() < 0.6: + return -if not maximized and gui.maximized: - SDL_MaximizeWindow(t_window) + w = ddt.get_text_w(self.text, 312) + self.height + x = self.x # - int(self.width / 2) + y = self.y + h = self.height -# logging.error(SDL_GetError()) + border = 1 * gui.scale -# t_window = SDL_CreateShapedWindow( -# window_title, -# SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, -# window_size[0], window_size[1], -# flags) + 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) -# logging.error(SDL_GetError()) + if not tauon.coll(self.rect): + self.show = False -if system == "Windows" or msys: - gui.window_id = sss.info.win.window +class RenameTrackBox: + def __init__(self): -# try: -# SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, b"1") -# -# except Exception: -# logging.exception("old version of SDL detected") + self.active = False + self.target_track_id = None + self.single_only = False -# get window surface and set up renderer -# renderer = SDL_CreateRenderer(t_window, 0, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC) + def activate(self, track_id): -# 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) + self.active = True + self.target_track_id = track_id + if inp.key_shift_down or inp.key_shiftr_down: + self.single_only = True + else: + self.single_only = False -# 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) + def disable_test(self, track_id): + if inp.key_shift_down or inp.key_shiftr_down: + single_only = True + else: + single_only = False + if not single_only: + for item in pctl.default_playlist: + if pctl.master_library[item].parent_folder_path == pctl.master_library[track_id].parent_folder_path: -def bass_player_thread(player): - # logging.basicConfig(filename=user_directory + '/crash.log', level=logging.ERROR, - # format='%(asctime)s %(levelname)s %(name)s %(message)s') + if pctl.master_library[item].is_network is True: + return True + return 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 + def render(self): + if not self.active: + return -if (system == "Windows" or msys) and taskbar_progress: + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False - class WinTask: + 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) - 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() + 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 - self.d_timer = Timer() + if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not tauon.coll((x, y, w, h))): + rename_track_box.active = False - def update(self, force=False): - if self.d_timer.get() > 2 or force: - self.d_timer.set() + r_todo = [] - if pctl.playing_state == 1 and self.updated_state != 1: - self.taskbar.SetProgressState(self.window_id, 0x2) + # Find matching folder tracks in playlist + if not self.single_only: + for item in pctl.default_playlist: + if pctl.master_library[item].parent_folder_path == pctl.master_library[ + self.target_track_id].parent_folder_path: - 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 + # 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: - perc = 0 - - self.taskbar.SetProgressValue(self.window_id, perc, 100) - - 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) - - - if (install_directory / "TaskbarLib.tlb").is_file(): - logging.info("Taskbar progress enabled") - pctl.windows_progress = WinTask() - - else: - pctl.taskbar_progress = False - logging.warning("Could not find TaskbarLib.tlb") + 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) -# --------------------------------------------------------------------------------------------- -# ABSTRACT SDL DRAWING FUNCTIONS ----------------------------------------------------- + # 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 -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] + 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 -def coll(r): - return r[0] < mouse_position[0] <= r[0] + r[2] and r[1] <= mouse_position[1] <= r[1] + r[3] + 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 -ddt = TDraw(renderer) -ddt.scale = gui.scale -ddt.force_subpixel_text = prefs.force_subpixel_text + if item == self.target_track_id: + afterline = parse_template2(NRN, pctl.master_library[item]) -launch = Launch(tauon, pctl, gui, ddt) + 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) -class Drawing: + 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) - 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=""): + 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] - if w is None: - w = ddt.get_text_w(text, font) + 18 * gui.scale - if h is None: - h = 22 * gui.scale + label = _("Write") + " (" + str(len(r_todo)) + ")" - rect = (x, y, w, h) - fields.add(rect) + 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: - 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 + inp.mouse_click = False + total_todo = len(r_todo) + pre_state = 0 - click = False + for item in r_todo: - if press is None: - press = inp.mouse_click + if pctl.playing_state > 0 and item == pctl.track_queue[pctl.queue_step]: + pre_state = pctl.stop(True) - if coll(rect): - if tooltip: - tool_tip.test(x + 15 * gui.scale, y - 28 * gui.scale, tooltip) - ddt.rect(rect, background_highlight_colour) + try: - # if background_highlight_colour[3] != 255: - # background_highlight_colour = None + afterline = parse_template2(NRN, pctl.master_library[item], strict=True) - 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 + oldname = pctl.master_library[item].filename + oldpath = pctl.master_library[item].fullpath + logging.info("Renaming...") -draw = Drawing() + star = star_store.full_get(item) + star_store.remove(item) + oldpath = pctl.master_library[item].fullpath -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) + oldsplit = os.path.split(oldpath) - ddt.prime_font(standard_font, 9, 412) - ddt.prime_font(standard_font, 10, 413) + if os.path.exists(os.path.join(oldsplit[0], afterline)): + logging.error("A file with that name already exists") + total_todo -= 1 + continue - 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" + if not afterline: + logging.error("Rename Error") + total_todo -= 1 + continue - 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) + if "." in afterline and not afterline.split(".")[0]: + logging.error("A file does not have a target filename") + total_todo -= 1 + continue - standard_font = prefs.linux_font_bold - # if msys: - # standard_font = prefs.linux_font_bold + ", Noto Sans, Sans Bold" + os.rename(pctl.master_library[item].fullpath, os.path.join(oldsplit[0], afterline)) - 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) + pctl.master_library[item].fullpath = os.path.join(oldsplit[0], afterline) + pctl.master_library[item].filename = afterline - 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) + search_string_cache.pop(item, None) + search_dia_string_cache.pop(item, 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) + if star is not None: + star_store.insert(item, star) + except Exception: + logging.exception("Rendering error") + total_todo -= 1 -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) + 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 DropShadow: +class TransEditBox: def __init__(self): - self.readys = {} - self.underscan = int(15 * gui.scale) - self.radius = 4 - self.grow = 2 * gui.scale - self.opacity = 90 + self.active = False + self.active_field = 1 + self.selected = [] + self.playlist = -1 - def prepare(self, w, h): - fh = h + self.underscan - fw = w + self.underscan + def render(self): - 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") + if not self.active: + return - im = im.filter(ImageFilter.GaussianBlur(self.radius)) + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False - g = io.BytesIO() - g.seek(0) - im.save(g, "PNG") - g.seek(0) + 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) - wop = rw_from_object(g) - s_image = IMG_Load_RW(wop, 0) - c = SDL_CreateTextureFromSurface(renderer, s_image) - SDL_SetTextureAlphaMod(c, self.opacity) + 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 - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) - SDL_QueryTexture(c, None, None, tex_w, tex_h) + if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not tauon.coll((x, y, w, h))): + self.active = False - dst = SDL_Rect(0, 0) - dst.w = int(tex_w.contents.value) - dst.h = int(tex_h.contents.value) + select = list(set(shift_selection)) + if not select and pctl.selected_ready(): + select = [pctl.selected_in_playlist] - SDL_FreeSurface(s_image) - g.close() - im.close() + titles = [pctl.get_track(pctl.default_playlist[s]).title for s in select] + artists = [pctl.get_track(pctl.default_playlist[s]).artist for s in select] + albums = [pctl.get_track(pctl.default_playlist[s]).album for s in select] + album_artists = [pctl.get_track(pctl.default_playlist[s]).album_artist for s in select] - unit = (dst, c) - self.readys[(w, h)] = unit + #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() - def render(self, x, y, w, h): - if (w, h) not in self.readys: - self.prepare(w, h) + if len(select) == 0: + return - 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]) + tr = pctl.get_track(pctl.default_playlist[select[0]]) + edit_title.set_text(tr.title) + if check_equal(artists): + edit_artist.set_text(artists[0]) -drop_shadow = DropShadow() + if check_equal(albums): + edit_album.set_text(albums[0]) + if check_equal(album_artists): + edit_album_artist.set_text(album_artists[0]) -class LyricsRenMini: + x += round(20 * gui.scale) + y += round(18 * gui.scale) - def __init__(self): - self.index = -1 - self.text = "" + ddt.text((x, y), _("Simple tag editor"), colours.box_title_text, 215) - self.lyrics_position = 0 + 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 generate(self, index, w): - self.text = pctl.master_library[index].lyrics - self.lyrics_position = 0 + y += round(24 * gui.scale) + ddt.text((x, y), _("Number of tracks selected: {N}").format(N=len(select)), colours.box_title_text, 313) - 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) + y += round(24 * gui.scale) - colour = colours.side_bar_line1 + if inp.key_tab_press: + if inp.key_shift_down or inp.key_shiftr_down: + self.active_field -= 1 + else: + self.active_field += 1 - # if key_ctrl_down: - # if mouse_wheel < 0: - # prefs.lyrics_font_size += 1 - # if mouse_wheel > 0: - # prefs.lyrics_font_size -= 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 - ddt.text((x, y, 4, w), self.text, colour, prefs.lyrics_font_size, w - (w % 2), colours.side_panel_background) + 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)) + tauon.fields.add(rect1) + if (tauon.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), _(""), 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) -lyrics_ren_mini = LyricsRenMini() + y += round(40 * gui.scale) + for s in select: + tr = pctl.get_track(pctl.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: -class LyricsRen: + gui.pl_update += 1 + if self.active_field == 0 and len(select) == 1: + for s in select: + tr = pctl.get_track(pctl.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) - def __init__(self): + if self.active_field == 1: + for s in select: + tr = pctl.get_track(pctl.default_playlist[s]) + tr.album = edit_album.text + if self.active_field == 2: + for s in select: + tr = pctl.get_track(pctl.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(pctl.default_playlist[s]) + tr.album_artist = edit_album_artist.text + tauon.bg_save() - self.index = -1 - self.text = "" - self.lyrics_position = 0 + 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 test_update(self, track_object: TrackClass): + if gui.write_tag_in_progress: + return - 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 write_tag_go(): - def render(self, x, y, w, h, p): - colour = colours.lyrics - if test_lumi(colours.gallery_background) < 0.5: - colour = colours.grey(40) + for s in select: + tr = pctl.get_track(pctl.default_playlist[s]) - ddt.text((x, y, 4, w), self.text, colour, 17, w, colours.playlist_panel_background) + 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) -lyrics_ren = LyricsRen() + 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") -def find_synced_lyric_data(track: TrackClass) -> list[str] | None: - if track.is_network: - return None + 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) - direc = track.parent_folder_path - name = os.path.splitext(track.filename)[0] + ".lrc" +class TransEditBox: - if len(track.lyrics) > 20 and track.lyrics[0] == "[" and ":" in track.lyrics[:20] and "." in track.lyrics[:20]: - return track.lyrics.splitlines() + def __init__(self): + self.active = False + self.active_field = 1 + self.selected = [] + self.playlist = -1 - 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 + def render(self): - return data + if not self.active: + return + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False -class TimedLyricsToStatic: + 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) - def __init__(self): - self.cache_key = None - self.cache_lyrics = "" + 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 - 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 key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not tauon.coll((x, y, w, h))): + self.active = False - if data is None: - self.cache_lyrics = "" - self.cache_key = track - return "" - text = "" + select = list(set(shift_selection)) + if not select and pctl.selected_ready(): + select = [pctl.selected_in_playlist] - for line in data: - if len(line) < 10: - continue + titles = [pctl.get_track(pctl.default_playlist[s]).title for s in select] + artists = [pctl.get_track(pctl.default_playlist[s]).artist for s in select] + albums = [pctl.get_track(pctl.default_playlist[s]).album for s in select] + album_artists = [pctl.get_track(pctl.default_playlist[s]).album_artist for s in select] - if line[0] != "[" or line[9] != "]" or ":" not in line or "." not in line: - continue + #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() - text += line.split("]")[-1].rstrip("\n") + "\n" + if len(select) == 0: + return - self.cache_lyrics = text - self.cache_key = track - return text + tr = pctl.get_track(pctl.default_playlist[select[0]]) + edit_title.set_text(tr.title) + if check_equal(artists): + edit_artist.set_text(artists[0]) -tauon.synced_to_static_lyrics = TimedLyricsToStatic() + if check_equal(albums): + edit_album.set_text(albums[0]) + if check_equal(album_artists): + edit_album_artist.set_text(album_artists[0]) -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) + x += round(20 * gui.scale) + y += round(18 * gui.scale) + ddt.text((x, y), _("Simple tag editor"), colours.box_title_text, 215) -class TimedLyricsRen: + 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 __init__(self): + y += round(24 * gui.scale) + ddt.text((x, y), _("Number of tracks selected: {N}").format(N=len(select)), colours.box_title_text, 313) - self.index = -1 + y += round(24 * gui.scale) - self.scanned = {} - self.ready = False - self.data = [] + if inp.key_tab_press: + if inp.key_shift_down or inp.key_shiftr_down: + self.active_field -= 1 + else: + self.active_field += 1 - self.scroll_position = 0 + 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 generate(self, track: TrackClass) -> bool | None: + 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)) + tauon.fields.add(rect1) + if (tauon.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), _(""), 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 - if self.index == track.index: - return self.ready + 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) - self.ready = False - self.index = track.index - self.scroll_position = 0 - self.data.clear() + y += round(40 * gui.scale) + for s in select: + tr = pctl.get_track(pctl.default_playlist[s]) + if tr.is_network: + ddt.text((x, y), _("Editing network tracks is not recommended!"), [245, 90, 90, 255], 312) - data = find_synced_lyric_data(track) - if data is None: - return None + if inp.key_return_press: - for line in data: - if len(line) < 10: - continue + gui.pl_update += 1 + if self.active_field == 0 and len(select) == 1: + for s in select: + tr = pctl.get_track(pctl.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 line[0] != "[" or "]" not in line or ":" not in line or "." not in line: - continue + if self.active_field == 1: + for s in select: + tr = pctl.get_track(pctl.default_playlist[s]) + tr.album = edit_album.text + if self.active_field == 2: + for s in select: + tr = pctl.get_track(pctl.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(pctl.default_playlist[s]) + tr.album_artist = edit_album_artist.text + tauon.bg_save() - try: - text = line.split("]")[-1].rstrip("\n") - t = line + 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 - while t[0] == "[" and t[9] == "]" and ":" in t and "." in t: + if gui.write_tag_in_progress: + return - a = t.lstrip("[") - t = t.split("]")[1] + "]" + def write_tag_go(): - 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 + for s in select: + tr = pctl.get_track(pctl.default_playlist[s]) - self.data.append((s, text)) + 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 - if len(t) < 10: - break - except Exception: - logging.exception("Failed generating timed lyrics") - continue + muta = mutagen.File(tr.fullpath, easy=True) - self.data = sorted(self.data, key=lambda x: x[0]) - # logging.info(self.data) + 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 - self.ready = True - return 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") - def render(self, index: int, x: int, y: int, side_panel: bool = False, w: int = 0, h: int = 0) -> bool | None: + 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) - if index != self.index: - self.ready = False - self.generate(pctl.master_library[index]) +class SubLyricsBox: - if right_click and x and y and coll((x, y, w, h)): - showcase_menu.activate(pctl.master_library[index]) + def __init__(self, tauon: Tauon): + self.ddt = tauon.bag.ddt + self.active = False + self.target_track = None + self.active_field = 1 - if not self.ready: - return False + def activate(self, track: TrackClass): - 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) + self.active = True + gui.box_over = True + self.target_track = track - line_active = -1 - last = -1 + sub_lyrics_a.text = prefs.lyrics_subs.get(self.target_track.artist, "") + sub_lyrics_b.text = prefs.lyrics_subs.get(self.target_track.title, "") - highlight = True + 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 - 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) + def render(self): - test_time = get_real_time() + if not self.active: + return - if pctl.track_queue[pctl.queue_step] == index: + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False - for i, line in enumerate(self.data): - if line[0] < test_time: - last = i + 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) - if line[0] > test_time: - pctl.wake_past_time = line[0] - line_active = last - break - else: - line_active = len(self.data) - 1 + 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 pctl.playing_state == 1: - self.scroll_position = (max(0, line_active)) * spacing * -1 + if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not tauon.coll((x, y, w, h))): + self.active = False + gui.box_over = False - yy = y + self.scroll_position + 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] - for i, line in enumerate(self.data): + 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] - if 0 < yy < window_size[1]: + ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Substitute Lyric Search"), colours.grey(230), 213) - colour = colours.lyrics - if test_lumi(colours.gallery_background) < 0.5: - colour = colours.grey(40) + y += round(35 * gui.scale) + x += round(23 * gui.scale) - if i == line_active and highlight: - colour = [255, 210, 50, 255] - if colours.lm: - colour = [180, 130, 210, 255] + 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) - 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 + 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)) + tauon.fields.add(rect1) + ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) + if (tauon.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 -timed_lyrics_ren = TimedLyricsRen() + 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) -def draw_internel_link(x, y, text, colour, font): - tweak = font - while tweak > 100: - tweak -= 100 + 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) - if gui.scale == 2: - tweak *= 2 - tweak += 4 - if gui.scale == 1.25: - tweak = round(tweak * 1.25) - tweak += 1 + 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)) + tauon.fields.add(rect1) + if (tauon.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) - sp = ddt.text((x, y), text, colour, font) +class ExportPlaylistBox: - rect = [x - 5 * gui.scale, y - 2 * gui.scale, sp + 11 * gui.scale, 23 * gui.scale] - fields.add(rect) + def __init__(self, bag: Bag): - 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 + self.active = False + self.id = None + self.directory_text_box = TextBox2() + self.default = { + "path": str(bag.dirs.music_directory) if bag.dirs.music_directory else str(bag.dirs.user_directory / "playlists"), + "type": "xspf", + "relative": False, + "auto": False, + } + def activate(self, playlist): -# No hit detect -def draw_linked_text(location, text, colour, font, force=False, replace=""): - base = "" - link_text = "" - rest = "" - on_base = True + self.active = True + gui.box_over = True + self.id = pl_to_id(playlist) - 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] + # 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] - target_link = link_text - if replace: - link_text = replace + def render(self) -> None: + if not self.active: + return - left = ddt.get_text_w(base, font) - right = ddt.get_text_w(base + link_text, font) + 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) - x = location[0] - y = location[1] + 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 - 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) + if key_esc_press or ((inp.mouse_click or gui.level_2_click or right_click or level_2_right_click) and not tauon.coll( + (x, y, w, h))): + self.active = False + gui.box_over = False - tweak = font - while tweak > 100: - tweak -= 100 + current = prefs.playlist_exports.get(self.id) + if not current: + current = copy.copy(self.default) - if gui.scale == 2: - tweak *= 2 - tweak += 4 - elif gui.scale != 1: - tweak = round(tweak * gui.scale) - tweak += 2 + ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Export Playlist"), colours.grey(230), 213) - if system == "Windows": - tweak += 1 + x += round(15 * gui.scale) + y += round(25 * gui.scale) - # 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)) + ddt.text((x, y + 8 * gui.scale), _("Save directory"), colours.grey(230), 11) + y += round(30 * gui.scale) - return left, right - left, target_link + rect1 = (x, y, round(450 * gui.scale), round(16 * gui.scale)) + tauon.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) -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) + 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 -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] + if draw.button(_("Export"), x, y, press=gui.level_2_click): + self.run_export(current, self.id, warnings=True) - if click is None: - click = inp.mouse_click + 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) - 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 + if warnings and target != 1: + show_message(_("Playlist exported"), target, mode="done") +class KoelService: -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) + def __init__(self) -> None: + self.connected: bool = False + self.resource = None + self.scanning: bool = False + self.server: str = "" + self.token: str = "" -def pixel_to_logical(x): - return round((x / window_size[0]) * logical_size[0]) + def connect(self) -> None: -class TextBox2: - cursor = True + 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 - def __init__(self) -> None: + if self.token: + self.connected = True + logging.info("Already authorised") + return - self.text: str = "" - self.cursor_position = 0 - self.selection = 0 - self.offset = 0 - self.down_lock = False - self.paste_text = "" + password = prefs.koel_password + username = prefs.koel_username + server = prefs.koel_server_url + self.server = server - def paste(self) -> None: + target = server + "/api/me" - if SDL_HasClipboardText(): - clip = SDL_GetClipboardText().decode("utf-8") - self.paste_text = clip + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + body = { + "email": username, + "password": password, + } - def copy(self) -> None: + 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 - text = self.get_selection() - if not text: - text = self.text - if text != "": - SDL_SetClipboardText(text.encode("utf-8")) + 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 - def set_text(self, text: str) -> None: + else: + logging.info("AUTH ERROR") - 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: + error = "" + j = r.json() + if "message" in j: + error = j["message"] - self.selection = len(self.text) - self.cursor_position = 0 + gui.show_message(_("Could not establish connection/authorisation"), error, mode="error") - 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] + def resolve_stream(self, id: str) -> tuple[str, dict[str, str]]: - 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 not self.connected: + self.connect() + if prefs.network_stream_bitrate > 0: + target = f"{self.server}/api/{id}/play/1/{prefs.network_stream_bitrate}" else: - return "" - - def draw( - self, x, y, colour, active=True, secret=False, font=13, width=0, click=False, selection_height=18, big=False): + target = f"{self.server}/api/{id}/play/0/0" + params = {"jwt-token": self.token } - # 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 + # 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" - SDL_SetRenderTarget(renderer, text_box_canvas) - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + #params = {"token": self.token, } - text_box_canvas_rect.x = 0 - text_box_canvas_rect.y = 0 - SDL_RenderFillRect(renderer, text_box_canvas_rect) + #target = f"{self.server}/api/download/songs" + #params["songs"] = [id,] + logging.info(target) + logging.info(urllib.parse.urlencode(params)) - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + return target, params - selection_height *= gui.scale + 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", + } - if click is False: - click = inp.mouse_click - if mouse_down: - gui.update = 2 # TODO, more elegant fix + 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") - 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) + def get_albums(self, return_list: bool = False) -> list[int] | None: - fields.add(rect) + gui.update += 1 + self.scanning = True - # Activate Menu - if coll(rect): - if right_click or level_2_right_click: - field_menu.activate(self) + if not self.connected: + self.connect() - if width > 0 and active: + if not self.connected: + self.scanning = False + return [] - if click and field_menu.active: - # field_menu.click() - click = False + playlist = [] - # 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:] + target = self.server + "/api/data" + headers = { + "Authorization": "Bearer " + self.token, + "Accept": "application/json", + "Content-Type": "application/json", + } - 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] + r = requests.get(target, headers=headers, timeout=10) + data = r.json() - def g2(): - if len(self.text) == 0 or self.cursor_position == 0: - return None - return self.text[len(self.text) - self.cursor_position] + artists = data["artists"] + albums = data["albums"] + songs = data["songs"] - 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 + artist_ids = {} + for artist in artists: + id = artist["id"] + if id not in artist_ids: + artist_ids[id] = artist["name"] - # 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() + 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"] - # 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 + existing = {} - # 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 + for track_id, track in pctl.master_library.items(): + if track.is_network and track.file_ext == "KOEL": + existing[track.url_key] = track_id - # 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() + for song in songs: - # 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 + id = pctl.master_count + replace_existing = False - 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 + e = existing.get(song["id"]) + if e is not None: + id = e + replace_existing = True - if self.paste_text: - if "http://" in self.text and "http://" in self.paste_text: - self.text = "" + nt = TrackClass() - self.paste_text = self.paste_text.rstrip(" ").lstrip(" ") - self.paste_text = self.paste_text.replace("\n", " ").replace("\r", "") + 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"]) - 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 = "" + 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 - # 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:] + nt.art_url_key = covers.get(song["album_id"], "") + nt.url_key = song["id"] - if key_ctrl_down and key_c_press: - self.copy() + nt.is_network = True + nt.file_ext = "KOEL" - 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() + pctl.master_library[id] = nt - if key_ctrl_down and key_a_press: - self.cursor_position = 0 - self.selection = len(self.text) + if not replace_existing: + pctl.master_count += 1 - # ddt.rect(rect, [255, 50, 50, 80], True) - if coll(rect) and not field_menu.active: - gui.cursor_want = 2 + playlist.append(nt.index) - # 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 + self.scanning = False - 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 return_list: + return playlist - 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 + 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) - if cursor_x < self.offset: - self.offset = cursor_x - round(15 * gui.scale) +class TauService: + def __init__(self) -> None: + self.processing = False - self.offset = max(self.offset, 0) - else: - self.offset = 0 + def resolve_stream(self, key: str) -> str: + return "http://" + prefs.sat_url + ":7814/api1/file/" + key - x -= self.offset + def resolve_picture(self, key: str) -> str: + return "http://" + prefs.sat_url + ":7814/api1/pic/medium/" + key - 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) + 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 - 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 + def get_playlist(self, playlist_name: str | None = None, return_list: bool = False) -> list[int] | None: - 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: + p = self.get("playlists") - for i in range(len(text)): - post = ddt.get_text_w(text[0:i + 1], font) - # pre_half = int((post - pre) / 2) + if not p or not p["playlists"]: + self.processing = False + return [] - if x + pre - 0 <= mouse_position[0] <= x + post + 0: - diff = post - pre + if playlist_name is None: + playlist_name = text_sat_playlist.text.strip() + if not playlist_name: + show_message(_("No playlist name")) + return [] - if mouse_position[0] >= x + pre + int(diff / 2): - self.selection = len(text) - i - 1 + id = None + name = "" + for pp in p["playlists"]: + if pp["name"].lower() == playlist_name.lower(): + id = pp["id"] + name = pp["name"] - else: - self.selection = len(text) - i + if id is None: + show_message(_("Playlist not found on target"), mode="error") + self.processing = False + return [] - break - pre = post + try: + t = self.get("tracklist/" + id) + except Exception: + logging.exception("error getting tracklist") + return [] + at = t["tracks"] - else: - self.selection = 0 + exist = {} + for k, v in pctl.master_library.items(): + if v.is_network and v.file_ext == "TAU": + exist[v.url_key] = k - text = self.text[0: len(self.text) - self.cursor_position] - if secret: - text = "●" * len(text) - a = ddt.get_text_w(text, font) + playlist = [] + for item in at: + replace_existing = True - text = self.text[0: len(self.text) - self.selection] - if secret: - text = "●" * len(text) - b = ddt.get_text_w(text, font) + tid = item["id"] + id = exist.get(str(tid)) + if id is None: + id = pctl.master_count + replace_existing = False - top = y - if big: - top -= 12 * gui.scale + 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) - ddt.rect([a, 0, b - a, selection_height], [40, 120, 180, 255]) + 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) - 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) + nt.url_key = str(tid) + nt.art_url_key = str(tid) - text = self.text[0: len(self.text) - self.cursor_position] - if secret: - text = "●" * len(text) - space = ddt.get_text_w(text, font) + nt.is_network = True + nt.file_ext = "TAU" + pctl.master_library[id] = nt - if TextBox.cursor and self.selection == self.cursor_position: - # ddt.line(x + space, y + 2, x + space, y + 15, colour) + if not replace_existing: + pctl.master_count += 1 + playlist.append(nt.index) - ddt.rect((0 + space, 0 + 2, 1 * gui.scale, 14 * gui.scale), colour) + if return_list: + self.processing = False + return playlist - if click: - self.selection = self.cursor_position + 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 - 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 +class SearchOverlay: - 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]) + def __init__(self, tauon: Tauon): + self.active = False + self.search_text = TextBox() - 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) + self.worker2_lock = tauon.worker2_lock + 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 - animate_monitor_timer.set() + def clear(self): + self.search_text.text = "" + self.results.clear() + self.searched_text = "" + self.on = 0 + self.all_folders = False - text_box_canvas_hide_rect.x = 0 - text_box_canvas_hide_rect.y = 0 + def click_artist(self, name, get_list=False, search_lists=None): + playlist = [] - # if self.offset: - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) + if search_lists is None: + search_lists = [] + for pl in pctl.multi_playlist: + search_lists.append(pl.playlist_ids) - text_box_canvas_hide_rect.w = round(self.offset) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_RenderFillRect(renderer, text_box_canvas_hide_rect) + 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) - 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) + if get_list: + return playlist - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) - SDL_SetRenderTarget(renderer, gui.main_texture) + pctl.multi_playlist.append(pl_gen( + title=_("Artist: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - 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 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 -class TextBox: - cursor = True + 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) - def __init__(self) -> None: + if get_list: + return playlist - self.text = "" - self.cursor_position = 0 - self.selection = 0 - self.down_lock = False + pctl.multi_playlist.append(pl_gen( + title=_("Year: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - def paste(self) -> None: + if gui.combo_mode: + exit_combo() - if SDL_HasClipboardText(): - clip = SDL_GetClipboardText().decode("utf-8") + switch_playlist(len(pctl.multi_playlist) - 1) - if "http://" in self.text and "http://" in clip: - self.text = "" + inp.key_return_press = False - clip = clip.rstrip(" ").lstrip(" ") - clip = clip.replace("\n", " ").replace("\r", "") + def click_composer(self, name: str, get_list: bool = False): - self.eliminate_selection() - self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( - self.text) - self.cursor_position:] + 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) - def copy(self) -> None: + if get_list: + return playlist - text = self.get_selection() - if not text: - text = self.text - if text != "": - SDL_SetClipboardText(text.encode("utf-8")) + pctl.multi_playlist.append(pl_gen( + title=_("Composer: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - def set_text(self, text): + if gui.combo_mode: + exit_combo() - self.text = text - self.cursor_position = 0 - self.selection = 0 + switch_playlist(len(pctl.multi_playlist) - 1) - def clear(self) -> None: - self.text = "" + inp.key_return_press = False - def highlight_all(self) -> None: + def click_meta(self, name: str, get_list: bool = False, search_lists=None): - self.selection = len(self.text) - self.cursor_position = 0 + if search_lists is None: + search_lists = [] + for pl in pctl.multi_playlist: + search_lists.append(pl.playlist_ids) - def highlight_none(self) -> None: - self.selection = 0 - self.cursor_position = 0 + 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) - 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 + if get_list: + return playlist - 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] + pctl.multi_playlist.append(pl_gen( + title=os.path.basename(name).upper(), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - 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 gui.combo_mode: + exit_combo() - else: - return "" + switch_playlist(len(pctl.multi_playlist) - 1) - 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): + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "p\"" + name + "\"" - # 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 + inp.key_return_press = False - selection_height *= gui.scale + def click_genre(self, name: str, get_list: bool = False, search_lists=None): - if click is False: - click = inp.mouse_click + playlist = [] - if width > 0 and active: + if search_lists is None: + search_lists = [] + for pl in pctl.multi_playlist: + search_lists.append(pl.playlist_ids) - 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) + include_multi = False + if name.endswith("+") or not prefs.sep_genre_multi: + name = name.rstrip("+") + include_multi = True - # Activate Menu - if coll(rect): - if right_click or level_2_right_click: - field_menu.activate(self) + 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 click and field_menu.active: - # field_menu.click() - click = False + if get_list: + return playlist - # 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:] + pctl.multi_playlist.append(pl_gen( + title=_("Genre: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - 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] + if gui.combo_mode: + exit_combo() - def g2(): - if len(self.text) == 0 or self.cursor_position == 0: - return None - return self.text[len(self.text) - self.cursor_position] + switch_playlist(len(pctl.multi_playlist) - 1) - 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 + 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 + "\"" - # 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() + inp.key_return_press = False - # 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 click_album(self, index): - # 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 + pctl.jump(index) + if gui.combo_mode: + exit_combo() - # 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() + pctl.show_current() - # 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 + inp.key_return_press = False - 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 + def render(self): + global input_text + if self.active is False: - # 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:] + # Activate search overlay on key presses + if prefs.search_on_letter and input_text != "" and gui.layer_focus == 0 and \ + not inp.key_lalt and not inp.key_ralt and \ + not inp.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: - if key_ctrl_down and key_c_press: - self.copy() + # Divert to artist list if mouse over + if gui.lsp and prefs.left_panel_mode == "artist list" and 2 < inp.mouse_position[0] < gui.lspw \ + and gui.panelY < inp.mouse_position[1] < window_size[1] - gui.panelBY: + artist_list_box.locate_artist_letter(input_text) + return - 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() + activate_search_overlay() + self.old_mouse = copy.deepcopy(inp.mouse_position) - if key_ctrl_down and key_a_press: - self.cursor_position = 0 - self.selection = len(self.text) + if self.active: - # ddt.rect_r(rect, [255, 50, 50, 80], True) - if coll(rect) and not field_menu.active: - gui.cursor_want = 2 + x = 0 + y = 0 + w = window_size[0] + h = window_size[1] - fields.add(rect) + if keymaps.test("add-to-queue"): + input_text = "" - # 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 inp.backspace_press: + # self.searched_text = "" + # self.results.clear() - if mouse_up: - self.down_lock = False - if self.down_lock: - pre = 0 - post = 0 - if mouse_position[0] < x + 1: + if len(self.search_text.text) - inp.backspace_press < 1: + self.active = False + self.search_text.text = "" + self.results.clear() + self.searched_text = "" + return - self.selection = len(self.text) + 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 - 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 gui.level_2_click and inp.mouse_position[0] > 350 * gui.scale: + self.active = False + self.search_text.text = "" - if mouse_position[0] >= x + pre + int(diff / 2): - self.selection = len(self.text) - i - 1 + mouse_change = False + if not point_proximity_test(self.old_mouse, inp.mouse_position, 25): + mouse_change = True + # mouse_change = True - else: - self.selection = len(self.text) - i + ddt.rect((x, y, w, h), [3, 3, 3, 235]) + ddt.text_background_colour = [12, 12, 12, 255] - break - pre = post - else: - self.selection = 0 + input_text_x = 80 * gui.scale + highlight_x = 30 * gui.scale + thumbnail_rx = 100 * gui.scale + text_lx = 120 * gui.scale - 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) + s_font = 15 + s_b_font = 214 + b_font = 215 - b = ddt.get_text_w(self.text[0: len(self.text) - self.selection], font) + 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 - # rint((a, b)) + #album_art_size_s = 0 * gui.scale - top = y - if big: - top -= 12 * 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) - ddt.rect([x + a, top, b - a, selection_height], [40, 120, 180, 255]) + t = self.animate_timer.get() + if abs(t - self.last_animate_time) > 0.3: + self.animate_timer.set() + t = 0 - 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) + self.last_animate_time = t - space = ddt.get_text_w(self.text[0: len(self.text) - self.cursor_position], font) + 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) - if TextBox.cursor and self.selection == self.cursor_position: - # ddt.line(x + space, y + 2, x + space, y + 15, colour) + ddt.rect((x, y, s, s), colour) + x += g + s - 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) + gui.update += 1 - if click: - self.selection = self.cursor_position + # 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]) - else: - if active: - self.text += input_text - if input_text != "": - self.cursor = True + # 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]) - while inp.backspace_press and len(self.text) > 0: - self.text = self.text[:-1] - inp.backspace_press -= 1 + 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 key_ctrl_down and key_v_press: - self.paste() + if inp.key_tab_press: + tauon.search_over.spotify_mode ^= True + self.sip = True + tauon.search_over.searched_text = tauon.search_over.search_text.text + if self.worker2_lock.locked(): + try: + self.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 secret: - space = ddt.text((x, y), "●" * len(self.text), colour, font) - else: - space = ddt.text((x, y), self.text, colour, font) + if input_text or key_backspace_press: + self.input_timer.set() - 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) + gui.update += 1 + elif self.input_timer.get() >= 0.20 and \ + (len(tauon.search_over.search_text.text) > 1 or (len(tauon.search_over.search_text.text) == 1 and ord(tauon.search_over.search_text.text) > 128)) \ + and tauon.search_over.search_text.text != tauon.search_over.searched_text: + self.sip = True + if self.worker2_lock.locked(): + try: + self.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 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 self.input_timer.get() < 10: + gui.frame_callback_list.append(TestTimer(0.1)) - 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) + yy = 110 * gui.scale - animate_monitor_timer.set() + 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(inp.mouse_position) -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() + if key_up_press: -rename_files.text = prefs.rename_tracks_template -if rename_files_previous: - rename_files.text = rename_files_previous + if self.force_select > -1: + self.force_select -= 1 + self.force_select = max(self.force_select, 0) -text_plex_usr = TextBox2() -text_plex_pas = TextBox2() -text_plex_ser = TextBox2() + if self.force_select < self.on + 4: + self.on = self.force_select - 4 + self.on = max(self.on, 0) -text_jelly_usr = TextBox2() -text_jelly_pas = TextBox2() -text_jelly_ser = TextBox2() + self.old_mouse = copy.deepcopy(inp.mouse_position) -text_koel_usr = TextBox2() -text_koel_pas = TextBox2() -text_koel_ser = TextBox2() + if inp.mouse_wheel == -1: + self.on += 1 + self.force_select += 1 + if inp.mouse_wheel == 1 and self.on > -1: + self.on -= 1 + self.force_select -= 1 -text_air_usr = TextBox2() -text_air_pas = TextBox2() -text_air_ser = TextBox2() + enter = False -text_spot_client = TextBox2() -text_spot_secret = TextBox2() -text_spot_username = TextBox2() -text_spot_password = TextBox2() + if self.delay_enter and not self.sip and self.search_text.text == self.searched_text: + enter = True + self.delay_enter = False -text_maloja_url = TextBox2() -text_maloja_key = TextBox2() + 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 -text_sat_url = TextBox2() -text_sat_playlist = TextBox2() + inp.key_return_press = False -rename_folder = TextBox2() -rename_folder.text = prefs.rename_folder_template -if rename_folder_previous: - rename_folder.text = rename_folder_previous + bar_colour = [140, 80, 240, 255] + track_in_bar_colour = [244, 209, 66, 255] -temp_dest = SDL_Rect(0, 0) + self.on = max(self.on, 0) + self.on = min(len(self.results) - 1, self.on) -def img_slide_update_gall(value, pause: bool = True) -> None: - global album_mode_art_size - gui.halt_image_rendering = True + full_count = 0 - album_mode_art_size = value + sec = False - clear_img_cache(False) - if pause: - gallery_load_delay.set() - gui.frame_callback_list.append(TestTimer(0.6)) - gui.halt_image_rendering = False + p = -1 - # Update sizes - tauon.gall_ren.size = album_mode_art_size + if self.on > 4: + p += self.on - 4 + p = self.on - 1 + clear = False - if album_mode_art_size > 150: - prefs.thin_gallery_borders = False + for i, item in enumerate(self.results): + p += 1 -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 = [] + if p > len(self.results) - 1: + break - i = 0 - while len(tauon.gall_ren.queue) > 0: - time.sleep(0.01) - i += 1 - if i > 5 / 0.01: - break + item: list[int] = self.results[p] - for key, value in tauon.gall_ren.gall.items(): - SDL_DestroyTexture(value[2]) - tauon.gall_ren.gall = {} + fade = 1 + selected = self.on + if self.force_select > -1: + selected = self.force_select - 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) + #logging.info(selected) - 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 + if selected != p: + fade = 0.8 + start = yy -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) + n = item[0] - 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)) + 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) - 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) + pad = round(4 * gui.scale) + height = round(25 * gui.scale) + if n in (1, 11): + height = round(50 * gui.scale) + album_art_size = height - gui.halt_image_rendering = False - album_art_gen.clear_cache() + # Selection bar + s_rect = (highlight_x, yy, 600 * gui.scale, height + pad + pad - 1) + tauon.fields.add(s_rect) + if fade == 1: + ddt.rect((highlight_x, yy + pad, 4 * gui.scale, height), bar_colour) + if n in (2,): + if inp.key_ctrl_down and item[2] in pctl.default_playlist: + ddt.rect((highlight_x + round(5 * gui.scale), yy + pad, 4 * gui.scale, height), track_in_bar_colour) -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 = "" + # 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") -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 + # 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) - self.blur_texture = None - self.blur_rect = None - self.loaded_bg_type = 0 + if n in (1,): # Two line album + track = pctl.master_library[item[2]] + artist = track.album_artist + if not artist: + artist = track.artist - self.download_in_progress = False - self.downloaded_image = None - self.downloaded_track = None + xx = ddt.text((text_lx, yy + pad + round(5 * gui.scale)), item[1], [255, 255, 255, int(255 * fade)], s_b_font) - self.base64cache = (0, 0, "") - self.processing64on = None + 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) - self.bin_cached = (None, None, None) # track, subsource, bin - self.embed_cached = (None, None) + yy += height + pad + pad - def async_download_image(self, track: TrackClass, subsource: list[tuple[int, str]]) -> None: + show = False + go = False + extend = False + if tauon.coll(s_rect) and mouse_change: + if self.force_select != p: + self.force_select = p + gui.update = 2 - 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 + if gui.level_2_click: + if inp.key_ctrl_down: + extend = True + else: + go = True + clear = True - 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 + if level_2_right_click: + show = True + clear = True - offset = self.get_offset(track_object.fullpath, sources) + if enter and inp.key_shift_down and fade == 1: + show = True + clear = True - o_size = (0, 0) - format = "ERROR" + elif enter and fade == 1: + if inp.key_shift_down or inp.key_shiftr_down: + show = True + clear = True + else: + go = True + clear = True - 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 + if extend: + match n: + case 0: + pctl.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: + pctl.default_playlist.extend( + get_album_from_first_track(pl.playlist_ids.index(item[2]), item[2], k)) + break + case 2: + pctl.default_playlist.append(item[2]) + case 3: + pctl.default_playlist.extend(self.click_genre(item[1], get_list=True)) + case 5: + pctl.default_playlist.extend(self.click_meta(item[1], get_list=True)) + case 6: + pctl.default_playlist.extend(self.click_composer(item[1], get_list=True)) + case 7: + pctl.default_playlist.extend(self.click_year(item[1], get_list=True)) + case 8: + pctl.default_playlist.extend(pctl.multi_playlist[pl].playlist_ids) + case 12: + tauon.spot_ctl.append_track(item[2]) + reload_albums() - return [sources[offset][0], len(sources), offset, o_size, format] + 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 prefs.album_mode: + show_in_gal(0) + case 8: + pl = id_to_pl(item[3]) + if pl: + switch_playlist(pl) - def get_sources(self, tr: TrackClass) -> list[tuple[int, str]]: + 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() - filepath = tr.fullpath - ext = tr.file_ext + 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) - # 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, + # --- + if i > 40: + break + if yy > window_size[1] - (100 * gui.scale): + break - # Source type the is first element in list - # 0 = File - # 1 = Embedded in tag - # 2 = Network location + continue - 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 clear: + self.active = False + self.search_text.text = "" + self.results.clear() + self.searched_text = "" - # 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") +class MessageBox: - if not tr.is_network: + def __init__(self): + pass - 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 get_rect(self): - 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]) + 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) - for i in range(len(dirs_in_dir)): - subdirec = os.path.join(direc, dirs_in_dir[i]) - items_in_dir2 = os.listdir(subdirec) + w = max(w, 210 * gui.scale) - 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]) + h = round(60 * gui.scale) + if gui.message_subtext2: + h += round(15 * gui.scale) - self.source_cache[tr.index] = source_list + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) - return source_list + return x, y, w, h - 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 render(self): - def fast_display(self, index, location, box, source: list[tuple[int, str]], offset) -> int: - """Renders cached image only by given size for faster performance""" + 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 (inp.k_input and message_box_min_timer.get() > 1.2): - found_unit = None - max_h = 0 + if not key_focused and message_box_min_timer.get() > 0.4: + gui.message_box = False + gui.update += 1 + inp.key_return_press = False - 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 + x, y, w, h = self.get_rect() - if found_unit == None: - return 1 + 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) - unit = found_unit + ddt.text_background_colour = colours.message_box_bg - temp_dest.x = round(location[0]) - temp_dest.y = round(location[1]) + 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 - temp_dest.w = unit.original_size[0] # round(box[0]) - temp_dest.h = unit.original_size[1] # round(box[1]) + 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) - bh = round(box[1]) - bw = round(box[0]) + if gui.message_subtext2: + ddt.text((x + 63 * gui.scale, y + (9 + 42) * gui.scale), gui.message_subtext2, colours.message_box_text, + 12) - if prefs.zoom_art: - temp_dest.w, temp_dest.h = fit_box((unit.original_size[0], unit.original_size[1]), box) else: + ddt.text((x + 62 * gui.scale, y + 20 * gui.scale), gui.message_text, colours.message_box_text, 15) - # 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])) +class NagBox: + def __init__(self): + self.wiggle_timer = Timer(10) - if temp_dest.h > bh: - temp_dest.h = bh - temp_dest.w = int(temp_dest.h * (unit.original_size[0] / unit.original_size[1])) + 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) - # 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] + # 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 - # 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 + 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) - # render the image - SDL_RenderCopy(renderer, unit.texture, None, temp_dest) - style_overlay.hole_punches.append(temp_dest) + # if gui.level_2_click and not tauon.coll((x, y, w, h)): + # if core_timer.get() < 2: + # self.wiggle_timer.set() + # else: + # prefs.show_nag = False + # + # gui.update += 1 - gui.art_drawn_rect = (temp_dest.x, temp_dest.y, temp_dest.w, temp_dest.h) + ddt.text_background_colour = colours.message_box_bg - return 0 + 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) - def open_external(self, track_object: TrackClass) -> int: + 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) - index = track_object.index + heart_notify_icon.render(x + round(425 * gui.scale), y + round(80 * gui.scale), [255, 90, 90, 255]) - source = self.get_sources(track_object) - if len(source) == 0: - return 0 + y += round(30 * gui.scale) + ddt.text((x, y), _("New supporter bonuses!"), colours.message_box_text, 212) - offset = self.get_offset(track_object.fullpath, source) + y += round(20 * gui.scale) - 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 + 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) - source_image = io.BytesIO(pic) - im = Image.open(source_image) - source_image.close() + y += round(20 * gui.scale) + ddt.text((x, y), _("Your support means a lot! Love you!"), colours.message_box_text, 12) - 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) + y += round(30 * gui.scale) - if len(pic) > 30: - with open(target, "wb") as w: - w.write(pic) + 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 - else: - target = source[offset][1] +class PowerTag: - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", target]) - else: - subprocess.call(["xdg-open", target]) + def __init__(self): + self.name = "BLANK" + self.path = "" + self.position = 0 + self.colour = None - return 0 + self.peak_x = 0 + self.ani_timer = Timer() + self.ani_timer.force_set(10) - def cycle_offset(self, track_object: TrackClass, reverse: bool = False) -> int: +class Over: + def __init__(self, bag: Bag, gui: GuiVar): - 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: + global window_size + self.dirs = bag.dirs + self.prefs = bag.prefs + self.gui = gui + self.init2done = False - if reverse: - folder_image_offsets[parent_folder] -= 1 - else: - folder_image_offsets[parent_folder] += 1 + self.about_image = asset_loader(bag, bag.loaded_asset_dc, "v4-a.png") + self.about_image2 = asset_loader(bag, bag.loaded_asset_dc, "v4-b.png") + self.about_image3 = asset_loader(bag, bag.loaded_asset_dc, "v4-c.png") + self.about_image4 = asset_loader(bag, bag.loaded_asset_dc, "v4-d.png") + self.about_image5 = asset_loader(bag, bag.loaded_asset_dc, "v4-e.png") + self.about_image6 = asset_loader(bag, bag.loaded_asset_dc, "v4-f.png") + self.title_image = asset_loader(bag, bag.loaded_asset_dc, "title.png", True) - folder_image_offsets[parent_folder] %= len(sources) - return 0 + # self.tab_width = round(115 * gui.scale) + self.w = 100 + self.h = 100 - def cycle_offset_reverse(self, track_object: TrackClass) -> None: - self.cycle_offset(track_object, True) + self.box_x = 100 + self.box_y = 100 + self.item_x_offset = round(25 * self.gui.scale) - def get_offset(self, filepath: str, source: list[tuple[int, str]]) -> int: + self.current_path = os.path.expanduser("~") + self.view_offset = 0 + self.ext_ratio = {} + self.last_db_size = -1 - # Check if folder offset already exsts, if not, make it - parent_folder = os.path.dirname(filepath) + self.enabled = False + self.click = False + self.right_click = False + self.scroll = 0 + self.lock = False - if parent_folder in folder_image_offsets: + self.drives = [] - # 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 + self.temp_lastfm_user = "" + self.temp_lastfm_pass = "" + self.lastfm_input_box = 0 - return folder_image_offsets[parent_folder] + 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], + ] - def get_embed(self, track: TrackClass): + 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 - # cached = self.embed_cached - # if cached[0] == track: - # #logging.info("used cached") - # return cached[1] + self.ani_cred = 0 + self.cred_page = 0 + self.ani_fade_on_timer = Timer(force=10) + self.ani_fade_off_timer = Timer(force=10) - filepath = track.fullpath + self.device_scroll_bar_position = 0 - # Use cached file if present - if prefs.precache and tauon.cachement: - path = tauon.cachement.get_file_cached_only(track) - if path: - filepath = path + 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 - pic = None + self.account_text_field = -1 - 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}") + self.themes = [] + self.view_supporters = False + self.key_box = TextBox2() + self.key_box_focused = False - if pic is not None and len(pic) < 30: - pic = None + def theme(self, x0, y0, w0, h0): + global update_layout - 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 + y = y0 + 13 * gui.scale + x = x0 + 25 * gui.scale - elif track.file_ext == "APE": - with Ape(filepath) as tag: - tag.read() - if tag.has_picture and len(tag.picture) > 30: - pic = tag.picture + ddt.text_background_colour = colours.box_background + ddt.text((x, y), _("Theme"), colours.box_text_label, 12) - 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 + y += 25 * gui.scale - 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.toggle_square(x, y, toggle_auto_bg, _("Use album art as background")) - # self.embed_cached = (track, pic) - return pic + y += 23 * gui.scale - def get_source_raw(self, offset: int, sources: list[tuple[int, str]] | int, track: TrackClass, subsource: list[tuple[int, str]] | None = None): + old = prefs.enable_fanart_bg + prefs.enable_fanart_bg = self.toggle_square( + x + 10 * self.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 - source_image = None + 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")) - if subsource is None: - subsource = sources[offset] + #y += 23 * gui.scale + self.toggle_square(x + 120 * gui.scale, y, toggle_auto_bg_blur, _("Blur")) - if subsource[0] == 1: - # Target is a embedded image\\\ - pic = self.get_embed(track) - assert pic - source_image = io.BytesIO(pic) + y += 23 * gui.scale + self.toggle_square(x + 10 * gui.scale, y, toggle_auto_bg_showcase, _("Showcase only")) - 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 + 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")) - 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) + y += 25 * gui.scale - except Exception: - logging.exception("Failed to get source") + self.toggle_square(x, y, toggle_auto_theme, _("Auto-theme from album art")) - else: - source_image = open(subsource[1], "rb") + y += 55 * gui.scale - return source_image + square = round(8 * gui.scale) + border = round(4 * gui.scale) + outer_border = round(2 * gui.scale) - def get_base64(self, track: TrackClass, size): + # theme_files = get_themes() + xx = x + yy = y + hover_name = None + for c, theme_name, theme_number in self.themes: - # 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 + 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) - cahced = self.base64cache - if track == cahced[0] and size == cahced[1]: - return cahced[2] + rect = [xx, yy, border * 2 + square * 2, border * 2 + square * 2] + ddt.rect(rect, [5, 5, 5, 255]) - self.processing64on = track + rect = grow_rect(rect, 3) + tauon.fields.add(rect) + if tauon.coll(rect): + hover_name = theme_name + if self.click: + prefs.theme = theme_number + gui.reload_theme = True - filepath = track.fullpath - sources = self.get_sources(track) + c1 = c.playlist_panel_background + c2 = c.artist_playing + c3 = c.title_playing + c4 = c.bottom_panel_colour - if len(sources) == 0: - self.processing64on = None - return False + if theme_name == "Carbon": + c1 = c.title_playing + c2 = c.playlist_panel_background + c3 = c.top_panel_background - offset = self.get_offset(filepath, sources) + if theme_name == "Lavender Light": + c1 = c.tab_background_active - # Get source IO - source_image = self.get_source_raw(offset, sources, track) + if theme_name == "Neon Love": + c2 = c.artist_text + c4 = [118, 85, 194, 255] + c1 = c4 - if source_image is None: - self.processing64on = None - return "" + if theme_name == "Sky": + c2 = c.artist_text - 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()) + if theme_name == "Sunken": + c2 = c.title_text + c3 = c.artist_text + c4 = [59, 115, 109, 255] + c1 = c4 - self.base64cache = (track, size, sss) - self.processing64on = None - return sss + 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: - 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 + # tl + rect = [xx + border, yy + border, square, square] + ddt.rect(rect, c1) - # 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") + # tr + rect = [xx + border + square, yy + border, square, square] + ddt.rect(rect, c2) - # 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") + # bl + rect = [xx + border, yy + border + square, square, square] + ddt.rect(rect, c3) - # Check we've not already attempted a search for this artist - if artist in prefs.failed_background_artists: - return None + # br + rect = [xx + border + square, yy + border + square, square, square] + ddt.rect(rect, c4) - # 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 + yy += round(27 * gui.scale) + if yy > y + 40 * gui.scale: + yy = y + xx += round(27 * gui.scale) - # Search fanart.tv for background - try: + 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 - r = requests.get( - "https://webservice.fanart.tv/v3/music/" \ - + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) + 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) - artlink = r.json()["artistbackground"][0]["url"] + def rg(self, x0, y0, w0, h0): + y = y0 + 55 * gui.scale + x = x0 + 130 * gui.scale - response = urllib.request.urlopen(artlink, context=ssl_context) - info = response.info() + if self.button(x - 110 * gui.scale, y + 180 * gui.scale, _("Return"), width=75 * gui.scale): + self.rg_view = False - assert info.get_content_maintype() == "image" + y = y0 + round(15 * gui.scale) + x = x0 + round(50 * gui.scale) - t = io.BytesIO() - t.seek(0) - t.write(response.read()) - t.seek(0, 2) - l = t.tell() - t.seek(0) + ddt.text((x, y), _("ReplayGain"), colours.box_text_label, 14) + y += round(25 * gui.scale) - assert l > 1000 + 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")) - # 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 + 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) - 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") + 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) - prefs.failed_background_artists.append(artist) - return None + slider = (x, y, sw, sh) - def get_blur_im(self, track: TrackClass) -> BytesIO | bool | None: + gh = round(14 * gui.scale) + gw = round(8 * gui.scale) + grip = [0, y - (gh // 2), gw, gh] - 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 + grip[0] = x - if source_image is None: - filepath = track.fullpath - sources = self.get_sources(track) + bp = prefs.replay_preamp + 15 - if len(sources) == 0: - return False + grip[0] += (bp / 30 * sw) - offset = self.get_offset(filepath, sources) + m1 = (x, y, sh, sh * 2) + m2 = ((x + sw // 2), y, sh, sh * 2) + m3 = ((x + sw), y, sh, sh * 2) - source_image = self.get_source_raw(offset, sources, track) + if tauon.coll(grow_rect(slider, 15)) and inp.mouse_down: + bp = (inp.mouse_position[0] - x) / sw * 30 + gui.update += 1 - if source_image is None: - return None + bp = round(bp) + bp = max(bp, 0) + bp = min(bp, 30) + prefs.replay_preamp = bp - 15 - im = Image.open(source_image) + # grip[0] += (bp / 30 * sw) - ox_size = im.size[0] - oy_size = im.size[1] + 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) - format = im.format - if im.format == "JPEG": - format = "JPG" + text = f"{prefs.replay_preamp} dB" + if prefs.replay_preamp > 0: + text = "+" + text - #logging.info(im.size) - if im.mode != "RGB": - im = im.convert("RGB") + 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) - ratio = window_size[0] / ox_size - ratio += 0.2 + 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) - 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 + def eq(self, x0, y0, w0, h0): - new_x = round(ox_size * ratio) - new_y = round(oy_size * ratio) + y = y0 + 55 * gui.scale + x = x0 + 130 * gui.scale - im = im.resize((new_x, new_y)) + if self.button(x - 110 * gui.scale, y + 180 * gui.scale, _("Return"), width=75 * gui.scale): + self.eq_view = False - 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) + base_dis = 160 * gui.scale + center = base_dis // 2 + width = 25 * gui.scale - 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) + range = 12 - gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 4 * 3)) + self.toggle_square(x - 90 * gui.scale, y - 35 * gui.scale, toggle_eq, _("Enable")) - im = im.filter(ImageFilter.GaussianBlur(blur)) + 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): - gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 2)) + bar = [x, y, width, base_dis] - g = io.BytesIO() - g.seek(0) + ddt.rect(bar, [255, 255, 255, 20]) - a_channel = Image.new("L", im.size, 255) # 'L' 8-bit pixels, black and white - im.putalpha(a_channel) + bar[0] -= 2 * gui.scale + bar[1] -= 10 * gui.scale + bar[2] += 4 * gui.scale + bar[3] += 20 * gui.scale - im.save(g, "PNG") - g.seek(0) + if tauon.coll(bar): + if inp.mouse_down: + target = inp.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 - # source_image.close() + prefs.eq[i] = target - return g + pctl.playerCommand = "seteq" + pctl.playerCommandReady = True - def save_thumb(self, track_object: TrackClass, size: tuple[int, int], save_path: str, png=False, zoom=False): + if self.right_click: + prefs.eq[i] = 0 + pctl.playerCommand = "seteq" + pctl.playerCommandReady = True - filepath = track_object.fullpath - sources = self.get_sources(track_object) + start = (q / range) * center - if len(sources) == 0: - logging.error("Error thumbnailing; no source images found") - return False + bar = [x, y + center, width, start] - offset = self.get_offset(filepath, sources) - source_image = self.get_source_raw(offset, sources, track_object) + ddt.rect(bar, [100, 200, 100, 255]) - im = Image.open(source_image) - if im.mode != "RGB": - im = im.convert("RGB") + x += round(29 * gui.scale) - 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, - )) + def audio(self, x0, y0, w0, h0): + ddt.text_background_colour = colours.box_background + y = y0 + 40 * gui.scale + x = x0 + 20 * gui.scale - im = im.resize(size, Image.Resampling.LANCZOS) + if self.eq_view: + self.eq(x0, y0, w0, h0) + return - 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 self.rg_view: + self.rg(x0, y0, w0, h0) + return - if png: - im.save(save_path + ".png", "PNG") - else: - im.save(save_path + ".jpg", "JPEG") + colour = colours.box_sub_text - def display(self, track: TrackClass, location, box, fast: bool = False, theme_only: bool = False) -> int | None: - index = track.index - filepath = track.fullpath + # 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) - 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 + elif prefs.backend == 4: - source = self.get_sources(track) + y = y0 + round(20 * gui.scale) + x = x0 + 20 * gui.scale - if len(source) == 0: - return 1 + x += round(2 * gui.scale) - offset = self.get_offset(filepath, source) + 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")) - 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 + y += round(40 * gui.scale) + if self.button(x, y, _("ReplayGain")): + inp.mouse_down = False + self.rg_view = True - if fast: - return self.fast_display(track, location, box, source, offset) + 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__() - # 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 + 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) - 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]) + 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) - 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 + # if prefs.device_buffer > 100: + # prefs.pa_fast_seek = True + # else: + # prefs.pa_fast_seek = False - elif self.download_in_progress: - return 0 + 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) - 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 + 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") - except Exception: - logging.exception("IMAGE NETWORK LOAD ERROR") - raise + 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 - else: - # source_image = open(source[offset][1], 'rb') - source_image = self.get_source_raw(0, 0, track, source[offset]) + 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) - # Generate - g = io.BytesIO() - g.seek(0) - im = Image.open(source_image) - o_size = im.size + i = 0 + reload = False + for name in prefs.phazor_devices: - format = im.format + if i < self.device_scroll_bar_position: + continue + if y > self.box_y + self.h - 40 * gui.scale: + break - try: - if im.format == "JPEG": - format = "JPG" + rect = (x, y + 4 * gui.scale, 245 * gui.scale, 13) - 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 self.click and tauon.coll(rect): + prefs.phazor_device_selected = name + reload = True + line = trunc_line(name, 10, 245 * gui.scale) - if not theme_only: + tauon.fields.add(rect) - 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) + 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 tauon.coll(rect): + ddt.text((x, y), line, colours.box_sub_text, 10) 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]) + ddt.text((x, y), line, colours.box_text_label, 10) + y += 14 * gui.scale + i += 1 - l = .51 - s = .44 + if reload: + pctl.playerCommand = "set-device" + pctl.playerCommandReady = 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 + def reload_device(self, _) -> None: + pctl.playerCommand = "reload" + pctl.playerCommandReady = True - colours.bottom_panel_colour = hls_to_rgb(hh, l, s) - colours.last_album = track.parent_folder_path + def toggle_lyrics_view(self) -> None: + self.lyrics_panel ^= True - # 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 + def lyrics(self, x0, y0, w0, h0): - colours = copy.deepcopy(colours) + x = x0 + 25 * gui.scale + y = y0 - 10 * gui.scale + y += 30 * gui.scale - 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) + ddt.text_background_colour = colours.box_background - min_colour_varience = 75 + # 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 - 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 + #self.toggle_square(x, y, toggle_guitar_chords, _("Enable chord lyrics")) - 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,) + y += 40 * gui.scale + ddt.text((x, y), _("Sources:"), colours.box_text_label, 11) + y += 23 * gui.scale - colours.queue_background = colours.side_panel_background - # Check artist text colour - if contrast_ratio(colours.artist_text, colours.playlist_panel_background) < 1.9: + 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) - black = [25, 25, 25, 255] - white = [220, 220, 220, 255] + 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 - con_b = contrast_ratio(black, colours.playlist_panel_background) - con_w = contrast_ratio(white, colours.playlist_panel_background) + def view2(self, x0, y0, w0, h0): + x = x0 + 25 * gui.scale + y = y0 + 20 * gui.scale - choice = black - if con_w > con_b: - choice = white + ddt.text_background_colour = colours.box_background - colours.artist_text = choice - colours.artist_playing = choice + ddt.text((x, y), _("Metadata side panel"), colours.box_text_label, 12) - # Check title text colour - if contrast_ratio(colours.title_text, colours.playlist_panel_background) < 1.9: + 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() - black = [60, 60, 60, 255] - white = [180, 180, 180, 255] + global update_layout + y += 35 * gui.scale + ddt.text((x, y), _("Gallery"), colours.box_text_label, 12) - con_b = contrast_ratio(black, colours.playlist_panel_background) - con_w = contrast_ratio(white, colours.playlist_panel_background) + 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")) - choice = black - if con_w > con_b: - choice = white + y += 30 * gui.scale - colours.title_text = choice - colours.title_playing = choice + # y += 25 * gui.scale - 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] + x -= 80 * gui.scale + x += ddt.get_text_w(_("Thumbnail size"), 312) + # x += 20 * gui.scale - colours.album_text = colours.title_text - colours.album_playing = colours.title_playing + if bag.album_mode_art_size < 160: + self.toggle_square(x + 235 * gui.scale, y + 2 * gui.scale, toggle_gallery_thin, _("Prefer thinner padding")) - gui.pl_update = 1 + # ddt.text((x, y), _("Gallery art size"), colours.grey(220), 11) - prcl = 100 - int(test_lumi(colours.playlist_panel_background) * 100) + bag.album_mode_art_size = self.slide_control( + x + 25 * gui.scale, y, _("Thumbnail size"), "px", bag.album_mode_art_size, 70, 400, 10, img_slide_update_gall) - 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) + def funcs(self, x0, y0, w0, h0): - gui.temp_themes[track.album] = copy.deepcopy(colours) - colours = gui.temp_themes[track.album] - gui.theme_temp_current = track.album + x = x0 + 25 * gui.scale + y = y0 - 10 * gui.scale - if theme_only: - source_image.close() - g.close() - return None + ddt.text_background_colour = colours.box_background - wop = rw_from_object(g) - s_image = IMG_Load_RW(wop, 0) - #logging.error(IMG_GetError()) + if self.func_page == 0: - c = SDL_CreateTextureFromSurface(renderer, s_image) + y += 23 * gui.scale - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) + self.toggle_square( + x, y, toggle_enable_web, _("Enable Listen Along"), subtitle=_("Start server for remote web playback")) - SDL_QueryTexture(c, None, None, tex_w, tex_h) + if toggle_enable_web(1): - dst = SDL_Rect(round(location[0]), round(location[1])) - dst.w = int(tex_w.contents.value) - dst.h = int(tex_h.contents.value) + 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] + tauon.fields.add(link_rect2) - # Clean uo - SDL_FreeSurface(s_image) - source_image.close() - g.close() - # if close: - # source_image.close() + if tauon.coll(link_rect2): + if not self.click: + gui.cursor_want = 3 - 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 + if self.click: + webbrowser.open(link_pa2[2], new=2, autoraise=True) - self.current_wu = unit - self.image_cache.append(unit) + y += 38 * gui.scale - self.render(unit, location) + 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) - 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] + y += 38 * gui.scale - # temp fix - global move_on_title - global playlist_hold - global quick_drag - quick_drag = False - move_on_title = False - playlist_hold = False + self.toggle_square( + x, y, toggle_auto_artist_dl, + _("Auto fetch artist data"), + subtitle=_("Downloads data in background when artist panel is open")) - except Exception: - logging.exception("Image load error") - logging.error("-- Associated track: " + track.fullpath) + 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")) - self.current_wu = None - try: - del self.source_cache[index][offset] - except Exception: - logging.exception(" -- Error, no source cache?") + y += 38 * gui.scale + self.toggle_square( + x, y, toggle_top_tabs, _("Tabs in top panel"), + subtitle=_("Uncheck to disable the tab pin function")) - return 1 + y += 45 * gui.scale + # y += 30 * gui.scale - return 0 + 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 - def render(self, unit, location) -> None: + ww = max(wa, wc) - rect = unit.rect + 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) - gui.art_aspect_ratio = unit.actual_size[0] / unit.actual_size[1] + self.button(x + wa + round(20 * gui.scale), y, _("Open data folder"), open_data_directory, ww) - 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]) + 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 - style_overlay.hole_punches.append(rect) + 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")) - SDL_RenderCopy(renderer, unit.texture, None, rect) + y += 28 * gui.scale - gui.art_drawn_rect = (rect.x, rect.y, rect.w, rect.h) + x = x0 + self.item_x_offset - def clear_cache(self) -> None: + ddt.text((x, y), _("End of playlist action"), colours.box_text_label, 12) - for unit in self.image_cache: - SDL_DestroyTexture(unit.texture) + 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.image_cache.clear() - self.source_cache.clear() - self.current_wu = None - self.downloaded_track = None + 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")) - self.base64cahce = (0, 0, "") - self.processing64on = None - self.bin_cached = (None, None, None) - self.loading_bin = (None, None) - self.embed_cached = (None, None) + 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")) - gui.temp_themes.clear() - gui.theme_temp_current = -1 - colours.last_album = "" + 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")) -album_art_gen = AlbumArt() + 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")) -# 0 - blank -# 1 - preparing first -# 2 - render first -# 3 - preparing 2nd + else: + self.toggle_square(x, y, toggle_min_tray, _("Close to tray")) -class StyleOverlay: + 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 - def __init__(self): + 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 - self.min_on_timer = Timer() - self.fade_on_timer = Timer(0) - self.fade_off_timer = Timer() + 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") - self.stage = 0 + 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")) - self.im = None - - self.a_texture = None - self.a_rect = None - - self.b_texture = None - self.b_rect = None + 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") - self.a_type = 0 - self.b_type = 0 + 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() - self.window_size = None - self.parent_path = None + 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")) - self.hole_punches = [] - self.hole_refills = [] + 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") - self.go_to_sleep = False + 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() - self.current_track_album = "none" - self.current_track_id = -1 + if old and not prefs.discord_enable: + if prefs.discord_active: + prefs.disconnect_discord = True - def worker(self) -> None: + 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) - if self.stage == 0: + # Switcher + pages = 5 + x = x0 + round(18 * gui.scale) + y = (y0 + h0) - round(29 * gui.scale) + ww = round(40 * gui.scale) - if (gui.mode == 3 and prefs.mini_mode_mode == 5): - pass - elif prefs.bg_showcase_only and not gui.combo_mode: - return + 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 - if pctl.playing_ready() and self.min_on_timer.get() > 0: + # self.button(x, y, _("Open keymap file"), open_keymap_file, width=wc) - track = pctl.playing_object() + 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) - 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 + h = round(20 * gui.scale) + border_size = round(2 * gui.scale) - try: - self.im = album_art_gen.get_blur_im(track) - except Exception: - logging.exception("Blur blackground error") - raise - #logging.debug(track.fullpath) + 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 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 + if bg is None: + bg = colours.box_background - self.stage = 1 - gui.update += 1 - return + real_bg = bg + hit = False - def flush(self): + ddt.rect(rect2, colours.box_check_border) + ddt.rect(rect, bg) - 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 + tauon.fields.add(rect) + if tauon.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) - def display(self) -> None: + return hit - if self.min_on_timer.get() < 0: - return + 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) - if self.stage == 1: + bg_colour = colours.box_button_background + real_bg = bg_colour - wop = rw_from_object(self.im) - s_image = IMG_Load_RW(wop, 0) + ddt.rect(rect, bg_colour) + tauon.fields.add(rect) + hit = False - c = SDL_CreateTextureFromSurface(renderer, s_image) + 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) - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) + if tauon.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 - SDL_QueryTexture(c, None, None, tex_w, tex_h) + def toggle_square(self, x, y, function, text: str , click: bool = False, subtitle: str = "") -> bool: + x = round(x) + y = round(y) - dst = SDL_Rect(round(-40, 0)) - dst.w = int(tex_w.contents.value) - dst.h = int(tex_h.contents.value) + border = round(2 * gui.scale) + gap = round(2 * gui.scale) + inner_square = round(6 * gui.scale) - # Clean uo - SDL_FreeSurface(s_image) - self.im.close() + full_w = border * 2 + gap * 2 + inner_square - # SDL_SetTextureAlphaMod(c, 10) - self.fade_on_timer.set() + 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) - 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 + 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) - self.a_texture = c - self.a_rect = dst - self.a_type = album_art_gen.loaded_bg_type + # 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)) - self.stage = 2 - self.radio_meta = None + # Check if box clicked + clicked = False + if (self.click or click) and tauon.coll(hit_rect): + clicked = True - gui.update += 1 + # There are two mode, function type, and passthrough bool type + active = False + if type(function) is bool: + active = function + else: + active = function(1) - if self.stage == 2: - track = pctl.playing_object() + if clicked: + if type(function) is bool: + active ^= True + else: + function() + active = function(1) - 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 + # 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) - 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 + return active - if gui.mode == 3 and prefs.mini_mode_mode == 5: - pass - elif prefs.bg_showcase_only: - if not gui.combo_mode: - return + def last_fm_box(self, x0, y0, w0, h0): + x = x0 + round(20 * gui.scale) + y = y0 + round(15 * gui.scale) - t = self.fade_on_timer.get() - SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) - SDL_RenderClear(renderer) + ddt.text_background_colour = colours.box_background - if self.a_texture is not None: - if self.window_size != window_size: - self.flush() + 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")) - if self.b_texture is not None: + y += 28 * gui.scale - self.b_rect.y = 0 - self.b_rect.h // 4 - if self.b_type == 1: - self.b_rect.y = 0 + 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")) - if t < 0.4: + y += 28 * gui.scale - SDL_RenderCopy(renderer, self.b_texture, None, self.b_rect) + 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")) - else: - SDL_DestroyTexture(self.b_texture) - self.b_texture = None - self.b_rect = None + # if self.button2(x, y, "Discogs", width=84*gui.scale): + # self.account_view = 3 - if self.a_texture is not None: + y += 28 * gui.scale - self.a_rect.y = 0 - self.a_rect.h // 4 - if self.a_type == 1: - self.a_rect.y = 0 + if self.button2(x, y, "fanart.tv", width=84 * gui.scale): + self.account_view = 4 - if t < 0.4: - fade = round(t / 0.4 * 255) - gui.update += 1 + y += 28 * gui.scale + y += 28 * gui.scale - else: - fade = 255 + y += 15 * gui.scale - if self.go_to_sleep: - t = self.fade_off_timer.get() - gui.update += 1 + if inp.key_shift_down and self.button2(x + round(95 * gui.scale), y, "koel", width=84 * gui.scale): + self.account_view = 6 - 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 self.button2(x, y, "Jellyfin", width=84 * gui.scale): + self.account_view = 10 - 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) + if self.button2(x + round(95 * gui.scale), y, "TIDAL", width=84 * gui.scale): + self.account_view = 12 - # 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 + y += 28 * gui.scale - SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) + if self.button2(x, y, "Airsonic", width=84 * gui.scale): + self.account_view = 7 - SDL_SetTextureAlphaMod(self.a_texture, fade) - SDL_RenderCopy(renderer, self.a_texture, None, self.a_rect) + if self.button2(x + round(95 * gui.scale), y, "PLEX", width=84 * gui.scale): + self.account_view = 5 - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) + y += 28 * gui.scale - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - for rect in self.hole_punches: - SDL_RenderFillRect(renderer, rect) + if self.button2(x, y, "Spotify", width=84 * gui.scale): + self.account_view = 8 - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + if self.button2(x + round(95 * gui.scale), y, "Satellite", width=84 * gui.scale): + self.account_view = 11 - SDL_SetRenderTarget(renderer, gui.main_texture) - opacity = prefs.art_bg_opacity - if prefs.mini_mode_mode == 5 and gui.mode == 3: - opacity = 255 + if self.account_view in (9, 2): + self.toggle_square( + x0 + 230 * gui.scale, y + 2 * gui.scale, toggle_scrobble_mark, + _("Show threshold marker")) - SDL_SetTextureAlphaMod(gui.main_texture_overlay_temp, opacity) - SDL_RenderCopy(renderer, gui.main_texture_overlay_temp, None, None) + x = x0 + 230 * gui.scale + y = y0 + round(20 * gui.scale) - SDL_SetRenderTarget(renderer, gui.main_texture) + if self.account_view == 12: + ddt.text((x, y), "TIDAL", colours.box_sub_text, 213) - else: - SDL_SetRenderTarget(renderer, gui.main_texture) + 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) -style_overlay = StyleOverlay() + 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) -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 + if self.account_view == 11: + ddt.text((x, y), "Tauon Satellite", colours.box_sub_text, 213) - 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 + y += round(30 * gui.scale) - while ddt.get_text_w(line, font) > px: + 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)) + tauon.fields.add(rect1) + if tauon.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() - line = line[:-1] - if len(line) < 2: - break + y += round(25 * gui.scale) - return line + 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)) + tauon.fields.add(rect1) + if tauon.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) -def right_trunc(line: str, font: str, px: int, dots: bool = True) -> str: - if ddt.get_text_w(line, font) < px + 10: - return line + y += round(25 * gui.scale) - 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(" ") + if self.button(x, y, _("Get playlist")): + if tau.processing: + show_message(_("An operation is already running")) + else: + shooter(tau.get_playlist()) - 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 + 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") -# 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 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) -click_time = time.time() -scroll_hold = False -scroll_point = 0 -scroll_bpoint = 0 -sbl = 50 -sbp = 100 + 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)) + tauon.fields.add(rect1) + if tauon.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() -asbp = 50 -album_scroll_hold = False + 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)) + tauon.fields.add(rect1) + if tauon.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) -def fix_encoding(index, mode, enc): - global default_playlist - global enc_field + if self.button(x, y, _("Test connectivity")): - todo = [] + 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" - 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]) + 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") - for q in range(len(todo)): + y += round(30 * gui.scale) - # 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]) + 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 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 + if self.account_view == 8: - 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 + ddt.text((x, y), "Spotify", colours.box_sub_text, 213) - 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 + prefs.spot_mode = self.toggle_square(x + 80 * gui.scale, y + 2 * gui.scale, prefs.spot_mode, _("Enable")) + y += round(30 * gui.scale) - if old_star != None: - star_store.insert(todo[q], old_star) + if self.button(x, y, _("View setup instructions")): + webbrowser.open("https://github.com/Taiko2k/Tauon/wiki/Spotify", new=2, autoraise=True) - # 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] + field_width = round(245 * gui.scale) + y += round(26 * gui.scale) -def transfer_tracks(index, mode, to): - todo = [] + 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)) + tauon.fields.add(rect1) + if tauon.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() - 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 + 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)) + tauon.fields.add(rect1) + if tauon.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() - pctl.multi_playlist[to].playlist_ids += todo + 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) -def prep_gal(): - global albums - albums = [] + tauon.spot_ctl.auth() - folder = "" + y += round(31 * gui.scale) + prefs.launch_spotify_web = self.toggle_square( + x, y, prefs.launch_spotify_web, + _("Prefer launching web player")) - for index in default_playlist: + y += round(24 * gui.scale) - if folder != pctl.master_library[index].parent_folder_name: - albums.append([index, 0]) - folder = pctl.master_library[index].parent_folder_name + 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 -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() + if self.account_view == 7: -def load_m3u(path: str) -> None: - name = os.path.basename(path)[:-4] - playlist = [] - stations = [] + ddt.text((x, y), _("Airsonic/Subsonic network streaming"), colours.box_sub_text, 213) - location_dict = {} - titles = {} + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 - if not os.path.isfile(path): - return + field_width = round(245 * gui.scale) - with Path(path).open(encoding="utf-8") as file: - lines = file.readlines() + 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)) + tauon.fields.add(rect1) + if tauon.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 - for i, line in enumerate(lines): - line = line.strip("\r\n").strip() - if not line.startswith("#"): # line.startswith("http"): + 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)) + tauon.fields.add(rect1) + if tauon.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 - # 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() + 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)) + tauon.fields.add(rect1) + if tauon.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 - if line.startswith("http"): - radio: dict[str, int | str] = {} - radio["stream_url"] = line + y += round(40 * gui.scale) + self.button(x, y, _("Import music to playlist"), sub_get_album_thread) - if line_title: - radio["title"] = line_title - else: - radio["title"] = os.path.splitext(os.path.basename(path))[0].strip() + 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")) - stations.append(radio) + if self.account_view == 10: + ddt.text((x, y), _("Jellyfin network streaming"), colours.box_sub_text, 213) - 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) + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 - # 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 + field_width = round(245 * gui.scale) - # 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") + 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)) + tauon.fields.add(rect1) + if tauon.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 - if playlist: - pctl.multi_playlist.append( - pl_gen(title=name, playlist_ids=playlist)) - if stations: - add_stations(stations, name) + 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)) + tauon.fields.add(rect1) + if tauon.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 - gui.update = 1 + 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)) + tauon.fields.add(rect1) + if tauon.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) -def read_pls(lines: list[str], path: str, followed: bool = False) -> None: - ids = [] - urls = {} - titles = {} + self.button(x, y, _("Import music to playlist"), jellyfin_get_library_thread) - 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() + 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() - 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() + y += round(35 * gui.scale) + if self.button(x, y, _("Test connectivity")): + jellyfin.test() - 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 ".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 self.account_view == 6: + ddt.text((x, y), _("koel network streaming"), colours.box_sub_text, 213) -def load_pls(path: str) -> None: - if os.path.isfile(path): - f = open(path) - lines = f.readlines() - read_pls(lines, path) - f.close() + 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) -def load_xspf(path: str) -> None: - global to_got + 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)) + tauon.fields.add(rect1) + if tauon.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 - name = os.path.basename(path)[:-5] - # tauon.log("Importing XSPF playlist: " + path, title=True) - logging.info("Importing XSPF playlist: " + path) + 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)) + tauon.fields.add(rect1) + if tauon.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 - try: - parser = ET.XMLParser(encoding="utf-8") - e = ET.parse(path, parser).getroot() + 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)) + tauon.fields.add(rect1) + if tauon.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 - a = [] - b = {} - info = "" + y += round(40 * gui.scale) - for top in e: + self.button(x, y, _("Import music to playlist"), koel_get_album_thread) - 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 + if self.account_view == 5: - 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 + ddt.text((x, y), _("PLEX network streaming"), colours.box_sub_text, 213) - b["info"] = info - b["name"] = name - a.append(copy.deepcopy(b)) - b = {} + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 - except Exception: - logging.exception("Error importing/parsing XSPF playlist") - show_message(_("Error importing XSPF playlist."), _("Sorry about that."), mode="warning") - return + field_width = round(245 * gui.scale) - # 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"] + 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)) + tauon.fields.add(rect1) + if tauon.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 - stations.append(radio) + 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)) + tauon.fields.add(rect1) + if tauon.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 - if gui.auto_play_import: - gui.auto_play_import = False - radiobox.start(radio) + 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)) + tauon.fields.add(rect1) + if tauon.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 - del a[i] - if stations: - add_stations(stations, os.path.basename(path)) - playlist = [] - missing = 0 + y += round(40 * gui.scale) + self.button(x, y, _("Import music to playlist"), plex_get_album_thread) - if len(a) > 5000: - to_got = "xspfl" + if self.account_view == 4: - # 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 + ddt.text((x, y), "fanart.tv", colours.box_sub_text, 213) - for track in a: - found = False + 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 - # Check if we already have a track with full file path in database - if not found and "location" in track: + 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) - location = track["location"] - if location in location_dict: - playlist.append(location_dict[location]) - if not os.path.isfile(location): - missing += 1 - found = True + 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 inp.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 found is True: - continue + # 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] + # tauon.fields.add(link_rect2) + # if tauon.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) + # - # 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 + if self.account_view == 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 + text = "Last.fm" + if prefs.use_libre_fm: + text = "Libre.fm" - if (not found and "location" in track) or "title" in track: - nt = TrackClass() - nt.index = pctl.master_count - nt.found = False + ddt.text((x, y), text, colours.box_sub_text, 213) - 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) + 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) - pctl.master_library[pctl.master_count] = nt - playlist.append(pctl.master_count) - pctl.master_count += 1 - if nt.found: - continue + y += 25 * gui.scale - 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"]) + 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 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 + 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")) - # tauon.log("Finished importing XSPF") + 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 (paste from clipboard if it didn't open) 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) -bb_type = 0 + x = x0 + 230 * gui.scale + y = y0 + round(130 * gui.scale) -# gui.scroll_hide_box = (0, gui.panelY, 28, window_size[1] - gui.panelBY - gui.panelY) + # self.toggle_square(x, y, toggle_scrobble_mark, "Show scrobble marker") -encoding_menu = False -enc_index = 0 -enc_setting = 0 -enc_field = "All" + 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) -gen_menu = False + 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) -transfer_setting = 0 + # y += 26 * gui.scale + # self.button(x, y, _("Clear local loves"), self.clear_local_loves, width=ww) -b_panel_size = 300 -b_info_bar = False + y += 26 * gui.scale -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") + 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) -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 + y += 33 * gui.scale - def test(self, x, y, text): + 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")) - if self.text != text or x != self.x or y != self.y: - self.text = text - # self.timer.set() - self.a = False + y += 25 * gui.scale - self.x = x - self.y = y - self.w = ddt.get_text_w(text, self.font) + 20 * gui.scale + self.toggle_square( + x, y, toggle_scrobble_mark, + _("Show threshold marker")) - self.called = True + if self.account_view == 2: - if self.a is False: - self.timer.set() - gui.frame_callback_list.append(TestTimer(self.trigger)) - self.a = True + ddt.text((x, y), "ListenBrainz", colours.box_sub_text, 213) - def render(self) -> None: + y += 30 * gui.scale + self.button(x, y, _("Paste Token"), lb.paste_key) - if self.called is True: + self.button(x + ddt.get_text_w(_("Paste Token"), 211) + 21 * gui.scale, y, _("Clear"), lb.clear_key) - if self.timer.get() > self.trigger: + y += 35 * gui.scale - 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 prefs.lb_token: + line = prefs.lb_token + ddt.text((x + 0 * gui.scale, y - 0 * gui.scale), line, colours.box_input_text, 212) - self.called = False + 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] + tauon.fields.add(link_rect2) + if tauon.coll(link_rect2): + if not self.click: + gui.cursor_want = 3 -tool_tip = ToolTip() -tool_tip2 = ToolTip() -tool_tip2.trigger = 1.8 -track_box_path_tool_timer = Timer() + if self.click: + webbrowser.open(link_pa2[2], new=2, autoraise=True) + def clear_local_loves(self): -def ex_tool_tip(x, y, text1_width, text, font): - text2_width = ddt.get_text_w(text, font) - if text2_width == text1_width: - return + if not inp.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 - y -= 10 * gui.scale + for key, star in star_store.db.items(): + star[1] = star[1].replace("L", "") + star_store.db[key] = star - w = ddt.get_text_w(text, 312) + 24 * gui.scale - h = 24 * gui.scale + gui.pl_update += 1 + show_message(_("Cleared all loves"), mode="done") - x -= int(w / 2) + def get_scrobble_counts(self): - 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 not inp.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.")) -class ToolTip3: + def clear_scrobble_counts(self): - 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) + for track in pctl.master_library.values(): + track.lfm_scrobbles = 0 - def set(self, x, y, text, font, rect): + show_message(_("Cleared all scrobble counts"), mode="done") - 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() + def get_friend_love(self): - if point_proximity_test(self.click_exclude_point, mouse_position, 20 * gui.scale): - self.timer.set() + if not inp.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 inp.mouse_click: - self.click_exclude_point = copy.copy(mouse_position) - self.timer.set() - 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.")) - 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 get_user_love(self): - def render(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.")) - if not self.show: - return + def codec_config(self, x0, y0, w0, h0): - if not point_proximity_test(self.click_exclude_point, mouse_position, 20 * gui.scale): - self.click_exclude_point = (0, 0) + x = x0 + round(25 * gui.scale) + y = y0 - 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 + y += 20 * gui.scale + ddt.text_background_colour = colours.box_background - gui.frame_callback_list.append(TestTimer(0.02)) + if self.sync_view: - if self.timer.get() < 0.6: - return + pl = None + if prefs.sync_playlist: + pl = id_to_pl(prefs.sync_playlist) + if pl is None: + prefs.sync_playlist = None - w = ddt.get_text_w(self.text, 312) + self.height - x = self.x # - int(self.width / 2) - y = self.y - h = self.height + 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) - border = 1 * gui.scale + y += 25 * gui.scale + ww = ddt.text((x, y), _("Path to device music folder: "), colours.box_text_label, 11) + y += 20 * 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) + rect1 = (x + 0 * gui.scale, y, round(450 * gui.scale), round(17 * gui.scale)) + tauon.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) - if not coll(self.rect): - self.show = False + rect = [x + rect1[2] + 11 * gui.scale, y - 2 * gui.scale, 15 * gui.scale, 19 * gui.scale] + tauon.fields.add(rect) + colour = colours.box_text_label + if tauon.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 -columns_tool_tip = ToolTip3() + 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 -tool_tip_instant = ToolTip3() + text = _("Start Transcode and Sync") + ww = ddt.get_text_w(text, 211) + 25 * gui.scale + if prefs.bypass_transcode: + text = _("Start Sync") -def close_all_menus(): - for menu in Menu.instances: - menu.active = False - Menu.active = False + 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 -def menu_standard_or_grey(bool: bool): - if bool: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + if self.button(x, y, _("Return"), width=round(75 * gui.scale)): + self.sync_view = False - return [line_colour, colours.menu_background, None] + 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 -# 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) + # ---------- + 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 -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() + 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) -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() - - -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 prefs.transcode_codec != "flac": + y += 35 * gui.scale -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() + prefs.transcode_bitrate = self.slide_control(x, y, _("Bitrate"), "kbs", prefs.transcode_bitrate, 32, 320, 8) + y -= 1 * gui.scale + x += 280 * gui.scale -def lsp_menu_test_queue(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "queue" + 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 lsp_menu_test_playlist(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "playlist" + def previous_theme(self): + prefs.theme -= 1 + gui.reload_theme = True + if prefs.theme < 0: + prefs.theme = len(get_themes()) + def config_b(self, x0, y0, w0, h0): + global update_layout -def lsp_menu_test_tree(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "folder view" + 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) -def lsp_menu_test_artist(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "artist list" + 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")) -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 + # 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 tauon.draw_border: + self.toggle_square(x, y, toggle_titlebar_line, _("Show playing in titlebar")) -# . Menu entry: A side panel view layout + #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")) -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)) + 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")) -class RenameTrackBox: + y += 25 * gui.scale + if prefs.backend == 4: + self.toggle_square(x, y, toggle_showcase_vis, _("Showcase visualisation")) - def __init__(self): + y += round(30 * gui.scale) + # if not msys: + # y += round(15 * gui.scale) - self.active = False - self.target_track_id = None - self.single_only = False + ddt.text((x, y), _("UI scale for HiDPI displays"), colours.box_text_label, 12) - def activate(self, track_id): + y += round(25 * gui.scale) - 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 + sw = round(200 * gui.scale) + sh = round(2 * gui.scale) - def disable_test(self, track_id): - if key_shift_down or key_shiftr_down: - single_only = True - else: - single_only = False + slider = (x, y, sw, sh) - 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: + gh = round(14 * gui.scale) + gw = round(8 * gui.scale) + grip = [0, y - (gh // 2), gw, gh] - if pctl.master_library[item].is_network is True: - return True - return False + grip[0] = x + grip[0] += ((prefs.scale_want - 0.5) / 3 * sw) - def render(self): + 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 not self.active: - return + if tauon.coll(grow_rect(slider, round(16 * gui.scale))) and inp.mouse_down: + prefs.scale_want = ((inp.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 - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False + text = str(prefs.scale_want) + if len(text) == 3: + text += "0" + text += "x" - 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) + if prefs.x_scale: + text = "auto" - 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 + 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 - 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 + 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) - r_todo = [] + 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) - # 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: + y += round(23 * gui.scale) + self.toggle_square(x, y, self.toggle_x_scale, _("Auto scale")) - # 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] + if prefs.scale_want != gui.scale: + gui.update += 1 + if not inp.mouse_down: + gui.update_layout() - ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Track Renaming"), colours.grey(230), 213) + 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) - # 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 toggle_x_scale(self, mode=0): + if mode == 1: + return prefs.x_scale + prefs.x_scale ^= True + auto_scale(bag) + gui.update_layout() - # 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 about(self, x0, y0, w0, h0): - ddt.rect_s( - (x + 8 * gui.scale, y + 36 * gui.scale, 300 * gui.scale, 22 * gui.scale), colours.box_text_border, 1 * gui.scale) + x = x0 + int(w0 * 0.3) - 10 * gui.scale + y = y0 + 85 * gui.scale - afterline = "" - warn = False - underscore = False + ddt.text_background_colour = colours.box_background - for item in r_todo: + icon_rect = (x - 110 * gui.scale, y - 15 * gui.scale, self.about_image.w, self.about_image.h) - 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 + genre = "" + if pctl.playing_object() is not None: + genre = pctl.playing_object().genre.lower() - if item == self.target_track_id: - afterline = parse_template2(NRN, pctl.master_library[item]) + 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 = "" - 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) + if not genre: + self.about_image.render(icon_rect[0], icon_rect[1]) - 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) + x += 20 * gui.scale + y -= 10 * gui.scale - 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) + self.title_image.render(x - 1, y, alpha_mod(colours.box_sub_text, 240)) - 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] + credit_pages = 5 - label = _("Write") + " (" + str(len(r_todo)) + ")" + if self.click and tauon.coll(icon_rect) and self.ani_cred == 0: + self.ani_cred = 1 + self.ani_fade_on_timer.set() - 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: + fade = 0 - inp.mouse_click = False - total_todo = len(r_todo) - pre_state = 0 + if self.ani_cred == 1: + t = self.ani_fade_on_timer.get() + fade = round(t / 0.7 * 255) + fade = min(fade, 255) - for item in r_todo: + 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() - if pctl.playing_state > 0 and item == pctl.track_queue[pctl.queue_step]: - pre_state = pctl.stop(True) + gui.update = 2 - try: + if self.ani_cred == 2: - afterline = parse_template2(NRN, pctl.master_library[item], strict=True) + 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 - oldname = pctl.master_library[item].filename - oldpath = pctl.master_library[item].fullpath + gui.update = 2 - logging.info("Renaming...") + y += 32 * gui.scale - star = star_store.full_get(item) - star_store.remove(item) + block_y = y - 10 * gui.scale - oldpath = pctl.master_library[item].fullpath + if self.cred_page == 0: - oldsplit = os.path.split(oldpath) + 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) - if os.path.exists(os.path.join(oldsplit[0], afterline)): - logging.error("A file with that name already exists") - total_todo -= 1 - continue + 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 tauon.coll(link_rect): + if not self.click: + gui.cursor_want = 3 + if self.click: + webbrowser.open(link_pa[2], new=2, autoraise=True) - if not afterline: - logging.error("Rename Error") - total_todo -= 1 - continue + tauon.fields.add(link_rect) - if "." in afterline and not afterline.split(".")[0]: - logging.error("A file does not have a target filename") - total_todo -= 1 - continue + 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 tauon.coll(link_rect): + if not self.click: + gui.cursor_want = 3 + if self.click: + webbrowser.open(link_pa[2], new=2, autoraise=True) + tauon.fields.add(link_rect) - os.rename(pctl.master_library[item].fullpath, os.path.join(oldsplit[0], afterline)) + elif self.cred_page == 1: - pctl.master_library[item].fullpath = os.path.join(oldsplit[0], afterline) - pctl.master_library[item].filename = afterline + y += 15 * gui.scale - search_string_cache.pop(item, None) - search_dia_string_cache.pop(item, None) + 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) - if star is not None: - star_store.insert(item, star) + 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 tauon.coll(link_rect): + if not self.click: + gui.cursor_want = 3 + if self.click: + webbrowser.open(link_pa[2], new=2, autoraise=True) + tauon.fields.add(link_rect) - except Exception: - logging.exception("Rendering error") - total_todo -= 1 - rename_track_box.active = False - logging.info("Done") - if pre_state == 1: - pctl.revert() + 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") - 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() + 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") -rename_track_box = RenameTrackBox() + 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") -class TransEditBox: - def __init__(self): - self.active = False - self.active_field = 1 - self.selected = [] - self.playlist = -1 + 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") - def render(self): + 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") - if not self.active: - return + 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") - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False + 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") - 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) + # 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") - 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 + 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") - 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 + 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") - select = list(set(shift_selection)) - if not select and pctl.selected_ready(): - select = [pctl.selected_in_playlist] + 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") - 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] + 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") - #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() + 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") - if len(select) == 0: - return + 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") - tr = pctl.get_track(default_playlist[select[0]]) - edit_title.set_text(tr.title) + 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") - if check_equal(artists): - edit_artist.set_text(artists[0]) + 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") - if check_equal(albums): - edit_album.set_text(albums[0]) + 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") - if check_equal(album_artists): - edit_album_artist.set_text(album_artists[0]) + 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") - x += round(20 * gui.scale) - y += round(18 * gui.scale) + 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") - ddt.text((x, y), _("Simple tag editor"), colours.box_title_text, 215) + 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") - 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 += 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") - y += round(24 * gui.scale) - ddt.text((x, y), _("Number of tracks selected: {N}").format(N=len(select)), colours.box_title_text, 313) + ddt.rect((x, block_y, 369 * gui.scale, 140 * gui.scale), alpha_mod(colours.box_background, fade)) - y += round(24 * gui.scale) + y = y0 + h0 - round(33 * gui.scale) + x = x0 + w0 - 0 * gui.scale - if inp.key_tab_press: - if key_shift_down or key_shiftr_down: - self.active_field -= 1 - else: - self.active_field += 1 + w = max(ddt.get_text_w(_("Credits"), 211), ddt.get_text_w(_("Next"), 211)) + x -= w + round(40 * gui.scale) - 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 + 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 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), _(""), 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 + def topchart(self, x0, y0, w0, h0): + x = x0 + round(25 * gui.scale) + y = y0 + 20 * gui.scale - 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) + ddt.text_background_colour = colours.box_background - 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) + ddt.text((x, y), _("Chart Grid Generator"), colours.box_text, 214) - if inp.key_return_press: + 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 - 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) + y += 30 * gui.scale - 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() + 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 - 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 + y += 35 * gui.scale + x += 5 * gui.scale - if gui.write_tag_in_progress: - return + 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 - def write_tag_go(): + 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")) - for s in select: - tr = pctl.get_track(default_playlist[s]) + x = x0 + 15 * gui.scale + 320 * gui.scale + y = y0 + 100 * gui.scale - 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 + # . Limited width. Max 13 chars + if self.button(x, y, _("Randomise BG")): - muta = mutagen.File(tr.fullpath, easy=True) + r = round(random.random() * 40) + g = round(random.random() * 40) + b = round(random.random() * 40) - 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 + prefs.chart_bg = [r, g, b] - 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") + d = random.randrange(0, 4) - 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) + if d == 1: + c = 5 + round(random.random() * 20) + prefs.chart_bg = [c, c, c] -trans_edit_box = TransEditBox() + x += 100 * gui.scale + y -= 20 * gui.scale + display_colour = (prefs.chart_bg[0], prefs.chart_bg[1], prefs.chart_bg[2], 255) -class SubLyricsBox: + rect = (x, y, 70 * gui.scale, 70 * gui.scale) + ddt.rect(rect, display_colour) - def __init__(self): + ddt.rect_s(rect, (50, 50, 50, 255), round(1 * gui.scale)) - self.active = False - self.target_track = None - self.active_field = 1 + # x = self.box_x + self.item_x_offset + 200 * gui.scale + # y = self.box_y + 180 * gui.scale - def activate(self, track: TrackClass): + x = x0 + 260 * gui.scale + y = y0 + 180 * gui.scale - self.active = True - gui.box_over = True - self.target_track = track + dex = reload_albums(quiet=True, return_playlist=pctl.active_playlist_viewing) - sub_lyrics_a.text = prefs.lyrics_subs.get(self.target_track.artist, "") - sub_lyrics_b.text = prefs.lyrics_subs.get(self.target_track.title, "") + x = x0 + round(110 * gui.scale) + y = y0 + 240 * gui.scale - 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 + # . 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 - def render(self): + 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 - if not self.active: - return + line = _("{N} Album chart").format(N=str(count)) - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False + ww = ddt.text((x, y + round(1 * gui.scale)), line, colours.box_text_label, 12) - 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) + 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) - 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 + x = x0 + round(20 * gui.scale) + y = y0 + 240 * gui.scale - 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 + # . Limited width. Max 8 chars + if self.button(x, y, _("Return"), width=75 * gui.scale): + self.chart_view = 0 - 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] + def stats(self, x0, y0, w0, h0): + x = x0 + 10 * gui.scale + y = y0 - 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] + if self.chart_view == 1: + self.topchart(x0, y0, w0, h0) + return - ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Substitute Lyric Search"), colours.grey(230), 213) + 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 - y += round(35 * gui.scale) - x += round(23 * gui.scale) + ddt.text_background_colour = colours.box_background + lt_font = 312 + lt_colour = colours.box_text_label - 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) + 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) - 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 + x1 = x + (8 + 10 + 10) * gui.scale + x2 = x1 + max(w1, w2, w3, w4, w5, w6) + 20 * gui.scale + y1 = y + 50 * gui.scale - sub_lyrics_a.draw( - xx + round(4 * gui.scale), y, colours.box_input_text, self.active_field == 1, - width=rect1[2] - 8 * 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() - y += round(28 * gui.scale) + album_names = set() + folder_names = set() + count = 0 - 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) + for track_id in pctl.default_playlist: + tr = pctl.get_track(track_id) - 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) + 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 -sub_lyrics_box = SubLyricsBox() + self.stats_pl_length = 0 + for item in pctl.default_playlist: + self.stats_pl_length += pctl.master_library[item].length + line = seconds_to_day_hms(self.stats_pl_length, strings.day, strings.days) -class ExportPlaylistBox: + ddt.text((x1, y1), _("Tracks in playlist"), lt_colour, lt_font) + ddt.text((x2, y1), py_locale.format_string("%d", len(pctl.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) - def __init__(self): + ddt.text((x2, y1), line, colours.box_sub_text, 12) - 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, - } + if self.stats_timer.get() > 5: + album_names = set() + folder_names = set() + count = 0 - def activate(self, playlist): + for pl in pctl.multi_playlist: + for track_id in pl.playlist_ids: + tr = pctl.get_track(track_id) - self.active = True - gui.box_over = True - self.id = pl_to_id(playlist) + 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) - # 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] + self.total_albums = count - def render(self) -> None: - if not self.active: - return + self.stats_timer.set() - 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) + 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) - 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 + 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) - 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 + # Ratio bar + if len(pctl.master_library) > 115 * gui.scale: + x = x0 + y = y0 + h0 - 7 * gui.scale - current = prefs.playlist_exports.get(self.id) - if not current: - current = copy.copy(self.default) + full_rect = [x, y, w0, 7 * gui.scale] + d = 0 - ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Export Playlist"), colours.grey(230), 213) + # 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 - x += round(15 * gui.scale) - y += round(25 * gui.scale) + for key, value in self.ext_ratio.items(): - ddt.text((x, y + 8 * gui.scale), _("Save directory"), colours.grey(230), 11) - y += round(30 * gui.scale) + colour = [200, 200, 200, 255] + if key in format_colours: + colour = format_colours[key] - 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 + 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] - 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) + h = int(round(value / len(pctl.master_library) * full_rect[2])) + block_rect = [full_rect[0] + d, full_rect[1], h, full_rect[3]] - 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)) + ddt.rect(block_rect, colour) + d += h - prefs.playlist_exports[self.id] = current + block_rect = (block_rect[0], block_rect[1], block_rect[2] - 1, block_rect[3]) + tauon.fields.add(block_rect) + if tauon.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 draw.button(_("Export"), x, y, press=gui.level_2_click): - self.run_export(current, self.id, warnings=True) + if self.click: + gen_codec_pl(key) + except Exception: + logging.exception("Error draw ext bar") - 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 config_v(self, x0, y0, w0, h0): - if warnings and target != 1: - show_message(_("Playlist exported"), target, mode="done") + ddt.text_background_colour = colours.box_background + x = x0 + self.item_x_offset + y = y0 + 17 * gui.scale -export_playlist_box = ExportPlaylistBox() + 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) + tauon.fields.add(rect) + if tauon.coll(rect): + ex_tool_tip(x + round(45 * gui.scale), y - 20 * gui.scale, 0, _("Show track loves"), 12) -def toggle_repeat() -> None: - gui.update += 1 - pctl.repeat_mode ^= True - if pctl.mpris is not None: - pctl.mpris.update_loop() + 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) + tauon.fields.add(rect) + if tauon.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) + tauon.fields.add(rect) + if tauon.coll(rect): + ex_tool_tip(x + round(35 * gui.scale), y - 20 * gui.scale, 0, _("Represent playcount as lines"), 12) -tauon.toggle_repeat = toggle_repeat + x = x0 + self.item_x_offset + # y += round(25 * gui.scale) -def menu_repeat_off() -> None: - pctl.repeat_mode = False - pctl.album_repeat_mode = False - if pctl.mpris is not None: - pctl.mpris.update_loop() + # 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) -def menu_set_repeat() -> None: - pctl.repeat_mode = True - pctl.album_repeat_mode = False - if pctl.mpris is not None: - pctl.mpris.update_loop() + y += round(25 * gui.scale) -def menu_album_repeat() -> None: - pctl.repeat_mode = True - pctl.album_repeat_mode = True - if pctl.mpris is not None: - pctl.mpris.update_loop() + 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) -tauon.menu_album_repeat = menu_album_repeat -tauon.menu_repeat_off = menu_repeat_off -tauon.menu_set_repeat = menu_set_repeat + 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) -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)) + 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) -def toggle_random(): - gui.update += 1 - pctl.random_mode ^= True - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + 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 -tauon.toggle_random = toggle_random + 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 toggle_random_on(): - pctl.random_mode = True - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + def set_playlist_cycle(self, mode=0): + if mode == 1: + return True if prefs.end_setting == "cycle" else False + prefs.end_setting = "cycle" + # 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" + # pl_follow = False -def toggle_random_off(): - pctl.random_mode = False - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + 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 menu_shuffle_off(): - pctl.random_mode = False - pctl.album_shuffle_mode = False - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + def small_preset(self) -> None: + 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) -> None: + prefs.playlist_row_height = round(27 * prefs.ui_scale) + prefs.playlist_font_size = 15 + gui.update_layout() -def menu_set_random(): - pctl.random_mode = True - pctl.album_shuffle_mode = False - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + def slide_control(self, x: int, y: int, label: str, units: str, value: int, lower_limit: int, upper_limit: int, step: int = 1, callback=None, width: int = 58) -> int: + 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) + tauon.fields.add(rect) + ddt.rect(rect, colours.box_button_background) + abg = [255, 255, 255, 40] + if tauon.coll(rect): -def menu_album_random(): - pctl.random_mode = True - pctl.album_shuffle_mode = True - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + if self.click: + if value > lower_limit: + value -= step + gui.update_layout() + if callback is not None: + callback(value) + if inp.mouse_down: + abg = [230, 120, 20, 255] + else: + abg = [220, 150, 20, 255] -def toggle_shuffle_layout(albums=False): - prefs.shuffle_lock ^= True - if prefs.shuffle_lock: + if colour_value(colours.box_background) > 300: + abg = colours.box_sub_text - gui.shuffle_was_showcase = gui.showcase_mode - gui.shuffle_was_random = pctl.random_mode - gui.shuffle_was_repeat = pctl.repeat_mode + dec_arrow.render(x + 1 * gui.scale, y, abg) - 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() + 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) -def toggle_shuffle_layout_albums(): - toggle_shuffle_layout(albums=True) + x += width + rect = (x, y, 33 * gui.scale, 15 * gui.scale) + tauon.fields.add(rect) + ddt.rect(rect, colours.box_button_background) + abg = [255, 255, 255, 40] + if tauon.coll(rect): + if self.click: + if value < upper_limit: + value += step + gui.update_layout() + if callback is not None: + callback(value) + if inp.mouse_down: + abg = [230, 120, 20, 255] + else: + abg = [220, 150, 20, 255] -def exit_shuffle_layout(_): - return prefs.shuffle_lock + if colour_value(colours.box_background) > 300: + abg = colours.box_sub_text + inc_arrow.render(x + 1 * gui.scale, y, abg) -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)) + return value + # def style_up(self): + # prefs.line_style += 1 + # if prefs.line_style > 5: + # prefs.line_style = 1 -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) + def inside(self): + return tauon.coll((self.box_x, self.box_y, self.w, self.h)) + def init2(self): + self.init2done = True -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 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() -def artist_info_panel_close(): - gui.artist_info_panel ^= True - gui.update_layout() + if key_esc_press: + self.close() + tab_width = 115 * gui.scale -def toggle_bio_size_deco(): - line = _("Make Large Size") - if prefs.bio_large: - line = _("Make Compact Size") + side_width = 115 * gui.scale + header_width = 0 - return [colours.menu_text, colours.menu_background, line] + 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 -def toggle_bio_size(): - if prefs.bio_large: - prefs.bio_large = False - update_layout_do() - # bio_set_small() + full_width += side_width + full_height += header_width - else: - prefs.bio_large = True - update_layout_do() - # bio_set_large() - # gui.update_layout() + 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 -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 + 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) -def test_shift(_): - return key_shift_down or key_shiftr_down + 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) -def test_artist_dl(_): - return not prefs.auto_dl_artist_data + if top_mode: + xx = x + yy = y + tab_width = 90 * gui.scale + ddt.rect_a((x, y), (full_width, header_width), tab_bg) -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)) + 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] + tauon.fields.add(box2) -def show_in_playlist(): - if album_mode and window_size[0] < 750 * gui.scale: - toggle_album_mode() + if self.click and tauon.coll(box2): + self.tab_active = current_tab + self.lyrics_panel = False - 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() + 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 tauon.coll(box2): + ddt.rect(box, tab_over) -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 + alpha = 100 + if current_tab == self.tab_active: + alpha = 240 -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)) + ddt.text((xx + (tab_width // 2), yy + 4 * gui.scale, 2), item[0], tab_text, 212) -folder_icon.colour = [244, 220, 66, 255] -info_icon.colour = [61, 247, 163, 255] + 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 tauon.coll(message_box.get_rect()): + gui.message_box = False + else: + inp.mouse_click = True + self.click = False -power_bar_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "power.png", True) + box = [x, y + (current_tab * tab_height), tab_width, tab_height] + box2 = [x, y + (current_tab * tab_height), tab_width, tab_height - 1] + tauon.fields.add(box2) + if self.click and tauon.coll(box2): + self.tab_active = current_tab + self.lyrics_panel = False -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 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 tauon.coll(box2): + ddt.rect(box, tab_over) -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) + yy = box[1] + 4 * gui.scale -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 + 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) - 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]) + current_tab += 1 + # ddt.line(x + 110, self.box_y + 1, self.box_x + 110, self.box_y + self.h, colours.grey(50)) -def tag_to_new_playlist(tag_item): - path_stem_to_playlist(tag_item.path, tag_item.name) + self.tabs[self.tab_active][1](x + side_width, y + header_width, content_width, content_height) + self.click = False + self.right_click = False -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) + ddt.text_background_colour = colours.box_background +class Fields: + def __init__(self) -> None: + self.id = [] + self.last_id = [] -def stem_to_new_playlist(path: str) -> None: - path_stem_to_playlist(path, os.path.basename(path)) + self.field_array = [] + self.force = False + def add(self, rect, callback=None): + self.field_array.append((rect, callback)) -move_jobs = [] -move_in_progress = False + def test(self): + if self.force: + self.force = False + return True + self.last_id = self.id + #logging.info(len(self.id)) + self.id = [] -def move_playing_folder_to_tree_stem(path: str) -> None: - move_playing_folder_to_stem(path, pl_id=tree_view_box.get_pl_id()) + for f in self.field_array: + if tauon.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 -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 + return True - track = pctl.playing_object() + def clear(self): - if not track or pctl.playing_state == 0: - show_message(_("No item is currently playing")) - return + self.field_array = [] - move_folder = track.parent_folder_path +class TopPanel: + def __init__(self, tauon: Tauon) -> None: + self.tauon = tauon + self.prefs = tauon.prefs + self.pctl = tauon.pctl + self.gui = tauon.gui + self.inp = tauon.gui.inp + bag = tauon.bag + self.fonts = tauon.bag.fonts + self.colours = tauon.bag.colours + self.ddt = tauon.bag.ddt + self.draw_max_button = bag.draw_max_button + self.window_size = bag.window_size + self.height = self.gui.panelY + self.ty = 0 + + self.start_space_left = round(46 * self.gui.scale) + self.start_space_compact_left = 46 * self.gui.scale + + self.tab_text_font = self.fonts.tabs + self.tab_extra_width = round(17 * self.gui.scale) + self.tab_text_start_space = 8 * self.gui.scale + self.tab_text_y_offset = 7 * self.gui.scale + self.tab_spacing = 0 - # 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) + self.ini_menu_space = 17 * self.gui.scale # 17 + self.menu_space = 17 * self.gui.scale + self.click_buffer = 4 * self.gui.scale - target_base = path + self.tabs_right_x = 0 # computed for drag and drop code elsewhere (hacky) + self.tabs_left_x = 1 - # Determine name for artist folder - artist = track.artist - if track.album_artist: - artist = track.album_artist + self.prime_tab = self.gui.saved_prime_tab + self.prime_side = self.gui.saved_prime_direction # 0=left, 1=right + self.shown_tabs = [] - # Make filename friendly - artist = filename_safe(artist) - if not artist: - artist = "unknown artist" + # --- + self.space_left = 0 + self.tab_text_spaces = [] + self.index_playing = -1 + self.drag_zone_start_x = 300 * self.gui.scale + + self.exit_button = asset_loader(bag, bag.loaded_asset_dc, "ex.png", True) + self.maximize_button = asset_loader(bag, bag.loaded_asset_dc, "max.png", True) + self.restore_button = asset_loader(bag, bag.loaded_asset_dc, "restore.png", True) + self.restore_button = asset_loader(bag, bag.loaded_asset_dc, "restore.png", True) + self.playlist_icon = asset_loader(bag, bag.loaded_asset_dc, "playlist.png", True) + self.return_icon = asset_loader(bag, bag.loaded_asset_dc, "return.png", True) + self.artist_list_icon = asset_loader(bag, bag.loaded_asset_dc, "artist-list.png", True) + self.folder_list_icon = asset_loader(bag, bag.loaded_asset_dc, "folder-list.png", True) + self.dl_button = asset_loader(bag, bag.loaded_asset_dc, "dl.png", True) + self.overflow_icon = asset_loader(bag, bag.loaded_asset_dc, "overflow.png", True) - # Sanity checks - if track.is_network: - show_message(_("This track is a networked track."), mode="error") - return + self.drag_slide_timer = Timer(100) + self.tab_d_click_timer = Timer(10) + self.tab_d_click_ref = None - if not os.path.isdir(move_folder): - show_message(_("The source folder does not exist."), mode="error") - return + self.adds = [] - if not os.path.isdir(target_base): - show_message(_("The destination folder does not exist."), mode="error") - return + def left_overflow_switch_playlist(self, pl): + self.prime_side = 0 + self.prime_tab = pl + switch_playlist(pl) - if os.path.normpath(target_base) == os.path.normpath(move_folder): - show_message(_("The destination and source folders are the same."), mode="error") - return + def right_overflow_switch_playlist(self, pl): + self.prime_side = 1 + self.prime_tab = pl + switch_playlist(pl) - if len(target_base) < 4: - show_message(_("Safety interupt! The source path seems oddly short."), target_base, mode="error") - return + def render(self): + tauon = self.tauon + pctl = self.pctl + gui = self.gui + ddt = self.ddt + inp = self.inp + colours = self.colours + prefs = self.prefs + window_size = self.window_size - 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 - - if directory_size(move_folder) > 3000000000: - show_message(_("Folder size safety limit reached! (3GB)"), move_folder, mode="warning") - return - - # 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) + # C-TD + global update_layout - # 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] + hh = gui.panelY2 + yy = gui.panelY - hh + self.height = hh - # Find insert location - pl = pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids + if inp.quick_drag is True: + # gui.pl_update = 1 + gui.update_on_drag = True - matches = [] - insert = 0 + # Draw the background + ddt.rect((0, 0, window_size[0], gui.panelY), colours.top_panel_background) - for i, item in enumerate(pl): - if pctl.get_track(item).fullpath.startswith(target_base): - insert = i + 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 pctl.loading_in_progress or \ + tauon.to_scan or \ + tauon.cm_clean_db or \ + lastfm.scanning_friends or \ + tauon.after_scan or \ + tauon.move_in_progress or \ + tauon.plex.scanning or \ + tauon.transcode_list or tauon.spot_ctl.launching_spotify or tauon.spot_ctl.spotify_com or tauon.subsonic.scanning or \ + tauon.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) - for i, item in enumerate(pl): - if pctl.get_track(item).fullpath.startswith(artist_folder): - insert = i + 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 - 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 pctl.playing_state == 3 and not radiobox.dummy_track.title: + title = pctl.tag_meta + artist = radiobox.loaded_url # pctl.url - 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") + 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) -def move_playing_folder_to_tag(tag_item): - move_playing_folder_to_stem(tag_item.path) + wwx = 0 + if prefs.left_window_control and not gui.compact_bar: + if gui.macstyle: + wwx = 24 + # wwx = round(64 * gui.scale) + if self.draw_min_button: + wwx += 20 + if self.draw_max_button: + wwx += 20 + wwx = round(wwx * gui.scale) + else: + wwx = 26 + # wwx = round(90 * gui.scale) + if self.draw_min_button: + wwx += 35 + if self.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) + tauon.fields.add(rect) -def re_import4(id): - p = None - for i, idd in enumerate(default_playlist): - if idd == id: - p = i - break + if tauon.coll(rect) and not prefs.shuffle_lock: + if inp.mouse_click: - load_order = LoadClass() + if gui.combo_mode: + gui.switch_showcase_off = True + else: + gui.lsp ^= True - if p is not None: - load_order.playlist_position = p + update_layout = True + gui.update += 1 + if inp.mouse_down and inp.quick_drag: + gui.lsp = True + update_layout = True + gui.update += 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") + 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(tauon=tauon) -def re_import3(stem): - p = None - for i, id in enumerate(default_playlist): - if pctl.get_track(id).fullpath.startswith(stem + "/"): - p = i - break + colour = colours.corner_button # [230, 230, 230, 255] - load_order = LoadClass() + if gui.lsp: + colour = colours.corner_button_active + if gui.combo_mode: + colour = colours.corner_button + if tauon.coll(rect): + colour = colours.corner_button_active - if p is not None: - load_order.playlist_position = p + 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) - 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") + # 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 tauon.playlist_box.drag: + inp.drag_mode = False -def collapse_tree_deco(): - pl_id = tree_view_box.get_pl_id() + # Need to test length + self.tab_text_spaces = [] - 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] + 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 -def collapse_tree(): - tree_view_box.collapse_all() + # Calculate position for playing text and text + offset = 15 * gui.scale + if tauon.draw_border and not prefs.left_window_control: + offset += 61 * gui.scale + if self.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 -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 + x_start = x + if tauon.playlist_box.drag and not gui.radio_view: + if inp.mouse_up: + if inp.mouse_up_position[0] > (gui.lspw if gui.lsp else 0) and inp.mouse_up_position[1] > gui.panelY: + tauon.playlist_box.drag = False + if prefs.drag_to_unpin: + if tauon.playlist_box.drag_source == 0: + pctl.multi_playlist[tauon.playlist_box.drag_on].hidden = True + else: + pctl.multi_playlist[tauon.playlist_box.drag_on].hidden = False + gui.update += 1 + gui.update_on_drag = True -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")] + # 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)) -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)) + left_tabs = [] + right_tabs = [] + if prefs.shuffle_lock: + for p in ready_tabs: + left_tabs.append(p) -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)) + else: + for p in ready_tabs: + if p < self.prime_tab: + left_tabs.append(p) -folder_tree_stem_menu.add(MenuItem(_("Move Playing Folder Here"), move_playing_folder_to_tree_stem, pass_ref=True)) + for p in ready_tabs: + if p > self.prime_tab: + right_tabs.append(p) + left_tabs.reverse() -folder_tree_stem_menu.br() + run = max_w -folder_tree_stem_menu.add(MenuItem(_("Collapse All"), collapse_tree, collapse_tree_deco)) + 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 -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) + 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 -gallery_menu.add(MenuItem(_("Open Folder"), open_folder, pass_ref=True, pass_ref_deco=True, icon=folder_icon, disable_test=open_folder_disable_test)) + # 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 -gallery_menu.add(MenuItem(_("Show in Playlist"), show_in_playlist)) + 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 -def finish_current(): - playing_object = pctl.playing_object() - if playing_object is None: - show_message("") + if inp.mouse_click and tauon.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])) - 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)) + 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 tauon.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])) -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) + if gui.radio_view: + if not inp.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 inp.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 - 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 + if tauon.playlist_box.drag and inp.mouse_position[0] > xx and inp.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 tauon.playlist_box.drag and inp.mouse_position[0] < x and inp.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() - 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 + # 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 -def add_album_to_queue_fc(ref): - playing_object = pctl.playing_object() - if playing_object is None: - show_message("") + if len(pctl.multi_playlist) != len(self.tab_text_spaces): + break - queue_item = None + if i not in show_tabs: + continue - 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 + # Determine the tab width + tab_width = self.tab_text_spaces[i] + self.tab_extra_width - 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: + # Save the far right boundary of the tabs (hacky) + self.tabs_right_x = x + tab_width - p = pctl.get_track(ref).parent_folder_path - p = "" - if pctl.playing_ready(): - p = pctl.playing_object().parent_folder_path + # Detect mouse over and add tab to mouse over detection + f_rect = [x, y + 1, tab_width - 1, self.height - 1] + tab_hit = tauon.coll(f_rect) - # fixme for network tracks + # Tab functions + if tab_hit: + if not gui.radio_view: + # Double click to play + if inp.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( + inp.last_click_location, inp.mouse_up_position) < 5 * gui.scale: - for i, item in enumerate(pctl.force_queue): + 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(pctl.default_playlist[pctl.selected_in_playlist], pl_position=pctl.selected_in_playlist) + if inp.mouse_up: + self.tab_d_click_timer.set() + self.tab_d_click_ref = pl_to_id(i) - 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 + # Click to change playlist + if inp.mouse_click: + gui.pl_update = 1 + tauon.playlist_box.drag = True + tauon.playlist_box.drag_source = 0 + tauon.playlist_box.drag_on = i + if gui.radio_view: + pctl.radio_playlist_viewing = i + else: + switch_playlist(i) + set_drag_source() - 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 + # Drag to move playlist + if inp.mouse_up and tauon.playlist_box.drag and coll_point(inp.mouse_up_position, f_rect): + if gui.radio_view: + move_radio_playlist(playlist_box.drag_on, i) + else: + if tauon.playlist_box.drag_source == 1: + pctl.multi_playlist[tauon.playlist_box.drag_on].hidden = False + if i != tauon.playlist_box.drag_on: -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)) + # # Reveal the tab in case it has been hidden + # pctl.multi_playlist[tauon.playlist_box.drag_on].hidden = False + if inp.key_shift_down: + pctl.multi_playlist[i].playlist_ids += pctl.multi_playlist[tauon.playlist_box.drag_on].playlist_ids + delete_playlist(tauon.playlist_box.drag_on, check_lock=True, force=True) + else: + move_playlist(tauon.playlist_box.drag_on, i) -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") + tauon.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 -cancel_menu.add(MenuItem(_("Cancel"), cancel_import)) + # 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 inp.quick_drag is True and inp.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)) + inp.quick_drag = False + modified = False + gui.pl_update += 1 -def toggle_lyrics_show(a): - return not gui.combo_mode + for item in shift_selection: + pctl.multi_playlist[i].playlist_ids.append(pctl.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") -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 inp.mouse_up and radio_view.drag: + pctl.radio_playlists[i].stations.append(radio_view.drag) + toast(_("Added station to: ") + pctl.radio_playlists[i].name) - if gui.combo_mode: - colour = colours.menu_text_disabled + radio_view.drag = None - return [colour, colours.menu_background, line] + x += tab_width + self.tab_spacing + # Test dupelicate tab function + if tauon.playlist_box.drag: + rect = (0, x, self.height, window_size[0]) + tauon.fields.add(rect) -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 inp.mouse_up and tauon.playlist_box.drag and inp.mouse_position[0] > x and inp.mouse_position[1] < self.height: + if gui.radio_view: + pass + elif inp.key_ctrl_down: + gen_dupe(tauon.playlist_box.drag_on) - if gui.combo_mode or not prefs.show_side_lyrics_art_panel: - colour = colours.menu_text_disabled + else: + if tauon.playlist_box.drag_source == 1: + pctl.multi_playlist[tauon.playlist_box.drag_on].hidden = False - return [colour, colours.menu_background, line] + move_playlist(tauon.playlist_box.drag_on, i) + tauon.playlist_box.drag = False + # Need to test length again + # Need to test length + self.tab_text_spaces = [] -def toggle_lyrics_panel_position(): - prefs.lyric_metadata_panel_top ^= True - - -def lyrics_in_side_show(track_object: TrackClass): - if gui.combo_mode or not prefs.show_lyrics_side: - return False - return True + 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) -def toggle_side_art(): - prefs.show_side_lyrics_art_panel ^= True + # 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 -def toggle_lyrics_deco(track_object: TrackClass): - colour = colours.menu_text + if len(pctl.multi_playlist) != len(self.tab_text_spaces): + break - 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 tab.hidden is True: + # continue - if prefs.side_panel_layout == 1: # and prefs.show_side_art: + if i not in show_tabs: + continue - 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 window_size[0] - x - (self.tab_text_spaces[i] + self.tab_extra_width) < right_space_es: + # break - 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] + shown.append(i) + tab_width = self.tab_text_spaces[i] + self.tab_extra_width + rect = [x, y, tab_width, self.height] -def toggle_lyrics(track_object: TrackClass): - if not track_object: - return + # Detect mouse over and add tab to mouse over detection + f_rect = [x, y + 1, tab_width - 1, self.height - 1] + tauon.fields.add(f_rect) + tab_hit = tauon.coll(f_rect) + playing_hint = False + active = False - 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: + # 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 tauon.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 - # 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 + # Draw tab background + ddt.rect(rect, bg) + if playing_hint: + ddt.rect(rect, [255, 255, 255, 7]) - 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") + # 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) -def get_lyric_fire(track_object: TrackClass, silent: bool = False) -> str | None: - lyrics_ren.lyrics_position = 0 + # 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 - 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 + # Drag to move playlist + if tab_hit: + if inp.mouse_down and i != tauon.playlist_box.drag_on and tauon.playlist_box.drag is True: + if inp.key_shift_down: + ddt.rect((x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size), [80, 160, 200, 255]) + elif tauon.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]) - 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.")) + elif inp.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 inp.quick_drag and not point_proximity_test(gui.drag_source_position, inp.mouse_position, 15 * gui.scale): + for item in shift_selection: + if item < len(pctl.default_playlist) and pctl.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 inp.quick_drag and not point_proximity_test(gui.drag_source_position, inp.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 t < -65: - show_message(_("Stop requesting lyrics AAAAAA."), mode="error") + 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 - # If the user keeps pressing, lets mess with them haha - lyrics_fetch_timer.force_set(t - 5) + ddt.text( + (x + tab_width - 3, int(round(ay)), 1), "+" + str(self.adds[k][1]), colours.pluse_colour, 212, bg=bg) + gui.update += 1 - return "later" + x += tab_width + self.tab_spacing - if t > 0: - lyrics_fetch_timer.set() - t = 0 + # Quick drag single track onto bar to create new playlist function and indicator + if prefs.tabs_on_top: + if inp.quick_drag and inp.mouse_position[0] > x and inp.mouse_position[1] < gui.panelY and quick_d_timer.get() > 1: + ddt.rect((x, y, 2 * gui.scale, gui.panelY2), [80, 200, 180, 255]) - lyrics_fetch_timer.force_set(t - 10) + if inp.mouse_up: + drop_tracks_to_new_playlist(shift_selection) - if not silent: - show_message(_("Searching...")) + # Draw end drag tab indicator + if tauon.playlist_box.drag and inp.mouse_position[0] > x and inp.mouse_position[1] < gui.panelY: + if inp.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]) - s_artist = track_object.artist - s_title = track_object.title + if prefs.tabs_on_top and right_overflow: + x += 24 * gui.scale + self.tabs_right_x += 24 * gui.scale - 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] + # ------------- + # Other input + if inp.mouse_up: + inp.quick_drag = False + tauon.playlist_box.drag = False + radio_view.drag = None - logging.info(f"Searching for lyrics: {s_artist} - {s_title}") + # Scroll anywhere on panel to cycle playlist + # (This is a bit complicated because we need to skip over hidden playlists) + if inp.mouse_wheel != 0 and 1 < inp.mouse_position[1] < gui.panelY + 1 and len(pctl.multi_playlist) > 1 and inp.mouse_position[0] > 5: - found = False - for name in prefs.lyrics_enables: + cycle_playlist_pinned(inp.mouse_wheel) - if name in lyric_sources.keys(): - func = lyric_sources[name] + 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 - 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") + x += self.ini_menu_space + y += 7 * gui.scale + ddt.text_background_colour = colours.top_panel_background - if not found: - logging.error(f"Could not find lyrics from source {name}") + # MENU ----------------------------- - 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() + 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 = tauon.coll(rect) + tauon.fields.add(rect) + if (tauon.x_menu.active or hit) and not tauon.tab_menu.active: + bg = colours.status_text_over + else: + bg = colours.status_text_normal + ddt.text((x, y), word, bg, 212) -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 hit and inp.mouse_click: + if tauon.x_menu.active: + tauon.x_menu.active = False + else: + xx = x + if x > window_size[0] - (210 * gui.scale): + xx = window_size[0] - round(210 * gui.scale) + tauon.x_menu.activate(position=(xx + round(12 * gui.scale), gui.panelY)) + view_box.activate(xx) - shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object])) - shoot_dl.daemon = True - shoot_dl.start() + # 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)) + # - logging.info("..Done") + dl = len(tauon.dl_mon.ready) + watching = len(tauon.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) + tauon.fields.add(rect) -def get_lyric_wiki_silent(track_object: TrackClass): - logging.info("Searching for lyrics...") + if tauon.coll(rect): + colour = colours.corner_button_active + # if colours.lm: + # colour = [40, 40, 40, 255] + if dl > 0 or watching > 0: + if right_click: + tauon.dl_menu.activate(position=(inp.mouse_position[0], gui.panelY)) + if dl > 0: + if inp.mouse_click: + pln = 0 + for item in tauon.dl_mon.ready: + load_order = LoadClass() + load_order.target = item + pln = pctl.active_playlist_viewing + load_order.playlist = pctl.multi_playlist[pln].uuid_int - if track_object.artist == "" or track_object.title == "": - return + 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 - shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object, True])) - shoot_dl.daemon = True - shoot_dl.start() + load_orders.append(copy.deepcopy(load_order)) - logging.info("..Done") + if len(tauon.dl_mon.ready) > 0: + tauon.dl_mon.ready.clear() + switch_playlist(pln) + pctl.playlist_view_position = len(pctl.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") -def test_auto_lyrics(track_object: TrackClass): - if not track_object: - return - 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) + colour = colours.corner_button # [60, 60, 60, 255] + if colours.lm: + # colour = [180, 180, 180, 255] + if tauon.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] -def get_bio(track_object: TrackClass): - if track_object.artist != "": - lastfm.get_bio(track_object.artist) + # LAYOUT -------------------------------- + x += self.menu_space + word_length + self.drag_zone_start_x = x - 5 * gui.scale + status = True -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 pctl.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 tauon.coll([x, y, 180 * gui.scale, 18 * gui.scale]): + tauon.cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) + elif tauon.after_scan: + # bg = colours.status_info_text + bg = [100, 200, 100, 255] + text = _("Scanning Tags... {N} remaining").format(N=str(len(tauon.after_scan))) + elif tauon.move_in_progress: + text = _("File copy in progress...") + bg = colours.status_info_text + elif tauon.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 tauon.to_scan: + text = _("Rescanning Tags... {N} remaining").format(N=str(len(tauon.to_scan))) + bg = [100, 200, 100, 255] + elif tauon.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 tauon.subsonic.scanning: + text = _("Accessing AIRSONIC library...") + if gui.to_got: + text += f" {gui.to_got}" + bg = [58, 194, 224, 255] + elif tauon.koel.scanning: + text = _("Accessing KOEL library...") + bg = [111, 98, 190, 255] + elif tauon.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 tauon.transcode_list: + text = gui.sync_progress + bg = [100, 200, 100, 255] + if right_click and tauon.coll([x, y, 280 * gui.scale, 18 * gui.scale]): + cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) + elif tauon.transcode_list and gui.tc_cancel: + bg = [150, 150, 150, 255] + text = _("Stopping transcode...") + elif tauon.lastfm.scanning_friends or tauon.lastfm.scanning_loves: + text = _("Scanning: ") + tauon.lastfm.scanning_username + bg = [200, 150, 240, 255] + elif tauon.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] - return [line_colour, colours.menu_background, None] + elif tauon.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 -showcase_menu.add(MenuItem(_("Search for Lyrics"), get_lyric_wiki, search_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + if status: + x += ddt.text((x, y), text, bg, 311) + # x += ddt.get_text_w(text, 11) + # TODO list listenieng clients + elif tauon.transcode_list: + bg = colours.status_info_text + # if inp.key_ctrl_down and key_c_press: + # del transcode_list[1:] + # gui.tc_cancel = True + if right_click and tauon.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 -def toggle_synced_lyrics(tr): - prefs.prefer_synced_lyrics ^= True + if gui.transcoding_batch_total: -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") + # 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] - return [line_colour, colours.menu_background, text] + c1 = [40, 40, 40, 255] + c2 = [100, 59, 200, 200] + c3 = [150, 70, 200, 255] -showcase_menu.add(MenuItem("Toggle synced", toggle_synced_lyrics, toggle_synced_lyrics_deco, pass_ref=True, pass_ref_deco=True)) + if colours.lm: + c1 = [100, 100, 100, 255] + c2 = [170, 140, 255, 255] + c3 = [230, 170, 255, 255] -def paste_lyrics_deco(): - if SDL_HasClipboardText(): - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + 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) - return [line_colour, colours.menu_background, None] + done = round(gui.transcoding_bach_done / gui.transcoding_batch_total * 100) + doing = round(core_use / gui.transcoding_batch_total * 100) -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") + ddt.rect([x, yy, done, h], c3) + ddt.rect([x + done, yy, doing, h], c2) -#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)) + x += w + 8 * gui.scale -#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)) + if gui.sync_progress: + text = gui.sync_progress + else: + text = _("{N} Folder Remaining {T}").format(N=str(len(transcode_list)), T=tauon.transcode_state) + if len(transcode_list) > 1: + text = _("{N} Folders Remaining {T}").format(N=str(len(transcode_list)), T=tauon.transcode_state) + x += ddt.text((x, y), text, bg, 311) + 8 * gui.scale -def copy_lyrics_deco(track_object: TrackClass): - if track_object.lyrics: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled - return [line_colour, colours.menu_background, None] + 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, tauon: Tauon): + self.tauon = tauon + self.gui = tauon.gui + self.inp = tauon.gui.inp + self.prefs = tauon.prefs + self.pctl = tauon.pctl + self.colours = tauon.bag.colours + self.window_size = tauon.bag.window_size + self.ddt = tauon.bag.ddt + self.mode = 0 -def copy_lyrics(track_object: TrackClass): - copy_to_clipboard(track_object.lyrics) + 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 * self.gui.scale + self.repeat_click_off = False + self.random_click_off = False -def clear_lyrics(track_object: TrackClass): - track_object.lyrics = "" + self.seek_bar_position = [300 * self.gui.scale, self.window_size[1] - self.gui.panelBY] + self.seek_bar_size = [self.window_size[0] - (300 * self.gui.scale), 15 * self.gui.scale] + self.volume_bar_size = [135 * self.gui.scale, 14 * self.gui.scale] + self.volume_bar_position = [0, 45 * self.gui.scale] + self.play_button = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "play.png", True) + self.forward_button = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "ff.png", True) + self.back_button = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "bb.png", True) + self.repeat_button = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "tauon_repeat.png", True) + self.repeat_button_off = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "tauon_repeat_off.png", True) + self.shuffle_button_off = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "tauon_shuffle_off.png", True) + self.shuffle_button = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "tauon_shuffle.png", True) + self.repeat_button_a = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "tauon_repeat_a.png", True) + self.shuffle_button_a = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "tauon_shuffle_a.png", True) -def clear_lyrics_deco(track_object: TrackClass): - if track_object.lyrics: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + self.buffer_shard = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "shard.png", True) - return [line_colour, colours.menu_background, None] + self.scrob_stick = 0 + def update(self): + if self.mode == 0: + self.volume_bar_position[0] = self.window_size[0] - (210 * self.gui.scale) + self.volume_bar_position[1] = self.window_size[1] - (27 * self.gui.scale) + self.seek_bar_position[1] = self.window_size[1] - self.gui.panelBY -def split_lyrics(track_object: TrackClass): - if track_object.lyrics != "": - track_object.lyrics = track_object.lyrics.replace(". ", ". \n") - else: - pass - - -def show_sub_search(track_object: TrackClass): - sub_lyrics_box.activate(track_object) - + seek_bar_x = 300 * self.gui.scale + if self.window_size[0] < 600 * self.gui.scale: + seek_bar_x = 250 * self.gui.scale -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)) - -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 + self.seek_bar_size[0] = self.window_size[0] - seek_bar_x + self.seek_bar_position[0] = seek_bar_x -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 gui.bb_show_art: + # self.seek_bar_position[0] = 300 + gui.panelBY + # self.seek_bar_size[0] = window_size[0] - 300 - gui.panelBY - if save_embed_img_disable_test(track_object): - show_message(_("Saving network images not implemented")) - return + # self.seek_bar_position[0] = 0 + # self.seek_bar_size[0] = window_size[0] - try: - pic = album_art_gen.get_embed(track_object) + def render(self): + global volume_store + global clicked - if not pic: - show_message(_("Image save error."), _("No embedded album art found file."), mode="warning") - return + window_size = self.window_size + tauon = self.tauon + ddt = self.ddt + gui = self.gui + prefs = self.prefs + pctl = self.pctl + inp = self.inp + colours = self.colours + fonts = self.tauon.bag.fonts - source_image = io.BytesIO(pic) - im = Image.open(source_image) + ddt.rect_a((0, self.window_size[1] - self.gui.panelBY), (self.window_size[0], self.gui.panelBY), colours.bottom_panel_colour) - source_image.close() + ddt.rect_a(self.seek_bar_position, self.seek_bar_size, colours.seek_bar_background) - ext = "." + im.format.lower() - if im.format == "JPEG": - ext = ".jpg" + right_offset = 0 + if gui.display_time_mode >= 2: + right_offset = 22 * self.gui.scale - target = os.path.join(folder, "embed-" + str(im.height) + "px-" + str(track_object.index) + ext) + if self.window_size[0] < 670 * self.gui.scale: + right_offset -= 90 * self.gui.scale + # Scrobble marker - if len(pic) > 30: - with open(target, "wb") as w: - w.write(pic) + 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 - open_folder(track_object.index) + 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)) - except Exception: - logging.exception("Unknown error trying to save an image") - show_message(_("Image save error."), _("A mysterious error occurred"), mode="error") + 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 * self.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])) -picture_menu = Menu(175) + # ddt.rect_r(rect, [255, 255, 255, 20]) + # SEEK BAR------------------ + if pctl.playing_time < 1: + self.seek_time = 0 -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) + if inp.mouse_click and coll_point( + inp.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 inp.right_click and coll_point( + inp.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() - if info is None: - return [colours.menu_text_disabled, colours.menu_background, None] + tauon.fields.add(self.seek_bar_position + self.seek_bar_size) + if tauon.coll(self.seek_bar_position + self.seek_bar_size): - line_colour = colours.menu_text + if middle_click and pctl.playing_state > 0: + gui.seek_cur_show = True - return [line_colour, colours.menu_background, None] + clicked = True + if inp.mouse_wheel != 0: + pctl.seek_time(pctl.playing_time + (inp.mouse_wheel * 3)) + if gui.seek_cur_show: + gui.update += 1 -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 + # tauon.fields.add([inp.mouse_position[0] - 1, inp.mouse_position[1] - 1, 1, 1]) + # ddt.rect_r([inp.mouse_position[0] - 1, inp.mouse_position[1] - 1, 1, 1], [255,0,0,180], True) -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) + bargetX = inp.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 inp.mouse_position[0] == 0: + self.seek_down = False + self.seek_hit = True -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 (inp.mouse_up and tauon.coll(self.seek_bar_position + self.seek_bar_size) \ + and coll_point(inp.last_click_location, self.seek_bar_position + self.seek_bar_size) \ + and coll_point( inp.click_location, self.seek_bar_position + self.seek_bar_size)) \ + or (inp.mouse_up and self.volume_hit) or self.seek_hit: + self.volume_hit = False + self.seek_down = False + self.seek_hit = False - if info is None: - return [colours.menu_text_disabled, colours.menu_background, None] + bargetX = inp.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] - if info[0] == 1: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + pctl.seek_decimal(seek) + #logging.info(seek) - return [line_colour, colours.menu_background, None] + self.seek_time = pctl.playing_time + if tauon.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) -picture_menu.add(MenuItem(_("Open Image"), open_image, open_image_deco, pass_ref=True, pass_ref_deco=True, disable_test=open_image_disable_test)) + # 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 -def cycle_image_deco(track_object: TrackClass): - info = album_art_gen.get_info(track_object) + 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_state != 0 and (info is not None and info[1] > 1): - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + if pctl.playing_length > 0: + if pctl.download_time != 0: + if pctl.download_time == -1: + pctl.download_time = pctl.playing_length - return [line_colour, colours.menu_background, None] + colour = (255, 255, 255, 10) + if gui.theme_name == "Lavender Light" or gui.theme_name == "Carbon": + colour = (255, 255, 255, 40) -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) + 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) - if info is not None and info[1] > 1: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + 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) - return [line_colour, colours.menu_background, None] + if gui.seek_cur_show: + if tauon.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 inp.mouse_position[0] > self.seek_bar_position[0] - 1: + cur = [inp.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( + (inp.mouse_position[0] - 40 + 3, self.seek_bar_position[1] - 24), gui.cur_time, + colours.grey(180), 213, + bg=colours.grey(15)) -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) + ddt.rect( + [inp.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 inp.mouse_wheel != 0 and inp.mouse_position[1] > self.seek_bar_position[1] + 4 \ + and not coll_point(inp.mouse_position, self.seek_bar_position + self.seek_bar_size): + pctl.player_volume += inp.mouse_wheel * prefs.volume_wheel_increment + if pctl.player_volume < 1: + pctl.player_volume = 0 + elif pctl.player_volume > 100: + pctl.player_volume = 100 -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) + 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) -# 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) + rect = (x - 8 * gui.scale, y - 17 * gui.scale, 55 * gui.scale, 23 * gui.scale) + # ddt.rect(rect, [255,255,255,25]) + if tauon.coll(rect) and inp.mouse_down: + gui.update_on_drag = 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)) + h_rect = (x - 6 * gui.scale, y - 17 * gui.scale, 4 * gui.scale, 23 * gui.scale) + if tauon.coll(h_rect) and inp.mouse_down: + pctl.player_volume = 0 + step = round(1 * gui.scale) + min_h = round(4 * gui.scale) + spacing = round(5 * gui.scale) -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] + if right_click and tauon.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): -def download_art1(tr): - if tr.is_network: - show_message(_("Cannot download art for network tracks.")) - return + 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) - # Determine noise of folder ---------------- - siblings = [] - parent = tr.parent_folder_path + if tauon.coll(h_rect): + if inp.mouse_down or inp.mouse_up: + gui.update_on_drag = True - 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) + 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 - album_tags = [] - date_tags = [] + pctl.set_volume() - for tr in siblings: - album_tags.append(tr.album) - date_tags.append(tr.date) + colour = colours.mode_button_off - album_tags = set(album_tags) - date_tags = set(date_tags) + 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 - 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 + ddt.rect(rect, colour) + x += spacing - # ------------------------------------------- + # Volume Bar -------------------------------------------------------- + else: + if (inp.mouse_click and tauon.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 not os.path.isdir(tr.parent_folder_path): - show_message(_("Directory missing.")) - return + if inp.mouse_click is True or self.volume_bar_being_dragged is True: + gui.update = 2 - try: - show_message(_("Looking up MusicBrainz ID...")) + self.volume_bar_being_dragged = True + volgetX = inp.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 - if "musicbrainz_releasegroupid" not in tr.misc or "musicbrainz_artistids" not in tr.misc or not tr.misc[ - "musicbrainz_artistids"]: + time.sleep(0.02) - logging.info("MusicBrainz ID lookup...") + if inp.mouse_down is False: + self.volume_bar_being_dragged = False + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume(True) - artist = tr.album_artist - if not tr.album: - return - if not artist: - artist = tr.artist + if inp.mouse_down: + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume(False) - s = musicbrainzngs.search_release_groups(tr.album, artist=artist, limit=1) + if inp.right_click and tauon.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)): - album_id = s["release-group-list"][0]["id"] - artist_id = s["release-group-list"][0]["artist-credit"][0]["artist"]["id"] + if pctl.player_volume > 0: + volume_store = pctl.player_volume + pctl.player_volume = 0 + else: + pctl.player_volume = volume_store - logging.info("Found release group ID: " + album_id) - logging.info("Found artist ID: " + artist_id) + pctl.set_volume() - else: + ddt.rect_a( + (self.volume_bar_position[0] - right_offset, self.volume_bar_position[1]), + self.volume_bar_size, colours.volume_bar_background) # 22 - album_id = tr.misc["musicbrainz_releasegroupid"] - artist_id = tr.misc["musicbrainz_artistids"][0] + 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]) - logging.info("Using tagged release group ID: " + album_id) - logging.info("Using tagged artist ID: " + artist_id) + ddt.rect(gui.volume_bar_rect, colours.volume_bar_fill) - if prefs.enable_fanart_cover: - try: - show_message(_("Searching fanart.tv for cover art...")) + tauon.fields.add(self.volume_bar_position + self.volume_bar_size) + if pctl.active_replaygain != 0 and (tauon.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): - r = requests.get("https://webservice.fanart.tv/v3/music/albums/" \ - + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) + 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) - artlink = r.json()["albums"][album_id]["albumcover"][0]["url"] - id = r.json()["albums"][album_id]["albumcover"][0]["id"] + gui.show_bottom_title = gui.showed_title ^ True + if not prefs.hide_bottom_title: + gui.show_bottom_title = True - response = urllib.request.urlopen(artlink, context=ssl_context) - info = response.info() + if gui.show_bottom_title and pctl.playing_state > 0 and window_size[0] > 820 * gui.scale: + line = pctl.title_text() - t = io.BytesIO() - t.seek(0) - t.write(response.read()) - t.seek(0, 2) - l = t.tell() - t.seek(0) + 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 - if info.get_content_maintype() == "image" and l > 1000: + # 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 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 + if (inp.mouse_click or inp.right_click) and tauon.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 inp.right_click: + # input.mouse_click = False + # inp.right_click = False + # else: + if inp.mouse_click and pctl.playing_state != 3: + pctl.show_current() - f = open(filepath, "wb") - f.write(t.read()) - f.close() + if pctl.playing_ready() and not gui.fullscreen: + if inp.right_click: + mode_menu.activate() - 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)) + if d_click_timer.get() < 0.3 and inp.mouse_click: + set_mini_mode() + gui.update += 1 return - except Exception: - logging.exception("Failed to get from fanart.tv") - - 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() + d_click_timer.set() - show_message(_("Cover art downloaded from MusicBrainz"), mode="done") - # clear_img_cache() - clear_track_image_cache(tr) + # TIME---------------------- - 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)) + x = window_size[0] - 57 * gui.scale + y = window_size[1] - 29 * gui.scale - return + 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 tauon.coll(rect): + gui.display_time_mode += 1 + if gui.display_time_mode > 3: + gui.display_time_mode = 0 - 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 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) -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 + 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) -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() + offset1 = 10 * gui.scale + if system == "Windows": + offset1 += 2 * gui.scale -def remove_embed_picture(track_object: TrackClass, dry: bool = True) -> int | None: - """Return amount of removed objects or None""" - index = track_object.index + offset2 = offset1 + 7 * gui.scale - 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) + 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: - removed = 0 - if not dry: - pr = pctl.stop(True) - try: - for item in tracks: + gui.dtm3_cum = 0 + gui.dtm3_total = 0 + run = True + collected = [] + for item in pctl.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 - tr = pctl.get_track(item) + x -= 4 + text_time = get_display_time(gui.dtm3_cum + pctl.playing_time) - if tr.is_cue: - continue + ddt.text( + (x - 25 * gui.scale, y), text_time, colours.time_playing, + fonts.bottom_panel_time) - if tr.is_network: - continue + offset1 = 10 * gui.scale + if system == "Windows": + offset1 += 2 * gui.scale + offset2 = offset1 + 7 * gui.scale - 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") + 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) - 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") + # BUTTONS + # bottom buttons - 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 gui.mode == 1: - 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") + # PLAY--- + buttons_x_offset = 0 + compact = False + if window_size[0] < 650 * gui.scale: + compact = True - clear_track_image_cache(tr) + 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 - except Exception: - logging.exception("Image remove error") - show_message(_("Image remove error"), mode="error") - return None + if pctl.playing_state == 1: + play_colour = colours.media_buttons_active - if dry: - return removed + if pctl.auto_stop: + stop_colour = colours.media_buttons_active - 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() + 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) + tauon.fields.add(rect) + if tauon.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")) -del_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "del.png", True) -delete_icon = MenuIcon(del_icon) + 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) -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") + # PAUSE--- + if compact: + buttons_x_offset = -46 * gui.scale + x = (75 * gui.scale) + buttons_x_offset + y = window_size[1] - self.control_line_bottom -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 not compact or (compact and pctl.playing_state == 1): - text = _("Delete Image File") - line_colour = colours.menu_text + rect = (x - 15 * gui.scale, y - 13 * gui.scale, 50 * gui.scale, 40 * gui.scale) + tauon.fields.add(rect) + if tauon.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")) - if info is None or track_object.is_network: - return [colours.menu_text_disabled, colours.menu_background, None] + # 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) - if info and info[0] == 0: - text = _("Delete Image File") + # STOP--- + x = 125 * gui.scale + buttons_x_offset + rect = (x - 14 * gui.scale, y - 13 * gui.scale, 50 * gui.scale, 40 * gui.scale) + tauon.fields.add(rect) + if tauon.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")) - 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 + ddt.rect_a((x, y + 0), (13 * gui.scale, 13 * gui.scale), stop_colour) + # ddt.rect_r(rect,[255,0,0,255], True) - text = _("Delete Embedded | Folder") - if key_shift_down or key_shiftr_down: - text = _("Delete Embedded | Track") + if compact: + buttons_x_offset -= 5 * gui.scale - return [line_colour, colours.menu_background, text] + # FORWARD--- + rect = (buttons_x_offset + 230 * gui.scale, window_size[1] - self.control_line_bottom - 10 * gui.scale, + 50 * gui.scale, 35 * gui.scale) + tauon.fields.add(rect) + if tauon.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) -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") + # 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) + tauon.fields.add(rect) + if tauon.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) -picture_menu.add( - MenuItem(_("Delete Image File"), delete_track_image, delete_track_image_deco, pass_ref=True, - pass_ref_deco=True, icon=delete_icon)) + # menu button -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)) + 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) + tauon.fields.add(rect) + if tauon.coll(rect): + if not tauon.extra_menu.active: + tool_tip.test(x, y - 28 * gui.scale, _("Playback menu")) + rpbc = colours.mode_button_over + if inp.mouse_click: + tauon.extra_menu.activate(position=(x - 115 * gui.scale, y - 6 * gui.scale)) + elif inp.right_click: + mode_menu.activate(position=(x - 115 * gui.scale, y - 6 * gui.scale)) + if tauon.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) -def toggle_gimage(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_gimage - prefs.show_gimage ^= True - return None + 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 -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 + rect = (x - 5 * gui.scale, y - 5 * gui.scale, 60 * gui.scale, 25 * gui.scale) + tauon.fields.add(rect) - return [line_colour, colours.menu_background, None] + rpbc = colours.mode_button_off + off = True + if (inp.mouse_click or inp.right_click) and tauon.coll(rect): + if inp.mouse_click: + # pctl.random_mode ^= True + toggle_random() + if pctl.random_mode is False: + self.random_click_off = True + else: + tauon.shuffle_menu.activate(position=(x + 30 * gui.scale, y - 7 * gui.scale)) + if pctl.random_mode: + rpbc = colours.mode_button_active + off = False + if tauon.coll(rect): + tool_tip.test(x, y - 28 * gui.scale, _("Shuffle")) + elif tauon.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 -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) + # Keep hover highlight on if menu is open + if tauon.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) -# picture_menu.add(_('Search Google for Images'), ser_gimage, search_image_deco, pass_ref=True, pass_ref_deco=True, show_test=toggle_gimage) + #y += round(3 * gui.scale) + #ddt.rect_a((x, y), (25 * gui.scale, 3 * gui.scale), rpbc) -# picture_menu.add(_('Toggle art box'), toggle_side_art, toggle_side_art_deco) + 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) -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)) + #ddt.rect_a((x + 25 * gui.scale, y), (23 * gui.scale, 3 * gui.scale), rpbc) -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 "), 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)) + #y += round(5 * gui.scale) + #ddt.rect_a((x, y), (48 * gui.scale, 3 * gui.scale), rpbc) -def append_here(): - global cargo - global default_playlist - default_playlist += cargo + # 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 -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") + rect = (x - 6 * gui.scale, y - 5 * gui.scale, 61 * gui.scale, 25 * gui.scale) + tauon.fields.add(rect) + if (inp.mouse_click or inp.right_click) and tauon.coll(rect): + if inp.mouse_click: + toggle_repeat() + if pctl.repeat_mode is False: + self.repeat_click_off = True + else: # right click + tauon.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 active: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + if pctl.repeat_mode: + rpbc = colours.mode_button_active + off = False + if tauon.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 tauon.coll(rect): - return [line_colour, colours.menu_background, line] + # Tooltips. But don't show tooltips if menus open + if not tauon.repeat_menu.active and not tauon.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 -def lightning_move_test(discard): - return gui.lightning_copy and prefs.show_transfer + # Keep hover highlight on if menu is open + if tauon.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 -# 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] + 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) -# playlist_menu.add('Paste', append_here, paste_deco) + 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) -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 + # 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, bag: Bag, gui: GuiVar): + self.window_size = bag.window_size + self.gui = gui -def re_template_word(word, tr): - if word == "aa" or word == "albumartist": + 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 * self.gui.scale + self.repeat_click_off = False + self.random_click_off = False - if tr.album_artist: - return tr.album_artist - return tr.artist + self.seek_bar_position = [300 * self.gui.scale, self.window_size[1] - self.gui.panelBY] + self.seek_bar_size = [self.window_size[0] - (300 * self.gui.scale), 15 * self.gui.scale] + self.volume_bar_size = [135 * self.gui.scale, 14 * self.gui.scale] + self.volume_bar_position = [0, 45 * self.gui.scale] - if word == "a" or word == "artist": - return tr.artist + self.play_button = asset_loader(bag, bag.loaded_asset_dc, "play.png", True) + self.forward_button = asset_loader(bag, bag.loaded_asset_dc, "ff.png", True) + self.back_button = asset_loader(bag, bag.loaded_asset_dc, "bb.png", True) - if word == "t" or word == "title": - return tr.title + self.scrob_stick = 0 - 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) + def update(self): - if word == "sn" or word == "singlenumber" or word == "singletracknumber" or word == "s": - return str(tr.track_number) + if self.mode == 0: + self.volume_bar_position[0] = self.window_size[0] - (210 * self.gui.scale) + self.volume_bar_position[1] = self.window_size[1] - (27 * self.gui.scale) + self.seek_bar_position[1] = self.window_size[1] - self.gui.panelBY - if word == "d" or word == "date" or word == "year": - return str(tr.date) + seek_bar_x = 300 * self.gui.scale + if self.window_size[0] < 600 * self.gui.scale: + seek_bar_x = 250 * self.gui.scale - if word == "b" or "album" in word: - return str(tr.album) + self.seek_bar_size[0] = self.window_size[0] - seek_bar_x + self.seek_bar_position[0] = seek_bar_x - if word == "g" or word == "genre": - return tr.genre + # if gui.bb_show_art: + # self.seek_bar_position[0] = 300 + gui.panelBY + # self.seek_bar_size[0] = window_size[0] - 300 - gui.panelBY - if word == "x" or "ext" in word or "file" in word: - return tr.file_ext.lower() + # self.seek_bar_position[0] = 0 + # self.seek_bar_size[0] = window_size[0] - if word == "ux" or "upper" in word: - return tr.file_ext.upper() + def render(self): + global volume_store + global clicked - if word == "c" or "composer" in word: - return tr.composer + ddt.rect_a((0, self.window_size[1] - self.gui.panelBY), (self.window_size[0], self.gui.panelBY), colours.bottom_panel_colour) - if "comment" in word: - return tr.comment.replace("\n", "").replace("\r", "") + right_offset = 0 + if gui.display_time_mode >= 2: + right_offset = 22 * self.gui.scale - return "" + if self.window_size[0] < 670 * self.gui.scale: + right_offset -= 90 * self.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])) -def parse_template2(string: str, track_object: TrackClass, strict: bool = False): - temp = "" - out = "" + # ddt.rect_r(rect, [255, 255, 255, 20]) - mode = 0 + # Volume mouse wheel control ----------------------------------------- + if inp.mouse_wheel != 0 and inp.mouse_position[1] > self.seek_bar_position[1] + 4 and not coll_point( + inp.mouse_position, self.seek_bar_position + self.seek_bar_size): - for c in string: + pctl.player_volume += inp.mouse_wheel * prefs.volume_wheel_increment + if pctl.player_volume < 1: + pctl.player_volume = 0 + elif pctl.player_volume > 100: + pctl.player_volume = 100 - if mode == 0: + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() - if c == "<": - mode = 1 - else: - out += c + # mode menu + if inp.right_click: + if inp.mouse_position[0] > 190 * gui.scale and \ + inp.mouse_position[1] > window_size[1] - gui.panelBY and \ + inp.mouse_position[0] < window_size[0] - 190 * gui.scale: + mode_menu.activate() - else: + # Volume Bar 2 ------------------------------------------------ + if True: + x = window_size[0] - right_offset - 120 * gui.scale + y = window_size[1] - round(21 * gui.scale) - if c == ">": + if gui.compact_bar: + x -= 90 * gui.scale - test = re_template_word(temp, track_object) - if strict: - assert test - out += test + rect = (x - 8 * gui.scale, y - 17 * gui.scale, 55 * gui.scale, 23 * gui.scale) + # ddt.rect(rect, [255,255,255,25]) + if tauon.coll(rect) and inp.mouse_down: + gui.update_on_drag = True - mode = 0 - temp = "" + h_rect = (x - 6 * gui.scale, y - 17 * gui.scale, 4 * gui.scale, 23 * gui.scale) + if tauon.coll(h_rect) and inp.mouse_down: + pctl.player_volume = 0 - else: + step = round(1 * gui.scale) + min_h = round(4 * gui.scale) + spacing = round(5 * gui.scale) - temp += c + if inp.right_click and tauon.coll((h_rect[0], h_rect[1], h_rect[2] + 50 * gui.scale, h_rect[3])): + if inp.right_click: + if pctl.player_volume > 0: + volume_store = pctl.player_volume + pctl.player_volume = 0 + else: + pctl.player_volume = volume_store - if "<und" in string: - out = out.replace(" ", "_") + pctl.set_volume() - return parse_template(out, track_object, strict=strict) + 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 tauon.coll(h_rect): + if inp.mouse_down: + gui.update_on_drag = True -def parse_template(string, track_object: TrackClass, up_ext: bool = False, strict: bool = False): - set = 0 - underscore = False - output = "" + 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 - 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.set_volume() - output = output.rstrip(" -").lstrip(" -") + colour = colours.mode_button_off - if underscore: - output = output.replace(" ", "_") + 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 - # Attempt to ensure the output text is filename safe - output = filename_safe(output) + ddt.rect(rect, colour) + x += spacing - return output + # TIME---------------------- + x = window_size[0] - 57 * gui.scale + y = window_size[1] - 35 * gui.scale -# Create playlist tab menu -tab_menu = Menu(160, show_icons=True) -radio_tab_menu = Menu(160, show_icons=True) + 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 tauon.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: -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] + colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) - 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) + 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) - rename_playlist_box.y = min(rename_playlist_box.y, round(350 * gui.scale)) + offset1 = 10 * gui.scale - if rename_playlist_box.y < gui.panelY: - rename_playlist_box.y = gui.panelY + 10 * gui.scale + if system == "Windows": + offset1 += 2 * gui.scale - 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 + offset2 = offset1 + 7 * gui.scale - if generator: - rename_playlist_box.toggle_edit_gen() + 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: -def edit_generator_box(index: int) -> None: - rename_playlist(index, generator=True) + 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: -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")) + gui.dtm3_cum = 0 + gui.dtm3_total = 0 + run = True + collected = [] + for item in pctl.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) -def pin_playlist_toggle(pl: int) -> None: - pctl.multi_playlist[pl].hidden ^= True + 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 -def pl_pin_deco(pl: int): - # if pctl.multi_playlist[pl].hidden == True and tab_menu.pos[1] > + 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) - if pctl.multi_playlist[pl].hidden == True: - return [colours.menu_text, colours.menu_background, _("Pin")] - return [colours.menu_text, colours.menu_background, _("Unpin")] + # BUTTONS + # bottom buttons + if gui.mode == 1: -tab_menu.add(MenuItem("Pin", pin_playlist_toggle, pl_pin_deco, pass_ref=True, pass_ref_deco=True)) + # 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 -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 pctl.playing_state == 1: + play_colour = colours.media_buttons_active + if pctl.auto_stop: + stop_colour = colours.media_buttons_active -def view_pl_is_locked(_) -> bool: - return pctl.multi_playlist[pctl.active_playlist_viewing].locked + 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) + tauon.fields.add(rect) + if tauon.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")) -def pl_is_locked(pl: int) -> bool: - if not pctl.multi_playlist: - return False - return pctl.multi_playlist[pl].locked + 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) -def lock_playlist_toggle(pl: int) -> None: - pctl.multi_playlist[pl].locked ^= 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 -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 not compact or (compact and pctl.playing_state == 2): + rect = (x - 15 * gui.scale, y - 13 * gui.scale, 50 * gui.scale, 40 * gui.scale) + tauon.fields.add(rect) + if tauon.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")) -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 + # 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) -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)) + # FORWARD--- + rect = ( + buttons_x_offset + 125 * gui.scale, + window_size[1] - self.control_line_bottom - 10 * gui.scale, 50 * gui.scale, 35 * gui.scale) + tauon.fields.add(rect) + if tauon.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) -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 +class MiniMode: + def __init__(self, bag: Bag, gui: GuiVar): + self.window_size = bag.window_size + self.gui = gui + self.save_position = None + self.was_borderless = True + self.volume_timer = Timer() + self.volume_timer.force_set(100) - 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") + self.left_slide = asset_loader(bag, bag.loaded_asset_dc, "left-slide.png", True) + self.right_slide = asset_loader(bag, bag.loaded_asset_dc, "right-slide.png", True) + self.repeat = asset_loader(bag, bag.loaded_asset_dc, "repeat-mini-mode.png", True) + self.shuffle = asset_loader(bag, bag.loaded_asset_dc, "shuffle-mini-mode.png", True) - 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 + self.shuffle_fade_timer = Timer(100) + self.repeat_fade_timer = Timer(100) - 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() + 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 - 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 + w = self.window_size[0] + h = self.window_size[1] + y1 = w + if w == h: + y1 -= 79 * self.gui.scale -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 + h1 = h - y1 - if not direc: - direc = str(user_directory / "playlists") - if not os.path.exists(direc): - os.makedirs(direc) + # Draw background + bg = colours.mini_mode_background + # bg = [250, 250, 250, 255] - target = os.path.join(direc, pctl.multi_playlist[pl].title + ".xspf") + ddt.rect((0, 0, w, h), bg) + ddt.text_background_colour = bg - xspf_root = ET.Element("playlist", version="1", xmlns="http://xspf.org/ns/0/") - xspf_tracklist_tag = ET.SubElement(xspf_root, "trackList") + detect_mouse_rect = (3, 3, w - 6, h - 6) + tauon.fields.add(detect_mouse_rect) + mouse_in = tauon.coll(detect_mouse_rect) - 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) + # Play / Pause when right clicking below art + if right_click: # and inp.mouse_position[1] > y1: + pctl.play_pause() - 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) + # Volume change on scroll + if inp.mouse_wheel != 0: + self.volume_timer.set() - ET.SubElement(xspf_track_tag, "duration").text = str(int(track.length * 1000)) + pctl.player_volume += inp.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 - xspf_tree = ET.ElementTree(xspf_root) - ET.indent(xspf_tree, space=' ', level=0) - xspf_tree.write(target, encoding='UTF-8', xml_declaration=True) + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() - 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]) + track = pctl.playing_object() - return target + control_hit_area = (3, y1 - 15 * gui.scale, w - 6, h1 - 3 + 15 * gui.scale) + mouse_in_area = tauon.coll(control_hit_area) + tauon.fields.add(control_hit_area) + ddt.rect((0, 0, w, w), (0, 0, 0, 45)) + if track is not None: -def reload(): - if album_mode: - reload_albums(quiet=True) + # Render album art + album_art_gen.display(track, (0, 0), (w, w)) - # tree_view_box.clear_all() - # elif gui.combo_mode: - # reload_albums(quiet=True) - # combo_pl_render.prep() + 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] -def clear_playlist(index: int): - global default_playlist + # Double click bottom text to return to full window + text_hit_area = (60 * gui.scale, y1 + 4, 230 * gui.scale, 50 * gui.scale) - if pl_is_locked(index): - show_message(_("Playlist is locked to prevent accidental erasure")) - return + if tauon.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() - pctl.multi_playlist[index].last_folder.clear() # clear import folder list # TODO(Martin): This was actually a string not a list wth? + # Draw title texts + line1 = track.artist + line2 = track.title - if not pctl.multi_playlist[index].playlist_ids: - logging.info("Playlist is already empty") - return + # Calculate seek bar position + seek_w = int(w * 0.70) - li = [] - for i, ref in enumerate(pctl.multi_playlist[index].playlist_ids): - li.append((i, ref)) + 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] - undo.bk_tracks(index, list(reversed(li))) + if w != h or mouse_in_area: - del pctl.multi_playlist[index].playlist_ids[:] - if pctl.active_playlist_viewing == index: - default_playlist = pctl.multi_playlist[index].playlist_ids - reload() + 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: - # pctl.playlist_playing = 0 - pctl.multi_playlist[index].position = 0 - if index == pctl.active_playlist_viewing: - pctl.playlist_view_position = 0 + ddt.text((w // 2, y1 + 10 * gui.scale, 2), line1, line2c, 514, window_size[0] - 30 * gui.scale) - gui.pl_update = 1 + ddt.text((w // 2, y1 + 31 * gui.scale, 2), line2, line1c, 414, window_size[0] - 30 * gui.scale) + # Test click to seek + if inp.mouse_up and tauon.coll(seek_r_hit): -def convert_playlist(pl: int, get_list: bool = False) -> list[list[int]]| None: - global transcode_list - - 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) - - 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!")) - - folders.append(folder) + click_x = inp.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 get_list: - return folders + if click_x < 6 * gui.scale: + click_x = 0 + seek = click_x / seek_r[2] - transcode_list.extend(folders) + pctl.seek_decimal(seek) + # Draw progress bar background + ddt.rect(seek_r, [255, 255, 255, 32]) -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 + # 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] -def test_pl_tab_locked(pl: int) -> bool: - if gui.radio_view: - return False - return pctl.multi_playlist[pl].locked + 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 -# Clear playlist -tab_menu.add(MenuItem(_("Clear"), clear_playlist, pass_ref=True, disable_test=test_pl_tab_locked, pass_ref_deco=True)) + 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) -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.fields.add(left_area) + tauon.fields.add(right_area) + hint = 0 + if tauon.coll(control_hit_area): + hint = 30 + if tauon.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]) -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] + hint = 0 + if tauon.coll(control_hit_area): + hint = 30 + if tauon.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]) - temp = pctl.multi_playlist[source] - pctl.multi_playlist[source] = "old" - pctl.multi_playlist.insert(dest, temp) - pctl.multi_playlist.remove("old") + # Shuffle - 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") + shuffle_area = (seek_r[0] + seek_w, seek_r[1] - 10 * gui.scale, 50 * gui.scale, 30 * gui.scale) + # tauon.fields.add(shuffle_area) + # ddt.rect_r(shuffle_area, [255, 0, 0, 100], True) + if tauon.coll(control_hit_area) and not prefs.shuffle_lock: + colour = [255, 255, 255, 20] + if inp.mouse_click and tauon.coll(shuffle_area): + # pctl.random_mode ^= True + toggle_random() + if pctl.random_mode: + colour = [255, 255, 255, 190] -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 + sx = seek_r[0] + seek_w + 12 * gui.scale + sy = seek_r[1] - 2 * gui.scale + self.shuffle.render(sx, sy, colour) - global default_playlist - if check_lock and pl_is_locked(index): - show_message(_("Playlist is locked to prevent accidental deletion")) - return + # 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) - if not force: - if pl_is_locked(index): - show_message(_("Playlist is locked to prevent accidental deletion")) - return + shuffle_area = (seek_r[0] - 41 * gui.scale, seek_r[1] - 10 * gui.scale, 40 * gui.scale, 30 * gui.scale) + if tauon.coll(control_hit_area) and not prefs.shuffle_lock: + colour = [255, 255, 255, 20] + if inp.mouse_click and tauon.coll(shuffle_area): + toggle_repeat() + if pctl.repeat_mode: + colour = [255, 255, 255, 190] - if gui.rename_playlist_box: - return - # Set screen to be redrawn - gui.pl_update = 1 - gui.update += 1 + sx = seek_r[0] - 36 * gui.scale + sy = seek_r[1] - 1 * gui.scale + self.repeat.render(sx, sy, colour) - # 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) - # 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 + # sx = seek_r[0] - 39 * gui.scale + # sy = seek_r[1] - 1 * gui.scale - # Take note of the id of the playing playlist - old_playing_id = pctl.multi_playlist[pctl.active_playlist_playing].uuid_int + #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) - # Take note of the id of the viewed open playlist - old_view_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - # Delete the requested playlist - del pctl.multi_playlist[index] + # Forward and back clicking + if inp.mouse_click: + if tauon.coll(left_area) and not prefs.shuffle_lock: + pctl.back() + if tauon.coll(right_area): + pctl.advance() - # Re-set the open viewed playlist number by uid - for i, pl in enumerate(pctl.multi_playlist): + # 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 + tauon.fields.add(tool_rect) + if tauon.coll(tool_rect): + draw_window_tools(tauon) - 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 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) - # 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] +class MiniMode2: + def __init__(self, bag: Bag, gui: GuiVar): + self.window_size = bag.window_size + self.gui = gui + self.save_position = None + self.was_borderless = True + self.volume_timer = Timer() + self.volume_timer.force_set(100) - if album_mode: - reload_albums(True) - goto_album(pctl.playlist_view_position) + self.left_slide = asset_loader(bag, bag.loaded_asset_dc, "left-slide.png", True) + self.right_slide = asset_loader(bag, bag.loaded_asset_dc, "right-slide.png", True) - # Re-set the playing playlist number by uid - for i, pl in enumerate(pctl.multi_playlist): + def render(self): + w = window_size[0] + h = window_size[1] - 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 + x1 = h - test_show_add_home_music() + # Draw background + ddt.rect((0, 0, w, h), colours.mini_mode_background) + ddt.text_background_colour = colours.mini_mode_background - # Cleanup - ids = [] - for p in pctl.multi_playlist: - ids.append(p.uuid_int) + detect_mouse_rect = (2, 2, w - 4, h - 4) + tauon.fields.add(detect_mouse_rect) + mouse_in = tauon.coll(detect_mouse_rect) - 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] + # Play / Pause when right clicking below art + if right_click: # and inp.mouse_position[1] > y1: + pctl.play_pause() - pctl.db_inc += 1 + # Volume change on scroll + if inp.mouse_wheel != 0: + self.volume_timer.set() -to_scan = [] + pctl.player_volume += inp.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() -def delete_playlist_force(index: int): - delete_playlist(index, force=True, check_lock=True) + track = pctl.playing_object() + if track is not 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) + # Render album art + album_art_gen.display(track, (0, 0), (h, h)) + text_hit_area = (x1, 0, w, h) -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 + if tauon.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() - 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") + # Draw title texts + line1 = track.artist + line2 = track.title + if not line1 and not line2: -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") + 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: -# 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)) + 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) -def re_import2(pl: int) -> None: - paths = pctl.multi_playlist[pl].last_folder + # 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 + tauon.fields.add(tool_rect) + if tauon.coll(tool_rect): + draw_window_tools(tauon) - reduce_paths(paths) + # Seek bar + bg_rect = (h, h - round(5 * gui.scale), w - h, round(5 * gui.scale)) + ddt.rect(bg_rect, [255, 255, 255, 18]) - 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 pctl.playing_state > 0: - if paths: - show_message(_("Rescanning folders..."), mode="info") + hit_rect = h - 5 * gui.scale, h - 12 * gui.scale, w - h + 5 * gui.scale, 13 * gui.scale + if tauon.coll(hit_rect) and inp.mouse_up: + p = (inp.mouse_position[0] - h) / (w - h) -def rescan_all_folders(): - for i, p in enumerate(pctl.multi_playlist): - re_import2(i) + if p < 0 or inp.mouse_position[0] - h < 6 * gui.scale: + pctl.seek_time(0) + elif p > .96: + pctl.advance() + else: + pctl.seek_decimal(p) -def s_append(index: int): - paste(playlist_no=index) + 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, bag: Bag, gui: GuiVar): + self.window_size = bag.window_size + self.gui = gui + self.save_position = None + self.was_borderless = True + self.volume_timer = Timer() + self.volume_timer.force_set(100) -def append_playlist(index: int): - global cargo - pctl.multi_playlist[index].playlist_ids += cargo + self.left_slide = asset_loader(bag, bag.loaded_asset_dc, "left-slide.png", True) + self.right_slide = asset_loader(bag, bag.loaded_asset_dc, "right-slide.png", True) - gui.pl_update = 1 - reload() + self.shuffle_fade_timer = Timer(100) + self.repeat_fade_timer = Timer(100) -def index_key(index: int): - tr = pctl.master_library[index] - s = str(tr.track_number) - d = str(tr.disc_number) + 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 - if "/" in d: - d = d.split("/")[0] + w = window_size[0] + h = window_size[1] - # 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 = "" + y1 = w #+ 10 * gui.scale + # if w == h: + # y1 -= 79 * gui.scale + h1 = h - y1 - # 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 + # Draw background + bg = colours.mini_mode_background + bg = [0, 0, 0, 0] + # bg = [250, 250, 250, 255] - # 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 + ddt.rect((0, 0, w, h), bg) - if (not tr.disc_number or tr.disc_number == "0") and tr.is_cue: - s = tr.filename + "-" + s + style_overlay.display() - # 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" + 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) + tauon.fields.add(detect_mouse_rect) + mouse_in = tauon.coll(detect_mouse_rect) -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 + # Play / Pause when right clicking below art + if right_click: # and inp.mouse_position[1] > y1: + pctl.play_pause() - 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) + # Volume change on scroll + if inp.mouse_wheel != 0: + self.volume_timer.set() - 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) + pctl.player_volume += inp.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 - gui.pl_update += 1 + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() + track = pctl.playing_object() -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 + control_hit_area = (3, y1 - 15 * gui.scale, w - 6, h1 - 3 + 15 * gui.scale) + mouse_in_area = tauon.coll(control_hit_area) + tauon.fields.add(control_hit_area) - 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) + #ddt.rect((0, 0, w, w), (0, 0, 0, 45)) + if track is not None: - 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) + # Render album art - gui.pl_update += 1 + 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)) -tauon.sort_track_2 = sort_track_2 + 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] -def key_filepath(index: int): - track = pctl.master_library[index] - return track.parent_folder_path.lower(), track.filename + # Double click bottom text to return to full window + text_hit_area = (60 * gui.scale, y1 + 4, 230 * gui.scale, 50 * gui.scale) + if tauon.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() -def key_fullpath(index: int): - return pctl.master_library[index].fullpath - + # 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: -def key_filename(index: int): - track = pctl.master_library[index] - return track.filename + 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) -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 + # Calculate seek bar position + seek_w = int(w * 0.80) - if use_natsort and False: - target[:] = natsort.os_sorted(target, key=key_fullpath) - else: - target.sort(key=key_filepath) + 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: -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 + # Test click to seek + if inp.mouse_up and tauon.coll(seek_r_hit): + click_x = inp.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] -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 click_x < 6 * gui.scale: + click_x = 0 + seek = click_x / seek_r[2] - seen_files = {} - seen_types = {} + pctl.seek_decimal(seek) - mp3_bitrates = {} - ogg_bitrates = {} - m4a_bitrates = {} + # Draw progress bar background + ddt.rect(seek_r, [255, 255, 255, 32]) - are_cue = 0 + # 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 - for index in pctl.multi_playlist[pl].playlist_ids: - track = pctl.get_track(index) + if pctl.playing_state != 1: + seek_colour = [210, 40, 100, 255] - playlist_time += int(track.length) - play_time += star_store.get(index) + seek_r[2] = progress_w - if track.is_cue: - are_cue += 1 + ddt.rect(seek_r, seek_colour) - 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 - type = track.file_ext - if type == "OGA": - type = "OGG" - seen_types[type] = seen_types.get(type, 0) + 1 - 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 + 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] - total_size = sum(seen_files.values()) + # Test click to volume + if (inp.mouse_up or inp.mouse_down) and tauon.coll(volume_r_hit): + gui.update_on_drag = True + click_x = inp.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] - 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" + if click_x < 6 * gui.scale: + click_x = 0 + volume = click_x / volume_r[2] - 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" + pctl.player_volume = int(volume * 100) + pctl.set_volume() - 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) + ddt.rect(volume_r, [255, 255, 255, 32]) - line += str(perc) + "%" - line += "\n\n" + #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 - 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}%); " + ddt.rect(volume_r, volume_colour) - if others: - others = round(others, 1) - if others < 0.1: - others = "<0.1" - line += _("Others") + f"({others}%);" - line = line.rstrip("; ") - line += "\n\n" - 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}%); " + 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) - if others: - others = round(others, 1) - if others < 0.1: - others = "<0.1" - line += _("Others") + f"({others}%);" - line = line.rstrip("; ") - line += "\n\n" + tauon.fields.add(left_area) + tauon.fields.add(right_area) - # 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" + hint = 0 + if True: #tauon.coll(control_hit_area): + hint = 30 + if tauon.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]) - line += "\n" + f"-------------- {_('Top Artists')} --------------------" + "\n\n" + hint = 0 + if True: #tauon.coll(control_hit_area): + hint = 30 + if tauon.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]) - ls = stats_gen.artist_list - for i, item in enumerate(ls[:50]): - line += str(i + 1) + ".\t" + stt2(item[1]) + "\t" + item[0] + "\n" + # Shuffle + shuffle_area = (volume_r[0] + volume_w, volume_r[1] - 10 * gui.scale, 50 * gui.scale, 30 * gui.scale) + # tauon.fields.add(shuffle_area) + # ddt.rect_r(shuffle_area, [255, 0, 0, 100], 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 True: #tauon.coll(control_hit_area) and not prefs.shuffle_lock: + colour = [255, 255, 255, 20] + if inp.mouse_click and tauon.coll(shuffle_area): + # pctl.random_mode ^= True + toggle_random() + if pctl.random_mode: + colour = [255, 255, 255, 190] - 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]) + 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) -def imported_sort(pl: int) -> None: - if pl_is_locked(pl): - show_message(_("Playlist is locked")) - return + shuffle_area = (volume_r[0] - 41 * gui.scale, volume_r[1] - 10 * gui.scale, 40 * gui.scale, 30 * gui.scale) + if True: #tauon.coll(control_hit_area) and not prefs.shuffle_lock: + colour = [255, 255, 255, 20] + if inp.mouse_click and tauon.coll(shuffle_area): + toggle_repeat() + if pctl.repeat_mode: + colour = [255, 255, 255, 190] - og = pctl.multi_playlist[pl].playlist_ids - og.sort(key=lambda x: pctl.get_track(x).index) + sx = volume_r[0] - 39 * gui.scale + sy = volume_r[1] - 1 * gui.scale + mini_mode.repeat.render(sx, sy, colour) - reload_albums() - tree_view_box.clear_target_pl(pl) + # 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) -def imported_sort_folders(pl: int) -> None: - if pl_is_locked(pl): - show_message(_("Playlist is locked")) - return + # Forward and back clicking + if inp.mouse_click: + if tauon.coll(left_area) and not prefs.shuffle_lock: + pctl.back() + if tauon.coll(right_area): + pctl.advance() - og = pctl.multi_playlist[pl].playlist_ids - og.sort(key=lambda x: pctl.get_track(x).index) + tauon.search_over.render() - 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 - og.sort(key=lambda x: first_occurrences[pctl.get_track(x).parent_folder_path]) + # 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 + tauon.fields.add(tool_rect) + if tauon.coll(tool_rect): + draw_window_tools(tauon) - reload_albums() - tree_view_box.clear_target_pl(pl) + # 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 -def standard_sort(pl: int) -> None: - if pl_is_locked(pl): - show_message(_("Playlist is locked")) - return +class StandardPlaylist: + def __init__(self, tauon: Tauon, pl_bg: LoadImageAsset | None) -> None: + self.tauon = tauon + self.pctl = tauon.pctl + self.deco = tauon.deco + self.gui = tauon.gui + self.inp = tauon.gui.inp + self.ddt = tauon.bag.ddt + self.colours = tauon.bag.colours + self.renderer = tauon.bag.renderer + self.window_size = tauon.bag.window_size + self.pl_bg = pl_bg - sort_path_pl(pl) - sort_track_2(pl) - reload_albums() - tree_view_box.clear_target_pl(pl) + def full_render(self): + global highlight_left + global highlight_right + global playlist_hold + global playlist_hold_position + global shift_selection -def year_s(plt): - sorted_temp = sorted(plt, key=lambda x: x[1]) - temp = [] + global click_time + global selection_stage - for album in sorted_temp: - temp += album[0] - return temp + global r_menu_index + global r_menu_position + tauon = self.tauon + pctl = self.pctl + gui = self.gui + inp = self.inp + window_size = self.window_size + ddt = self.ddt + colours = self.colours + pl_bg = self.pl_bg + deco = self.deco + left = gui.playlist_left + width = gui.plw -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 = "" + 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 - p = 0 - while p < len(playlist): + w = 0 + gui.row_extra = 0 + cv = 0 # update gui.playlist_current_visible_tracks - track = get_object(playlist[p]) + # Draw the background + SDL_SetRenderTarget(self.renderer, gui.tracklist_texture) + SDL_SetRenderDrawColor(self.renderer, 0, 0, 0, 0) + SDL_RenderClear(self.renderer) - 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 = [] + rect = (left, gui.panelY, width, window_size[1]) + ddt.rect(rect, colours.playlist_panel_background) - if track.album_artist: - album_artist = track.album_artist + # 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) - if p > len(playlist) - 1: - break + # Mouse wheel scrolling + if inp.mouse_wheel != 0 and window_size[1] - gui.panelBY - 1 > inp.mouse_position[ + 1] > gui.panelY - 2 and gui.playlist_left < inp.mouse_position[0] < gui.playlist_left + gui.plw \ + and not (tauon.coll(pl_rect)) and not tauon.search_over.active and not radiobox.active: - album = [] - on = get_object(playlist[p]).parent_folder_path - album.append(playlist[p]) - t = 1 + # Set scroll speed + mx = 4 - while t + p < len(playlist) - 1 and get_object(playlist[p + t]).parent_folder_path == on: - album.append(playlist[p + t]) - t += 1 + if gui.playlist_view_length < 25: + mx = 3 + if gui.playlist_view_length < 10: + mx = 2 + pctl.playlist_view_position -= inp.mouse_wheel * mx - date = get_object(playlist[p]).date + if gui.playlist_view_length > 40: + pctl.playlist_view_position -= inp.mouse_wheel - # 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 inp.mouse_wheel: + #logging.debug("Position changed by mouse wheel scroll: " + str(inp.mouse_wheel)) - # 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] + pctl.playlist_view_position = min(pctl.playlist_view_position, len(pctl.default_playlist)) + #logging.debug("Position changed by range bound") + if pctl.playlist_view_position < 1: + pctl.playlist_view_position = 0 + if pctl.default_playlist: + # edge_playlist.pulse() + edge_playlist2.pulse() - plt.append((album, date, artist + " " + get_object(playlist[p]).album)) - p += len(album) - #logging.info(album) + scroll_hide_timer.set() + gui.frame_callback_list.append(TestTimer(0.9)) - if plt: - pl2 += year_s(plt) - plt = [] + # Show notice if playlist empty + if len(pctl.default_playlist) == 0: + colour = alpha_mod(colours.index_text, 200) # colours.playlist_text_missing - if custom_list is not None: - return pl2 + top_a = gui.panelY + if gui.artist_info_panel: + top_a += gui.artist_panel_height - # 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) + 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 -def pl_toggle_playlist_break(ref): - pctl.multi_playlist[ref].hide_title ^= 1 - gui.pl_update = 1 + 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 -delete_icon.xoff = 3 -delete_icon.colour = [249, 70, 70, 255] + # Show notice if at end of playlist + elif pctl.playlist_view_position > len(pctl.default_playlist) - 1: + colour = alpha_mod(colours.index_text, 200) -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)) + 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)) -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 + 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 - return title + ddt.text( + (left + int(width / 2) + 10 * gui.scale, half, 2), _("End of Playlist"), + colour, 213) + ddt.pretty_rect = None + ddt.alpha_bg = False -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 + # line = "Contains " + str(len(pctl.default_playlist)) + ' track' + # if len(pctl.default_playlist) > 1: + # line += "s" + # + # ddt.draw_text((left + int(width / 2) + 10 * gui.scale, half + 24 * gui.scale, 2), line, + # colour, 12) - title = gen_unique_pl_title(_("New Playlist")) + # Process Input - top_panel.prime_side = 1 - top_panel.prime_tab = len(pctl.multi_playlist) + # type (0 is track, 1 is fold title), track_position, track_object, box, input_box, + list_items = [] + number = 0 - 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 + for i in range(gui.playlist_view_length + 1): + track_position = i + pctl.playlist_view_position -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 + # Make sure the view position is valid + pctl.playlist_view_position = max(pctl.playlist_view_position, 0) -jell_icon = MenuIcon(spot_asset) -jell_icon.colour = [190, 100, 210, 255] -jell_icon.xoff = 5 -jell_icon.yoff = 2 + # Break if we are at end of playlist + if len(pctl.default_playlist) <= track_position or number > gui.playlist_view_length: + break -tab_menu.br() + track_object = pctl.get_track(pctl.default_playlist[track_position]) + track_id = track_object.index + move_on_title = False + line_y = gui.playlist_top + gui.playlist_row_height * number -def append_deco(): - if pctl.playing_state > 0: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + track_box = ( + left + highlight_left, line_y, highlight_width, + gui.playlist_row_height - 1) - text = None - if tauon.spot_ctl.coasting: - text = _("Add Spotify Album") + input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) - return [line_colour, colours.menu_background, text] + # 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( + pctl.default_playlist[track_position - 1]).parent_folder_path: + # Make folder title + highlight = False + drag_highlight = False -def rescan_deco(pl: int): - if pctl.multi_playlist[pl].last_folder: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + # Shift selection highlight + if (track_position in shift_selection and len(shift_selection) > 1): + highlight = True - # base = os.path.basename(pctl.multi_playlist[pl].last_folder) + # Tracks have been dropped? + if playlist_hold is True and tauon.coll(input_box): + if inp.mouse_up: + move_on_title = True - return [line_colour, colours.menu_background, None] + # Ignore click in ratings box + click_title = (inp.mouse_click or right_click or middle_click) and tauon.coll(input_box) + if click_title and gui.show_album_ratings: + if inp.mouse_position[0] > (input_box[0] + input_box[2]) - 80 * gui.scale: + click_title = False + # Detect folder title click + if click_title and inp.mouse_position[1] < window_size[1] - gui.panelBY: -def regenerate_deco(pl: int): - id = pl_to_id(pl) - value = pctl.gen_codes.get(id) + gui.pl_update += 1 + # Add folder to queue if middle click + if middle_click and is_level_zero(): + if inp.key_ctrl_down: # Add as ungrouped tracks + i = track_position + parent = pctl.get_track(pctl.default_playlist[i]).parent_folder_path + while i < len(pctl.default_playlist) and parent == pctl.get_track( + pctl.default_playlist[i]).parent_folder_path: + pctl.force_queue.append(queue_item_gen(pctl.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 - if value: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + 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 - return [line_colour, colours.menu_background, None] + # Play if double click: + if d_mouse_click and track_position in shift_selection and coll_point( + inp.last_click_location, (input_box)): + click_time -= 1.5 + pctl.jump(track_id, track_position) + line_hit = False + inp.mouse_click = False + if prefs.album_mode: + goto_album(pctl.playlist_playing_position) -column_names = ( - "Artist", - "Album Artist", - "Album", - "Title", - "Composer", - "Time", - "Date", - "Genre", - "#", - "P", - "Starline", - "Rating", - "Comment", - "Codec", - "Lyrics", - "Bitrate", - "S", - "Filename", - "Disc", - "CUE", -) + # 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(pctl.default_playlist) and track_object.parent_folder_path == \ + pctl.master_library[ + pctl.default_playlist[u]].parent_folder_path: + shift_selection.append(u) + u += 1 -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 + # Add folder to selection if clicked + if inp.mouse_click \ + and not (scroll_enable and inp.mouse_position[0] < 30 * gui.scale) and not gui.side_drag: + inp.quick_drag = True + set_drag_source() - current += cha + if not pl_is_locked(pctl.active_playlist_viewing) or inp.key_shift_down: + playlist_hold = True - if inquote and cha != "\"": - q_string += cha + selection_stage = 1 + temp = get_folder_tracks_local(track_position) + pctl.selected_in_playlist = track_position - if current: - cmds.append(current) - quotes.append(q_string) + if len(shift_selection) > 0 and inp.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) - return cmds, quotes, inquote + else: + shift_selection = copy.copy(temp) + # Should draw drag highlight? -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 + if inp.mouse_down and playlist_hold and tauon.coll(input_box) and track_position not in shift_selection: + if len(shift_selection) < 2 and not inp.key_shift_down: + pass + else: + drag_highlight = True - urls = [] - playlist = pctl.multi_playlist[pl].playlist_ids + # Something to do with quick search, I forgot + if pctl.selected_in_playlist > track_position + 1: + gui.row_extra += 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) + list_items.append( + (1, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, False)) + number += 1 - if warn: - show_message(_("Playlist contains non-Spotify tracks"), mode="error") - return + if number > gui.playlist_view_length: + break - 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) + # Standard track --------------------------------------------------------------------- + playing = False + highlight = False + drag_highlight = False + line_y = gui.playlist_top + gui.playlist_row_height * number -def regenerate_playlist(pl: int = -1, silent: bool = False, id: int | None = None) -> None: - if id is None and pl == -1: - return + track_box = ( + left + highlight_left, line_y, highlight_width, + gui.playlist_row_height - 1) - if id is None: - id = pl_to_id(pl) + input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) - if pl == -1: - pl = id_to_pl(id) - if pl is None: - return + # Test if line has mouse over or been clicked + line_over = False + line_hit = False + if tauon.coll(input_box) and inp.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 - source_playlist = pctl.multi_playlist[pl].playlist_ids + else: + line_hit = False + else: + line_hit = False + line_over = False - string = pctl.gen_codes.get(id) - if not string: - if not silent: - show_message(_("This playlist has no generator")) - return + # Prevent click if near scroll bar + if scroll_enable and inp.mouse_position[0] < 30: + line_hit = False - cmds, quotes, inquote = parse_generator(string) + # Double click to play + if inp.key_shift_down is False and d_mouse_click and line_hit and track_position == pctl.selected_in_playlist and coll_point( + inp.last_click_location, input_box): - if inquote: - gui.gen_code_errors = "close" - return + pctl.jump(track_id, track_position) - playlist = [] - selections = [] - errors = False - selections_searched = 0 + click_time -= 1.5 + inp.quick_drag = False + inp.mouse_down = False + inp.mouse_up = False + line_hit = False - def is_source_type(code: str | None) -> bool: - return \ - code is None or \ - code == "" or \ - code.startswith(("self", "jelly", "plex", "koel", "tau", "air", "sal")) + if prefs.album_mode: + goto_album(pctl.playlist_playing_position) - #logging.info(cmds) - #logging.info(quotes) + 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 - pctl.regen_in_progress = 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 - for i, cm in enumerate(cmds): + # 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 inp.mouse_up and line_over and not inp.key_shift_down and not inp.key_ctrl_down and point_proximity_test( + gui.drag_source_position, inp.mouse_position, 15): # and not playlist_hold: + shift_selection = [track_position] + pctl.selected_in_playlist = track_position + gui.pl_update = 1 + gui.update = 2 - quote = quotes[i] + # # Begin drag block selection + # if inp.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 inp.key_shift_down: + # playlist_hold = True - if cm.startswith("\"") and (cm.endswith((">", "<"))): - cm_found = False + # Begin drag single track + if inp.mouse_click and line_hit and not gui.side_drag: + inp.quick_drag = True + set_drag_source() - for col in column_names: + # Shift Move Selection + if move_on_title or (inp.mouse_up and playlist_hold is True and tauon.coll(( + left + highlight_left, line_y, highlight_width, gui.playlist_row_height))): - if quote.lower() == col.lower() or _(quote).lower() == col.lower(): - cm_found = True + if len(shift_selection) > 1 or inp.key_shift_down: + if track_position not in shift_selection: # p_track != playlist_hold_position and - 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 + if len(shift_selection) == 0: - elif cm == "self": - selections.append(pctl.multi_playlist[pl].playlist_ids) + ref = pctl.default_playlist[playlist_hold_position] + pctl.default_playlist[playlist_hold_position] = "old" + if move_on_title: + pctl.default_playlist.insert(track_position, "new") + else: + pctl.default_playlist.insert(track_position + 1, "new") + pctl.default_playlist.remove("old") + pctl.selected_in_playlist = pctl.default_playlist.index("new") + pctl.default_playlist[pctl.default_playlist.index("new")] = ref - elif cm == "auto": - pass + gui.pl_update = 1 - elif cm.startswith("spl\""): - playlist.extend(tauon.spot_ctl.playlist(quote, return_list=True)) - elif cm.startswith("tpl\""): - playlist.extend(tauon.tidal.playlist(quote, return_list=True)) + else: + ref = [] + selection_stage = 2 + for item in shift_selection: + ref.append(pctl.default_playlist[item]) - elif cm == "tfa": - playlist.extend(tauon.tidal.fav_albums(return_list=True)) + for item in shift_selection: + pctl.default_playlist[item] = "old" - elif cm == "tft": - playlist.extend(tauon.tidal.fav_tracks(return_list=True)) + for item in shift_selection: + if move_on_title: + pctl.default_playlist.insert(track_position, "new") + else: + pctl.default_playlist.insert(track_position + 1, "new") - elif cm.startswith("tar\""): - playlist.extend(tauon.tidal.artist(quote, return_list=True)) + for b in reversed(range(len(pctl.default_playlist))): + if pctl.default_playlist[b] == "old": + del pctl.default_playlist[b] + shift_selection = [] + for b in range(len(pctl.default_playlist)): + if pctl.default_playlist[b] == "new": + shift_selection.append(b) + pctl.default_playlist[b] = ref.pop(0) - elif cm.startswith("tmix\""): - playlist.extend(tauon.tidal.mix(quote, return_list=True)) + pctl.selected_in_playlist = shift_selection[0] + gui.pl_update += 1 - elif cm == "sal": - playlist.extend(tauon.spot_ctl.get_library_albums(return_list=True)) + reload_albums(True) + pctl.notify_change() - elif cm == "slt": - playlist.extend(tauon.spot_ctl.get_library_likes(return_list=True)) + # Test show drag indicator + if inp.mouse_down and playlist_hold and tauon.coll(input_box) and track_position not in shift_selection: + if len(shift_selection) > 1 or inp.key_shift_down: + drag_highlight = True - elif cm == "plex": - if not plex.scanning: - playlist.extend(plex.get_albums(return_list=True)) + # Right click menu activation + if right_click and line_hit and inp.mouse_position[0] > gui.playlist_left + 10: - elif cm.startswith("jelly\""): - if not jellyfin.scanning: - playlist.extend(jellyfin.get_playlist(quote, return_list=True)) + if len(shift_selection) > 1 and track_position in shift_selection: + selection_menu.activate(pctl.default_playlist[track_position]) + selection_stage = 2 + else: + r_menu_index = pctl.default_playlist[track_position] + r_menu_position = track_position + track_menu.activate(pctl.default_playlist[track_position]) + gui.pl_update += 1 + gui.update += 1 - elif cm == "jelly": - if not jellyfin.scanning: - playlist.extend(jellyfin.ingest_library(return_list=True)) + if track_position not in shift_selection: + pctl.selected_in_playlist = track_position + shift_selection = [pctl.selected_in_playlist] - elif cm == "koel": - if not koel.scanning: - playlist.extend(koel.get_albums(return_list=True)) + if line_over and inp.mouse_click: - elif cm == "tau": - if not tau.processing: - playlist.extend(tau.get_playlist(pctl.multi_playlist[pl].title, return_list=True)) + if track_position in shift_selection: + pass + else: + selection_stage = 2 + if inp.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 inp.key_ctrl_down: + shift_selection.append(track_position) + else: + pctl.selected_in_playlist = track_position + shift_selection = [pctl.selected_in_playlist] - elif cm == "air": - if not subsonic.scanning: - playlist.extend(subsonic.get_music3(return_list=True)) + if not pl_is_locked(pctl.active_playlist_viewing) or inp.key_shift_down: + playlist_hold = True + playlist_hold_position = track_position - 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) + # Activate drag if shift key down + if inp.quick_drag and pl_is_locked(pctl.active_playlist_viewing) and inp.mouse_down: + if inp.key_shift_down: + playlist_hold = True + else: + playlist_hold = False - temp = [] - for selection in selections: - temp += selection + # Multi Select Highlight + if track_position in shift_selection or track_position == pctl.selected_in_playlist: + highlight = True - playlist += list(OrderedDict.fromkeys(temp)) - selections.clear() + if pctl.playing_state != 3 and len(pctl.track_queue) > 0 and pctl.track_queue[pctl.queue_step] == \ + pctl.default_playlist[track_position]: + if track_position == pctl.playlist_playing_position and pctl.active_playlist_viewing == pctl.active_playlist_playing: + playing = True - elif cm == "cue": + list_items.append( + (0, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, playing)) + number += 1 - 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)) + if number > gui.playlist_view_length: + break + # --------------------------------------------------------------------------------------- - 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)) + # 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 - 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)) + for type, track_position, tr, track_box, input_box, highlight, number, drag_highlight, playing in list_items: - elif cm.startswith("ext"): - value = quote.upper() - if value: - if not selections: - for plist in pctl.multi_playlist: - selections.append(plist.playlist_ids) + line_y = gui.playlist_top + gui.playlist_row_height * number - temp = [] - for selection in selections: - for track in selection: - tr = pctl.get_track(track) - if tr.file_ext == value: - temp.append(track) + ddt.text_background_colour = colours.playlist_panel_background - playlist += list(OrderedDict.fromkeys(temp)) + if type == 1: - elif cm == "ypa": - playlist = year_sort(0, playlist) + # Is type ALBUM TITLE + separator = " - " + if prefs.row_title_separator_type == 1: + separator = " ‒ " + if prefs.row_title_separator_type == 2: + separator = " ⦁ " - elif cm == "tn": - sort_track_2(0, playlist) + date = "" + duration = "" - elif cm == "ia>": - playlist = gen_last_imported_folders(0, playlist) + line = tr.parent_folder_name - elif cm == "ia<": - playlist = gen_last_imported_folders(0, playlist, reverse=True) + # Use folder name if mixed/singles? + if len(pctl.default_playlist) > track_position + 1 and pctl.get_track( + pctl.default_playlist[track_position + 1]).album != tr.album and \ + pctl.get_track(pctl.default_playlist[track_position + 1]).parent_folder_path == tr.parent_folder_path: + line = tr.parent_folder_name + else: - elif cm == "m>": - playlist = gen_last_modified(0, playlist) + if tr.album_artist != "" and tr.album != "": + line = tr.album_artist + separator + tr.album - elif cm == "m<": - playlist = gen_last_modified(0, playlist, reverse=False) + if prefs.left_align_album_artist_title and not True: + album_artist_mode = True + line = tr.album - elif cm == "ly" or cm == "lyrics": - playlist = gen_lyrics(0, playlist) + if len(line) < 6 and "CD" in line: + line = tr.album - elif cm == "l" or cm == "love" or cm == "loved": - playlist = gen_love(0, playlist) + 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 + ")" - elif cm == "clr": - selections.clear() + if line.endswith(")"): + b = line.split("(") + if len(b) > 1 and len(b[1]) <= 11: - elif cm == "rv" or cm == "reverse": - playlist = gen_reverse(0, playlist) + match = year_search.search(b[1]) - elif cm == "rva": - playlist = gen_folder_reverse(0, playlist) + if match: + line = b[0] + date = "(" + b[1] - elif cm == "rata>": + elif line.startswith("("): - playlist = gen_folder_top_rating(0, custom_list=playlist) + b = line.split(")") + if len(b) > 1 and len(b[0]) <= 11: - elif cm == "rat>": + match = year_search.search(b[0]) - def rat_key(track_id): - return star_store.get_rating(track_id) + if match: + line = b[1] + date = b[0] + ")" - playlist = sorted(playlist, key=rat_key, reverse=True) + if "(" in line and year_search.search(line): + date = "" - elif cm == "rat<": + line = line.replace(" - ", separator) - def rat_key(track_id): - return star_store.get_rating(track_id) + qq = 0 + d_date = date + title_line = line - playlist = sorted(playlist, key=rat_key) + # Calculate folder duration - 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 + q = track_position - 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 - - 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 + total_time = 0 + while q < len(pctl.default_playlist): - 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 pctl.get_track(pctl.default_playlist[q]).parent_folder_path != tr.parent_folder_path: + break - elif cm == "norat": - temp = [] - for item in playlist: - if star_store.get_rating(item) == 0: - temp.append(item) - playlist = temp + total_time += pctl.get_track(pctl.default_playlist[q]).length - elif cm == "d>": - playlist = gen_sort_len(0, custom_list=playlist) + q += 1 + qq += 1 - elif cm == "d<": - playlist = gen_sort_len(0, custom_list=playlist) - playlist = list(reversed(playlist)) + if qq > 1: + duration = " [ " + get_display_time(total_time) + " ]" # Hair space inside brackets for better visual spacing - 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] + if prefs.append_total_time: + date += duration - 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] + ex = left + highlight_left + highlight_width - 7 * gui.scale - elif cm == "path": - sort_path_pl(0, custom_list=playlist) + height = line_y + gui.playlist_row_height - 19 * gui.scale # gui.pl_title_y_offset - elif cm == "pa>": - playlist = gen_folder_top(0, custom_list=playlist) + 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) - elif cm == "pa<": - playlist = gen_folder_top(0, custom_list=playlist) - playlist = gen_folder_reverse(0, playlist) + light_offset = 0 + if colours.lm: + light_offset = 3 * gui.scale + ex -= light_offset - elif cm == "pt>" or cm == "pc>": - playlist = gen_top_100(0, custom_list=playlist) + if qq > 1: + ex += 1 * gui.scale - elif cm == "pt<" or cm == "pc<": - playlist = gen_top_100(0, custom_list=playlist) - playlist = list(reversed(playlist)) + ddt.text_background_colour = colours.playlist_panel_background - 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 gui.scale == 2: + height += 1 - 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 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) - 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] - 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(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) - elif cm == "y<": - playlist = gen_sort_date(0, False, playlist) - elif cm == "y>": - playlist = gen_sort_date(0, True, playlist) - 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 prefs.row_title_format == 2: - 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 + separator = " | " - 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 + start_offset = round(15 * gui.scale) + xx = left + highlight_left + start_offset + ww = highlight_width - 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 + 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)] - 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 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) - elif cm == "st" or cm == "rt" or cm == "r": - random.shuffle(playlist) - elif cm == "sf" or cm == "rf" or cm == "ra" or cm == "sa": - playlist = gen_folder_shuffle(0, custom_list=playlist) + 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))) - elif cm.startswith("n"): - value = cm[1:] - if value.isdigit(): - playlist = playlist[:int(value)] - # SEARCH FOLDER - elif cm.startswith("p\"") and len(cm) > 3: - 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) + 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 - found_name = "" + aa = 0 - for result in search_over.results: - if result[0] == 5: - found_name = result[1] - break - else: - logging.info("No folder search result found") - continue + ft_width = ddt.get_text_w(line, gui.row_font_size + gui.pl_title_font_offset) - search_over.clear() + left_align = highlight_width - date_w - 13 * gui.scale - light_offset - playlist += search_over.click_meta(found_name, get_list=True, search_lists=selections) + left_align -= star_offset - # SEARCH GENRE - elif (cm.startswith(('g"', 'gm"', 'g="'))) and len(cm) > 3: + extra = aa - 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) + left_align -= extra - g_search = quote.lower().replace("-", "") # .replace(" ", "") + 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) - 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) + ddt.text( + (ex - date_w, height, 1), line, + colours.folder_title, + gui.row_font_size + gui.pl_title_font_offset) - found_name = "" + # ----- - 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 + # 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) - if not found_name: - logging.warning("No genre search result found") - continue + # 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]) - search_over.clear() + continue - playlist += search_over.click_genre(found_name, get_list=True, search_lists=selections) + # 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) - # 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) + 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]) - 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) - found_name = "" + # Blue drop line + if drag_highlight: # playlist_hold_position != p_track: - for result in search_over.results: - if result[0] == 0: - found_name = result[1] - break - else: - logging.warning("No artist search result found") - continue + ddt.rect( + [left + highlight_left, line_y + gui.playlist_row_height - 1 * gui.scale, highlight_width, + 3 * gui.scale], [125, 105, 215, 255]) - 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) + # Highlight + if highlight: + ddt.rect_a( + (left + highlight_left, line_y), (highlight_width, gui.playlist_row_height), + colours.row_select_highlight) - elif cm.startswith("ff\""): + ddt.text_background_colour = alpha_blend(colours.row_select_highlight, ddt.text_background_colour) - 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 track_position > 0 and track_position < len(pctl.default_playlist) and tr.disc_number != "" and tr.disc_number != "0" and tr.album and tr.disc_number != pctl.get_track(pctl.default_playlist[track_position - 1]).disc_number \ + and tr.album == pctl.get_track(pctl.default_playlist[track_position - 1]).album and tr.parent_folder_path == pctl.get_track(pctl.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 prefs.diacritic_search and all([ord(c) < 128 for c in quote]): - line = str(unidecode(line)) + if not gui.set_mode: - if not search_magic(quote.lower(), line): - del playlist[i] + line_render( + tr, track_position, gui.playlist_text_offset + line_y, + playing, 255, left + inset_left, inset_width, 1, line_y) - playlist = list(OrderedDict.fromkeys(playlist)) + else: + # NEE --------------------------------------------------------- + n_track = tr + p_track = track_position + this_line_playing = playing - elif cm.startswith("fx\""): + start = 18 * 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 prefs.diacritic_search and all([ord(c) < 128 for c in quote]): - line = str(unidecode(line)) + if center_mode: + start = inset_left - if search_magic(quote.lower(), line): - del playlist[i] + elif gui.lsp: + start += gui.lspw + run = start + end = start + gui.plw - elif cm.startswith(('find"', 'f"', 'fs"')): + if center_mode: + end = highlight_width + start - 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) + # 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) - cooldown = 0 - dones = {} - for selection in selections: - for track_id in selection: - if track_id not in dones: - tr = pctl.get_track(track_id) + for h, item in enumerate(gui.pl_st): - 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) + 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 - else: - line = " ".join([tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() + if run > end - 50 * gui.scale: + break - # if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): - # line = str(unidecode(line)) + if len(gui.pl_st) == h + 1: + wid -= 6 * gui.scale - if search_magic(quote.lower(), line): - playlist.append(track_id) + 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) - cooldown += 1 - if cooldown > 300: - time.sleep(0.005) - cooldown = 0 + if item[0] == "Starline": - dones[track_id] = None + total = star_store.get_by_object(n_track) - playlist = list(OrderedDict.fromkeys(playlist)) + 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) - 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 + 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 - 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] + else: - else: - errors = True + ratio = total / n_track.length + if ratio > 0.55: + star_x = int(ratio * (4 * gui.scale)) + star_x = min(star_x, wid) - gui.gen_code_errors = errors - if not playlist and not errors: - gui.gen_code_errors = "empty" + colour = colours.star_line + if playing and colours.star_line_playing is not None: + colour = colours.star_line_playing - if gui.rename_playlist_box and (not playlist or cmds.count("a") > 1): - pass - else: - source_playlist[:] = playlist[:] + 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) - tree_view_box.clear_target_pl(0, id) - pctl.regen_in_progress = False - gui.pl_update = 1 - reload() - pctl.notify_change() + 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 - #logging.info(cmds) + 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] == "#": -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") + 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 = "" -extra_tab_menu = Menu(155, show_icons=True) + 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 -extra_tab_menu.add(MenuItem(_("New Playlist"), new_playlist, icon=add_icon)) + 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 -def spotify_show_test(_): - return prefs.spot_mode + 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 -def jellyfin_show_test(_): - return prefs.jelly_password and prefs.jelly_username + 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 -tab_menu.add(MenuItem(_("Upload"), - upload_spotify_playlist, pass_ref=True, pass_ref_deco=True, icon=jell_icon, show_test=spotify_show_test)) + j = 0 # justify right + if run < start + 100 * gui.scale: + j = 1 # justify left -def upload_jellyfin_playlist(pl: TauonPlaylist) -> None: - if jellyfin.scanning: - return - shooter(jellyfin.upload_playlist, [pl]) + display_friend_heart(x, yy, name, j) + count += 1 -tab_menu.add(MenuItem(_("Upload"), - upload_jellyfin_playlist, pass_ref=True, pass_ref_deco=True, icon=spot_icon, show_test=jellyfin_show_test)) + # 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) -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() + 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 -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) + if prefs.dim_art and prefs.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 -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 text: + if item[0] in colours.column_colours: + colour = colours.column_colours[item[0]] -tab_menu.br() + if this_line_playing and item[0] in colours.column_colours_playing: + colour = colours.column_colours_playing[item[0]] -tab_menu.add(MenuItem(_("Rescan Folder"), re_import2, rescan_deco, pass_ref=True, pass_ref_deco=True)) + if run + 6 * gui.scale + wid > end: + wid = end - run - 40 * gui.scale + if center_mode: + wid += 25 * gui.scale -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() + wid = max(0, wid) -# tab_menu.add("Sort By Filepath", sort_path_pl, pass_ref=True) + # # 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) -tab_menu.add(MenuItem(_("Export…"), export_playlist_box.activate, pass_ref=True)) + ddt.text( + (run + 6 * gui.scale, y + y_off), + text, + colour, + font, + max_w=wid) -tab_menu.add_sub(_("Misc…"), 175) + if ddt.was_truncated: + #logging.info(text) + rect = (run, y, wid - 1, gui.playlist_row_height - 1) + gui.heart_fields.append(rect) + if tauon.coll(rect): + columns_tool_tip.set(run - 7 * gui.scale, y, text, font, rect) -def forget_pl_import_folder(pl: int) -> None: - pctl.multi_playlist[pl].last_folder = [] + 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 -def remove_duplicates(pl: int) -> None: - playlist = [] + # w += 1 + # if w > gui.playlist_view_length: + # break - for item in pctl.multi_playlist[pl].playlist_ids: - if item not in playlist: - playlist.append(item) + # 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 - 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 (inp.right_click and gui.playlist_top + 5 * gui.scale + gui.playlist_row_height * len(list_items) < + inp.mouse_position[1] < window_size[ + 1] - 55 and width + left > inp.mouse_position[0] > gui.playlist_left + 15): + playlist_menu.activate() - pctl.multi_playlist[pl].playlist_ids[:] = playlist[:] + SDL_SetRenderTarget(self.renderer, gui.main_texture) + SDL_RenderCopy(self.renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) + if inp.mouse_down is False: + playlist_hold = False -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")) + ddt.pretty_rect = None + ddt.alpha_bg = False + def cache_render(self): + SDL_RenderCopy(self.renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) -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 +class ArtBox: + def __init__(self, tauon: Tauon): + self.tauon = tauon + self.gui = tauon.gui + self.inp = tauon.gui.inp + self.ddt = tauon.bag.ddt + self.colours = tauon.bag.colours -def auto_sync_thread(pl: int) -> None: - if prefs.transcode_inplace: - show_message(_("Cannot sync when in transcode inplace mode")) - return + def draw(self, x, y, w, h, target_track=None, tight_border=False, default_border=None): + tauon = self.tauon + ddt = self.ddt + colours = self.colours + gui = self.gui + inp = self.inp - # Find target path - gui.sync_progress = "Starting Sync..." - gui.update += 1 + # 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) - 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 + # We need to find the size of the inner square for the artwork + # box = min(w, h) - prefs.sync_target = str(path) + box_w = w + box_h = h - # Get list of folder names on device - logging.info("Getting folder list from device...") - d_folder_names = path.iterdir() - logging.info("Got list") + box_w -= 17 * gui.scale # Inset the square a bit + box_h -= 17 * gui.scale # Inset the square a bit - # Get list of folders we want - folders = convert_playlist(pl, get_list=True) - folder_names: list[str] = [] - folder_dict = {} + box_x = x + ((w - box_w) // 2) + box_y = y + ((h - box_h) // 2) - if gui.stop_sync: - gui.sync_progress = "" - gui.stop_sync = False - gui.update += 1 + # And position the square + rect = (box_x, box_y, box_w, box_h) + gui.main_art_box = rect - # 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 + # Draw the album art. If side bar is being dragged set quick draw flag + showc = None + result = 1 - # ------ - # 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) + 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), gui.side_drag) + showc = album_art_gen.get_info(target_track) - # ------- - # 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}") + # 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: - logging.error(f"Already exists: {folder}") + ddt.rect_s(rect, colours.art_box, 1 * gui.scale) + border = rect - gui.update += 1 - # ----- - # Prepare and copy - for i, item in enumerate(todos): - gui.sync_progress = _("Copying files to device") - if gui.stop_sync: - break + tauon.fields.add(border) - 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 + # 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 - if prefs.bypass_transcode or (prefs.smart_bypass and 0 < pctl.get_track(folder_dict[item][0]).bitrate <= 128): - logging.info("Smart bypass...") + # Input for album art + if target_track: - 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 + # Cycle images on click - (path / item).mkdir() - encode_done = source_parent - else: - show_message(_("One or more folders is missing")) - continue + if tauon.coll(gui.main_art_box) and inp.mouse_click is True and key_focused == 0: - else: + album_art_gen.cycle_offset(target_track) - 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 pctl.mpris: + pctl.mpris.update(force=True) - if encode_done.exists(): + # Activate picture context menu on right click + if tight_border and gui.art_drawn_rect: + if inp.right_click and tauon.coll(gui.art_drawn_rect) and target_track: + picture_menu.activate(in_reference=target_track) + elif inp.right_click and tauon.coll(rect) and target_track: + picture_menu.activate(in_reference=target_track) - if (path / item).exists(): - show_message( - _("Sync warning"), _("One or more folders to sync has the same name. Skipping."), mode="warning") - continue + # Draw picture metadata + if showc is not None and tauon.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: - (path / item).mkdir() + padding = 6 * gui.scale - for file in encode_done.iterdir(): - file = file.name - logging.info(f"Copy file {file} to {path / item}…") - # gui.sync_progress += "." - gui.update += 1 + 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] - 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") + art_metadata_overlay(xw, yh, showc) - 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" +class ScrollBox: - logging.info("Finished copying folder") - - gui.sync_speed = 0 - gui.sync_progress = "" - gui.stop_sync = False - gui.update += 1 - show_message(_("Sync completed"), mode="done") + 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 auto_sync(pl: int) -> None: - shoot_dl = threading.Thread(target=auto_sync_thread, args=([pl])) - shoot_dl.daemon = True - shoot_dl.start() + 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 -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) + if click is None: + click = inp.mouse_click + bar_height = round(90 * gui.scale) -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 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] -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) + 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] -def set_podcast_playlist(pl: int) -> None: - pctl.multi_playlist[pl].persist_time_positioning ^= True + ddt.rect((x, y, w, h), bg) + half = bar_height // 2 -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] + ratio = value / max_value -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] + mi = y + half + mo = y + h - half + distance = mo - mi + position = int(round(distance * ratio)) + fw = w + extend_field + fx = x - extend_field -def csv_string(item): - item = str(item) - item.replace("\"", "\"\"") - return f"\"{item}\"" + if tauon.coll((fx, y, fw, h)): + if inp.mouse_down: + gui.update += 1 -def export_playlist_albums(pl: int) -> None: - p = pctl.multi_playlist[pl] - name = p.title - playlist = p.playlist_ids + if r_click: + p = inp.mouse_position[1] - half - y + p = max(0, p) - 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) + range = h - bar_height + p = min(p, range) - playtimes[last_folder] = playtimes.get(last_folder, 0) + int(star_store.get(id)) + per = p / range - filename = f"{user_directory}/{name}.csv" - xport = open(filename, "w") + value = int(round(max_value * per)) - xport.write("Album name;Artist;Release date;Genre;Rating;Playtime;Folder path") + ratio = value / max_value - for id in albums: - track = pctl.get_track(id) - artist = track.album_artist - if not artist: - artist = track.artist + mi = y + half + mo = y + h - half + distance = mo - mi + position = int(round(distance * ratio)) - 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)) + in_bar = False + if tauon.coll((x, mi + position - half, w, bar_height)): + in_bar = True + if click: + self.held = True - xport.close() - show_message(_("Export complete."), _("Saved as: ") + filename, mode="done") + # p_y = pointer(c_int(0)) + # SDL_GetGlobalMouseState(None, p_y) + get_sdl_input.mouse_capture_want = True + self.source_click_y = inp.mouse_position[1] + self.source_bar_y = position + if pctl.playlist_view_position < 0: + pctl.playlist_view_position = 0 + elif inp.mouse_down and not self.held: + if click and not in_bar: + self.slide_hold = True + self.direction_lock = 1 + if inp.mouse_position[1] - y < position: + self.direction_lock = 0 -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)) + self.d_position = value / max_value + if self.slide_hold: + if (self.direction_lock == 1 and inp.mouse_position[1] - y < position + half) or \ + (self.direction_lock == 0 and inp.mouse_position[1] - y > position + half): + pass + else: -# tab_menu.add_to_sub("Empty Playlist", 0, new_playlist) + tt = scroll_timer.hit() + if tt > 0.1: + tt = 0 -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)) + 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 -def key_rating(index: int): - return star_store.get_rating(index) + if (self.held and inp.mouse_up) or not inp.mouse_down: + self.held = False -def key_scrobbles(index: int): - return pctl.get_track(index).lfm_scrobbles + if self.held and not window_is_focused(): + self.held = False -def key_disc(index: int): - return pctl.get_track(index).disc_number + if self.held: + get_sdl_input.mouse_capture_want = True + new_y = inp.mouse_position[1] + gui.update += 1 -def key_cue(index: int): - return pctl.get_track(index).is_cue + offset = new_y - self.source_click_y -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 + position = self.source_bar_y + offset + position = max(position, 0) + position = min(position, distance) -def add_pl_tag(text): - return f" <{text}>" + ratio = position / distance + value = int(round(max_value * ratio)) + colour = fg_off + rect = (x, mi + position - half, w, bar_height) + tauon.fields.add(rect) + if tauon.coll(rect): + colour = fg + if self.held: + colour = fg_h -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.rect(rect, colour) - if custom_list is not None: - return playlist + if self.slide_hold: + return round(max_value * self.d_position) - 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)) + return value - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rat>" +class RadioBox: + def __init__(self, tauon: Tauon): + 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() -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) + self.x = 1 + self.y = 1 + self.w = 1 + self.h = 1 + self.center = False - if custom_list is not None: - return playlist + self.scroll_position = 0 + self.scroll = ScrollBox() - 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)) + 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 = "" - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a pt>" + 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 = "" -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)) + self.drag = None + self.tab = 0 + self.temp_list = [] -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 + self.hosts = None + self.host = None - if len(source) < 3: - return [] + self.search_menu = Menu(tauon, 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)) - 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)) + 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 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 + 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 - if get_sets: - r = [] - for item in sets: - r.append((item, best(item))) - return r + def search_country(self, text): - sets = sorted(sets, key=best, reverse=True) + 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") - playlist = [] + def search_tag(self, text): - for se in sets: - playlist += se + text = text.lower() + self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&tag=" + text) - # 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 + def search_title(self, text): - 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)) + text = text.lower() + self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&name=" + text) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a pa>" + 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...") -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)) + try: + response = requests.get(url, timeout=10) + if response.status_code != 200: + logging.error(f"M3U Fetch error code: {response.status_code}") + return None -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)) + 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) -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 + return None - if len(source) < 3: - return [] + except Exception: + logging.exception("Failed to extract M3U") + return None - 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 start(self, station: RadioStation): + url = station.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 - def best(folder): - return album_star_store.get_rating(pctl.get_track(folder[0])) + if self.load_connecting: + return - if get_sets: - r = [] - for item in sets: - r.append((item, best(item))) - return r + if tauon.spot_ctl.playing or tauon.spot_ctl.coasting: + tauon.spot_ctl.control("stop") - sets = sorted(sets, key=best, reverse=True) + if self.websocket: + self.websocket.close() + logging.info("Websocket closed") - playlist = [] + self.playing_title = "" + self.playing_title = station.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 = "" - for se in sets: - playlist += se + album_art_gen.clear_cache() - if custom_list is not None: - return playlist + if not tauon.test_ffmpeg(): + prefs.auto_rec = False + return - 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)) + self.run_proxy = True + if url.endswith(".ts"): + self.run_proxy = False - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a rata>" + 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 = station.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 = station -def gen_lyrics(plpl: int, custom_list=None): - playlist = [] + if tauon.stream_proxy.download_running: + tauon.stream_proxy.abort = True - source = custom_list - if source is None: - source = pctl.multi_playlist[pl].playlist_ids + self.load_connecting = True + self.load_failed = False - for item in source: - if pctl.master_library[item].lyrics != "": - playlist.append(item) + shoot = threading.Thread(target=self.start2, args=[url]) + shoot.daemon = True + shoot.start() - if custom_list is not None: - return playlist + def start2(self, url: str): - if len(playlist) > 0: - pctl.multi_playlist.append( - pl_gen( - title=_("Tracks with lyrics"), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + 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 - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a ly" + 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() - else: - show_message(_("No tracks with lyrics were found.")) + 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 -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)) + 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 gen_incomplete(plpl: int, custom_list=None): - playlist = [] + 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() - source = custom_list - if source is None: - source = pctl.multi_playlist[pl].playlist_ids + if d["op"] == 0: + self.ws_interval = d["d"]["heartbeat"] / 1000 + ws.send("{\"op\":9}") - albums = {} - nums = {} - for id in source: - track = pctl.get_track(id) - if track.album and track.track_number: + if d["op"] == 1: + try: - if type(track.track_number) is str and not track.track_number.isdigit(): - continue + 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 - if track.album not in albums: - albums[track.album] = [] - nums[track.album] = [] + pctl.found_tags = found_tags + pctl.tag_meta = line - if track not in albums[track.album]: - albums[track.album].append(track) - nums[track.album].append(int(track.track_number)) + filename = d["d"]["song"]["albums"][0]["image"] + fulllink = "https://cdn.listen.moe/covers/" + filename - 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) + #logging.info(fulllink) + art_response = requests.get(fulllink, timeout=10) + #logging.info(art_response.status_code) - if custom_list is not None: - return playlist + 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") - 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" + 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 - else: - show_message(_("No incomplete albums were found.")) + def on_error(ws, error): + logging.error(error) + def on_close(ws): + logging.info("### closed ###") -def gen_codec_pl(codec): - playlist = [] + 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...") - 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) + th.start_new_thread(run, ()) - if len(playlist) > 0: - pctl.multi_playlist.append( - pl_gen( - title=_("Codec: ") + codec, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + # 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, station: RadioStation) -> None: + for i, saved in enumerate(prefs.radio_urls): + if saved.stream_url == station.stream_url and saved.title == station.title: + del prefs.radio_urls[i] -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 + def delete_radio_entry_after(self, station) -> None: + p = radiobox.right_clicked_station_p + del prefs.radio_urls[p + 1:] - a_cache = {} + def edit_entry(self, station: RadioStation) -> None: + self.radio_field_title.text = station.title + self.radio_field.text = station.stream_url - def key_import(index: int): + def browser_get_hosts(self): - track = pctl.master_library[index] - cached = a_cache.get((track.album, track.parent_folder_name)) - if cached is not None: - return cached + import socket + """ + Get all base urls of all currently available radiobrowser servers - if track.album: - a_cache[(track.album, track.parent_folder_name)] = index - return index + Returns: + list: a list of strings - playlist = copy.deepcopy(source) - playlist = sorted(playlist, key=key_import, reverse=reverse) - sort_track_2(0, playlist) + """ + 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] - if custom_list is not None: - return playlist + # 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 gen_last_modified(index, custom_list=None, reverse=True): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + def search_page(self): - a_cache = {} + y = self.y + x = self.x + w = self.w + h = self.h - def key_modified(index: int): + yy = y + round(40 * gui.scale) - track = pctl.master_library[index] - cached = a_cache.get((track.album, track.parent_folder_name)) - if cached is not None: - return cached + width = round(330 * gui.scale) + rect = (x + 8 * gui.scale, yy - round(2 * gui.scale), width, 22 * gui.scale) + tauon.fields.add(rect) + # if (tauon.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) - if track.album: - a_cache[(track.album, track.parent_folder_name)] = pctl.master_library[index].modified_time - return pctl.master_library[index].modified_time + ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) - playlist = copy.deepcopy(source) - playlist = sorted(playlist, key=key_modified, reverse=reverse) - sort_track_2(0, playlist) + 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: - if custom_list is not None: - return playlist + 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") - 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)) + ww = ddt.get_text_w(_("Get Top Voted"), 212) + if inp.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() - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a m>" + self.temp_list.append( + RadioStation( + title="Nightwave Plaza", + stream_url_fallback="https://radio.plaza.one/ogg", + stream_url="https://radio.plaza.one/ogg", + website_url="https://plaza.one/", + icon="https://plaza.one/icons/apple-touch-icon.png", + country="Japan")) + + self.temp_list.append( + RadioStation( + title="Gensokyo Radio", + stream_url_fallback="https://stream.gensokyoradio.net/GensokyoRadio-enhanced.m3u", + stream_url="https://stream.gensokyoradio.net/1", + website_url="https://gensokyoradio.net/", + icon="https://gensokyoradio.net/favicon.ico", + country="Japan")) + + self.temp_list.append( + RadioStation( + title="Listen.moe | Jpop", + stream_url_fallback="https://listen.moe/stream", + stream_url="https://listen.moe/stream", + website_url="https://listen.moe/", + icon="https://avatars.githubusercontent.com/u/26034028?s=200&v=4", + country="Japan")) + + self.temp_list.append( + RadioStation( + title="Listen.moe | Kpop", + stream_url_fallback="https://listen.moe/kpop/stream", + stream_url="https://listen.moe/kpop/stream", + website_url="https://listen.moe/", + icon="https://avatars.githubusercontent.com/u/26034028?s=200&v=4", + country="Korea")) + + self.temp_list.append( + RadioStation( + title="HBR1 Dream Factory | Ambient", + stream_url_fallback="http://radio.hbr1.com:19800/ambient.ogg", + stream_url="http://radio.hbr1.com:19800/ambient.ogg", + website_url="http://www.hbr1.com/")) + + self.temp_list.append( + RadioStation( + title="Yggdrasil Radio | Anime & Jpop", + stream_url_fallback="http://shirayuki.org:9200/", + stream_url="http://shirayuki.org:9200/", + website_url="https://yggdrasilradio.net/")) + for station in primary_stations: + self.temp_list.append(station) -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)) + 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): -# 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) + 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=tls_context) + data = response.read() + data = json.loads(data.decode()) + self.parse_data(data) + self.searching = False -def gen_love(pl: int, custom_list=None): - playlist = [] + def parse_data(self, data: list[RadioStation]): + self.temp_list.clear() + for station in data: + #logging.info(station) + radio: RadioStation = RadioStation( + title=station["name"], + stream_url_fallback=station["url"], + stream_url=station["url_resolved"], + icon=station["favicon"], + 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"] + self.temp_list.append(radio) + gui.update += 1 - source = custom_list - if source is None: - source = pctl.multi_playlist[pl].playlist_ids + def render(self) -> None: + if self.edit_mode: + w = round(510 * gui.scale) + h = round(120 * gui.scale) # + sh - for item in source: - if get_love_index(item): - playlist.append(item) + 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 tauon.coll((x, y, w, h))): + self.active = False - playlist.sort(key=lambda x: get_love_timestamp_index(x), reverse=True) + 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) - if custom_list is not None: - return playlist + self.saved() + return - 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.")) + 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 -def gen_comment(pl: int) -> None: - playlist = [] + yy = y - 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) + 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 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.")) + ddt.text_background_colour = colours.box_background + if key_esc_press or (gui.level_2_click and not tauon.coll((x, y, w, h))): + self.active = False -def gen_replay(pl: int) -> None: - playlist = [] + ddt.text((x + 10 * gui.scale, yy + 8 * gui.scale), _("Station Browser"), colours.box_title_text, 213) - for item in pctl.multi_playlist[pl].playlist_ids: - if pctl.master_library[item].misc.get("replaygain_track_gain"): - playlist.append(item) + # --- + 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 - 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.")) + 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") -def gen_sort_len(index: int, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + 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) - def length(index: int) -> int: + # --- ---------------------------------------------------------------------- + if self.tab == 1: + self.search_page() + elif self.tab == 0: + self.saved() + self.draw_list() + # self.footer() + return - if pctl.master_library[index].length < 1: - return 0 - return int(pctl.master_library[index].length) + def saved(self): + y = self.y + x = self.x + w = self.w + h = self.h - playlist = copy.deepcopy(source) - playlist = sorted(playlist, key=length, reverse=True) + yy = y + round(40 * gui.scale) - if custom_list is not None: - return playlist + width = round(370 * gui.scale) - # pctl.multi_playlist.append( - # [pctl.multi_playlist[index].title + " <Duration Sorted>", 0, copy.deepcopy(playlist), 0, 1, 0]) + rect = (x + 8 * gui.scale, yy - round(2 * gui.scale), width, 22 * gui.scale) + tauon.fields.add(rect) + if (tauon.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) - 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)) + ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a d>" + 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) + tauon.fields.add(rect) + if (tauon.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 -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)) + 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): -def gen_folder_duration(pl: int, get_sets: bool = False): - if len(pctl.multi_playlist[pl].playlist_ids) < 3: - return None + 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: RadioStation = RadioStation( + title=self.radio_field_title.text, + stream_url=self.radio_field.text) + radio.title = self.radio_field_title.text + if radio.stream_url != self.radio_field.text: + radio.stream_url = self.radio_field.text + radio.website_url = "" # Different URL, null the website # TODO(Martin): no way to edit for now - 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)) + if self.add_mode: + pctl.radio_playlists[pctl.radio_playlist_viewing].stations.append(radio) + self.active = False - def best(folder): - total_duration = 0 - for item in folder: - total_duration += pctl.master_library[item].length - return total_duration + else: + show_message(_("Could not validate URL. Must start with https:// or http://")) - if get_sets: - r = [] - for item in sets: - r.append((item, best(item))) - return r + def draw_list(self): - sets = sorted(sets, key=best, reverse=True) - playlist = [] + x = self.x + y = self.y + w = self.w + h = self.h - for se in sets: - playlist += se + if self.drag: + gui.update_on_drag = True - 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)) + 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 -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)) + rect = (x, y, w, h) + if tauon.coll(rect): + self.scroll_position += inp.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) -def gen_sort_date(index: int, rev: bool = False, custom_list=None): - def g_date(index: int): + self.scroll_position = max(self.scroll_position, 0) - if pctl.master_library[index].date != "": - return str(pctl.master_library[index].date) - return "z" + p = self.scroll_position * 2 + offset = 0 + to_delete = None + swap = None - playlist = [] - lowest = 0 - highest = 0 - first = True + while True: - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + if p > len(radio_list) - 1: + break - 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) + xx = x + offset + item = radio_list[p] - playlist = sorted(playlist, key=g_date, reverse=rev) + rect = (xx, yy, round(233 * gui.scale), round(40 * gui.scale)) + tauon.fields.add(rect) - if custom_list is not None: - return playlist + bg = colours.box_background + text_colour = colours.box_input_text - 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) + ">" + playing = pctl.playing_state == 3 and self.loaded_url == item.stream_url - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + line, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + if playing: + # bg = colours.box_sub_highlight + # ddt.rect(rect, bg, True) - 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<" + bg = colours.tab_background_active + text_colour = colours.tab_text_active + ddt.rect(rect, bg) + if radio_view.drag: + if station == 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 tauon.coll(rect)) and not playing): + text_colour = colours.box_sub_text + bg = [255, 255, 255, 10] + ddt.rect(rect, bg) -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)) + if tauon.coll(rect): + if gui.level_2_click: + # self.drag = p + # self.click_point = copy.copy(inp.mouse_position) + radio_view.drag = station + radio_view.click_point = copy.copy(inp.mouse_position) + if inp.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, inp.mouse_position, round(4 * gui.scale)): + self.start(station) + if middle_click: + to_delete = p + if level_2_right_click: + self.right_clicked_station = station + self.right_clicked_station_p = p + radio_entry_menu.activate(station) -def gen_sort_date_new(index: int): - gen_sort_date(index, True) + bg = alpha_blend(bg, colours.box_background) + boxx = round(32 * gui.scale) + toff = boxx + round(10 * gui.scale) + if station.title: + ddt.text( + (xx + toff, yy + round(3 * gui.scale)), station.title, text_colour, 212, bg=bg, + max_w=rect[2] - (15 * gui.scale + toff)) + else: + ddt.text( + (xx + toff, yy + round(3 * gui.scale)), station.stream_url, text_colour, 212, bg=bg, + max_w=rect[2] - (15 * gui.scale + toff)) -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)) + country = station.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(station, b_rect[0], b_rect[1], b_rect[2]) -# 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) + if offset == 0: + offset = rect[2] + round(4 * gui.scale) + else: + offset = 0 + yy += round(43 * gui.scale) -def gen_500_random(index: int): - playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) + if yy > y + 300 * gui.scale: + break - random.shuffle(playlist) + p += 1 - 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)) + # if to_delete is not None: + # del radio_list[to_delete] + # + # if inp.mouse_up and self.drag and inp.mouse_position[1] > yy + round(22 * gui.scale): + # swap = len(radio_list) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a st" + # if self.drag and not point_proximity_test(self.click_point, inp.mouse_position, round(4 * gui.scale)): + # ddt.rect(( + # inp.mouse_position[0] + round(8 * gui.scale), inp.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 -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)) + # if not inp.mouse_down: + # self.drag = None + def footer(self): -def gen_folder_shuffle(index, custom_list=None): - folders = [] - dick = {} + y = self.y + x = self.x + round(15 * gui.scale) + w = self.w + h = self.h - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + 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")) - 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) + 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") - random.shuffle(folders) - playlist = [] + 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 - for folder in folders: - playlist += dick[folder] +class RenamePlaylistBox: - if custom_list is not None: - return playlist + def __init__(self): - 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)) + self.x = 300 + self.y = 300 + self.playlist_index = 0 - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a ra" + self.edit_generator = False + def toggle_edit_gen(self): -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)) + 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 -def gen_best_random(index: int): - playlist = [] + pl = self.playlist_index + id = pl_to_id(pl) - for p in pctl.multi_playlist[index].playlist_ids: - time = star_store.get(p) + text = pctl.gen_codes.get(id) + if not text: + text = "" - if time > 300: - playlist.append(p) + rename_text_area.set_text(text) + rename_text_area.highlight_none() - random.shuffle(playlist) + gui.regen_single = rename_playlist_box.playlist_index + tauon.thread_manager.ready("worker") - 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" + 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): -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)) + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False + if inp.key_tab_press: + self.toggle_edit_gen() -def gen_reverse(index, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + text_w = ddt.get_text_w(rename_text_area.text, 315) + min_w = max(250 * gui.scale, text_w + 50 * gui.scale) - playlist = list(reversed(source)) + 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 - if custom_list is not None: - return playlist + # Draw background + ddt.rect(rect, bg) - 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)) + # 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) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rv" + # 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 -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)) + 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)] -def gen_folder_reverse(index: int, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + if hint_rect[0] + hint_rect[2] > window_size[0]: + hint_rect[0] = window_size[0] - hint_rect[2] - 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) + 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) - folders = list(reversed(folders)) - playlist = [] + text_colour = [150, 150, 150, 255] + title_colour = text_colour + code_colour = [250, 250, 250, 255] + hint_colour = [110, 110, 110, 255] - for folder in folders: - playlist += dick[folder] + title_font = 311 + code_font = 311 + hint_font = 310 - if custom_list is not None: - return playlist + # ddt.pretty_rect = hint_rect - 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)) + 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) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rva" + 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) -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)) + 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) -def gen_dupe(index: int) -> None: - playlist = pctl.multi_playlist[index].playlist_ids + 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) - 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)) + 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) -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)) + 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) -def gen_sort_path(index: int) -> None: - def path(index: int) -> str: - return pctl.master_library[index].fullpath + 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) - playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - playlist = sorted(playlist, key=path) + 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) - 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)) + 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 -# tab_menu.add_to_sub("Filepath", 1, gen_sort_path, pass_ref=True) + # 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 tauon.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 -def gen_sort_artist(index: int) -> None: - def artist(index: int) -> str: - return pctl.master_library[index].artist +class PlaylistBox: - playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - playlist = sorted(playlist, key=artist) + def recalc(self): + self.tab_h = round(25 * self.gui.scale) + self.gap = round(2 * self.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)) + self.text_offset = 2 * self.gui.scale + if self.gui.scale == 1.25: + self.text_offset = 3 + def __init__(self, tauon: Tauon): + bag = tauon.bag + self.gui = tauon.gui + self.scroll_on = bag.prefs.old_playlist_box_position + self.drag = False + self.drag_source = 0 + self.drag_on = -1 -# tab_menu.add_to_sub("Artist → gui.abc", 0, gen_sort_artist, pass_ref=True) + self.adds = [] + self.indicate_w = round(2 * self.gui.scale) -def gen_sort_album(index: int) -> None: - def album(index: int) -> None: - return pctl.master_library[index].album + self.lock_icon = asset_loader(bag, bag.loaded_asset_dc, "lock-corner.png", True) + self.pin_icon = asset_loader(bag, bag.loaded_asset_dc, "dia-pin.png", True) + self.gen_icon = asset_loader(bag, bag.loaded_asset_dc, "gen-gear.png", True) + self.spot_icon = asset_loader(bag, bag.loaded_asset_dc, "spot-playlist.png", True) - 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)) + # if gui.scale == 1.25: + self.tab_h = 0 + self.gap = 0 + self.text_offset = 2 * self.gui.scale + self.recalc() -# 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 draw(self, x, y, w, h): + # 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 * self.gui.scale) // (self.gap + self.tab_h) -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" + 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] -def reload_config_file(): - if transcode_list: - show_message(_("Cannot reload while a transcode is in progress!"), mode="error") - return + dark_mode = False + if bg_lumi > 0.8: + dark_mode = True - load_prefs() - gui.opened_config_file = False + if light_mode: + indicate_w = round(3 * gui.scale) + else: + indicate_w = round(2 * gui.scale) - 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() + show_scroll = False + tab_start = x + 10 * self.gui.scale + if window_size[0] < 700 * self.gui.scale: + tab_start = x + 4 * self.gui.scale -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 + if inp.mouse_wheel != 0 and tauon.coll((x, y, w, h)): + self.scroll_on -= inp.mouse_wheel + self.scroll_on = min(self.scroll_on, len(pctl.multi_playlist) - max_tabs + 1) -def open_keymap_file(): - target = str(config_directory / "input.txt") + self.scroll_on = max(self.scroll_on, 0) - if not os.path.isfile(target): - show_message(_("Input file missing")) - return + if len(pctl.multi_playlist) > max_tabs: + show_scroll = True + else: + self.scroll_on = 0 - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", target]) - else: - subprocess.call(["xdg-open", target]) + if show_scroll: + tab_start += 15 * self.gui.scale + if colours.lm: + w -= round(6 * gui.scale) + tab_width = w - tab_start # - 0 * gui.scale -def open_file(target): - if not os.path.isfile(target): - show_message(_("Input file missing")) - return + # Draw scroll bar + if show_scroll: + self.scroll_on = playlist_panel_scroll.draw( + x + 2, y + 1, 15 * self.gui.scale, h, self.scroll_on, len(pctl.multi_playlist) - max_tabs + 1) - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", target]) - else: - subprocess.call(["xdg-open", target]) + 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 -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]) + # 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 -def remove_folder(index: int): - global default_playlist + # if not pl.hidden and i in tabs_on_top: + # continue - 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] + tab_on += 1 - reload() + if tauon.coll((tab_start, yy - 1, tab_width, (self.tab_h + 1))): + if right_click: + if gui.radio_view: + radio_tab_menu.activate(i, inp.mouse_position) + else: + tab_menu.activate(i, inp.mouse_position) + gui.tab_menu_pl = i + if tab_menu.active is False and middle_click: + delete_pl = i + # delete_playlist(i) + # break -def convert_folder(index: int): - global default_playlist - global transcode_list + if inp.mouse_up and self.drag and coll_point(inp.mouse_up_position, (tab_start, yy - 1, tab_width, (self.tab_h + 1))): - if not tauon.test_ffmpeg(): - return + # 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 - 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] + # Move playlist tab + if i != self.drag_on and not point_proximity_test(gui.drag_source_position, inp.mouse_position, 10 * gui.scale): + if inp.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) - 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") + gui.update += 1 - return - folder = [index] + # Double click to play + if inp.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(inp.last_click_location, inp.mouse_up_position) < 5 * gui.scale: - 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: + 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(pctl.default_playlist[pctl.selected_in_playlist], pl_position=pctl.selected_in_playlist) + if inp.mouse_up: + top_panel.tab_d_click_timer.set() + top_panel.tab_d_click_ref = pl_to_id(i) - 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 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() - 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") + # Process input of dragging tracks onto tab + if inp.quick_drag is True and inp.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)) + inp.quick_drag = False + modified = False + gui.pl_update += 1 - return + for item in shift_selection: + pctl.multi_playlist[i].playlist_ids.append(pctl.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) - #logging.info(folder) - transcode_list.append(folder) - tauon.thread_manager.ready("worker") + # Toggle hidden flag on click + if draw_pin_indicator and inp.mouse_click and tauon.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 -def transfer(index: int, args) -> None: - global cargo - global default_playlist - old_cargo = copy.deepcopy(cargo) + # Draw tabs + # delete_pl = None + tab_on = 0 + yy = y + 5 * gui.scale + for i, pl in enumerate(pctl.multi_playlist): - 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] + # if yy + self.tab_h > y + h: + # break + if tab_on >= max_tabs: + break + if i < self.scroll_on: + continue - 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] + tab_on += 1 - elif args[1] == 3: # playlist - cargo += default_playlist - if args[0] == 0: # cut - default_playlist = [] + name = pl.title + hidden = pl.hidden - elif args[0] == 2: # Drop - if args[1] == 1: # Before + # Background is insivible by default (for hightlighting if selected) + bg = [0, 0, 0, 0] - 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 + # 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] - while len(cargo) > 0: - default_playlist.insert(insert, cargo.pop()) + # 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] - elif args[1] == 2: # After - insert = pctl.selected_in_playlist + # Highlight target playlist when tragging tracks over + if tauon.coll( + (tab_start + 50 * gui.scale, yy - 1, tab_width - 50 * gui.scale, (self.tab_h + 1))) and inp.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] - while insert < len(default_playlist) and pctl.master_library[default_playlist[insert]].parent_folder_name == \ - pctl.master_library[index].parent_folder_name: - insert += 1 + # Get actual bg from blend for text bg + real_bg = alpha_blend(bg, colours.playlist_box_background) - while len(cargo) > 0: - default_playlist.insert(insert, cargo.pop()) - elif args[1] == 3: # End - default_playlist += cargo - # cargo = [] + # Draw highlight + ddt.rect((tab_start, yy - round(1 * gui.scale), tab_width, self.tab_h), bg) - cargo = old_cargo + # Draw title text + text_start = 10 * gui.scale + if draw_pin_indicator: + # text_start = 40 * gui.scale + text_start = 32 * gui.scale - reload() + 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] -def temp_copy_folder(ref): - global cargo - cargo = [] - transfer(ref, args=[1, 2]) + if light_mode: + cl = [0, 0, 0, 40] + xx = tab_start + tab_width - self.lock_icon.w + self.lock_icon.render(xx, yy, cl) -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() + 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 = tauon.coll((tab_start + 50 * gui.scale, yy - 1, tab_width - 50 * gui.scale, (self.tab_h + 1))) -def menu_paste(position): - paste(None, position) + # 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 -def s_copy(): - # Copy tracks to internal clipboard - # gui.lightning_copy = False - # if key_shift_down: - gui.lightning_copy = True + ddt.rect((tab_start + 0 - 2 * gui.scale, yy - round(1 * gui.scale), indicate_w, self.tab_h), indicator_colour) - clip = copy_from_clipboard() - if "file://" in clip: - copy_to_clipboard("") + # # If mouse over + if hit: + # Draw indicator for dragging tracks + if inp.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]) - global cargo - cargo = [] - if default_playlist: - for item in shift_selection: - cargo.append(default_playlist[item]) + # Draw indicators for moving tab + if self.drag and i != self.drag_on and not point_proximity_test( + gui.drag_source_position, inp.mouse_position, 10 * gui.scale): + if inp.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]) - if not cargo and -1 < pctl.selected_in_playlist < len(default_playlist): - cargo.append(default_playlist[pctl.selected_in_playlist]) + elif inp.quick_drag and not point_proximity_test(gui.drag_source_position, inp.mouse_position, 15 * gui.scale): + for item in shift_selection: + if len(pctl.default_playlist) > item and pctl.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 inp.quick_drag and not point_proximity_test(gui.drag_source_position, inp.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]) - tauon.copied_track = None + # 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 - if len(cargo) == 1: - tauon.copied_track = cargo[0] + 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]) -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 + yy += self.tab_h + self.gap + if delete_pl is not None: + # delete_playlist(delete_pl) + delete_playlist_ask(delete_pl) + gui.update += 1 -def lightning_paste(): - move = True - # if not key_shift_down: - # move = False + # Create new playlist if drag in blank space after tabs + rect = (x, yy, w - 10 * gui.scale, h - (yy - y)) + tauon.fields.add(rect) - move_track = pctl.get_track(cargo[0]) - move_path = move_track.parent_folder_path + if tauon.coll(rect): + if inp.quick_drag: + ddt.rect((tab_start, yy, tab_width, self.indicate_w), [80, 160, 200, 255]) + if inp.mouse_up: + drop_tracks_to_new_playlist(shift_selection) - 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 right_click: + extra_tab_menu.activate(pctl.active_playlist_viewing) - match_track = pctl.get_track(default_playlist[shift_selection[0]]) - match_path = match_track.parent_folder_path + # Move tab to end playlist if dragged past end + if self.drag: + if inp.mouse_up: + if inp.key_ctrl_down: + # Duplicate playlist on ctrl + gen_dupe(tauon.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 - if pctl.playing_state > 0 and move: - if pctl.playing_object().parent_folder_path == move_path: - pctl.stop(True) + move_playlist(self.drag_on, i) + gui.update += 2 + self.drag = False + elif inp.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]) - p = Path(match_path) - s = list(p.parts) - base = s[0] - c = base - del s[0] +class ArtistList: + def __init__(self, tauon: Tauon) -> None: - 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]) + self.tab_h = round(60 * tauon.gui.scale) + self.thumb_size = round(55 * tauon.gui.scale) - to_move = list(set(to_move)) + self.current_artists = [] + self.current_album_counts = {} + self.current_artist_track_counts = {} - for level in s: - upper = c - c = os.path.join(c, level) + self.thumb_cache = {} - t_artist = match_track.artist - ta_artist = match_track.album_artist + self.to_fetch = "" + self.to_fetch_mbid_a = "" - t_artist = filename_safe(t_artist) - ta_artist = filename_safe(ta_artist) + self.scroll_position = 0 - if (len(t_artist) > 0 and t_artist in level) or \ - (len(ta_artist) > 0 and ta_artist in level): + self.id_to_load = "" - logging.info("found target artist level") - logging.info(t_artist) - logging.info("Upper folder is: " + upper) + self.d_click_timer = Timer() + self.d_click_ref = -1 - if len(move_path) < 4: - show_message(_("Safety interupt! The source path seems oddly short."), move_path, mode="error") - return + self.click_ref = -1 + self.click_highlight_timer = Timer() - if not os.path.isdir(upper): - show_message(_("The target directory is missing!"), upper, mode="warning") - return + self.saves = {} - if not os.path.isdir(move_path): - show_message(_("The source directory is missing!"), move_path, mode="warning") - return + self.load = False - 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 + self.shown_letters = [] - if directory_size(move_path) > 3000000000: - show_message(_("Folder size safety limit reached! (3GB)"), move_path, mode="warning") - return + self.hover_on = "NONE" + self.hover_timer = Timer(10) - 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 + self.sample_tracks = {} - artist = move_track.artist - if move_track.album_artist != "": - artist = move_track.album_artist + def load_img(self, artist): + filepath = artist_info_box.get_data(artist, get_img_path=True) - artist = filename_safe(artist) + if filepath and os.path.isfile(filepath): + try: + g = io.BytesIO() + g.seek(0) - if artist == "": - show_message(_("The track needs to have an artist name.")) - return + im = Image.open(filepath) - artist_folder = os.path.join(upper, artist) + 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), + )) - logging.info("Target will be: " + artist_folder) + im.thumbnail((self.thumb_size, self.thumb_size), Image.Resampling.LANCZOS) - 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) + im.save(g, "PNG") + g.seek(0) - 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 + 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) - 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 + self.thumb_cache[artist] = [texture, sdl_rect] + except Exception: + logging.exception("Artist thumbnail processing error") + self.thumb_cache[artist] = None - load_order.playlist_position = insert + elif artist in prefs.failed_artists: + self.thumb_cache[artist] = None + elif not self.to_fetch: - 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 + if prefs.auto_dl_artist_data: + self.to_fetch = artist + tauon.thread_manager.ready("worker") - # for file in os.listdir(artist_folder): - # + else: + self.thumb_cache[artist] = None - if album_mode: - prep_gal() - reload_albums(True) + def worker(self): + if self.load: + if tauon.after_scan: + return - cargo.clear() - gui.lightning_copy = False + self.prep() + self.load = False + return + if self.to_fetch: + if get_lfm_wait_timer.get() < 2: + return -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 + 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() - elif "tidal.com/playlist/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.playlist(num) - clip = False + if mbid and prefs.enable_fanart_artist: + save_fanart_artist_thumb(mbid, filepath3, preview=True) + got_image = True - elif "tidal.com/mix/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.mix(num) - clip = False + except Exception: + logging.exception("Failed to find image from fanart.tv") - elif "tidal.com/browse/track/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.track(num) - clip = False + if not got_image and verify_discogs(): + try: + save_discogs_artist_thumb(artist, filepath4) + except Exception: + logging.exception("Failed to find image from discogs") - elif "tidal.com/browse/artist/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.artist(num) - clip = False + 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) - 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 + self.to_fetch = "" - 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 + def prep(self): + self.scroll_position = 0 - 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 + 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] - load_orders.append(copy.deepcopy(load_order)) - found = True + all = [] + artist_parents = {} + counts = {} + play_time = {} + filtered = 0 + b = 0 - if not found: + try: + for item in current_pl.playlist_ids: + b += 1 + if b % 100 == 0: + time.sleep(0.001) - 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) + track = pctl.get_track(item) - gui.pl_update += 1 + 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(";")] -def s_cut(): - s_copy() - del_selected() + 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 -playlist_menu.add(MenuItem("Paste", paste, paste_deco)) + # 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 -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 + 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) -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 + 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 -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() + # Artist-list, album-counts, scroll-position, playlist-length, number ignored + save = [all, current_album_counts, 0, len(current_pl.playlist_ids), counts, filtered] -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 + # 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 - return [line_colour, colours.menu_background, None] + 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 -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 locate_artist_letter(self, text): + if not text or prefs.artist_list_sort_mode != "alpha": + return -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 + 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 del_selected(force_delete=False): - global shift_selection + 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 - gui.update += 1 - gui.pl_update = 1 + 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 - if not shift_selection: - shift_selection = [pctl.selected_in_playlist] + def draw_card_text_only(self, artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg): + prefs.album_mode = False + for albums in self.current_album_counts.values(): + if len(albums) > 1: + prefs.album_mode = True + break - if not default_playlist: - return + if not prefs.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)) - li = [] + if gui.preview_artist_loading == artist: + # . Max 20 chars. Alt: Downloading image, Loading image + text = _("Downloading data...") - for item in reversed(shift_selection): - if item > len(default_playlist) - 1: - return + 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) - li.append((item, default_playlist[item])) # take note for force delete + 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) - # 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 + 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) + tauon.fields.add(area) - del default_playlist[item] + 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 force_delete: - for item in li: + if thin_mode and tauon.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)) - 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")) + 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) - 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") + ddt.rect(tab_rect, back_colour_2) + bg = back_colour_2 - else: - undo.bk_tracks(pctl.active_playlist_viewing, li) + ddt.rect(rect, back_colour) + ddt.rect(rect, border_colour) - reload() - tree_view_box.clear_target_pl(pctl.active_playlist_viewing) + tauon.fields.add(rect) + if tauon.coll(rect) and is_level_zero(True): + self.hover_any = True - pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(default_playlist) - 1) + hover_delay = 0.5 + if gui.compact_artist_list: + hover_delay = 2 - shift_selection = [pctl.selected_in_playlist] - gui.pl_update += 1 - refind_playing() - pctl.notify_change() + 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)) -def force_del_selected(): - del_selected(force_delete=True) + 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) -def test_show(dummy): - return album_mode + 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 (tauon.coll(area) and is_level_zero() and y + self.tab_h < window_size[1] - gui.panelBY): + prefs.album_mode = False + for albums in self.current_album_counts.values(): + if len(albums) > 1: + prefs.album_mode = True + break -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() + if not prefs.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...") -# Create track context menu -track_menu = Menu(195, show_icons=True) + 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) -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 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) + tauon.fields.add(area) -def last_fm_test(ignore): - if lastfm.connected: - return True - return False + 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] -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 + # Fade on click + bg = colours.side_panel_background + if not thin_mode: + if tauon.coll(area) and is_level_zero(True): + # or pctl.get_track(pctl.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) -heartx_icon.colour = [55, 55, 55, 255] -heartx_icon.xoff = 1 -heartx_icon.yoff = 0 -heartx_icon.colour_callback = heart_xmenu_colour + gui.update += 1 + ddt.rect(area, [50, 50, 50, fade]) + bg = alpha_blend([50, 50, 50, fade], colours.side_panel_background) -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 + 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 tauon.coll(area) and inp.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 -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 + 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() -def love_decox(): - global r_menu_index + 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) - 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")] + blocks = [] + current_block = [] -def love_index(): - global r_menu_index + in_artist = False + this_artist = artist.casefold() + last_ref = None + on = 0 - notify = False - if not gui.show_hearts: - notify = True + for i in range(len(pctl.default_playlist)): + track = pctl.get_track(pctl.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) - # 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() + 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 = [] -# Mark track as 'liked' -track_menu.add(MenuItem("Love", love_index, love_decox, icon=heartx_icon)) + #logging.info(blocks) + # return -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() + # block_starts = [] + # current = False + # for i in range(len(pctl.default_playlist)): + # track = pctl.get_track(pctl.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 -def toggle_spotify_like3(): - toggle_spotify_like_active2(pctl.get_track(r_menu_index)) + if not blocks: + return -def toggle_spotify_like_row_deco(): - tr = pctl.get_track(r_menu_index) - text = _("Spotify Like Track") + #select = block_starts[0] - # 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") + # if len(block_starts) > 1: + # if -1 < pctl.selected_in_playlist < len(pctl.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] - return [colours.menu_text, colours.menu_background, text] + gui.pl_update += 1 -def spot_like_show_test(x): + self.click_highlight_timer.set() - return spotify_show_test and pctl.get_track(r_menu_index).file_ext == "SPTY" + select = blocks[0][0] -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 + if double_click: + # Stat first artist track in playlist -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 + pctl.jump(pctl.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 -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)) + 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 prefs.album_mode: + goto_album(select) -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 + 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() -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() + artist_list_menu.activate(in_reference=artist) + def render(self, x, y, w, h): -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))) + 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 -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] + # 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 -def split_queue_album(id: int) -> int | None: - item = pctl.force_queue[0] + 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] - pl = id_to_pl(item.playlist_id) - if pl is None: - return None + 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 - playlist = pctl.multi_playlist[pl].playlist_ids + else: - i = pctl.playlist_playing_position + 1 - parts = [] - album_parent_path = pctl.get_track(item.track_id).parent_folder_path + # 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") - while i < len(playlist): - if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: - break + area = (x, y, w, h) + area2 = (x + 1, y, w - 3, h) - parts.append((playlist[i], i)) - i += 1 + ddt.rect(area, colours.side_panel_background) + ddt.text_background_colour = colours.side_panel_background - del pctl.force_queue[0] + if tauon.coll(area) and inp.mouse_wheel: + mx = 1 + if prefs.artist_list_style == 2: + mx = 3 + self.scroll_position -= inp.mouse_wheel * mx + self.scroll_position = max(self.scroll_position, 0) - for part in reversed(parts): - pctl.force_queue.insert(0, queue_item_gen(part[0], part[1], item.type)) - return (len(parts)) + range = (h // self.tab_h) - 1 + whole_rage = math.floor(h // self.tab_h) -def add_to_queue_next(ref: int) -> None: - if pctl.force_queue and pctl.force_queue[0].album_stage == 1: - split_queue_album(None) + if range > 4 and self.scroll_position > len(self.current_artists) - range: + self.scroll_position = len(self.current_artists) - range - pctl.force_queue.insert(0, queue_item_gen(ref, r_menu_position, pl_to_id(pctl.active_playlist_viewing))) + if len(self.current_artists) <= whole_rage: + self.scroll_position = 0 + + tauon.fields.add(area2) + scroll_x = x + w - 18 * gui.scale + if colours.lm: + scroll_x = x + w - 22 * gui.scale + if (tauon.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") -# def toggle_queue(mode: int = 0) -> bool: -# if mode == 1: -# return prefs.show_queue -# prefs.show_queue ^= True -# prefs.show_queue ^= True + if pctl.default_playlist: + text = _("Artist threshold not met") + if self.load: + text = _("Loading Artist List...") + if pctl.loading_in_progress or tauon.transcode_list or tauon.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) -track_menu.add(MenuItem(_("Add to Queue"), add_to_queue, pass_ref=True, hint="MB3")) + yy = y + 12 * gui.scale -track_menu.add(MenuItem(_("↳ After Current Track"), add_to_queue_next, pass_ref=True, show_test=test_shift)) + i = int(self.scroll_position) -track_menu.add(MenuItem(_("Show in Gallery"), show_in_gal, pass_ref=True, show_test=test_show)) + if viewing_pl_id in self.saves: + self.saves[viewing_pl_id][2] = self.scroll_position -track_menu.add_sub(_("Meta…"), 160) + prefetch_mode = False + prefetch_distance = 22 -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)) + self.shown_letters.clear() -# track_menu.add(_('Paste + Transfer Folder'), lightning_paste, pass_ref=False, show_test=lightning_move_test) + self.hover_any = False -track_menu.add(MenuItem(_("Paste"), menu_paste, paste_deco, pass_ref=True)) + for i, artist in enumerate(self.current_artists[i:], start=i): + if not prefetch_mode: + self.draw_card(artist, x, round(yy), w) -def delete_track(track_ref): - tr = pctl.get_track(track_ref) - fullpath = tr.fullpath + yy += self.tab_h - if system == "Windows" or msys: - fullpath = fullpath.replace("/", "\\") + if yy - y > h - 24 * gui.scale: + prefetch_mode = True + continue - if tr.is_network: - show_message(_("Cannot delete a network track")) - return + 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 - while track_ref in default_playlist: - default_playlist.remove(track_ref) + if not self.hover_any: + gui.preview_artist = "" + self.hover_timer.force_set(10) + artist_preview_render.show = False + self.hover_on = False - try: - send2trash(fullpath) +class TreeView: - 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")) + def __init__(self): - 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") + self.trees = {} # Per playlist tree + self.rows = [] # For display (parsed from tree) + self.rows_id = "" - reload() - refind_playing() - pctl.notify_change() + self.opens = {} # Folders clicks to show per playlist + self.scroll_positions = {} -track_menu.add(MenuItem(_("Delete Track File"), delete_track, pass_ref=True, icon=delete_icon, show_test=test_shift)) + # Recursive gen_rows vars + self.count = 0 + self.depth = 0 -track_menu.br() + self.background_processing = False + self.d_click_timer = Timer(100) + self.d_click_id = "" + self.menu_selected = "" + self.folder_colour_cache = {} + self.dragging_name = "" -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…")] + self.force_opens = [] + self.click_drag_source = None + self.tooltip_on = "" + self.tooltip_timer = Timer(10) -# 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)) + self.lock_pl = None + # self.bold_colours = ColourGenCache(0.6, 0.7) -def activate_trans_editor(): - trans_edit_box.active = True + def clear_all(self): + self.rows_id = "" + self.trees.clear() + def collapse_all(self): + pl_id = pl_to_id(pctl.active_playlist_viewing) -track_menu.add_to_sub(0, MenuItem(_("Edit fields…"), activate_trans_editor)) + 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 -def delete_folder(index, force=False): - track = pctl.master_library[index] + opens.clear() + self.rows_id = "" - if track.is_network: - show_message(_("Cannot physically delete"), _("One or more tracks is from a network location!"), mode="info") - return + def clear_target_pl(self, pl_number, pl_id=None): - old = track.parent_folder_path + if pl_id is None: + pl_id = pl_to_id(pl_number) - if len(old) < 5: - show_message(_("This folder path seems short, I don't wanna try delete that"), mode="warning") - return + if gui.lsp and prefs.left_panel_mode == "folder view": - 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 + 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] - protect = ("", "Documents", "Music", "Desktop", "Downloads") + def show_track(self, track: TrackClass) -> None: - 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") + if track is None: return - if directory_size(old) > 1500000000: - show_message(_("Delete size safety limit reached! (1.5GB)"), old, mode="warning") - 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 - try: + tree = self.trees.get(pl_id) + if not tree: + return - 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) + scroll_position = self.scroll_positions.get(pl_id) + if scroll_position is None: + scroll_position = 0 - if force: - shutil.rmtree(old) - elif system == "Windows" or msys: - send2trash(old.replace("/", "\\")) - else: - send2trash(old) + # Clear all opened folders + opens.clear() - for i in reversed(range(len(default_playlist))): + # Set every folder in path as opened + path = "" + crumbs = track.parent_folder_path.split("/")[1:] + for c in crumbs: + path += "/" + c + opens.append(path) - if old == pctl.master_library[default_playlist[i]].parent_folder_path: - del default_playlist[i] + # Regenerate row display + self.gen_rows(tree, opens) - 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") + # Locate and set scroll position to playing folder + for i, row in enumerate(self.rows): + if row[1] + "/" + row[0] == track.parent_folder_path: - if album_mode: - prep_gal() - reload_albums() + scroll_position = i - 5 + scroll_position = max(scroll_position, 0) + break - 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") + 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) - tree_view_box.clear_target_pl(pctl.active_playlist_viewing) - gui.pl_update += 1 - pctl.notify_change() + self.scroll_positions[pl_id] = scroll_position + gui.update_layout() + gui.update += 1 -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 + def get_pl_id(self): + if self.lock_pl: + return self.lock_pl + return pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - old = track.parent_folder_path - #logging.info(old) + def render(self, x, y, w, h): + pl_id = self.get_pl_id() + tree = self.trees.get(pl_id) - new = parse_template2(template, track) + # 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() - if len(new) < 1: - show_message(_("Rename error."), _("The generated name is too short"), mode="warning") - return + self.playlist_id_on = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - if len(old) < 5: - show_message(_("Rename error."), _("This folder path seems short, I don't wanna try rename that"), mode="warning") - return + opens = self.opens.get(pl_id) + if opens is None: + opens = [] + self.opens[pl_id] = opens - if not os.path.exists(old): - show_message(_("Rename Failed. The original folder is missing."), mode="warning") - return + scroll_position = self.scroll_positions.get(pl_id) + if scroll_position is None: + scroll_position = 0 - protect = ("", "Documents", "Music", "Desktop", "Downloads") + area = (x, y, w, h) + tauon.fields.add(area) + ddt.rect(area, colours.side_panel_background) + ddt.text_background_colour = colours.side_panel_background - 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") + 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 - 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 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 os.path.normpath(new_parent_path) == os.path.normpath(old): - show_message(_("The folder already has that name.")) - 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) - if os.path.exists(new_parent_path): - show_message(_("Rename Failed."), _("A folder with that name already exists"), mode="warning") + else: return - if key == pctl.track_queue[pctl.queue_step] and pctl.playing_state > 0: - pre_state = pctl.stop(True) + 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 - object.parent_folder_name = new - object.parent_folder_path = new_parent_path - object.fullpath = new_fullpath + yy = y + round(11 * gui.scale) + xx = x + round(22 * gui.scale) - search_string_cache.pop(object.index, None) - search_dia_string_cache.pop(object.index, None) + spacing = round(21 * gui.scale) + max_scroll = len(self.rows) - (h // round(22 * gui.scale)) - # 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("\\/")) + mouse_in = tauon.coll(area) - search_string_cache.pop(object.index, None) - search_dia_string_cache.pop(object.index, None) + # Mouse wheel scrolling + if mouse_in and inp.mouse_wheel: + scroll_position += inp.mouse_wheel * -2 + scroll_position = max(scroll_position, 0) + scroll_position = min(scroll_position, max_scroll) - 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 + focused = is_level_zero() - show_message(_("Folder renamed."), _("Renamed to: {name}").format(name=new), mode="done") + # 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) - if pre_state == 1: - pctl.revert() + self.scroll_positions[pl_id] = scroll_position - tree_view_box.clear_target_pl(pctl.active_playlist_viewing) - pctl.notify_change() + # 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 -def rename_folders_disable_test(index: int) -> bool: - return pctl.get_track(index).is_network + for i, item in enumerate(self.rows): -def rename_folders(index: int): - global track_box - global rename_index - global input_text + if i < scroll_position: + continue - track_box = False - rename_index = index + if yy > y + h - spacing: + break - if rename_folders_disable_test(index): - show_message(_("Not applicable for a network track.")) - return + target = item[1] + "/" + item[0] - gui.rename_folder_box = True - input_text = "" - shift_selection.clear() + inset = item[2] * round(10 * gui.scale) + rect = (xx + inset - round(15 * gui.scale), yy, max_w - inset + round(15 * gui.scale), spacing - 1) + tauon.fields.add(rect) - global quick_drag - global playlist_hold - quick_drag = False - playlist_hold = False + # 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] -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)) + if semilight_mode: + text_colour = [255, 255, 255, 180] + if light_mode: + text_colour = [0, 0, 0, 200] -def move_folder_up(index: int, do: bool = False) -> bool | None: - track = pctl.master_library[index] + full_folder_path = item[1] + "/" + item[0] - if track.is_network: - show_message(_("Cannot move"), _("One or more tracks is from a network location!"), mode="info") - return None + # 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] - 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) + # Hold highlight while dragging folder + if inp.quick_drag and not point_proximity_test(gui.drag_source_position, inp.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] - 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 + # 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 len(os.listdir(parent_folder)) > 1: - return False + if right_click: + mouse_in = tauon.coll(rect) and is_level_zero(False) + else: + mouse_in = tauon.coll(rect) and focused and not ( + inp.quick_drag and not point_proximity_test(gui.drag_source_position, inp.mouse_position, 15)) - if do is False: - return True + if mouse_in and not tree_view_scroll.held: - 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) + if middle_click: + stem_to_new_playlist(full_folder_path) - try: + elif right_click: - # Rename the track folder to something temporary - os.rename(move_target, os.path.join(parent_folder, "RMTEMP000")) + if item[3]: - # Move the temporary folder up 2 levels - shutil.move(os.path.join(parent_folder, "RMTEMP000"), upper_folder) + 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 - # Delete the old directory that contained the original folder - shutil.rmtree(parent_folder) + elif inp.mouse_click: + # inp.quick_drag = True - # Rename the moved folder back to its original name - os.rename(os.path.join(upper_folder, "RMTEMP000"), os.path.join(upper_folder, folder_name)) + if not self.click_drag_source: + self.click_drag_source = item + set_drag_source() - except Exception as e: - logging.exception("System Error!") - show_message(_("System Error!"), str(e), mode="error") + elif inp.mouse_up and self.click_drag_source == item: + # Click tree level folder to open/close branch - # 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 target not in opens: + opens.append(target) + else: + for s in reversed(range(len(opens))): + if opens[s].startswith(target): + del opens[s] - 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("\\/")) + if item[3]: - search_string_cache.pop(object.index, None) - search_dia_string_cache.pop(object.index, None) + # Locate the first track of folder in playlist + track_id = None + for p, id in enumerate(pctl.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(pctl.default_playlist): + if pctl.get_track(id).parent_folder_name == item[0]: + track_id = id + break - logging.info(object.fullpath) - logging.info(object.parent_folder_path) + 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 - if pre_state == 1: - pctl.revert() + # Double click base folder to play + else: + pctl.jump(track_id) + # Regenerate display rows after clicking + self.gen_rows(tree, opens) -def clean_folder(index: int, do: bool = False) -> int | None: - track = pctl.master_library[index] + # Highlight folder text on mouse over + if (mouse_in and not inp.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] - if track.is_network: - show_message(_("Cannot clean"), _("One or more tracks is from a network location!"), mode="info") - return None + # 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 - 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": + if mouse_in: + tw = ddt.get_text_w(item[0], font) - 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 self.tooltip_on != item: + self.tooltip_on = item + self.tooltip_timer.set() + gui.frame_callback_list.append(TestTimer(0.6)) - 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() + 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) - 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)) + # # 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) - 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 + 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) - return found + # 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) -def reset_play_count(index: int): - star_store.remove(index) + yy += spacing + if self.click_drag_source and not point_proximity_test(gui.drag_source_position, inp.mouse_position, 15) and \ + pctl.default_playlist is pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids: + inp.quick_drag = True + global playlist_hold + playlist_hold = True -# track_menu.add_to_sub("Reset Track Play Count", 0, reset_play_count, pass_ref=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) -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) + 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 - for track in todo: + if self.dragging_name and not inp.quick_drag: + self.dragging_name = "" + if not inp.mouse_down: + self.click_drag_source = None - tr = pctl.get_track(track) + def gen_row(self, tree_point, path, opens): - total_playtime = 0 - flags = "" + 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 - to_del = [] + 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 - 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 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])]) - for key in to_del: - del star_store.db[key] + # 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 - 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?") + # 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 -def reload_metadata(input, keep_star: bool = True) -> None: - global todo + self.gen_row(item[0], p, opens) - # vacuum_playtimes(index) - # return - todo = [] + if enter_level: + self.depth -= 1 - if isinstance(input, list): - todo = input + def gen_rows(self, tree, opens): + self.count = 0 + self.depth = 0 + self.rows.clear() + self.force_opens.clear() - 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]) + self.gen_row(tree, "", opens) - for i in reversed(range(len(todo))): - if todo[i].is_cue: - del todo[i] + gui.update_layout() + gui.update += 1 - for track in todo: + def gen_tree(self, pl_id): + pl_no = id_to_pl(pl_id) + if pl_no is None: + return - search_string_cache.pop(track.index, None) - search_dia_string_cache.pop(track.index, None) + playlist = pctl.multi_playlist[pl_no].playlist_ids + # Generate list of all unique folder paths + paths = [] + z = 5000 + for p in playlist: - #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) + 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) - pctl.master_library[track.index] = tag_scan(track) + # 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] - # 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) + self.trees[pl_id] = tree + self.rows_id = "" + self.background_processing = False + gui.update += 1 + tauon.wake() - pctl.notify_change() +class QueueBox: - gui.pl_update += 1 - tauon.thread_manager.ready("worker") + def __init__(self, gui: GuiVar, queue_menu: Menu): + self.gui = gui + 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)) -def reload_metadata_selection() -> None: - cargo = [] - for item in shift_selection: - cargo.append(default_playlist[item]) + 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")) - for k in cargo: - if pctl.master_library[k].is_cue == False: - to_scan.append(k) - tauon.thread_manager.ready("worker") + 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 recalc(self): + self.tab_h = 34 * self.gui.scale -def editor(index: int | None) -> None: - todo = [] - obs = [] + def except_for_this_show_test(self, _): + return self.queue_remove_show(_) and test_shift(_) - 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]) + def make_as_playlist(self): + if pctl.force_queue: + playlist = [] + for item in pctl.force_queue: - # 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) + if item.type == 0: + playlist.append(item.track_id) + else: - file_line = "" - for track in todo: - file_line += ' "' - file_line += pctl.master_library[track].fullpath - file_line += '"' + pl = id_to_pl(item.playlist_id) + if pl is None: + logging.info("Lost the target playlist") + continue - if system == "Windows" or msys: - file_line = file_line.replace("/", "\\") + pp = pctl.multi_playlist[pl].playlist_ids - prefix = "" - app = prefs.tag_editor_target + i = item.position # = pctl.playlist_playing_position + 1 - if (system == "Windows" or msys) and app: - if app[0] != '"': - app = '"' + app - if app[-1] != '"': - app = app + '"' + parts = [] + album_parent_path = pctl.get_track(item.track_id).parent_folder_path - app_switch = "" + while i < len(pp): + if pctl.get_track(pp[i]).parent_folder_path != album_parent_path: + break - ok = False + parts.append((pp[i], i)) + i += 1 - prefix = launch_prefix + for part in parts: + playlist.append(part[0]) - if system == "Linux": - ok = whicher(prefs.tag_editor_target) - else: + pctl.multi_playlist.append( + pl_gen( + title=_("Queued Tracks"), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - 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") + def drop_tracks_insert(self, insert_position): + if not shift_selection: return - ok = True - - if not ok: - show_message(_("Tag editor app does not appear to be installed."), mode="warning") + # 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) - 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") + playlist_index = pctl.active_playlist_viewing + playlist_id = pl_to_id(pctl.active_playlist_viewing) - return + main_track_position = shift_selection[0] + main_track_id = pctl.default_playlist[main_track_position] + inp.quick_drag = False - if "picard" in prefs.tag_editor_target: - app_switch = " --d " + if len(shift_selection) > 1: - line = prefix + app + app_switch + file_line + # if shift selection contains only same folder + for position in shift_selection: + if pctl.get_track(pctl.default_playlist[position]).parent_folder_path != pctl.get_track( + main_track_id).parent_folder_path or inp.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 - show_message( - prefs.tag_editor_name + " launched.", "Fields will be updated once application is closed.", mode="arrow") - gui.update = 1 + 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(pctl.default_playlist[position], position, playlist_id)) - complete = subprocess.run(shlex.split(line), stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) + def clear_queue_crop(self): - 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('"') + save = False + for item in pctl.force_queue: + if item.uuid_int == self.right_click_id: + save = item + break - 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.") + clear_queue() + if save: + pctl.force_queue.append(save) - gui.message_box = False - reload_metadata(obs, keep_star=False) + def play_now(self): - # Re apply playtime data in case file names change - for item in old_stars: + 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 - old_key = item[1] - old_value = item[2] + del pctl.force_queue[queue_index] + # [trackid, position, pl_id, type, album_stage, uid_gen(), auto_stop] - if not old_value: # ignore if there was no old playcount metadata - continue + if pctl.force_queue and pctl.force_queue[0].album_stage == 1: + split_queue_album(None) - new_key = star_store.object_key(item[0]) - new_value = star_store.full_get(item[0].index) + target_track_id = queue_item.track_id - if old_key == new_key: - continue + pl = id_to_pl(queue_item.playlist_id) + if pl is not None: + pctl.active_playlist_playing = pl - if new_value is None: - new_value = [0, "", 0] + if target_track_id not in pctl.playing_playlist(): + pctl.advance() + return - new_value[0] += old_value[0] - new_value[1] = "".join(set(new_value[1] + old_value[1])) + pctl.jump(target_track_id, queue_item.position) - if old_key in star_store.db: - del star_store.db[old_key] + if queue_item.type == 1: # is album type + queue_item.album_stage = 1 # set as partway playing + pctl.force_queue.insert(0, queue_item) - star_store.db[new_key] = new_value + def toggle_auto_stop(self) -> None: - gui.pl_update = 1 - gui.update = 1 - pctl.notify_change() + 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): -def launch_editor(index: int): - if snap_mode: - show_message(_("Sorry, this feature isn't (yet) available with Snap.")) - return + enabled = False + for item in pctl.force_queue: + if item.uuid_int == self.right_click_id: + if item.auto_stop: + enabled = True + break - if launch_editor_disable_test(index): - show_message(_("Cannot edit tags of a network track.")) - return + if enabled: + return [colours.menu_text, colours.menu_background, _("Cancel Auto-Stop")] + return [colours.menu_text, colours.menu_background, _("Auto-Stop")] - mini_t = threading.Thread(target=editor, args=[index]) - mini_t.daemon = True - mini_t.start() + def queue_remove_show(self, id: int) -> bool: -def launch_editor_selection_disable_test(index: int): - for position in shift_selection: - if pctl.get_track(default_playlist[position]).is_network: + if self.right_click_id is not None: return True - return False + 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 + def right_remove_item(self) -> None: - mini_t = threading.Thread(target=editor, args=[None]) - mini_t.daemon = True - mini_t.start() + 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")) -# 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 toggle_pause(self) -> None: + pctl.pause_queue ^= 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") + 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: -mbp_icon.xoff = 2 -mbp_icon.yoff = -1 + # text_colour = [230, 230, 230, 255] + bg = colours.queue_background -if gui.scale == 1.25: - mbp_icon.yoff = 0 + # if fq[i].type == 0: -edit_icon = None -if prefs.tag_editor_name == "Picard": - edit_icon = mbp_icon + 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 -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] + 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] -def launch_editor_disable_test(index: int): - return pctl.get_track(index).is_network + tauon.gall_ren.render(track, (rect[0] + 4 * gui.scale, rect[1] + 4 * gui.scale), round(28 * gui.scale)) -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)) + 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 -def show_lyrics_menu(index: int): - global track_box - track_box = False - enter_showcase_view(track_id=r_menu_index) - inp.mouse_click = False + if not line: + line = clean_string(track.filename) + line2y = yy + 14 * gui.scale -track_menu.add_to_sub(0, MenuItem(_("Lyrics..."), show_lyrics_menu, pass_ref=True)) + 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 -def recode(text, enc): - return text.encode("Latin-1", "ignore").decode(enc, "ignore") + 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) -def intel_moji(index: int): - gui.pl_update += 1 - gui.update += 1 + 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]) - track = pctl.master_library[index] + 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]) - lot = [] + def draw(self, x: int, y: int, w: int, h: int): + yy = y + yy += round(4 * gui.scale) - for item in default_playlist: + sep_colour = alpha_blend([255, 255, 255, 11], colours.queue_background) - if track.album == pctl.master_library[item].album and \ - track.parent_folder_name == pctl.master_library[item].parent_folder_name: - lot.append(item) + 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) - lot = set(lot) + yy += round(3 * gui.scale) - l_artist = track.artist.encode("Latin-1", "ignore") - l_album = track.album.encode("Latin-1", "ignore") - detect = None + box_rect = (x, yy - 6 * gui.scale, w, h) + ddt.rect(box_rect, colours.queue_background) + ddt.text_background_colour = colours.queue_background - 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 tauon.coll(box_rect) and inp.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 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 + # if y < gui.panelY * 2: + # ddt.rect((x, y - 3 * gui.scale, w, 30 * gui.scale), colours.queue_background, True) - for item in lot: - t_track = pctl.master_library[item] + if h > 40 * gui.scale: + if not pctl.force_queue: + if inp.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) - 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 + qb_right_click = 0 - 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 tauon.coll(box_rect): + # Update scroll position + self.scroll_position += inp.mouse_wheel * -1 + self.scroll_position = max(self.scroll_position, 0) - 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) + if right_click: + qb_right_click = 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) + # 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] - if key != None: - star_store.insert(item, key) + line = _("Up Next:") + if pctl.force_queue: + # line = "Queue" + ddt.text((x + (10 * gui.scale), yy + 2 * gui.scale), line, text_colour, 211) - search_string_cache.pop(track.index, None) - search_dia_string_cache.pop(track.index, None) + yy += 7 * gui.scale - else: - show_message(_("Autodetect failed")) + 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 -track_menu.add_to_sub(0, MenuItem(_("Fix Mojibake"), intel_moji, pass_ref=True)) + 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]) -def sel_to_car(): - global default_playlist - cargo = [] + yy += 6 * gui.scale - for item in shift_selection: - cargo.append(default_playlist[item]) + yy += 10 * gui.scale + i = 0 -# track_menu.add_to_sub("Copy Playlist", 1, transfer, pass_ref=True, args=[1, 3]) -def cut_selection(): - sel_to_car() - del_selected() + # 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 inp.mouse_down and not inp.mouse_up: + self.dragging = None -def clip_ar_al(index: int): - line = pctl.master_library[index].artist + " - " + pctl.master_library[index].album - SDL_SetClipboardText(line.encode("utf-8")) + if not queue_menu.active: + self.right_click_id = None + fq = self.fq -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")) + list_top = yy + i = self.scroll_position -def clip_title(index: int): - n_track = pctl.master_library[index] + # Limit scroll distance + if i > len(fq): + self.scroll_position = len(fq) + i = self.scroll_position - 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 + showed_indicator = False + list_extends = False + x1 = x + 13 * gui.scale # highlight position + w1 = w - 28 * gui.scale - 10 * gui.scale - SDL_SetClipboardText(line.encode("utf-8")) + 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 -selection_menu = Menu(200, show_icons=False) -folder_menu = Menu(193, show_icons=True) + # 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) -folder_menu.add(MenuItem(_("Open Folder"), open_folder, pass_ref=True, pass_ref_deco=True, icon=folder_icon, disable_test=open_folder_disable_test)) + if self.dragging is not None and tauon.coll(h_rect) and inp.mouse_up: -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)) + ob = None + for u in reversed(range(len(pctl.force_queue))): -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)) + if pctl.force_queue[u].uuid_int == self.dragging: + ob = pctl.force_queue[u] + pctl.force_queue[u] = None + break -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)) + else: + self.dragging = None -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)) + if self.dragging: + pctl.force_queue.insert(i, ob) + self.dragging = None -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)) + 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 -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)) + # 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 -def lightning_copy(): - s_copy() - gui.lightning_copy = True + if i > len(fq) - 1: + break + track = pctl.get_track(fq[i].track_id) -# selection_menu.br() + rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h) -def toggle_transcode(mode: int = 0) -> bool: - if mode == 1: - return prefs.enable_transcode - prefs.enable_transcode ^= True - return None + if inp.mouse_click and tauon.coll(rect): + self.dragging = fq[i].uuid_int + self.drag_start_y = inp.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) -def toggle_chromecast(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_chromecast - prefs.show_chromecast ^= True - return None + 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() -def toggle_transfer(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_transfer - prefs.show_transfer ^= True + if self.dragging and tauon.coll(h_rect): + yy += self.tab_h + yy += 4 * gui.scale - 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 + if qb_right_click and tauon.coll(rect): + self.right_click_id = fq[i].uuid_int + qb_right_click = 2 + if middle_click and tauon.coll(rect): + pctl.force_queue.remove(fq[i]) + gui.pl_update += 1 -transcode_icon.colour = [239, 74, 157, 255] + 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 -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.draw_card(x, y, w, h, yy, track, fq[i], db) + # Drag tracks from main playlist and insert ------------ + if inp.quick_drag: + if x < inp.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 -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() + insert_position = None -tauon.spot_ctl.cache_saved_albums = spot_cache_saved_albums + if y1 < inp.mouse_position[1] < y1 + h1: + ddt.rect((x1, yy - 2 * gui.scale, w1, 2 * gui.scale), colours.queue_drag_indicator_colour) + showed_indicator = True -# Copy album title text to clipboard -folder_menu.add(MenuItem(_('Copy "Artist - Album"'), clip_title, pass_ref=True)) + if inp.mouse_up: + insert_position = i + elif y2 < inp.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 -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")) + if inp.mouse_up: + insert_position = i + 1 + if insert_position is not None: + self.drop_tracks_insert(insert_position) -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] + # ----------------------------------------- + yy += self.tab_h + yy += 4 * gui.scale + i += 1 -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)) + # Show drag marker if mouse holding below list + if inp.quick_drag and not list_extends and not showed_indicator and fq and inp.mouse_position[ + 1] > yy - 4 * gui.scale and tauon.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 -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) + # Calculate total queue duration + duration = 0 + tracks = 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") + 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 - return (colours.menu_text, colours.menu_background, text) + album_parent_path = pctl.get_track(item.track_id).parent_folder_path + playing_track = pctl.playing_object() -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) + 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 - 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) + 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 -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 + duration += pctl.get_track(playlist[i]).length + tracks += 1 + i += 1 - shoot_dl = threading.Thread(target=add_to_spotify_library2, args=([album_url])) - shoot_dl.daemon = True - shoot_dl.start() + # 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 -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)) + if self.dragging: + yyy = self.drag_start_top + (inp.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 inp.quick_drag and inp.mouse_up and tauon.coll(box_rect) and shift_selection: + self.drop_tracks_insert(len(fq)) -# Copy artist name text to clipboard -# folder_menu.add(_('Copy "Artist"'), clip_ar, pass_ref=True) + # 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=inp.mouse_position) -def selection_queue_deco(): - total = 0 - for item in shift_selection: - total += pctl.get_track(default_playlist[item]).length +class MetaBox: - total = get_hms_time(total) + def __init__(self, tauon: Tauon): + self.tauon = tauon + self.ddt = tauon.bag.ddt + self.colours = tauon.bag.colours - text = (_("Queue {N}").format(N=len(shift_selection))) + f" [{total}]" + def l_panel(self, x, y, w, h, track, top_border: bool = True): + colours = self.colours + ddt = self.ddt - return [colours.menu_text, colours.menu_background, text] + 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] -selection_menu.add(MenuItem(_("Add to queue"), add_selected_to_queue_multi, selection_queue_deco)) + rect = (x, y, w, h) -selection_menu.br() + 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) -selection_menu.add(MenuItem(_("Rescan Tags"), reload_metadata_selection)) + ddt.text_background_colour = colours.gallery_background -selection_menu.add(MenuItem(_("Edit fields…"), activate_trans_editor)) + insert = round(9 * gui.scale) + border = round(2 * gui.scale) -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)) + compact_mode = False + if w < h * 1.9: + compact_mode = True -selection_menu.br() -folder_menu.br() + art_rect = [ + x + insert - 2 * gui.scale, y + insert, h - insert * 2 + 1 * gui.scale, h - insert * 2 + 1 * gui.scale] -# It's complicated -# folder_menu.add(_('Copy Folder From Library'), lightning_copy) + if compact_mode: + art_rect[0] = x + round(w / 2 - art_rect[2] / 2) - round(1 * gui.scale) # - border -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)) + border_rect = ( + art_rect[0] - border, art_rect[1] - border, art_rect[2] + (border * 2), art_rect[3] + (border * 2)) -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)) + if (inp.mouse_click or right_click) and is_level_zero(False): + if tauon.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 tauon.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])) -def toggle_rym(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_rym - prefs.show_rym ^= True - return None + tauon.fields.add(border_rect) + if tauon.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) -def toggle_band(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_band - prefs.show_band ^= True - return None + 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 toggle_wiki(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_wiki - prefs.show_wiki ^= True - return None + def lyrics(self, x, y, w, h, track: TrackClass): + self.ddt.rect((x, y, w, h), colours.side_panel_background) + self.ddt.text_background_colour = colours.side_panel_background + if not track: + 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 + # Test for show lyric menu on right ckick + if tauon.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) -def toggle_gen(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_gen - prefs.show_gen ^= True - return None + # Test for scroll wheel input + if inp.mouse_wheel != 0 and tauon.coll((x + 10, y, w - 10, h)): + lyrics_ren_mini.lyrics_position += inp.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 -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")) + tw, th = ddt.get_text_wh(track.lyrics + "\n", 15, w - 50 * gui.scale, True) + oth = th -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...")) + 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() -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) + 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 -def copy_to_clipboard(text: str) -> None: - SDL_SetClipboardText(text.encode(errors="surrogateescape")) + 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) -def copy_from_clipboard(): - return SDL_GetClipboardText().decode() + self.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 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 draw(self, x, y, w, h, track=None): + colours = self.colours + self.ddt.rect((x, y, w, h), colours.side_panel_background) -def ser_gen_thread(tr): - s_artist = tr.artist - s_title = tr.title + if not track: + return - 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] + # Test for show lyric menu on right ckick + if tauon.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) - line = genius(s_artist, s_title, return_url=True) + 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 - r = requests.head(line, timeout=10) + if h < 15: + return - 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 + # 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) -def ser_gen(track_id, get_lyrics=False): - tr = pctl.master_library[track_id] - if len(tr.title) < 1: - return + # Draw standard metadata + if len(pctl.track_queue) > 0: - show_message(_("Searching...")) + 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 - shoot = threading.Thread(target=ser_gen_thread, args=[tr]) - shoot.daemon = True - shoot.start() + self.ddt.text_background_colour = colours.side_panel_background + if tauon.coll((x + 10, y, w - 10, h)): + # Click area to jump to current track + if inp.mouse_click: + pctl.show_current() + gui.update += 1 -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) + title = "" + album = "" + artist = "" + ext = "" + date = "" + genre = "" + margin = x + 10 * gui.scale + if colours.lm: + margin += 2 * gui.scale -track_menu.add(MenuItem(_("Search Artist on Wikipedia"), ser_wiki, pass_ref=True, show_test=toggle_wiki)) + text_width = w - 25 * gui.scale + tr = None -track_menu.add(MenuItem(_("Search Track on Genius"), ser_gen, pass_ref=True, show_test=toggle_gen)) + # if pctl.playing_state < 3: -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") + 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: -son_icon.xoff = 1 -track_menu.add(MenuItem(_("Search Artist on Sonemic"), ser_rym, pass_ref=True, icon=son_icon, show_test=toggle_rym)) + 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]) -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] + 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]) -track_menu.add(MenuItem(_("Search Artist on Bandcamp"), ser_band, pass_ref=True, icon=band_icon, show_test=toggle_band)) + 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 -def clip_ar_tr(index: int) -> None: - line = pctl.master_library[index].artist + " - " + pctl.master_library[index].title + if not title and not artist: + title = pctl.tag_meta - SDL_SetClipboardText(line.encode("utf-8")) + if h > 58 * gui.scale: + block_y = y + 7 * gui.scale + if not prefs.show_side_art: + block_y += 3 * gui.scale -# 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" + if title != "": + self.ddt.text( + (margin, block_y + 2 * gui.scale), title, colours.side_bar_line1, fonts.side_panel_line1, + max_w=text_width) + if artist != "": + self.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 -track_menu.add(MenuItem(_("Copy TIDAL Album URL"), tidal_copy_album, show_test=is_tidal_track, pass_ref=True)) + if h > 140 * gui.scale: -# def get_track_spot_url_show_test(_): -# if pctl.get_track(r_menu_index).misc.get("spotify-track-url"): -# return True -# return False + 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 -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")) + self.ddt.text( + (margin, block_y + 20 * gui.scale), line, colours.side_bar_line2, + fonts.side_panel_line2, max_w=text_width) -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 + if ext != "": + if ext == "SPTY": + ext = "Spotify" + if ext == "RADIO": + ext = radiobox.playing_title + sp = self.ddt.text( + (margin, block_y + 40 * gui.scale), ext, colours.side_bar_line2, + fonts.side_panel_line2, max_w=text_width) - return [line_colour, colours.menu_background, None] + 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) -track_menu.add_sub(_("Spotify…"), 190, show_test=spotify_show_test) +class PictureRender: -def get_spot_artist_track(index: int) -> None: - get_artist_spot(pctl.get_track(index)) + def __init__(self): + self.show = False + self.path = "" -track_menu.add_to_sub(1, MenuItem(_("Show Full Artist"), get_spot_artist_track, pass_ref=True, icon=spot_icon)) + self.image_data = None + self.texture = None + self.sdl_rect = None + self.size = (0, 0) -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 load(self, path, box_size=None): + if not os.path.isfile(path): + logging.warning("NO PICTURE FILE TO LOAD") + return -def get_spot_album_track(index: int): - get_album_spot_active(pctl.get_track(index)) + g = io.BytesIO() + g.seek(0) -track_menu.add_to_sub(1, MenuItem(_("Show Full Album"), get_spot_album_track, pass_ref=True, icon=spot_icon)) + 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): -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)) + if self.show is False: + return -# 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)) + 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 -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 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) - 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] +class PictureRender: - elif len(track_list) == 1 and artists: - pctl.multi_playlist[pl].title = artists[0] + def __init__(self): + self.show = False + self.path = "" - if tree_view_box.dragging_name: - pctl.multi_playlist[pl].title = tree_view_box.dragging_name + self.image_data = None + self.texture = None + self.sdl_rect = None + self.size = (0, 0) - pctl.notify_change() + def load(self, path, box_size=None): + if not os.path.isfile(path): + logging.warning("NO PICTURE FILE TO LOAD") + return -def queue_deco(): - if len(pctl.force_queue) > 0: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + g = io.BytesIO() + g.seek(0) - return [line_colour, colours.menu_background, None] + 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] -track_menu.br() -track_menu.add(MenuItem(_("Transcode Folder"), convert_folder, transcode_deco, pass_ref=True, icon=transcode_icon, - show_test=toggle_transcode)) + def draw(self, x, y): + if self.show is False: + return -def bass_test(_) -> bool: - # return True - return prefs.backend == 1 + 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 -def gstreamer_test(_) -> bool: - # return True - return prefs.backend == 2 + 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 ArtistInfoBox: -# 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) + def __init__(self, bag: Bag) -> None: + self.artist_on = None + self.min_rq_timer = Timer() + self.min_rq_timer.force_set(10) -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)) + self.text = "" -def field_copy(text_field) -> None: - text_field.copy() + self.status = "" + self.scroll_y = 0 -def field_paste(text_field) -> None: - text_field.paste() + self.process_text_artist = "" + self.processed_text = "" + self.th = 0 + self.w = 0 + self.lock = False + self.mini_box = asset_loader(bag, bag.loaded_asset_dc, "mini-box.png", True) -def field_clear(text_field) -> None: - text_field.clear() + def manual_dl(self) -> None: + 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 -# 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)) + 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 = "" -def vis_off() -> None: - gui.vis_want = 0 - gui.update_layout() - # gui.turbo = False + 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): -vis_menu.add(MenuItem(_("Off"), vis_off)) + 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 -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 + track = pctl.playing_object() + if track is None: + return - gui.vis_want = 1 - gui.update_layout() - # if prefs.backend == 2: - # show_message("Visualisers not implemented in GStreamer mode") - # gui.turbo = True + # Check if the artist has changed + artist = track.artist + wait = False + # Activate menu + if right_click and tauon.coll((x, y, w, h)): + artist_info_menu.activate(in_reference=artist) -vis_menu.add(MenuItem(_("Level Meter"), level_on)) + 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: -def spec_on() -> None: - gui.vis_want = 2 - # if prefs.backend == 2: - # show_message("Not implemented") - gui.update_layout() + 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 -vis_menu.add(MenuItem(_("Spectrum Visualizer"), spec_on)) + 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() -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 + self.scroll_y = 0 + self.status = _("Loading...") - gui.vis_want = 3 - if prefs.backend == 2: - show_message(_("Not implemented")) - # gui.turbo = True - prefs.spec2_colour_setting = "custom" - gui.update_layout() + 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 -# vis_menu.add(_("Spectrogram"), spec2_def) + text = self.text + lic = "" + link = "" -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.")) + if "<a" in text: + text, ex = text.split('<a href="', 1) + link, ex = ex.split('">', 1) -def sa_artist() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Artist", 220, False]) - gui.update_layout() + lic = ex.split("</a>. ", 1)[1] + text += "\n" -def sa_album_artist() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Album Artist", 220, False]) - gui.update_layout() + 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 -def sa_composer() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Composer", 220, False]) - gui.update_layout() + 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) -def sa_title() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Title", 220, False]) - gui.update_layout() + 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 -def sa_album() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Album", 220, False]) - gui.update_layout() + scroll_max = self.th - (h - 26) + if tauon.coll((x, y, w, h)): + self.scroll_y += inp.mouse_wheel * -20 + self.scroll_y = max(self.scroll_y, 0) + self.scroll_y = min(self.scroll_y, scroll_max) -def sa_comment() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Comment", 300, False]) - gui.update_layout() + 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 -def sa_track() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["#", 25, True]) - gui.update_layout() + 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: -def sa_count() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["P", 25, True]) - gui.update_layout() + rect = (right - 2, yy - 2, 16, 16) + tauon.fields.add(rect) + self.mini_box.render(right, yy, alpha_mod(item[1], 100)) + if tauon.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]) -def sa_scrobbles() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["S", 25, True]) - gui.update_layout() + 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 -def sa_time() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Time", 55, True]) - gui.update_layout() + 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: -def sa_date() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Date", 55, True]) - gui.update_layout() + 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 "" -def sa_genre() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Genre", 150, False]) - gui.update_layout() + 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) -def sa_file() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Filepath", 350, False]) - gui.update_layout() + 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 "" -def sa_filename() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Filename", 300, False]) - gui.update_layout() + # 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") -def sa_codec() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Codec", 65, True]) - gui.update_layout() + 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 -def sa_bitrate() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Bitrate", 65, True]) - gui.update_layout() + 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 "" -def sa_lyrics() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Lyrics", 50, True]) - gui.update_layout() + 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 -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() + # 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) -def sa_love() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["❤", 25, True]) - # gui.pl_st.append(["❤", 25, True]) - gui.update_layout() + 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") -def key_love(index: int) -> bool: - return get_love_index(index) + # 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 -def key_artist(index: int) -> str: - return pctl.master_library[index].artist.lower() + # 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" -def key_album_artist(index: int) -> str: - return pctl.master_library[index].album_artist.lower() + 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 "" -def key_composer(index: int) -> str: - return pctl.master_library[index].composer.lower() +class RadioThumbGen: + def __init__(self, tauon: Tauon) -> None: + self.gui = tauon.gui + self.cache = {} + self.requests = [] + self.size = 100 + def loader(self): -def key_comment(index: int) -> str: - return pctl.master_library[index].comment + 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") -def key_title(index: int) -> str: - return pctl.master_library[index].title.lower() + if src: + pass + #logging.info("found cached") + elif station.icon and station.icon not in prefs.radio_thumb_bans: + try: + r = requests.get(station.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.icon and station.icon not in prefs.radio_thumb_bans: + prefs.radio_thumb_bans.append(station.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.icon and station.icon not in prefs.radio_thumb_bans: + prefs.radio_thumb_bans.append(station.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.icon and station.icon not in prefs.radio_thumb_bans: + prefs.radio_thumb_bans.append(station.icon) + continue + if src is not None: + src.close() -def key_album(index: int) -> str: - return pctl.master_library[index].album.lower() + 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] + self.gui.update += 1 + def draw(self, station: RadioStation, x, y, w): + if not station.title: + return 0 + key = (station.title, w) -def key_duration(index: int) -> int: - return pctl.master_library[index].length + 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 RadioView: + def __init__(self, tauon: Tauon): + bag = tauon.bag + self.tauon = tauon + self.fields = tauon.fields + self.colours = tauon.bag.colours + self.ddt = tauon.bag.ddt + self.gui = tauon.gui + self.pctl = tauon.pctl + self.window_size = tauon.bag.window_size + self.add_icon = asset_loader(bag, bag.loaded_asset_dc, "add-station.png", True) + self.search_icon = asset_loader(bag, bag.loaded_asset_dc, "station-search.png", True) + self.save_icon = asset_loader(bag, bag.loaded_asset_dc, "save-station.png", True) + self.menu_icon = asset_loader(bag, bag.loaded_asset_dc, "radio-menu.png", True) + self.drag = None + self.click_point = (0, 0) -def key_date(index: int) -> str: - return pctl.master_library[index].date + def render(self): + pctl = self.pctl + gui = self.gui + window_size = self.window_size + # box = int(window_size[1] * 0.4 + 120 * gui.scale) + # box = min(window_size[0] // 2, box) + bg = self.colours.playlist_panel_background + self.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)) + self.fields.add(rect) -def key_genre(index: int) -> str: - return pctl.master_library[index].genre.lower() + # 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 self.tauon.coll(rect): + colour = b_colour + if inp.mouse_click: + add_station() + else: + colour = a_colour -def key_t(index: int): - # return str(pctl.master_library[index].track_number) - return index_key(index) + 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)) + self.fields.add(rect) -def key_codec(index: int) -> str: - return pctl.master_library[index].file_ext + if not self.tauon.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].stations -def key_bitrate(index: int) -> int: - return pctl.master_library[index].bitrate + 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)) + self.tauon.fields.add(rect) -def key_hl(index: int) -> int: - if len(pctl.master_library[index].lyrics) > 5: - return 0 - return 1 + if not tauon.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) -def sort_ass(h, invert=False, custom_list=None, custom_name=""): - global default_playlist + x = round(30 * gui.scale) + y = gui.panelY + round(30 * gui.scale) + yy = y - if custom_list is None: - if pl_is_locked(pctl.active_playlist_viewing): - show_message(_("Playlist is locked")) - return + rbg = rgb_add_hls(self.colours.playlist_panel_background, 0, 0.03, -0.03) + tbg = rgb_add_hls(self.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] - name = gui.pl_st[h][0] - playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - else: - name = custom_name - playlist = custom_list + w = round(400 * gui.scale) + h = round(55 * gui.scale) + gap = round(7 * gui.scale) - key = None - ns = False + mm = (window_size[1] - (gui.panelBY + yy + h + round(15 * gui.scale))) // (h + gap) + 1 - 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 + count = 0 + scroll = pctl.radio_playlists[pctl.radio_playlist_viewing].scroll + if not radiobox.active or (radiobox.active and not tauon.coll((radiobox.x, radiobox.y, radiobox.w, radiobox.h))): + if gui.panelY < inp.mouse_position[1] < window_size[1] - gui.panelBY \ + and inp.mouse_position[0] < w + round(70 * gui.scale): + scroll += inp.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 - if custom_list is None: - if key is not None: + pctl.radio_playlists[pctl.radio_playlist_viewing].scroll = scroll + insert = None - if ns: - key = natsort.natsort_keygen(key=key, alg=natsort.PATH) + 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]) - playlist.sort(key=key, reverse=invert) + 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] - pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids = playlist - default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + 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.country, l2_colour, 312, + max_w=w - (toff + round(90 * gui.scale)), bg=rbg) - pctl.playlist_view_position = 0 - logging.debug("Position changed by sort") - gui.pl_update = 1 + 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]) + self.tauon.fields.add(start_rect) + colour = rgb_add_hls(tbg, l=0.05) + if tauon.coll(start_rect): + if inp.mouse_click: + radiobox.start(radio) + hit = True + colour = rgb_add_hls(colour, l=0.3) - elif custom_list is not None: - playlist.sort(key=key, reverse=invert) + bottom_bar1.play_button.render(x + (w - round(30 * gui.scale)), yy + round(23 * gui.scale), colour) - reload() + 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]) + self.tauon.fields.add(extra_rect) + colour = rgb_add_hls(tbg, l=0.05) + if tauon.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) -def sort_dec(h): - sort_ass(h, True) + # bottom_bar1.play_button.render(x + (w - round(30 * gui.scale)), yy + round(23 * gui.scale), colour) + if inp.mouse_up and self.drag and tauon.coll(rect): + if radiobox.active and tauon.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 tauon.coll(rect) and not hit and inp.mouse_click: + self.drag = radio + self.click_point = copy.copy(inp.mouse_position) + yy += round(h + gap) -def hide_set_bar(): - gui.set_bar = False - gui.update_layout() - gui.pl_update = 1 + if inp.mouse_up and self.drag and not insert and self.drag not in radios: + if not (radiobox.active and tauon.coll((radiobox.x, radiobox.y, radiobox.w, radiobox.h))): + if inp.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) -def show_set_bar(): - gui.set_bar = True - gui.update_layout() - gui.pl_update = 1 + 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) -# Mark for translation -_("Time") -_("Filepath") + 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.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) -# -# 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)) + if self.drag: + gui.update_on_drag = True -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] + 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 -def toggle_dim_albums(mode: int = 0) -> bool: - if mode == 1: - return prefs.dim_art +class Showcase: + def __init__(self): + self.lastfm_artist = None + self.artist_mode = False - prefs.dim_art ^= True - gui.pl_update = 1 - gui.update += 1 + def render(self): + 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 -def toggle_gallery_combine(mode: int = 0) -> bool: - if mode == 1: - return prefs.gallery_combine_disc + x = int(window_size[0] * 0.15) + y = int((window_size[1] / 2) - (box / 2)) - 10 * gui.scale - prefs.gallery_combine_disc ^= True - reload_albums() -def toggle_gallery_click(mode: int = 0) -> bool: - if mode == 1: - return prefs.gallery_single_click + if hide_art: + box = 45 * gui.scale + elif window_size[1] / window_size[0] > 0.7: + x = int(window_size[0] * 0.07) - prefs.gallery_single_click ^= True + bbg = rgb_add_hls(self.colours.playlist_panel_background, 0, 0.05, 0) # [255, 255, 255, 18] + bfg = rgb_add_hls(self.colours.playlist_panel_background, 0, 0.09, 0) # [255, 255, 255, 30] + bft = self.colours.grey(235) + bbt = self.colours.grey(200) + t1 = self.colours.grey(250) -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() - + gui.vis_4_colour = None + light_mode = False + if self.colours.lm: + bbg = self.colours.vis_colour + bfg = alpha_blend([255, 255, 255, 60], self.colours.vis_colour) + bft = self.colours.grey(250) + bbt = self.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] -def toggle_gallery_row_space(mode: int = 0) -> bool: - if mode == 1: - return prefs.increase_gallery_row_spacing + if test_lumi(self.colours.playlist_panel_background) < 0.7: + light_mode = True + t1 = self.colours.grey(30) + gui.vis_4_colour = [40, 40, 40, 255] - prefs.increase_gallery_row_spacing ^= True - gui.update += 1 - update_layout_do() + ddt.rect((0, gui.panelY, window_size[0], window_size[1] - gui.panelY), self.colours.playlist_panel_background) + if prefs.bg_showcase_only and prefs.art_bg: + style_overlay.display() -def toggle_galler_text(mode: int = 0) -> bool: - if mode == 1: - return gui.gallery_show_text + # Draw textured background + if not light_mode and not self.colours.lm and prefs.showcase_overlay_texture: + rect = SDL_Rect() + rect.x = 0 + rect.y = 0 + rect.w = 300 + rect.h = 300 - gui.gallery_show_text ^= True - gui.update += 1 - update_layout_do() + 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 - # Jump to playing album - if album_mode and gui.first_in_grid is not None: + if prefs.bg_showcase_only and prefs.art_bg: + ddt.alpha_bg = True + ddt.force_gray = True - if gui.first_in_grid < len(default_playlist): - goto_album(gui.first_in_grid, force=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 -def toggle_card_style(mode: int = 0) -> bool: - if mode == 1: - return prefs.use_card_style + if pctl.playing_state == 3 and not radiobox.dummy_track.title: - prefs.use_card_style ^= True - gui.update += 1 + if not pctl.tag_meta: + y = int(window_size[1] / 2) - 60 - gui.scale + ddt.text((window_size[0] // 2, y, 2), pctl.url, self.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, self.colours.side_bar_line1, 216, w) -def toggle_side_panel(mode: int = 0) -> bool: - global update_layout - global album_mode + else: - if mode == 1: - return prefs.prefer_side + if len(pctl.track_queue) < 1: + ddt.alpha_bg = False + return - prefs.prefer_side ^= True - update_layout = True + # if draw.button("Return", 20, gui.panelY + 5, bg=colours.grey(30)): + # pass - if album_mode or prefs.prefer_side is True: - gui.rsp = True - else: - gui.rsp = False + if prefs.bg_showcase_only and prefs.art_bg: + ddt.alpha_bg = True + ddt.force_gray = True - if prefs.prefer_side: - gui.rspw = gui.pref_rspw + 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: -def force_album_view(): - toggle_album_mode(True) + 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: -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() + # 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), self.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)) -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 + # Click art to cycle + if tauon.coll((x, y, box, box)): + if inp.mouse_click is True: + album_art_gen.cycle_offset(track) + if inp.right_click: + picture_menu.activate(in_reference=track) + inp.right_click = False + # Check for lyrics if auto setting + test_auto_lyrics(track) -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() + gui.draw_vis4_top = False + if gui.panelY < inp.mouse_position[1] < window_size[1] - gui.panelBY: + if inp.mouse_wheel != 0: + lyrics_ren.lyrics_position += inp.mouse_wheel * 35 * gui.scale + if inp.right_click: + # track = pctl.playing_object() + if track != None: + showcase_menu.activate(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() + 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) -def standard_size(): - global album_mode - global window_size - global update_layout + if timed_ready and track.lyrics: - global album_mode_art_size + # 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 - album_mode = False - gui.rsp = True - window_size = window_default_size - SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) + timed_ready = prefs.prefer_synced_lyrics - gui.rspw = 80 + int(window_size[0] * 0.18) - update_layout = True - album_mode_art_size = 130 - # clear_img_cache() + #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) -def path_stem_to_playlist(path: str, title: str) -> None: - """Used with gallery power bar""" - playlist = [] + elif track.lyrics == "" or not prefs.show_lyrics_showcase: - # 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) + w = window_size[0] - (x + box) - round(30 * gui.scale) + x = int(x + box + (window_size[0] - x - box) / 2) - 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) + if hide_art: + x = window_size[0] // 2 - pctl.multi_playlist.append(pl_gen( - title=os.path.basename(title).upper(), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + # x = int((window_size[0]) / 2) + y = int(window_size[1] / 2) - round(60 * gui.scale) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pctl.active_playlist_viewing].title + "\" f\"" + path + "\"" + if prefs.showcase_vis and prefs.backend == 1: + y -= round(30 * gui.scale) - switch_playlist(len(pctl.multi_playlist) - 1) + 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) -def goto_album(playlist_no: int, down: bool = False, force: bool = False) -> list | int | None: - logging.debug("Postion set by album locate") + 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) - if core_timer.get() < 0.5: - return None + gui.spec4_rec.x = x - (gui.spec4_rec.w // 2) + gui.spec4_rec.y = y + round(50 * gui.scale) - global album_dex + if prefs.showcase_vis and window_size[1] > 369 and not tauon.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 - # ---- - 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 - # ---- + if key_up_press and not (inp.key_ctrl_down or inp.key_shift_down or inp.key_shiftr_down): + lyrics_ren.lyrics_position += 35 * gui.scale + if key_down_press and not (inp.key_ctrl_down or inp.key_shift_down or inp.key_shiftr_down): + lyrics_ren.lyrics_position -= 35 * gui.scale - px = 0 - row = 0 - re = 0 + lyrics_ren.test_update(track) + tw, th = ddt.get_text_wh(lyrics_ren.text + "\n", 17, w, True) - 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 + 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) - # 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]: + 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 - # 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 + def render_vis(self, top: bool = False): + SDL_SetRenderTarget(renderer, gui.spec4_tex) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_RenderClear(renderer) - else: - # Set the view to the calculated position - gui.album_scroll_px = px - gui.album_scroll_px -= album_v_slide_value + bx = 0 + by = 50 * gui.scale - gui.album_scroll_px = max(gui.album_scroll_px, 0 - album_v_slide_value) + 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 len(album_dex) > 0: - return album_dex[re] - return 0 + 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 - gui.update += 1 + 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 -def toggle_album_mode(force_on=False): - global album_mode - global window_size - global update_layout - global album_playlist_width - global old_album_pos + slide = 0.7 + for i, bar in enumerate(gui.spec4_array): - gui.gall_tab_enter = False + # We wont draw higher bars that may not move + if i > 40: + break - if album_mode is True: + # 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 - 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() + # 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)))) - gui.rsp = True + # 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) - gui.rspw = gui.pref_gallery_w + SDL_RenderFillRect(renderer, gui.bar4) - space = window_size[0] - gui.rspw - if gui.lsp: - space -= gui.lspw + # Set distance between bars + bx += 8 * gui.scale - 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 top: + SDL_SetRenderTarget(renderer, None) + else: + SDL_SetRenderTarget(renderer, gui.main_texture) - reload_albums(quiet=True) + # SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + SDL_RenderCopy(renderer, gui.spec4_tex, None, gui.spec4_rec) - # if pctl.active_playlist_playing == pctl.active_playlist_viewing: - # goto_album(pctl.playlist_playing_position) +class ColourPulse2: + """Animates colour between two colours""" + def __init__(self): - if album_mode: - if pctl.selected_in_playlist < len(pctl.playing_playlist()): - goto_album(pctl.selected_in_playlist) + 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): -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) + 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 -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) + 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) -def switch_playlist(number, cycle=False, quiet=False): - global default_playlist + elif hit is False and self.active is True: + self.active = False + self.out_timer.set() - global search_index - global shift_selection + in_time = self.in_timer.get() + if in_time < ani_time: + self.out_timer.force_set(ani_time - in_time) - # 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 + 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: - 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 + 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 - 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 + return colour_slide(low_hls, high_hls, pro, 1) - # if pl_follow: - # pctl.multi_playlist[pctl.playlist_active][1] = copy.deepcopy(pctl.playlist_playing) +class ViewBox: - if gui.showcase_mode and gui.combo_mode and not quiet: - view_standard() + def __init__(self, tauon: Tauon, reload: bool = False) -> None: + self.tauon = tauon + self.colours = tauon.bag.colours + self.x = 0 + self.y = tauon.gui.panelY + self.w = 52 * tauon.gui.scale + self.h = 260 * tauon.gui.scale # 257 + self.active = False - 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 + self.border = 3 * tauon.gui.scale - if gall_pl_switch_timer.get() > 240: - gui.gallery_positions.clear() - gall_pl_switch_timer.set() + self.tracks_img = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "tracks.png", True) + self.side_img = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "tracks+side.png", True) + self.gallery1_img = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "gallery1.png", True) + self.gallery2_img = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "gallery2.png", True) + self.combo_img = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "combo.png", True) + self.lyrics_img = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "lyrics.png", True) + self.gallery2_img = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "gallery2.png", True) + self.radio_img = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "radio.png", True) + self.col_img = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "col.png", True) + # self.artist_img = asset_loader(tauon.bag, tauon.bag.loaded_asset_dc, "artist.png", True) - gui.gallery_positions[gui.previous_playlist_id] = gui.album_scroll_px + # _ .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) - if cycle: - pctl.active_playlist_viewing += number - else: - pctl.active_playlist_viewing = number + self.on_colour = [255, 190, 50, 255] + self.over_colour = [255, 190, 50, 255] + self.off_colour = self.colours.grey(40) - 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 not reload: + tauon.gui.combo_was_album = False - 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] + def activate(self, x): + self.x = x + self.active = True + self.clicked = False - id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + 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) - 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() + 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 -def new_playlist_colour_callback(): - if gui.radio_view: - return [120, 90, 245, 255] - return [237, 80, 221, 255] + # 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): -add_icon.xoff = 3 -add_icon.yoff = 0 -add_icon.colour = [237, 80, 221, 255] -add_icon.colour_callback = new_playlist_colour_callback + on = test() + rect = [x - 8 * gui.scale, + y - 8 * gui.scale, + asset.w + 16 * gui.scale, + asset.h + 16 * gui.scale] + tauon.fields.add(rect) + if on: + colour = self.on_colour -def new_playlist_deco(): - if gui.radio_view: - text = _("New Radio List") - else: - text = _("New Playlist") - return [colours.menu_text, colours.menu_background, text] + else: + colour = self.off_colour + fun = None + col = False + if tauon.coll(rect): -x_menu.add(MenuItem(_("New Playlist"), new_playlist, new_playlist_deco, icon=add_icon)) + 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 -def clean_db_show_test(_): - return gui.suggest_clean_db + 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]) -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 + # if not on and not animate: + # colour = self.off_colour + asset.render(x, y, colour) -def clean_db_deco(): - return [colours.menu_text, [30, 150, 120, 255], _("Clean Database!")] + return fun + def tracks(self, hit=False): -x_menu.add(MenuItem(_("Clean Database!"), clean_db_fast, clean_db_deco, show_test=clean_db_show_test)) + if hit is False: + return prefs.album_mode is False and \ + gui.combo_mode is False and \ + gui.rsp is False -# x_menu.add(_("Internet Radio…"), activate_radio_box) + 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 -tauon.switch_playlist = switch_playlist + view_tracks() + def side(self, hit=False): -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 hit is False: + return prefs.album_mode is False and \ + gui.combo_mode is False and \ + gui.rsp is True + if not (prefs.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 - if album_mode: - reload_albums() - gui.pl_update += 1 + view_standard_meta() + def gallery1(self, hit: bool = False) -> bool | 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] + if hit is False: + return prefs.album_mode is True # and gui.show_playlist is True + if prefs.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 -x_menu.add(MenuItem(_("Paste Spotify Playlist"), import_spotify_playlist, import_spotify_playlist_deco, icon=spot_icon, - show_test=spotify_show_test)) + if x_menu.active: + x_menu.close_next_frame = True + force_album_view() -def show_import_music(_): - return gui.add_music_folder_ready + def radio(self, hit=False): + if hit is False: + return gui.radio_view -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 + if not gui.radio_view: + enter_radio_view(tauon=tauon) + else: + exit_combo(restore=True) + if x_menu.active: + x_menu.close_next_frame = True -x_menu.add(MenuItem(_("Import Music Folder"), import_music, show_test=show_import_music)) + def lyrics(self, hit=False): -x_menu.br() + if hit is False: + return gui.showcase_mode -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")) + if not gui.showcase_mode: + if gui.radio_view: + gui.was_radio = True + enter_showcase_view() - def dev_mode_disable_save_state() -> None: - global should_save_state - should_save_state = False - show_message(_("Disabled saving state")) + elif gui.was_radio: + enter_radio_view(tauon=tauon) + else: + exit_combo(restore=True) + if x_menu.active: + x_menu.close_next_frame = True - 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() + def col(self, hit=False): + if hit is False: + return gui.set_mode -# x_menu.add('Toggle Side panel', toggle_combo_view, combo_deco) + if not gui.set_mode: + if gui.combo_mode: + exit_combo() -def stt2(sec): - days, rem = divmod(sec, 86400) - hours, rem = divmod(rem, 3600) - min, sec = divmod(rem, 60) + if prefs.album_mode and gui.plw < 550 * gui.scale: + toggle_album_mode(tauon=tauon) - s_day = str(days) + "d" - if s_day == "0d": - s_day = " " + toggle_library_mode() - s_hours = str(hours) + "h" - if s_hours == "0h" and s_day == " ": - s_hours = " " + def artist_info(self, hit=False): - s_min = str(min) + "m" + if hit is False: + return gui.artist_info_panel - return s_day.rjust(3) + " " + s_hours.rjust(3) + " " + s_min.rjust(3) + gui.artist_info_panel ^= True + gui.update_layout() + def render(self): -def export_database(): - path = str(user_directory / "DatabaseExport.csv") - xport = open(path, "w") + if prefs.shuffle_lock: + self.active = False + self.clicked = False + return - xport.write("Artist;Title;Album;Album artist;Track number;Type;Duration;Release date;Genre;Playtime;File path") + if not self.active: + return - for index, track in pctl.master_library.items(): + # 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 - xport.write("\n") + x = self.x - 40 * gui.scale - 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)) + vr = [x, gui.panelY, self.w, self.h] + # vr = [x, gui.panelY, 52 * gui.scale, 220 * gui.scale] - xport.close() - show_message(_("Export complete."), _("Saved as: ") + path, mode="done") + 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 -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)) + 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) -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)) + # if colours.lm: + # low = (0, 0.5, 0) -cm_clean_db = False + # ---- + #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 -def clean_db() -> None: - global cm_clean_db - prefs.remove_network_tracks = False - cm_clean_db = True - tauon.thread_manager.ready("worker") + # ---- + y += 40 * gui.scale -def clean_db2() -> None: - global cm_clean_db - prefs.remove_network_tracks = True - cm_clean_db = True - tauon.thread_manager.ready("worker") + 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 -x_menu.add_to_sub(0, MenuItem(_("Remove Network Tracks"), clean_db2)) -x_menu.add_to_sub(0, MenuItem(_("Remove Missing Tracks"), clean_db)) + # --- + y += 40 * gui.scale + high = [76, 229, 229, 255] + if colours.lm: + # high = (.5, .7, .65) + high = [63, 63, 63, 255] -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) + 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 - show_message(_("{N} ratings imported").format(N=str(len(unique))), mode="done") + # --- - gui.pl_update += 1 + y += 45 * gui.scale -x_menu.add_to_sub(0, MenuItem(_("Import FMPS Ratings"), import_fmps)) + 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 -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) + y += 40 * gui.scale - s = str(len(unique)) + " ratings imported" - if len(skipped) > 0: - s += f", {len(skipped)} skipped" - show_message(s, mode="done") + high = [92, 86, 255, 255] + if colours.lm: + # high = (.7, .75, .75) + high = [63, 63, 63, 255] - gui.pl_update += 1 + 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 -x_menu.add_to_sub(0, MenuItem(_("Import POPM Ratings"), import_popm)) + # -- + y += 45 * gui.scale -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 + 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 -x_menu.add_to_sub(0, MenuItem(_("Reset User Ratings"), clear_ratings)) + # -- + # 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 -def find_incomplete() -> None: - gen_incomplete(pctl.active_playlist_viewing) + if func is not None: + func(True) + if gui.level_2_click and tauon.coll(vr): + x_menu.clicked = False -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)) + gui.level_2_click = False + if not x_menu.active: + self.active = False +class DLMon: -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 __init__(self): + self.ticker = Timer() + self.ticker.force_set(8) -def cast_search2() -> None: - chrome.rescan() + self.watching = {} + self.ready = set() + self.done = set() -def cast_search() -> None: + def scan(self): - if tauon.chrome_mode: - pctl.stop() - chrome.end() - else: - if not chrome: - show_message(_("pychromecast not found")) + if len(self.watching) == 0: + if self.ticker.get() < 10: + return + elif self.ticker.get() < 2: 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 + self.ticker.set() -#x_menu.add(_("Cast…"), cast_search, cast_deco) + for downloads in download_directories: + for item in os.listdir(downloads): -def clear_queue() -> None: - pctl.force_queue = [] - gui.pl_update = 1 - pctl.pause_queue = False + path = os.path.join(downloads, item) + if path in self.done: + continue -mode_menu = Menu(175) + 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 -def set_mini_mode_A1() -> None: - prefs.mini_mode_mode = 0 - set_mini_mode() + # 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() -def set_mini_mode_B1() -> None: - prefs.mini_mode_mode = 1 - set_mini_mode() + if msys and "TauonMusicBox" in path: + continue + if min_age < 240 and os.path.isfile(path) and ext in bag.formats.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] -def set_mini_mode_A2() -> None: - prefs.mini_mode_mode = 2 - set_mini_mode() + # 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") -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() + elif archive_file_scan(path, bag.formats.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, bag.formats.DA_Formats) > 0.5: -def set_mini_mode_D() -> None: - prefs.mini_mode_mode = 4 - set_mini_mode() + # 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 -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)) + if len(self.watching) > 0: + gui.update += 1 +class Fader: -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 + 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) -mode_menu.br() -mode_menu.add(MenuItem(_("Copy Title to Clipboard"), copy_bb_metadata)) + def render(self): -extra_menu = Menu(175, show_icons=True) + 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)]) -def stop() -> None: - pctl.stop() + if not (self.a == 0 or self.a == 1): + gui.update += 1 + def rise(self): -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() + self.state = 1 + self.timer.hit() + self.total_timer.set() + def fall(self): -extra_menu.add(MenuItem(_("Random Track"), random_track, hint=";")) + self.state = 0 + self.timer.hit() + self.total_timer.set() +class EdgePulse: -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) + def __init__(self): - key = random.choice(list(folders.keys())) - result = folders[key] - pctl.jump(*result) - pctl.show_current() + 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 radio_random() -> None: - pctl.advance(rr=True) + def pulse(self): + self.timer.set() +class EdgePulse2: -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)) + def __init__(self): -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)) + self.timer = Timer() + self.timer.force_set(10) + self.ani_duration = 0.22 -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)) + def render(self, x, y, w, h, bottom=False) -> bool | None: -# extra_menu.add('Toggle Repeat', toggle_repeat, hint='COMMA') + time = self.timer.get() + if time < self.ani_duration: + if bottom: + if inp.mouse_wheel > 0: + self.timer.force_set(10) + return None + elif inp.mouse_wheel < 0: + self.timer.force_set(10) + return None -# extra_menu.add('Toggle Random', toggle_random, hint='PERIOD') -extra_menu.add(MenuItem(_("Clear Queue"), clear_queue, queue_deco, hint="Alt+Shift+Q")) + 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) -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 + 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() -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) +class Undo: + def __init__(self): -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) + self.e = [] - rect = (x - round(5 * gui.scale), y - round(4 * gui.scale), round(80 * gui.scale), round(16 * gui.scale)) - gui.heart_fields.append(rect) + def undo(self): - 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 not self.e: + show_message(_("There are no more steps to undo.")) + return - if pp < 5 * gui.scale: - rat = 0 - elif pp > 70 * gui.scale: - rat = 10 - else: - rat = pp // (star_row_icon.w // 2) + job = self.e.pop() - 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) + if job[0] == "playlist": + pctl.multi_playlist.append(job[1]) + switch_playlist(len(pctl.multi_playlist) - 1) + elif job[0] == "tracks": - # bg = colours.grey(40) - bg = [255, 255, 255, 17] - fg = colours.grey(210) + uid = job[1] + li = job[2] - if gui.tracklist_bg_is_light: - bg = [0, 0, 0, 25] - fg = colours.grey(70) + 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 - 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 i, ref in reversed(li): - for ss in range(5): + if i > len(pl): + logging.error("restore track error - playlist not correct length") + continue + pl.insert(i, ref) - xx = x + ss * star_row_icon.w + 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 - 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: + gui.pl_update = 1 - 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) + def bk_playlist(self, pl_index: int) -> None: + self.e.append(("playlist", pctl.multi_playlist[pl_index])) -heart_colours = ColourGenCache(0.7, 0.7) + def bk_tracks(self, pl_index: int, indis) -> None: -heart_icon.colour = [245, 60, 60, 255] -heart_icon.xoff = 3 -heart_icon.yoff = 0 + 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)) +@dataclass +class Directories: + """Hold directories""" + 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 + +@dataclass +class Bag: + """Holder object for all configs""" + colours: ColoursClass + console: DConsole + dirs: Directories + prefs: Prefs + formats: Formats + renderer: renderer + ddt: TDraw + fonts: Fonts + tls_context: ssl.SSLContext + sdl_syswminfo: SDL_SysWMinfo + macos: bool + msys: bool + phone: bool + pump: bool + snap_mode: bool + smtc: bool + draw_min_button: bool + draw_max_button: bool + desktop: str | None + system: str + launch_prefix: str + album_mode_art_size: int + xdpi: int + master_count: int + playing_in_queue: int + playlist_active: int + playlist_playing: int + playlist_view_position: int + radio_playlist_viewing: int + selected_in_playlist: int + volume: float + track_queue: list[int] + logical_size: list[int] # X Y + window_size: list[int] # X Y + load_orders: list[LoadClass] + multi_playlist: list[TauonPlaylist] + radio_playlists: list[RadioPlaylist] + p_force_queue: list[TauonQueueItem] + gen_codes: dict[int, str] + master_library: dict[int, TrackClass] + loaded_asset_dc: dict[str, WhiteModImageAsset | LoadImageAsset] + +@dataclass +class Formats: + """Contains: + + * Colours used for the label icon in UI 'track info box' + * Extensions of files to be added when importing + """ -if gui.scale == 1.25: - heart_icon.yoff = 1 + format_colours: dict[str, tuple[int, int, int, int]] + VID_Formats: set[str] + MOD_Formats: set[str] + GME_Formats: set[str] + DA_Formats: set[str] + Archive_Formats: set[str] -heart_icon.colour_callback = heart_menu_colour +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_tls(holder: Holder) -> ssl.SSLContext: + """TLS setup (needed for frozen installs)""" + # Set the TLS certificate path environment variable + 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 -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")] + # Create default TLS context + tls_context = ssl.create_default_context(cafile=get_cert_path(holder)) + return tls_context +def whicher(target: str, flatpak_mode: bool) -> bool | str | None: + """Detect and launch programs outside of flatpak sandbox""" + try: + if flatpak_mode: + complete = subprocess.run( + shlex.split("flatpak-spawn --host which " + target), stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=True) + r = complete.stdout.decode() + return "bin/" + target in r + return shutil.which(target) + except Exception: + logging.exception("Failed to run flatpak-spawn") + return False -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 asset_loader( + bag: Bag, loaded_asset_dc: dict[str, WhiteModImageAsset | LoadImageAsset], name: str, mod: bool = False, +) -> WhiteModImageAsset | LoadImageAsset: + if name in loaded_asset_dc: + return loaded_asset_dc[name] + target = str(bag.dirs.scaled_asset_directory / name) + if mod: + item = WhiteModImageAsset(bag=bag, path=target, scale_name=name) + else: + item = LoadImageAsset(bag=bag, path=target, scale_name=name) + loaded_asset_dc[name] = item + return item -def bar_love_notify() -> None: - bar_love(notify=True) +def no_padding() -> int: + """This will remove all padding""" + return 0 +def uid_gen() -> int: + return random.randrange(1, 100000000) -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] +# TODO(Martin): Get rid of this +notify_change = lambda: None - shoot_love = threading.Thread(target=love, args=[True, track_id, False, notify]) - shoot_love.daemon = True - shoot_love.start() +def pl_gen( + title: str = "Default", + playing: int = 0, + playlist_ids: list[int] | None = None, + position: int = 0, + hide_title: bool = False, + selected: int = 0, + parent: str = "", + hidden: bool = False, +) -> TauonPlaylist: + """Generate a TauonPlaylist + Creates a default playlist when called without parameters + """ + if playlist_ids == None: + playlist_ids = [] -extra_menu.add(MenuItem("Love", bar_love_notify, love_deco, icon=heart_icon)) + #TODO(Martin): Change to pctl.notify_change() + notify_change() -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 + #return copy.deepcopy([title, playing, playlist, position, hide_title, selected, uid_gen(), [], hidden, False, parent, False]) + return TauonPlaylist(title=title, playing=playing, playlist_ids=playlist_ids, position=position, hide_title=hide_title, selected=selected, uuid_int=uid_gen(), last_folder=[], hidden=hidden, locked=False, parent_playlist_id=parent, persist_time_positioning=False) -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 queue_item_gen(track_id: int, position: int, pl_id: int, type: int = 0, album_stage: int = 0) -> TauonQueueItem: + # type; 0 is track, 1 is album + auto_stop = False + #return [track_id, position, pl_id, type, album_stage, uid_gen(), auto_stop] + return TauonQueueItem(track_id=track_id, position=position, playlist_id=pl_id, type=type, album_stage=album_stage, uuid_int=uid_gen(), auto_stop=auto_stop) -def toggle_spotify_like_active_deco(): - tr = pctl.playing_object() - text = _("Spotify Like Track") +def open_uri(uri:str) -> None: + logging.info("OPEN URI") + load_order = LoadClass() - 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") + for w in range(len(pctl.multi_playlist)): + if pctl.multi_playlist[w].title == "Default": + load_order.playlist = pctl.multi_playlist[w].uuid_int + break + else: + logging.warning("'Default' playlist not found, generating a new one!") + pctl.multi_playlist.append(pl_gen()) + load_order.playlist = pctl.multi_playlist[len(pctl.multi_playlist) - 1].uuid_int + switch_playlist(len(pctl.multi_playlist) - 1) - return [colours.menu_text, colours.menu_background, text] + load_order.target = str(urllib.parse.unquote(uri)).replace("file:///", "/").replace("\r", "") + if gui.auto_play_import is False: + load_order.play = True + gui.auto_play_import = True -def locate_artist() -> None: - track = pctl.playing_object() - if not track: - return + load_orders.append(copy.deepcopy(load_order)) + gui.update += 1 - artist = track.artist - if track.album_artist: - artist = track.album_artist +def toast(text: str) -> None: + gui.mode_toast_text = text + toast_mode_timer.set() + gui.frame_callback_list.append(TestTimer(1.5)) - 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 +def set_artist_preview(path, artist, x, y): + m = min(round(500 * gui.scale), window_size[1] - (gui.panelY + gui.panelBY + 50 * gui.scale)) + artist_preview_render.load(path, box_size=(m, m)) + artist_preview_render.show = True + ah = artist_preview_render.size[1] + ay = round(y) - (ah // 2) + if ay < gui.panelY + 20 * gui.scale: + ay = gui.panelY + round(20 * gui.scale) + if ay + ah > window_size[1] - (gui.panelBY + 5 * gui.scale): + ay = window_size[1] - (gui.panelBY + ah + round(5 * gui.scale)) + gui.preview_artist = artist + gui.preview_artist_location = (x + 15 * gui.scale, ay) - if block_starts: +def get_artist_preview(artist: str, x, y) -> None: + # show_message(_("Loading artist image...")) - next = False - for start in block_starts: + gui.preview_artist_loading = artist + artist_info_box.get_data(artist, force_dl=True) + path = artist_info_box.get_data(artist, get_img_path=True) + if not path: + show_message(_("No artist image found.")) + if not prefs.enable_fanart_artist and not verify_discogs(): + show_message(_("No artist image found."), _("No providers are enabled in settings!"), mode="warning") + gui.preview_artist_loading = "" + return + set_artist_preview(path, artist, x, y) + gui.message_box = False + gui.preview_artist_loading = "" - if next: - pctl.selected_in_playlist = start - pctl.playlist_view_position = start - shift_selection.clear() - break +def set_drag_source(): + gui.drag_source_position = tuple(inp.click_location) + gui.drag_source_position_persist = tuple(inp.click_location) - if pctl.selected_in_playlist == start: - next = True - continue +def update_set(tauon: Tauon) -> None: + """This is used to scale columns when windows is resized or items added/removed""" + gui = tauon.gui + 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: - pctl.selected_in_playlist = block_starts[0] - pctl.playlist_view_position = block_starts[0] - shift_selection.clear() + wid -= item[1] - 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")) + wid = max(75, wid) - logging.debug("Position changed by artist locate") + 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 - gui.pl_update += 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) -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 + total = wid + for item in gui.pl_st: + if item[2]: + fixed_n += 1 -extra_menu.add(MenuItem(_("Global Search"), activate_search_overlay, hint="Ctrl+G")) + 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) -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] + 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) -def goto_playing_extra() -> None: - pctl.show_current(highlight=True) + 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) -extra_menu.add(MenuItem(_("Locate Artist"), locate_artist)) + if item[0] == "Bitrate": + item[1] = round(67 * gui.scale) + total -= round(67 * gui.scale) -extra_menu.add(MenuItem(_("Go To Playing"), goto_playing_extra, hint="'")) + if item[0] == "❤": + item[1] = round(27 * gui.scale) + total -= round(27 * gui.scale) -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] + vr = len(gui.pl_st) - fixed_n -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 vr > 0 and total > 50: + space = round(total / vr) + for item in gui.pl_st: + if not item[2]: + item[1] = space -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) + gui.pl_update += 1 + update_set() +def set_colour(colour): + SDL_SetRenderDrawColor(renderer, colour[0], colour[1], colour[2], colour[3]) -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) +def get_themes(dirs: Directories, deco: bool = False) -> list[str] | dict[str, str]: + themes: list[str] = [] # full, name + decos: dict[str, str] = {} + direcs = [str(dirs.install_directory / "theme")] + if dirs.user_directory != dirs.install_directory: + direcs.append(str(dirs.user_directory / "theme")) + def scan_folders(folders: list[str]) -> None: + for folder in folders: + if not os.path.isdir(folder): + continue + paths = [os.path.join(folder, f) for f in os.listdir(folder)] + for path in paths: + if os.path.islink(path): + path = os.readlink(path) + if os.path.isfile(path): + if path[-7:] == ".ttheme": + themes.append((path, os.path.basename(path).split(".")[0])) + elif path[-6:] == ".tdeco": + decos[os.path.basename(path).split(".")[0]] = path + elif os.path.isdir(path): + scan_folders([path]) -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)) + scan_folders(direcs) + themes.sort() + if deco: + return decos + return themes -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")) +# 2025-02-02 - commented out as it was not used +#def advance_theme() -> None: +# prefs.theme += 1 +# gui.reload_theme = True -extra_menu.add_sub(_("Import Spotify…"), 140, show_test=spotify_show_test) +def get_theme_number(dirs: Directories, name: str) -> int: + if name == "Mindaro": + return 0 + themes = get_themes(dirs=dirs) + for i, theme in enumerate(themes): + if theme[1] == name: + return i + 1 + return 0 -extra_menu.add_to_sub(0, MenuItem(_("Liked Albums"), spot_import_albums, show_test=spotify_show_test, icon=spot_icon)) +def get_theme_name(number: int) -> str: + if number == 0: + return "Mindaro" + number -= 1 + themes = get_themes() + logging.info((number, themes)) + if len(themes) > number: + return themes[number][1] + return "" -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")) +def get_end_folder(direc: str) -> str | None: + for w in range(len(direc)): + if direc[-w - 1] == "\\" or direc[-w - 1] == "/": + direc = direc[-w:] + return direc + return None -extra_menu.add_to_sub(0, MenuItem(_("Liked Tracks"), spot_import_tracks, show_test=spotify_show_test, icon=spot_icon)) +def set_path(nt: TrackClass, path: str) -> None: + nt.fullpath = path.replace("\\", "/") + nt.filename = os.path.basename(path) + nt.parent_folder_path = os.path.dirname(path.replace("\\", "/")) + nt.parent_folder_name = get_end_folder(os.path.dirname(path)) + nt.file_ext = os.path.splitext(os.path.basename(path))[1][1:].upper() -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")) +def show_message(line1: str, line2: str ="", line3: str = "", mode: str = "info") -> None: + gui.message_box = True + gui.message_text = line1 + gui.message_mode = mode + gui.message_subtext = line2 + gui.message_subtext2 = line3 + message_box_min_timer.set() + match mode: + case "done" | "confirm": + logging.debug("Message: " + line1 + line2 + line3) + case "info": + logging.info("Message: " + line1 + line2 + line3) + case "warning": + logging.warning("Message: " + line1 + line2 + line3) + case "error": + logging.error("Message: " + line1 + line2 + line3) + case _: + logging.error(f"Unknown mode '{mode}' for message: " + line1 + line2 + line3) + gui.update = 1 +def pumper(bag: Bag): + if bag.macos: + return + while bag.pump: + time.sleep(0.005) + SDL_PumpEvents() -#extra_menu.add_to_sub(_("Import All Playlists"), 0, spot_import_playlists, show_test=spotify_show_test, icon=spot_icon) +def track_number_process(line: str) -> str: + line = str(line).split("/", 1)[0].lstrip("0") + if prefs.dd_index and len(line) == 1: + return "0" + line + return line -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])) +def save_prefs(bag: Bag, cf: Config): + prefs = bag.prefs + cf.update_value("sync-bypass-transcode", prefs.bypass_transcode) + cf.update_value("sync-bypass-low-bitrate", prefs.smart_bypass) + cf.update_value("radio-record-codec", prefs.radio_record_codec) - 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")) + cf.update_value("plex-username", prefs.plex_username) + cf.update_value("plex-password", prefs.plex_password) + cf.update_value("plex-servername", prefs.plex_servername) -extra_menu.add_to_sub(0, MenuItem(_("Playlist…"), spot_import_playlist_menu, show_test=spotify_show_test, icon=spot_icon)) + cf.update_value("subsonic-username", prefs.subsonic_user) + cf.update_value("subsonic-password", prefs.subsonic_password) + cf.update_value("subsonic-password-plain", prefs.subsonic_password_plain) + cf.update_value("subsonic-server-url", prefs.subsonic_server) + cf.update_value("jelly-username", prefs.jelly_username) + cf.update_value("jelly-password", prefs.jelly_password) + cf.update_value("jelly-server-url", prefs.jelly_server_url) -def spot_import_context() -> None: - shooter(tauon.spot_ctl.import_context) + cf.update_value("koel-username", prefs.koel_username) + cf.update_value("koel-password", prefs.koel_password) + cf.update_value("koel-server-url", prefs.koel_server_url) + cf.update_value("stream-bitrate", prefs.network_stream_bitrate) -extra_menu.add_to_sub(0, MenuItem(_("Current Context"), spot_import_context, show_spot_coasting_deco, show_test=spotify_show_test, icon=spot_icon)) + cf.update_value("display-language", prefs.ui_lang) + # cf.update_value("decode-search", prefs.diacritic_search) + # cf.update_value("use-log-volume-scale", prefs.log_vol) + # cf.update_value("audio-backend", prefs.backend) + cf.update_value("use-pipewire", prefs.pipewire) + cf.update_value("seek-interval", prefs.seek_interval) + cf.update_value("pause-fade-time", prefs.pause_fade_time) + cf.update_value("cross-fade-time", prefs.cross_fade_time) + cf.update_value("device-buffer-ms", prefs.device_buffer) + cf.update_value("output-samplerate", prefs.samplerate) + cf.update_value("resample-quality", prefs.resample) + cf.update_value("avoid_resampling", prefs.avoid_resampling) + # cf.update_value("fast-scrubbing", prefs.pa_fast_seek) + cf.update_value("precache-local-files", prefs.precache) + cf.update_value("cache-use-tmp", prefs.tmp_cache) + cf.update_value("cache-limit", prefs.cache_limit) + cf.update_value("always-ffmpeg", prefs.always_ffmpeg) + cf.update_value("volume-curve", prefs.volume_power) + # cf.update_value("force-mono", prefs.mono) + # cf.update_value("disconnect-device-pause", prefs.dc_device_setting) + # cf.update_value("use-short-buffering", prefs.short_buffer) -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") + # cf.update_value("gst-output", prefs.gst_output) + # cf.update_value("gst-use-custom-output", prefs.gst_use_custom_output) - return [colours.menu_text, colours.menu_background, text] + cf.update_value("separate-multi-genre", prefs.sep_genre_multi) + cf.update_value("tag-editor-name", prefs.tag_editor_name) + cf.update_value("tag-editor-target", prefs.tag_editor_target) -extra_menu.add(MenuItem("Show Full Album", get_album_spot_active, get_album_spot_deco, - show_test=spotify_show_test, icon=spot_icon)) + cf.update_value("playback-follow-cursor", prefs.playback_follow_cursor) + cf.update_value("spotify-prefer-web", prefs.launch_spotify_web) + cf.update_value("spotify-allow-local", prefs.launch_spotify_local) + cf.update_value("back-restarts", prefs.back_restarts) + cf.update_value("end-queue-stop", prefs.stop_end_queue) + cf.update_value("block-suspend", prefs.block_suspend) + cf.update_value("allow-video-formats", prefs.allow_video_formats) + cf.update_value("ui-scale", prefs.scale_want) + cf.update_value("auto-scale", prefs.x_scale) + cf.update_value("tracklist-y-text-offset", prefs.tracklist_y_text_offset) + cf.update_value("theme-name", prefs.theme_name) + cf.update_value("mac-style", prefs.macstyle) + cf.update_value("allow-art-zoom", prefs.zoom_art) -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,)) + cf.update_value("scroll-gallery-by-row", prefs.gallery_row_scroll) + cf.update_value("prefs.gallery_scroll_wheel_px", prefs.gallery_row_scroll) + cf.update_value("scroll-spectrogram", prefs.spec2_scroll) + cf.update_value("mascot-opacity", prefs.custom_bg_opacity) + cf.update_value("synced-lyrics-time-offset", prefs.sync_lyrics_time_offset) -extra_menu.add(MenuItem(_("Show Full Artist"), get_artist_spot, - show_test=spotify_show_test, icon=spot_icon)) + cf.update_value("artist-list-prefers-album-artist", prefs.artist_list_prefer_album_artist) + cf.update_value("side-panel-info-persists", prefs.meta_persists_stop) + cf.update_value("side-panel-info-selected", prefs.meta_shows_selected) + cf.update_value("side-panel-info-selected-always", prefs.meta_shows_selected_always) + cf.update_value("mini-mode-avoid-notifications", prefs.stop_notifications_mini_mode) + cf.update_value("hide-queue-when-empty", prefs.hide_queue) + # cf.update_value("show-playlist-list", prefs.show_playlist_list) + cf.update_value("enable-art-header-bar", prefs.art_in_top_panel) + cf.update_value("always-art-header-bar", prefs.always_art_header) + # cf.update_value("prefer-center-bg", prefs.center_bg) + cf.update_value("showcase-texture-background", prefs.showcase_overlay_texture) + cf.update_value("side-panel-style", prefs.side_panel_layout) + cf.update_value("side-lyrics-art", prefs.show_side_lyrics_art_panel) + cf.update_value("side-lyrics-art-on-top", prefs.lyric_metadata_panel_top) + cf.update_value("absolute-track-indices", prefs.use_absolute_track_index) + cf.update_value("auto-hide-bottom-title", prefs.hide_bottom_title) + cf.update_value("auto-show-playing", prefs.auto_goto_playing) + cf.update_value("notify-include-album", prefs.notify_include_album) + cf.update_value("show-rating-hint", prefs.rating_playtime_stars) + cf.update_value("drag-tab-to-unpin", prefs.drag_to_unpin) -extra_menu.add(MenuItem(_("Start Spotify Remote"), show_spot_playing, show_spot_playing_deco, show_test=spotify_show_test, - icon=spot_icon)) + cf.update_value("gallery-thin-borders", prefs.thin_gallery_borders) + cf.update_value("increase-row-spacing", prefs.increase_gallery_row_spacing) + cf.update_value("gallery-center-text", prefs.center_gallery_text) -# 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] + cf.update_value("use-custom-fonts", prefs.use_custom_fonts) + cf.update_value("font-main-standard", prefs.linux_font) + cf.update_value("font-main-medium", prefs.linux_font_semibold) + cf.update_value("font-main-bold", prefs.linux_font_bold) + cf.update_value("font-main-condensed", prefs.linux_font_condensed) + cf.update_value("font-main-condensed-bold", prefs.linux_font_condensed_bold) + cf.update_value("force-subpixel-text", prefs.force_subpixel_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)) + cf.update_value("double-digit-indices", prefs.dd_index) + cf.update_value("column-album-artist-fallsback", prefs.column_aa_fallback_artist) + cf.update_value("left-aligned-album-artist-title", prefs.left_align_album_artist_title) + cf.update_value("import-auto-sort", prefs.auto_sort) -def toggle_auto_theme(mode: int = 0) -> None: - if mode == 1: - return prefs.colour_from_image + cf.update_value("encode-output-dir", prefs.custom_encoder_output) + cf.update_value("sync-device-music-dir", prefs.sync_target) + cf.update_value("add_download_directory", prefs.download_dir1) - prefs.colour_from_image ^= True - gui.theme_temp_current = -1 + cf.update_value("use-system-tray", prefs.use_tray) + cf.update_value("use-gamepad", prefs.use_gamepad) + cf.update_value("enable-remote-interface", prefs.enable_remote) - gui.reload_theme = True + cf.update_value("enable-mpris", prefs.enable_mpris) + cf.update_value("hide-maximize-button", prefs.force_hide_max_button) + cf.update_value("restore-window-position", prefs.save_window_position) + cf.update_value("mini-mode-always-on-top", prefs.mini_mode_on_top) + cf.update_value("resume-playback-on-restart", prefs.reload_play_state) + cf.update_value("resume-playback-on-wake", prefs.resume_play_wake) + cf.update_value("auto-dl-artist-data", prefs.auto_dl_artist_data) - # if prefs.colour_from_image and prefs.art_bg and not key_shift_down: - # toggle_auto_bg() + cf.update_value("fanart.tv-cover", prefs.enable_fanart_cover) + cf.update_value("fanart.tv-artist", prefs.enable_fanart_artist) + cf.update_value("fanart.tv-background", prefs.enable_fanart_bg) + cf.update_value("auto-update-playlists", prefs.always_auto_update_playlists) + cf.update_value("write-ratings-to-tag", prefs.write_ratings) + cf.update_value("enable-spotify", prefs.spot_mode) + cf.update_value("enable-discord-rpc", prefs.discord_enable) + cf.update_value("auto-search-lyrics", prefs.auto_lyrics) + cf.update_value("shortcuts-ignore-keymap", prefs.use_scancodes) + cf.update_value("alpha_key_activate_search", prefs.search_on_letter) + cf.update_value("discogs-personal-access-token", prefs.discogs_pat) + cf.update_value("listenbrainz-token", prefs.lb_token) + cf.update_value("custom-listenbrainz-url", prefs.listenbrainz_url) -def toggle_auto_bg(mode: int= 0) -> bool | None: - if mode == 1: - return prefs.art_bg - prefs.art_bg ^= True + cf.update_value("maloja-key", prefs.maloja_key) + cf.update_value("maloja-url", prefs.maloja_url) + cf.update_value("maloja-enable", prefs.maloja_enable) - if prefs.art_bg: - gui.update = 60 + cf.update_value("tau-url", prefs.sat_url) - 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 + cf.update_value("lastfm-pull-love", prefs.lastfm_pull_love) + cf.update_value("broadcast-page-port", prefs.metadata_page_port) + cf.update_value("show-current-on-transition", prefs.show_current_on_transition) -def toggle_auto_bg_strong(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.art_bg_stronger == 2 + cf.update_value("chart-columns", prefs.chart_columns) + cf.update_value("chart-rows", prefs.chart_rows) + cf.update_value("chart-uses-text", prefs.chart_text) + cf.update_value("chart-font", prefs.chart_font) + cf.update_value("chart-sorts-top-played", prefs.topchart_sorts_played) - if prefs.art_bg_stronger == 2: - prefs.art_bg_stronger = 1 + if bag.dirs.config_directory.is_dir(): + cf.dump(str(bag.dirs.config_directory / "tauon.conf")) 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 + logging.error("Missing config directory") +def load_prefs(bag: Bag, cf: Config): + prefs = bag.prefs + cf.reset() + cf.load(str(bag.dirs.config_directory / "tauon.conf")) -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 + cf.add_comment("Tauon Music Box configuration file") + cf.br() + cf.add_comment( + "This file will be regenerated while app is running. Formatting and additional comments will be lost.") + cf.add_comment("Tip: Use TOML syntax highlighting") + cf.br() + cf.add_text("[audio]") -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 + # prefs.backend = cf.sync_add("int", "audio-backend", prefs.backend, "4: Built in backend (Phazor), 2: GStreamer") + prefs.pipewire = cf.sync_add( + "bool", "use-pipewire", prefs.pipewire, + "Experimental setting to use Pipewire native only.") + prefs.seek_interval = cf.sync_add( + "int", "seek-interval", prefs.seek_interval, + "In s. Interval to seek when using keyboard shortcut. Default is 15.") + # prefs.pause_fade_time = cf.sync_add("int", "pause-fade-time", prefs.pause_fade_time, "In milliseconds. Default is 400. (GStreamer Only)") -def toggle_notifications(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.show_notifications + prefs.pause_fade_time = max(prefs.pause_fade_time, 100) + prefs.pause_fade_time = min(prefs.pause_fade_time, 5000) - prefs.show_notifications ^= True + prefs.cross_fade_time = cf.sync_add( + "int", "cross-fade-time", prefs.cross_fade_time, + "In ms. Min: 200, Max: 2000, Default: 700. Applies to track change crossfades. End of track is always gapless.") - if prefs.show_notifications: - if not de_notify_support: - show_message(_("Notifications for this DE not supported"), "", mode="warning") - return None + prefs.device_buffer = cf.sync_add("int", "device-buffer-ms", prefs.device_buffer, "Default: 80") + #prefs.samplerate = cf.sync_add( + # "int", "output-samplerate", prefs.samplerate, + # "In hz. Default: 48000, alt: 44100. (restart app to apply change)") + prefs.avoid_resampling = cf.sync_add( + "bool", "avoid_resampling", prefs.avoid_resampling, + "Only implemented for FLAC, MP3, OGG, OPUS") + prefs.resample = cf.sync_add( + "int", "resample-quality", prefs.resample, + "0=best, 1=medium, 2=fast, 3=fastest. Default: 1. (applies on restart)") + if prefs.resample < 0 or prefs.resample > 4: + prefs.resample = 1 + # prefs.pa_fast_seek = cf.sync_add("bool", "fast-scrubbing", prefs.pa_fast_seek, "Seek without a delay but may cause audible popping") + prefs.cache_limit = cf.sync_add( + "int", "cache-limit", prefs.cache_limit, + "Limit size of network audio file cache. In MB.") + prefs.tmp_cache = cf.sync_add( + "bool", "cache-use-tmp", prefs.tmp_cache, + "Use /tmp for cache. When enabled, above setting overridden to a small value. (applies on restart)") + prefs.precache = cf.sync_add( + "bool", "precache-local-files", prefs.precache, + "Cache files from local sources too. (Useful for mounted network drives)") + prefs.always_ffmpeg = cf.sync_add( + "bool", "always-ffmpeg", prefs.always_ffmpeg, + "Prefer decoding using FFMPEG. Fixes stuttering on Raspberry Pi OS.") + prefs.volume_power = cf.sync_add( + "int", "volume-curve", prefs.volume_power, + "1=Linear volume control. Values above one give greater control bias over lower volume range. Default: 2") + # prefs.mono = cf.sync_add("bool", "force-mono", prefs.mono, "This is a placeholder setting and currently has no effect.") + # prefs.dc_device_setting = cf.sync_add("string", "disconnect-device-pause", prefs.dc_device_setting, "Can be \"on\" or \"off\". BASS only. When off, connection to device will he held open.") + # prefs.short_buffer = cf.sync_add("bool", "use-short-buffering", prefs.short_buffer, "BASS only.") -# 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 + # cf.br() + # cf.add_text("[audio (gstreamer only)]") + # + # prefs.gst_output = cf.sync_add("string", "gst-output", prefs.gst_output, "GStreamer output pipeline specification. Only used with GStreamer backend.") + # prefs.gst_use_custom_output = cf.sync_add("bool", "gst-use-custom-output", prefs.gst_use_custom_output, "Set this to true to apply any manual edits of the above string.") -def toggle_mini_lyrics(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.show_lyrics_side - prefs.show_lyrics_side ^= True - return None + if prefs.dc_device_setting == "on": + prefs.dc_device = True + elif prefs.dc_device_setting == "off": + prefs.dc_device = False + cf.br() + cf.add_text("[locale]") + prefs.ui_lang = cf.sync_add( + "string", "display-language", prefs.ui_lang, "Override display language to use if " + "available. E.g. \"en\", \"ja\", \"zh_CH\". " + "Default: \"auto\"") + # prefs.diacritic_search = cf.sync_add("bool", "decode-search", prefs.diacritic_search, "Allow searching of diacritics etc using ascii in search functions. (Disablng may speed up search)") + cf.br() + cf.add_text("[search]") + prefs.sep_genre_multi = cf.sync_add( + "bool", "separate-multi-genre", prefs.sep_genre_multi, + "If true, the standard genre result will exclude results from multi-value tags. These will be included in a separate result.") -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 + cf.br() + cf.add_text("[tag-editor]") + if bag.system == "Windows" or bag.msys: + prefs.tag_editor_name = cf.sync_add("string", "tag-editor-name", "Picard", "Name to display in UI.") + prefs.tag_editor_target = cf.sync_add( + "string", "tag-editor-target", + "C:\\Program Files (x86)\\MusicBrainz Picard\\picard.exe", + "The path of the exe to run.") + else: + prefs.tag_editor_name = cf.sync_add("string", "tag-editor-name", "Picard", "Name to display in UI.") + prefs.tag_editor_target = cf.sync_add( + "string", "tag-editor-target", "picard", + "The name of the binary to call.") + cf.br() + cf.add_text("[playback]") + prefs.playback_follow_cursor = cf.sync_add( + "bool", "playback-follow-cursor", prefs.playback_follow_cursor, + "When advancing, always play the track that is selected.") + prefs.launch_spotify_web = cf.sync_add( + "bool", "spotify-prefer-web", prefs.launch_spotify_web, + "Launch the web client rather than attempting to launch the desktop client.") + prefs.launch_spotify_local = cf.sync_add( + "bool", "spotify-allow-local", prefs.launch_spotify_local, + "Play Spotify audio through Tauon.") + prefs.back_restarts = cf.sync_add( + "bool", "back-restarts", prefs.back_restarts, + "Pressing the back button restarts playing track on first press.") + prefs.stop_end_queue = cf.sync_add( + "bool", "end-queue-stop", prefs.stop_end_queue, + "Queue will always enable auto-stop on last track") + prefs.block_suspend = cf.sync_add( + "bool", "block-suspend", prefs.block_suspend, + "Prevent system suspend during playback") + prefs.allow_video_formats = cf.sync_add( + "bool", "allow-video-formats", prefs.allow_video_formats, + "Allow the import of MP4 and WEBM formats") + if prefs.allow_video_formats: + for item in bag.formats.VID_Formats: + if item not in bag.formats.DA_Formats: + bag.formats.DA_Formats.add(item) -def toggle_level_meter(mode: int = 0) -> bool | None: - if mode == 1: - return gui.vis_want != 0 + cf.br() + cf.add_text("[HiDPI]") + prefs.scale_want = cf.sync_add( + "float", "ui-scale", prefs.scale_want, + "UI scale factor. Default is 1.0, try increase if using a HiDPI display.") + prefs.x_scale = cf.sync_add("bool", "auto-scale", prefs.x_scale, "Automatically choose above setting") + prefs.tracklist_y_text_offset = cf.sync_add( + "int", "tracklist-y-text-offset", prefs.tracklist_y_text_offset, + "If you're using a UI scale, you may need to tweak this.") - if gui.vis_want == 0: - gui.vis_want = 1 - else: - gui.vis_want = 0 + cf.br() + cf.add_text("[ui]") - gui.update_layout() - return None + prefs.theme_name = cf.sync_add("string", "theme-name", prefs.theme_name) + macstyle = cf.sync_add("bool", "mac-style", prefs.macstyle, "Use macOS style window buttons") + prefs.zoom_art = cf.sync_add("bool", "allow-art-zoom", prefs.zoom_art) + prefs.gallery_row_scroll = cf.sync_add("bool", "scroll-gallery-by-row", True) + prefs.gallery_scroll_wheel_px = cf.sync_add( + "int", "scroll-gallery-distance", 90, + "Only has effect if scroll-gallery-by-row is false.") + prefs.spec2_scroll = cf.sync_add("bool", "scroll-spectrogram", prefs.spec2_scroll) + prefs.custom_bg_opacity = cf.sync_add("int", "mascot-opacity", prefs.custom_bg_opacity) + if prefs.custom_bg_opacity < 0 or prefs.custom_bg_opacity > 100: + prefs.custom_bg_opacity = 40 + logging.warning("Invalid value for mascot-opacity") + prefs.sync_lyrics_time_offset = cf.sync_add( + "int", "synced-lyrics-time-offset", prefs.sync_lyrics_time_offset, + "In milliseconds. May be negative.") + prefs.artist_list_prefer_album_artist = cf.sync_add( + "bool", "artist-list-prefers-album-artist", + prefs.artist_list_prefer_album_artist, + "May require restart for change to take effect.") + prefs.meta_persists_stop = cf.sync_add( + "bool", "side-panel-info-persists", prefs.meta_persists_stop, + "Show album art and metadata of last played track when stopped.") + prefs.meta_shows_selected = cf.sync_add( + "bool", "side-panel-info-selected", prefs.meta_shows_selected, + "Show album art and metadata of selected track when stopped. (overides above setting)") + prefs.meta_shows_selected_always = cf.sync_add( + "bool", "side-panel-info-selected-always", + prefs.meta_shows_selected_always, + "Show album art and metadata of selected track at all times. (overides the above 2 settings)") + prefs.stop_notifications_mini_mode = cf.sync_add( + "bool", "mini-mode-avoid-notifications", + prefs.stop_notifications_mini_mode, + "Avoid sending track change notifications when in Mini Mode") + prefs.hide_queue = cf.sync_add("bool", "hide-queue-when-empty", prefs.hide_queue) + # prefs.show_playlist_list = cf.sync_add("bool", "show-playlist-list", prefs.show_playlist_list) -# 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() + prefs.show_current_on_transition = cf.sync_add( + "bool", "show-current-on-transition", + prefs.show_current_on_transition, + "Always jump to new playing track even with natural transition (broken setting, is always enabled") + prefs.art_in_top_panel = cf.sync_add( + "bool", "enable-art-header-bar", prefs.art_in_top_panel, + "Show art in top panel when window is narrow") + prefs.always_art_header = cf.sync_add( + "bool", "always-art-header-bar", prefs.always_art_header, + "Show art in top panel at any size. (Requires enable-art-header-bar)") + # prefs.center_bg = cf.sync_add("bool", "prefer-center-bg", prefs.center_bg, "Always center art for the background art function") + prefs.showcase_overlay_texture = cf.sync_add( + "bool", "showcase-texture-background", prefs.showcase_overlay_texture, + "Draw pattern over background art") + prefs.side_panel_layout = cf.sync_add("int", "side-panel-style", prefs.side_panel_layout, "0:default, 1:centered") + prefs.show_side_lyrics_art_panel = cf.sync_add("bool", "side-lyrics-art", prefs.show_side_lyrics_art_panel) + prefs.lyric_metadata_panel_top = cf.sync_add("bool", "side-lyrics-art-on-top", prefs.lyric_metadata_panel_top) + prefs.use_absolute_track_index = cf.sync_add( + "bool", "absolute-track-indices", prefs.use_absolute_track_index, + "For playlists with titles disabled only") + prefs.hide_bottom_title = cf.sync_add( + "bool", "auto-hide-bottom-title", prefs.hide_bottom_title, + "Hide title in bottom panel when already shown in side panel") + prefs.auto_goto_playing = cf.sync_add( + "bool", "auto-show-playing", prefs.auto_goto_playing, + "Show playing track in current playlist on track and playlist change even if not the playing playlist") -def level_meter_special_2(): - gui.level_meter_colour_mode = 2 + prefs.notify_include_album = cf.sync_add( + "bool", "notify-include-album", prefs.notify_include_album, + "Include album name in track change notifications") + prefs.rating_playtime_stars = cf.sync_add( + "bool", "show-rating-hint", prefs.rating_playtime_stars, + "Indicate playtime in rating stars") + prefs.drag_to_unpin = cf.sync_add( + "bool", "drag-tab-to-unpin", prefs.drag_to_unpin, + "Dragging a tab off the top-panel un-pins it") -theme_files = os.listdir(str(install_directory / "theme")) -theme_files.sort() + cf.br() + cf.add_text("[gallery]") + prefs.thin_gallery_borders = cf.sync_add("bool", "gallery-thin-borders", prefs.thin_gallery_borders) + prefs.increase_gallery_row_spacing = cf.sync_add("bool", "increase-row-spacing", prefs.increase_gallery_row_spacing) + prefs.center_gallery_text = cf.sync_add("bool", "gallery-center-text", prefs.center_gallery_text) + # show-current-on-transition", prefs.show_current_on_transition) + if bag.system != "windows": + cf.br() + cf.add_text("[fonts]") + cf.add_comment("Changes will require app restart.") + prefs.use_custom_fonts = cf.sync_add( + "bool", "use-custom-fonts", prefs.use_custom_fonts, + "Setting to false will reset below settings to default on restart") + if prefs.use_custom_fonts: + prefs.linux_font = cf.sync_add( + "string", "font-main-standard", prefs.linux_font, + "Suggested alternate: Liberation Sans") + prefs.linux_font_semibold = cf.sync_add("string", "font-main-medium", prefs.linux_font_semibold) + prefs.linux_font_bold = cf.sync_add("string", "font-main-bold", prefs.linux_font_bold) + prefs.linux_font_condensed = cf.sync_add("string", "font-main-condensed", prefs.linux_font_condensed) + prefs.linux_font_condensed_bold = cf.sync_add("string", "font-main-condensed-bold", prefs.linux_font_condensed_bold) -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() + cf.sync_add("string", "font-main-standard", prefs.linux_font, "Suggested alternate: Liberation Sans") + cf.sync_add("string", "font-main-medium", prefs.linux_font_semibold) + cf.sync_add("string", "font-main-bold", prefs.linux_font_bold) + cf.sync_add("string", "font-main-condensed", prefs.linux_font_condensed) + cf.sync_add("string", "font-main-condensed-bold", prefs.linux_font_condensed_bold) - global update_layout - update_layout = True - # gui.rspw = 80 + int(window_size[0] * 0.18) + # prefs.force_subpixel_text = cf.sync_add("bool", "force-subpixel-text", prefs.force_subpixel_text, "(Subpixel rendering defaults to off with Flatpak)") + cf.br() + cf.add_text("[tracklist]") + prefs.dd_index = cf.sync_add("bool", "double-digit-indices", prefs.dd_index) + prefs.column_aa_fallback_artist = cf.sync_add( + "bool", "column-album-artist-fallsback", + prefs.column_aa_fallback_artist, + "'Album artist' column shows 'artist' if otherwise blank.") + prefs.left_align_album_artist_title = cf.sync_add( + "bool", "left-aligned-album-artist-title", + prefs.left_align_album_artist_title, + "Show 'Album artist' in the folder/album title. Uses colour 'column-album-artist' from theme file") + prefs.auto_sort = cf.sync_add( + "bool", "import-auto-sort", prefs.auto_sort, + "This setting is deprecated and will be removed in a future version") -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") + cf.br() + cf.add_text("[transcode]") + prefs.bypass_transcode = cf.sync_add( + "bool", "sync-bypass-transcode", prefs.bypass_transcode, + "Don't transcode files with sync function") + prefs.smart_bypass = cf.sync_add("bool", "sync-bypass-low-bitrate", prefs.smart_bypass, + "Skip transcode of <=128kbs folders") + prefs.radio_record_codec = cf.sync_add("string", "radio-record-codec", prefs.radio_record_codec, + "Can be OPUS, OGG, FLAC, or MP3. Default: OPUS") - 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") + cf.br() + cf.add_text("[directories]") + cf.add_comment("Use full paths") + prefs.sync_target = cf.sync_add("string", "sync-device-music-dir", prefs.sync_target) + prefs.custom_encoder_output = cf.sync_add( + "string", "encode-output-dir", prefs.custom_encoder_output, + "E.g. \"/home/example/music/output\". If left blank, encode-output in home music dir will be used.") + if prefs.custom_encoder_output: + prefs.encoder_output = prefs.custom_encoder_output + prefs.download_dir1 = cf.sync_add( + "string", "add_download_directory", prefs.download_dir1, + "Add another folder to monitor in addition to home downloads and music.") + if prefs.download_dir1 and prefs.download_dir1 not in download_directories: + if os.path.isdir(prefs.download_dir1): + download_directories.append(prefs.download_dir1) + else: + logging.warning("Invalid download directory in config") - 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") + cf.br() + cf.add_text("[app]") + prefs.enable_remote = cf.sync_add( + "bool", "enable-remote-interface", prefs.enable_remote, + "For use with Tauon Music Remote for Android") + prefs.use_gamepad = cf.sync_add("bool", "use-gamepad", prefs.use_gamepad, "Use game controller for UI control, restart on change.") + prefs.use_tray = cf.sync_add("bool", "use-system-tray", prefs.use_tray) + prefs.force_hide_max_button = cf.sync_add("bool", "hide-maximize-button", prefs.force_hide_max_button) + prefs.save_window_position = cf.sync_add( + "bool", "restore-window-position", prefs.save_window_position, + "Save and restore the last window position on desktop on open") + prefs.mini_mode_on_top = cf.sync_add("bool", "mini-mode-always-on-top", prefs.mini_mode_on_top) + prefs.enable_mpris = cf.sync_add("bool", "enable-mpris", prefs.enable_mpris) + prefs.reload_play_state = cf.sync_add("bool", "resume-playback-on-restart", prefs.reload_play_state) + prefs.resume_play_wake = cf.sync_add("bool", "resume-playback-on-wake", prefs.resume_play_wake) + prefs.auto_dl_artist_data = cf.sync_add( + "bool", "auto-dl-artist-data", prefs.auto_dl_artist_data, + "Enable automatic downloading of thumbnails in artist list") + prefs.enable_fanart_cover = cf.sync_add("bool", "fanart.tv-cover", prefs.enable_fanart_cover) + prefs.enable_fanart_artist = cf.sync_add("bool", "fanart.tv-artist", prefs.enable_fanart_artist) + prefs.enable_fanart_bg = cf.sync_add("bool", "fanart.tv-background", prefs.enable_fanart_bg) + prefs.always_auto_update_playlists = cf.sync_add( + "bool", "auto-update-playlists", + prefs.always_auto_update_playlists, + "Automatically update generator playlists") + prefs.write_ratings = cf.sync_add( + "bool", "write-ratings-to-tag", prefs.write_ratings, + "This writes FMPS_Rating tags on disk. Only writing to MP3, OGG and FLAC files is currently supported.") + prefs.spot_mode = cf.sync_add("bool", "enable-spotify", prefs.spot_mode, "Enable Spotify specific features") + prefs.discord_enable = cf.sync_add( + "bool", "enable-discord-rpc", prefs.discord_enable, + "Show track info in running Discord application") + prefs.auto_lyrics = cf.sync_add( + "bool", "auto-search-lyrics", prefs.auto_lyrics, + "Automatically search internet for lyrics when display is wanted") - 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") + prefs.use_scancodes = cf.sync_add( + "bool", "shortcuts-ignore-keymap", prefs.use_scancodes, + "When enabled, shortcuts will map to the physical keyboard layout") + prefs.search_on_letter = cf.sync_add("bool", "alpha_key_activate_search", prefs.search_on_letter, + "When enabled, pressing single letter keyboard key will activate the global search") - ddt.rect((x, block_y, 369 * gui.scale, 140 * gui.scale), alpha_mod(colours.box_background, fade)) + cf.br() + cf.add_text("[tokens]") + temp = cf.sync_add( + "string", "discogs-personal-access-token", prefs.discogs_pat, + "Used for sourcing of artist thumbnails.") + if not temp: + prefs.discogs_pat = "" + elif len(temp) != 40: + logging.warning("Invalid discogs token in config") + else: + prefs.discogs_pat = temp - y = y0 + h0 - round(33 * gui.scale) - x = x0 + w0 - 0 * gui.scale + prefs.listenbrainz_url = cf.sync_add( + "string", "custom-listenbrainz-url", prefs.listenbrainz_url, + "Specify a custom Listenbrainz compatible api url. E.g. \"https://example.tld/apis/listenbrainz/\" Default: Blank") + prefs.lb_token = cf.sync_add("string", "listenbrainz-token", prefs.lb_token) - w = max(ddt.get_text_w(_("Credits"), 211), ddt.get_text_w(_("Next"), 211)) - x -= w + round(40 * gui.scale) + cf.br() + cf.add_text("[tauon_satellite]") + prefs.sat_url = cf.sync_add("string", "tau-url", prefs.sat_url, "Exclude the port") - 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() + cf.br() + cf.add_text("[lastfm]") + prefs.lastfm_pull_love = cf.sync_add( + "bool", "lastfm-pull-love", prefs.lastfm_pull_love, + "Overwrite local love status on scrobble") - def topchart(self, x0, y0, w0, h0): - x = x0 + round(25 * gui.scale) - y = y0 + 20 * gui.scale + cf.br() + cf.add_text("[maloja_account]") + prefs.maloja_url = cf.sync_add( + "string", "maloja-url", prefs.maloja_url, + "A Maloja server URL, e.g. http://localhost:32400") + prefs.maloja_key = cf.sync_add("string", "maloja-key", prefs.maloja_key, "One of your Maloja API keys") + prefs.maloja_enable = cf.sync_add("bool", "maloja-enable", prefs.maloja_enable) - ddt.text_background_colour = colours.box_background + cf.br() + cf.add_text("[plex_account]") + prefs.plex_username = cf.sync_add( + "string", "plex-username", prefs.plex_username, + "Probably the email address you used to make your PLEX account.") + prefs.plex_password = cf.sync_add( + "string", "plex-password", prefs.plex_password, + "The password associated with your PLEX account.") + prefs.plex_servername = cf.sync_add( + "string", "plex-servername", prefs.plex_servername, + "Probably your servers hostname.") - ddt.text((x, y), _("Chart Grid Generator"), colours.box_text, 214) + cf.br() + cf.add_text("[subsonic_account]") + prefs.subsonic_user = cf.sync_add("string", "subsonic-username", prefs.subsonic_user) + prefs.subsonic_password = cf.sync_add("string", "subsonic-password", prefs.subsonic_password) + prefs.subsonic_password_plain = cf.sync_add("bool", "subsonic-password-plain", prefs.subsonic_password_plain) + prefs.subsonic_server = cf.sync_add("string", "subsonic-server-url", prefs.subsonic_server) - 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 + cf.br() + cf.add_text("[koel_account]") + prefs.koel_username = cf.sync_add("string", "koel-username", prefs.koel_username, "E.g. admin@example.com") + prefs.koel_password = cf.sync_add("string", "koel-password", prefs.koel_password, "The default is admin") + prefs.koel_server_url = cf.sync_add( + "string", "koel-server-url", prefs.koel_server_url, + "The URL or IP:Port where the Koel server is hosted. E.g. http://localhost:8050 or https://localhost:8060") + prefs.koel_server_url = prefs.koel_server_url.rstrip("/") - y += 30 * gui.scale + cf.br() + cf.add_text("[jellyfin_account]") + prefs.jelly_username = cf.sync_add("string", "jelly-username", prefs.jelly_username, "") + prefs.jelly_password = cf.sync_add("string", "jelly-password", prefs.jelly_password, "") + prefs.jelly_server_url = cf.sync_add( + "string", "jelly-server-url", prefs.jelly_server_url, + "The IP:Port where the jellyfin server is hosted.") + prefs.jelly_server_url = prefs.jelly_server_url.rstrip("/") - 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) + cf.br() + cf.add_text("[network]") + prefs.network_stream_bitrate = cf.sync_add( + "int", "stream-bitrate", prefs.network_stream_bitrate, + "Optional bitrate koel/subsonic should transcode to (Server may need to be configured for this). Set to 0 to disable transcoding.") - 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 + cf.br() + cf.add_text("[listenalong]") + prefs.metadata_page_port = cf.sync_add( + "int", "broadcast-page-port", prefs.metadata_page_port, + "Change applies on app restart or setting re-enable") - else: + cf.br() + cf.add_text("[chart]") + prefs.chart_columns = cf.sync_add("int", "chart-columns", prefs.chart_columns) + prefs.chart_rows = cf.sync_add("int", "chart-rows", prefs.chart_rows) + prefs.chart_text = cf.sync_add("bool", "chart-uses-text", prefs.chart_text) + prefs.topchart_sorts_played = cf.sync_add("bool", "chart-sorts-top-played", prefs.topchart_sorts_played) + prefs.chart_font = cf.sync_add( + "string", "chart-font", prefs.chart_font, + "Format is fontname + size. Default is Monospace 10") - 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 +def auto_scale(bag: Bag) -> None: + prefs = bag.prefs + old = prefs.scale_want - y += 35 * gui.scale - x += 5 * gui.scale + if prefs.x_scale: + if bag.sdl_syswminfo.subsystem in (SDL_SYSWM_WAYLAND, SDL_SYSWM_COCOA, SDL_SYSWM_UNKNOWN): + prefs.scale_want = bag.window_size[0] / bag.logical_size[0] + if old != prefs.scale_want: + logging.info("Applying scale based on buffer size") + elif bag.sdl_syswminfo.subsystem == SDL_SYSWM_X11: + if bag.xdpi > 40: + prefs.scale_want = bag.xdpi / 96 + if old != prefs.scale_want: + logging.info("Applying scale based on xft setting") - 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 + prefs.scale_want = round(round(prefs.scale_want / 0.05) * 0.05, 2) - y -= 25 * gui.scale - x += 170 * gui.scale + if prefs.scale_want == 0.95: + prefs.scale_want = 1.0 + if prefs.scale_want == 1.05: + prefs.scale_want = 1.0 + if prefs.scale_want == 1.95: + prefs.scale_want = 2.0 + if prefs.scale_want == 2.05: + prefs.scale_want = 2.0 - 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")) + if old != prefs.scale_want: + logging.info(f"Using UI scale: {prefs.scale_want}") - x = x0 + 15 * gui.scale + 320 * gui.scale - y = y0 + 100 * gui.scale + if prefs.scale_want < 0.5: + prefs.scale_want = 1.0 - # . Limited width. Max 13 chars - if self.button(x, y, _("Randomise BG")): + if bag.window_size[0] < (560 * prefs.scale_want) * 0.9 or bag.window_size[1] < (330 * prefs.scale_want) * 0.9: + logging.info("Window overscale!") + show_message(_("Detected unsuitable UI scaling."), _("Scaling setting reset to 1x")) + prefs.scale_want = 1.0 - r = round(random.random() * 40) - g = round(random.random() * 40) - b = round(random.random() * 40) +def scale_assets(bag: Bag, scale_want: int, force: bool = False) -> None: + asset_directory = bag.dirs.asset_directory + scaled_asset_directory = bag.dirs.scaled_asset_directory + user_directory = bag.dirs.user_directory + svg_directory = bag.dirs.svg_directory + prefs = bag.prefs + if scale_want != 1: + scaled_asset_directory = user_directory / "scaled-icons" + if not scaled_asset_directory.exists() or len(os.listdir(str(svg_directory))) != len( + os.listdir(str(scaled_asset_directory))): + logging.info("Force rerender icons") + force = True + else: + scaled_asset_directory = asset_directory - prefs.chart_bg = [r, g, b] + if scale_want != prefs.ui_scale or force: + if scale_want != 1: + if scaled_asset_directory.is_dir() and scaled_asset_directory != asset_directory: + shutil.rmtree(str(scaled_asset_directory)) + from tauon.t_modules.t_svgout import render_icons - d = random.randrange(0, 4) + if scaled_asset_directory != asset_directory: + logging.info("Rendering icons...") + render_icons(str(svg_directory), str(scaled_asset_directory), scale_want) - if d == 1: - c = 5 + round(random.random() * 20) - prefs.chart_bg = [c, c, c] + logging.info("Done rendering icons") - x += 100 * gui.scale - y -= 20 * gui.scale + diff_ratio = scale_want / prefs.ui_scale + prefs.ui_scale = scale_want + prefs.playlist_row_height = round(22 * prefs.ui_scale) - display_colour = (prefs.chart_bg[0], prefs.chart_bg[1], prefs.chart_bg[2], 255) + # Save user values + column_backup = gui.pl_st + rspw = gui.pref_rspw + grspw = gui.pref_gallery_w - rect = (x, y, 70 * gui.scale, 70 * gui.scale) - ddt.rect(rect, display_colour) + gui.destroy_textures() + gui.rescale() - ddt.rect_s(rect, (50, 50, 50, 255), round(1 * gui.scale)) + # Scale saved values + gui.pl_st = column_backup + for item in gui.pl_st: + item[1] *= diff_ratio + gui.pref_rspw = rspw * diff_ratio + gui.pref_gallery_w = grspw * diff_ratio + bag.album_mode_art_size = int(bag.album_mode_art_size * diff_ratio) - # x = self.box_x + self.item_x_offset + 200 * gui.scale - # y = self.box_y + 180 * gui.scale +def get_global_mouse(): + i_y = pointer(c_int(0)) + i_x = pointer(c_int(0)) + SDL_GetGlobalMouseState(i_x, i_y) + return i_x.contents.value, i_y.contents.value - x = x0 + 260 * gui.scale - y = y0 + 180 * gui.scale +def get_window_position(): + i_y = pointer(c_int(0)) + i_x = pointer(c_int(0)) + SDL_GetWindowPosition(t_window, i_x, i_y) + return i_x.contents.value, i_y.contents.value - dex = reload_albums(quiet=True, return_playlist=pctl.active_playlist_viewing) +def use_id3(tags: ID3, nt: TrackClass): + def natural_get(tag: ID3, track: TrackClass, frame: str, attr: str) -> str | None: + frames = tag.getall(frame) + if frames and frames[0].text: + if track is None: + return str(frames[0].text[0]) + setattr(track, attr, str(frames[0].text[0])) + elif track is None: + return "" + else: + setattr(track, attr, "") - x = x0 + round(110 * gui.scale) - y = y0 + 240 * gui.scale + tag = tags - # . 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 + natural_get(tags, nt, "TIT2", "title") + natural_get(tags, nt, "TPE1", "artist") + natural_get(tags, nt, "TPE2", "album_artist") + natural_get(tags, nt, "TCON", "genre") # content type + natural_get(tags, nt, "TALB", "album") + natural_get(tags, nt, "TDRC", "date") + natural_get(tags, nt, "TCOM", "composer") + natural_get(tags, nt, "COMM", "comment") - x += round(95 * gui.scale) - if gui.generating_chart: - ddt.text((x, y + round(1 * gui.scale)), _("Generating..."), colours.box_text_label, 12) - else: + process_odat(nt, natural_get(tags, None, "TDOR", None)) - 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 + frames = tag.getall("POPM") + rating = 0 + if frames: + for frame in frames: + if frame.rating: + rating = frame.rating + nt.misc["POPM"] = frame.rating - line = _("{N} Album chart").format(N=str(count)) + if len(nt.comment) > 4 and nt.comment[2] == "+": + nt.comment = "" + if nt.comment[0:3] == "000": + nt.comment = "" - ww = ddt.text((x, y + round(1 * gui.scale)), line, colours.box_text_label, 12) + frames = tag.getall("USLT") + if frames: + nt.lyrics = frames[0].text + if 0 < len(nt.lyrics) < 150: + if "unavailable" in nt.lyrics or ".com" in nt.lyrics or "www." in nt.lyrics: + nt.lyrics = "" - 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) + frames = tag.getall("TPE1") + if frames: + d = [] + for frame in frames: + for t in frame.text: + d.append(t) + if len(d) > 1: + nt.misc["artists"] = d + nt.artist = "; ".join(d) - x = x0 + round(20 * gui.scale) - y = y0 + 240 * gui.scale + frames = tag.getall("TCON") + if frames: + d = [] + for frame in frames: + for t in frame.text: + d.append(t) + if len(d) > 1: + nt.misc["genres"] = d + nt.genre = " / ".join(d) - # . Limited width. Max 8 chars - if self.button(x, y, _("Return"), width=75 * gui.scale): - self.chart_view = 0 + track_no = natural_get(tags, None, "TRCK", None) + nt.track_total = "" + nt.track_number = "" + if track_no and track_no != "null": + if "/" in track_no: + a, b = track_no.split("/") + nt.track_number = a + nt.track_total = b + else: + nt.track_number = track_no - def stats(self, x0, y0, w0, h0): + disc = natural_get(tags, None, "TPOS", None) # set ? or ?/? + nt.disc_total = "" + nt.disc_number = "" + if disc: + if "/" in disc: + a, b = disc.split("/") + nt.disc_number = a + nt.disc_total = b + else: + nt.disc_number = disc - x = x0 + 10 * gui.scale - y = y0 + tx = tags.getall("UFID") + if tx: + for item in tx: + if item.owner == "http://musicbrainz.org": + nt.misc["musicbrainz_recordingid"] = item.data.decode() - if self.chart_view == 1: - self.topchart(x0, y0, w0, h0) - return + tx = tags.getall("TSOP") + if tx: + nt.misc["artist_sort"] = tx[0].text[0] - 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 + tx = tags.getall("TXXX") + if tx: + for item in tx: + if item.desc == "MusicBrainz Release Track Id": + nt.misc["musicbrainz_trackid"] = item.text[0] + if item.desc == "MusicBrainz Album Id": + nt.misc["musicbrainz_albumid"] = item.text[0] + if item.desc == "MusicBrainz Release Group Id": + nt.misc["musicbrainz_releasegroupid"] = item.text[0] + if item.desc == "MusicBrainz Artist Id": + artist_id_list: list[str] = [] + for uuid in item.text: + split_uuids = uuid.split("/") # UUIDs can be split by a special character + for split_uuid in split_uuids: + artist_id_list.append(split_uuid) + nt.misc["musicbrainz_artistids"] = artist_id_list - ddt.text_background_colour = colours.box_background - lt_font = 312 - lt_colour = colours.box_text_label + try: + desc = item.desc.lower() + if desc == "replaygain_track_gain": + nt.misc["replaygain_track_gain"] = float(item.text[0].strip(" dB")) + if desc == "replaygain_track_peak": + nt.misc["replaygain_track_peak"] = float(item.text[0]) + if desc == "replaygain_album_gain": + nt.misc["replaygain_album_gain"] = float(item.text[0].strip(" dB")) + if desc == "replaygain_album_peak": + nt.misc["replaygain_album_peak"] = float(item.text[0]) + except Exception: + logging.exception("Tag Scan: Read Replay Gain MP3 error") + logging.debug(nt.fullpath) - 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) + if item.desc == "FMPS_RATING": + nt.misc["FMPS_Rating"] = float(item.text[0]) - x1 = x + (8 + 10 + 10) * gui.scale - x2 = x1 + max(w1, w2, w3, w4, w5, w6) + 20 * gui.scale - y1 = y + 50 * gui.scale +def scan_ffprobe(nt: TrackClass): + startupinfo = None + if system == "Windows" or msys: + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + try: + result = subprocess.run( + [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format=duration", "-of", + "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) + nt.length = float(result.stdout.decode()) + except Exception: + logging.exception("FFPROBE couldn't supply a duration") + try: + result = subprocess.run( + [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format_tags=title", "-of", + "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) + nt.title = str(result.stdout.decode()) + except Exception: + logging.exception("FFPROBE couldn't supply a title") + try: + result = subprocess.run( + [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format_tags=artist", "-of", + "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) + nt.artist = str(result.stdout.decode()) + except Exception: + logging.exception("FFPROBE couldn't supply a artist") + try: + result = subprocess.run( + [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format_tags=album", "-of", + "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) + nt.album = str(result.stdout.decode()) + except Exception: + logging.exception("FFPROBE couldn't supply a album") + try: + result = subprocess.run( + [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format_tags=date", "-of", + "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) + nt.date = str(result.stdout.decode()) + except Exception: + logging.exception("FFPROBE couldn't supply a date") + try: + result = subprocess.run( + [tauon.get_ffprobe(), "-v", "error", "-show_entries", "format_tags=track", "-of", + "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) + nt.track_number = str(result.stdout.decode()) + except Exception: + logging.exception("FFPROBE couldn't supply a track") - 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() +def tag_scan(nt: TrackClass) -> TrackClass | None: + """This function takes a track object and scans metadata for it. (Filepath needs to be set)""" + if nt.is_embed_cue: + return nt + if nt.is_network or not nt.fullpath: + return None + try: + try: + nt.modified_time = os.path.getmtime(nt.fullpath) + nt.found = True + except FileNotFoundError: + logging.error("File not found when executing getmtime!") + nt.found = False + return nt + except Exception: + logging.exception("Unknown error executing getmtime!") + nt.found = False + return nt - album_names = set() - folder_names = set() - count = 0 + nt.misc.clear() - for track_id in default_playlist: - tr = pctl.get_track(track_id) + nt.file_ext = os.path.splitext(os.path.basename(nt.fullpath))[1][1:].upper() - 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) + if nt.file_ext.lower() in bag.formats.GME_Formats and gme: - self.stats_pl_albums = count + emu = ctypes.c_void_p() + track_info = ctypes.POINTER(GMETrackInfo)() + err = gme.gme_open_file(nt.fullpath.encode("utf-8"), ctypes.byref(emu), -1) + #logging.error(err) + if not err: + n = nt.subtrack + err = gme.gme_track_info(emu, byref(track_info), n) + #logging.error(err) + if not err: + nt.length = track_info.contents.play_length / 1000 + nt.title = track_info.contents.song.decode("utf-8") + nt.artist = track_info.contents.author.decode("utf-8") + nt.album = track_info.contents.game.decode("utf-8") + nt.comment = track_info.contents.comment.decode("utf-8") + gme.gme_free_info(track_info) + gme.gme_delete(emu) - self.stats_pl_length = 0 - for item in default_playlist: - self.stats_pl_length += pctl.master_library[item].length + filepath = nt.fullpath # this is the full file path + filename = nt.filename # this is the name of the file - line = seconds_to_day_hms(self.stats_pl_length, strings.day, strings.days) + # Get the directory of the file + dir_path = os.path.dirname(filepath) - 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) + # Loop through all files in the directory to find any matching M3U + for file in os.listdir(dir_path): + if file.endswith(".m3u"): + with open(os.path.join(dir_path, file), encoding="utf-8", errors="replace") as f: + content = f.read() + if "�" in content: # Check for replacement marker + with open(os.path.join(dir_path, file), encoding="windows-1252") as b: + content = b.read() + if "::" in content: + a, b = content.split("::") + if a == filename: + s = re.split(r"(?<!\\),", b) + try: + st = int(s[1]) + except Exception: + logging.exception("Failed to assign st to int") + continue + if st == n: + nt.title = s[2].split(" - ")[0].replace("\\", "") + nt.artist = s[2].split(" - ")[1].replace("\\", "") + nt.album = s[2].split(" - ")[2].replace("\\", "") + nt.length = hms_to_seconds(s[3]) + break + if not nt.title: + nt.title = "Track " + str(nt.subtrack + 1) - ddt.text((x2, y1), line, colours.box_sub_text, 12) + elif nt.file_ext in ("MOD", "IT", "XM", "S3M", "MPTM") and mpt: + with Path(nt.fullpath).open("rb") as file: + data = file.read() + MOD1 = MOD.from_address( + mpt.openmpt_module_create_from_memory( + ctypes.c_char_p(data), ctypes.c_size_t(len(data)), None, None, None)) + nt.length = mpt.openmpt_module_get_duration_seconds(byref(MOD1)) + nt.title = mpt.openmpt_module_get_metadata(byref(MOD1), ctypes.c_char_p(b"title")).decode() + nt.artist = mpt.openmpt_module_get_metadata(byref(MOD1), ctypes.c_char_p(b"artist")).decode() + nt.comment = mpt.openmpt_module_get_metadata(byref(MOD1), ctypes.c_char_p(b"message_raw")).decode() - if self.stats_timer.get() > 5: - album_names = set() - folder_names = set() - count = 0 + mpt.openmpt_module_destroy(byref(MOD1)) + del MOD1 - for pl in pctl.multi_playlist: - for track_id in pl.playlist_ids: - tr = pctl.get_track(track_id) + elif nt.file_ext == "FLAC": + with Flac(nt.fullpath) as audio: + audio.read() - 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) + nt.length = audio.length + nt.title = audio.title + nt.artist = audio.artist + nt.album = audio.album + nt.composer = audio.composer + nt.date = audio.date + nt.samplerate = audio.sample_rate + nt.bit_depth = audio.bit_depth + nt.size = os.path.getsize(nt.fullpath) + nt.track_number = audio.track_number + nt.genre = audio.genre + nt.album_artist = audio.album_artist + nt.disc_number = audio.disc_number + nt.lyrics = audio.lyrics + if nt.length: + nt.bitrate = int(nt.size / nt.length * 8 / 1024) + nt.track_total = audio.track_total + nt.disc_total = audio.disc_total + nt.comment = audio.comment + nt.cue_sheet = audio.cue_sheet + nt.misc = audio.misc - self.total_albums = count + elif nt.file_ext == "WAV": + with Wav(nt.fullpath) as audio: + try: + audio.read() - self.stats_timer.set() + nt.samplerate = audio.sample_rate + nt.length = audio.length + nt.title = audio.title + nt.artist = audio.artist + nt.album = audio.album + nt.track_number = audio.track_number - 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) + except Exception: + logging.exception("Failed saving WAV file as a Track, will try again differently") + audio = mutagen.File(nt.fullpath) + nt.samplerate = audio.info.sample_rate + nt.bitrate = audio.info.bitrate // 1000 + nt.length = audio.info.length + nt.size = os.path.getsize(nt.fullpath) + audio = mutagen.File(nt.fullpath) + if audio.tags and type(audio.tags) == mutagen.wave._WaveID3: + use_id3(audio.tags, nt) - 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) + elif nt.file_ext == "OPUS" or nt.file_ext == "OGG" or nt.file_ext == "OGA": - # Ratio bar - if len(pctl.master_library) > 115 * gui.scale: - x = x0 - y = y0 + h0 - 7 * gui.scale + #logging.info("get opus") + with Opus(nt.fullpath) as audio: + audio.read() - full_rect = [x, y, w0, 7 * gui.scale] - d = 0 + #logging.info(audio.title) - # 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 + nt.length = audio.length + nt.title = audio.title + nt.artist = audio.artist + nt.album = audio.album + nt.composer = audio.composer + nt.date = audio.date + nt.samplerate = audio.sample_rate + nt.size = os.path.getsize(nt.fullpath) + nt.track_number = audio.track_number + nt.genre = audio.genre + nt.album_artist = audio.album_artist + nt.bitrate = audio.bit_rate + nt.lyrics = audio.lyrics + nt.disc_number = audio.disc_number + nt.track_total = audio.track_total + nt.disc_total = audio.disc_total + nt.comment = audio.comment + nt.misc = audio.misc + if nt.bitrate == 0 and nt.length > 0: + nt.bitrate = int(nt.size / nt.length * 8 / 1024) - for key, value in self.ext_ratio.items(): + elif nt.file_ext == "APE": + with mutagen.File(nt.fullpath) as audio: + nt.length = audio.info.length + nt.bit_depth = audio.info.bits_per_sample + nt.samplerate = audio.info.sample_rate + nt.size = os.path.getsize(nt.fullpath) + if nt.length > 0: + nt.bitrate = int(nt.size / nt.length * 8 / 1024) - colour = [200, 200, 200, 255] - if key in format_colours: - colour = format_colours[key] + # # def getter(audio, key, type): + # # if + # t = audio.tags + # logging.info(t.keys()) + # nt.size = os.path.getsize(nt.fullpath) + # nt.title = str(t.get("title", "")) + # nt.album = str(t.get("album", "")) + # nt.date = str(t.get("year", "")) + # nt.disc_number = str(t.get("discnumber", "")) + # nt.comment = str(t.get("comment", "")) + # nt.artist = str(t.get("artist", "")) + # nt.composer = str(t.get("composer", "")) + # nt.composer = str(t.get("composer", "")) - 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] + with Ape(nt.fullpath) as audio: + audio.read() - h = int(round(value / len(pctl.master_library) * full_rect[2])) - block_rect = [full_rect[0] + d, full_rect[1], h, full_rect[3]] + # logging.info(audio.title) - ddt.rect(block_rect, colour) - d += h + # nt.length = audio.length + nt.title = audio.title + nt.artist = audio.artist + nt.album = audio.album + nt.date = audio.date + nt.composer = audio.composer + # nt.bit_depth = audio.bit_depth + nt.track_number = audio.track_number + nt.genre = audio.genre + nt.album_artist = audio.album_artist + nt.disc_number = audio.disc_number + nt.lyrics = audio.lyrics + nt.track_total = audio.track_total + nt.disc_total = audio.disc_total + nt.comment = audio.comment + nt.misc = audio.misc - 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) + elif nt.file_ext == "WV" or nt.file_ext == "TTA": - if self.click: - gen_codec_pl(key) - except Exception: - logging.exception("Error draw ext bar") + with Ape(nt.fullpath) as audio: + audio.read() - def config_v(self, x0, y0, w0, h0): + # logging.info(audio.title) - ddt.text_background_colour = colours.box_background + nt.length = audio.length + nt.title = audio.title + nt.artist = audio.artist + nt.album = audio.album + nt.date = audio.date + nt.composer = audio.composer + nt.samplerate = audio.sample_rate + nt.bit_depth = audio.bit_depth + nt.size = os.path.getsize(nt.fullpath) + nt.track_number = audio.track_number + nt.genre = audio.genre + nt.album_artist = audio.album_artist + nt.disc_number = audio.disc_number + nt.lyrics = audio.lyrics + if nt.length > 0: + nt.bitrate = int(nt.size / nt.length * 8 / 1024) + nt.track_total = audio.track_total + nt.disc_total = audio.disc_total + nt.comment = audio.comment + nt.misc = audio.misc - x = x0 + self.item_x_offset - y = y0 + 17 * gui.scale + else: + # Use MUTAGEN + try: + if nt.file_ext.lower() in bag.formats.VID_Formats: + scan_ffprobe(nt) + return nt - 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) + try: + audio = mutagen.File(nt.fullpath) + except Exception: + logging.exception("Mutagen scan failed, falling back to FFPROBE") + scan_ffprobe(nt) + return nt - 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) + nt.samplerate = audio.info.sample_rate + nt.bitrate = audio.info.bitrate // 1000 + nt.length = audio.info.length + nt.size = os.path.getsize(nt.fullpath) - 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) + if not nt.length: + try: + startupinfo = None + if system == "Windows" or msys: + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + result = subprocess.run([tauon.get_ffprobe(), "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", nt.fullpath], stdout=subprocess.PIPE, startupinfo=startupinfo, check=True) + nt.length = float(result.stdout.decode()) + except Exception: + logging.exception("FFPROBE couldn't supply a duration") - 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) + if type(audio.tags) == mutagen.mp4.MP4Tags: + tags = audio.tags - x = x0 + self.item_x_offset + def in_get(key, tags): + if key in tags: + return tags[key][0] + return "" - # y += round(25 * gui.scale) + nt.title = in_get("\xa9nam", tags) + nt.album = in_get("\xa9alb", tags) + nt.artist = in_get("\xa9ART", tags) + nt.album_artist = in_get("aART", tags) + nt.composer = in_get("\xa9wrt", tags) + nt.date = in_get("\xa9day", tags) + nt.comment = in_get("\xa9cmt", tags) + nt.genre = in_get("\xa9gen", tags) + if "\xa9lyr" in tags: + nt.lyrics = in_get("\xa9lyr", tags) + nt.track_total = "" + nt.track_number = "" + t = in_get("trkn", tags) + if t: + nt.track_number = str(t[0]) + if t[1]: + nt.track_total = str(t[1]) - # self.toggle_square(x, y, star_line_toggle, _('Show playtime lines')) - y += round(15 * gui.scale) + nt.disc_total = "" + nt.disc_number = "" + t = in_get("disk", tags) + if t: + nt.disc_number = str(t[0]) + if t[1]: + nt.disc_total = str(t[1]) - # 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) + if "----:com.apple.iTunes:MusicBrainz Track Id" in tags: + nt.misc["musicbrainz_recordingid"] = in_get( + "----:com.apple.iTunes:MusicBrainz Track Id", + tags).decode() + if "----:com.apple.iTunes:MusicBrainz Release Track Id" in tags: + nt.misc["musicbrainz_trackid"] = in_get( + "----:com.apple.iTunes:MusicBrainz Release Track Id", + tags).decode() + if "----:com.apple.iTunes:MusicBrainz Album Id" in tags: + nt.misc["musicbrainz_albumid"] = in_get( + "----:com.apple.iTunes:MusicBrainz Album Id", + tags).decode() + if "----:com.apple.iTunes:MusicBrainz Release Group Id" in tags: + nt.misc["musicbrainz_releasegroupid"] = in_get( + "----:com.apple.iTunes:MusicBrainz Release Group Id", + tags).decode() + if "----:com.apple.iTunes:MusicBrainz Artist Id" in tags: + nt.misc["musicbrainz_artistids"] = [x.decode() for x in + tags.get("----:com.apple.iTunes:MusicBrainz Artist Id")] - y += round(25 * gui.scale) + elif type(audio.tags) == mutagen.id3.ID3: + use_id3(audio.tags, nt) - 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) + except Exception: + logging.exception("Failed loading file through Mutagen") + raise - 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) + # Parse any multiple artists into list + artists = nt.artist.split(";") + if len(artists) > 1: + for a in artists: + a = a.strip() + if a: + if "artists" not in nt.misc: + nt.misc["artists"] = [] + if a not in nt.misc["artists"]: + nt.misc["artists"].append(a) - 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 + except Exception: + try: + if Exception is UnicodeDecodeError: + logging.exception("Unicode decode error on file:", nt.fullpath, "\n") + else: + logging.exception("Error: Tag read failed on file:", nt.fullpath, "\n") + except Exception: + logging.exception("Error printing error. Non utf8 not allowed:", nt.fullpath.encode("utf-8", "surrogateescape").decode("utf-8", "replace"), "\n") + return nt - 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 + return nt - 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 get_radio_art() -> None: + if radiobox.loaded_url in radiobox.websocket_source_urls: + return + if "ggdrasil" in radiobox.playing_title: + time.sleep(3) + url = "https://yggdrasilradio.net/data.php?" + response = requests.get(url, timeout=10) + if response.status_code == 200: + lines = response.content.decode().split("|") + if len(lines) > 11 and lines[11]: + art_id = lines[11].strip().strip("*") + art_url = "https://yggdrasilradio.net/images/albumart/" + art_id + art_response = requests.get(art_url, timeout=10) + 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" + pctl.update_tag_history() + elif "gensokyoradio.net" in radiobox.loaded_url: - 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 + response = requests.get("https://gensokyoradio.net/api/station/playing/", timeout=10) - 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 + if response.status_code == 200: + d = json.loads(response.text) + song_info = d.get("SONGINFO") + if song_info: + radiobox.dummy_track.artist = song_info.get("ARTIST", "") + radiobox.dummy_track.title = song_info.get("TITLE", "") + radiobox.dummy_track.album = song_info.get("ALBUM", "") - def set_playlist_stop(self, mode=0): - if mode == 1: - return True if prefs.end_setting == "stop" else False - prefs.end_setting = "stop" + misc = d.get("MISC") + if misc: + art = misc.get("ALBUMART") + if art: + art_url = "https://gensokyoradio.net/images/albums/500/" + art + art_response = requests.get(art_url, timeout=10) + 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" + pctl.update_tag_history() - def set_playlist_repeat(self, mode=0): - if mode == 1: - return True if prefs.end_setting == "repeat" else False - prefs.end_setting = "repeat" + elif "radio.plaza.one" in radiobox.loaded_url: + time.sleep(3) + logging.info("Fetching plaza art") + response = requests.get("https://api.plaza.one/status", timeout=10) + if response.status_code == 200: + d = json.loads(response.text) + if "song" in d: + tr = d["song"]["length"] - d["song"]["position"] + tr += 1 + tr = max(tr, 10) + pctl.radio_poll_timer.force_set(tr * -1) - def small_preset(self): + if "artist" in d["song"]: + radiobox.dummy_track.artist = d["song"]["artist"] + if "title" in d["song"]: + radiobox.dummy_track.title = d["song"]["title"] + if "album" in d["song"]: + radiobox.dummy_track.album = d["song"]["album"] + if "artwork_src" in d["song"]: + art_url = d["song"]["artwork_src"] + art_response = requests.get(art_url, timeout=10) + 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" + pctl.update_tag_history() - prefs.playlist_row_height = round(22 * prefs.ui_scale) - prefs.playlist_font_size = 15 - prefs.tracklist_y_text_offset = 0 - gui.update_layout() + # Failure + elif pctl.radio_image_bin: + pctl.radio_image_bin.close() + pctl.radio_image_bin = None - def large_preset(self): + gui.clear_image_cache_next += 1 - prefs.playlist_row_height = round(27 * prefs.ui_scale) - prefs.playlist_font_size = 15 - gui.update_layout() +def auto_name_pl(target_pl: int) -> None: + if not pctl.multi_playlist[target_pl].playlist_ids: + return - def slide_control(self, x, y, label, units, value, lower_limit, upper_limit, step=1, callback=None, width=58): + albums = [] + artists = [] + parents = [] - width = round(width * gui.scale) + track = None - 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): + 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.click: - if value > lower_limit: - value -= step - gui.update_layout() - if callback is not None: - callback(value) + nt = "" + artist = "" - if mouse_down: - abg = [230, 120, 20, 255] - else: - abg = [220, 150, 20, 255] + if track: + artist = track.artist + if track.album_artist: + artist = track.album_artist - if colour_value(colours.box_background) > 300: - abg = colours.box_sub_text + if track and albums and albums[0] and albums.count(albums[0]) == len(albums): + nt = artist + " - " + track.album - dec_arrow.render(x + 1 * gui.scale, y, abg) + elif track and artists and artists[0] and artists.count(artists[0]) == len(artists): + nt = artists[0] - x += 33 * gui.scale + else: + nt = os.path.basename(commonprefix(parents)) - 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) + pctl.multi_playlist[target_pl].title = nt - x += width +def get_object(index: int) -> TrackClass: + return pctl.master_library[index] - 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): +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 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] +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]) - if colour_value(colours.box_background) > 300: - abg = colours.box_sub_text +def g_open_encode_out(a, b, c) -> None: + open_encode_out() - inc_arrow.render(x + 1 * gui.scale, y, abg) +def notify_song_fire(notification, delay, id) -> None: + time.sleep(delay) + notification.show() + if id is None: + return - return value + time.sleep(8) + if id == gui.notify_main_id: + notification.close() - # def style_up(self): - # prefs.line_style += 1 - # if prefs.line_style > 5: - # prefs.line_style = 1 +def notify_song(notify_of_end: bool = False, delay: float = 0.0) -> None: + if not de_notify_support: + return - def inside(self): + if notify_of_end and prefs.end_setting != "stop": + return - return coll((self.box_x, self.box_y, self.w, self.h)) + 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 - def init2(self): + track = pctl.playing_object() - self.init2done = True + 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 - def close(self): - self.enabled = False - fader.fall() - if gui.opened_config_file: - reload_config_file() + 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") - def render(self): + top_line = track.title - if self.init2done is False: - self.init2() + if prefs.notify_include_album: + bottom_line = (track.artist + " | " + track.album).strip("| ") + else: + bottom_line = track.artist - if key_esc_press: - self.close() + if not track.title: + a, t = filename_to_metadata(clean_string(track.filename)) + if not track.artist: + bottom_line = a + top_line = t - tab_width = 115 * gui.scale + gui.notify_main_id = uid_gen() + id = gui.notify_main_id - side_width = 115 * gui.scale - header_width = 0 + if notify_of_end: + bottom_line = "Tauon Music Box" + top_line = (_("End of playlist")) + id = None - 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 + song_notification.update(top_line, bottom_line, i_path) - content_width = round(545 * gui.scale) - content_height = round(275 * gui.scale) # 275 - full_width = content_width - full_height = content_height + shoot_dl = threading.Thread(target=notify_song_fire, args=([song_notification, delay, id])) + shoot_dl.daemon = True + shoot_dl.start() - full_width += side_width - full_height += header_width +def get_backend_time(path): + pctl.time_to_get = path - x = int(window_size[0] / 2) - int(full_width / 2) - y = int(window_size[1] / 2) - int(full_height / 2) + pctl.playerCommand = "time" + pctl.playerCommandReady = True - self.box_x = x - self.box_y = y - self.w = full_width - self.h = full_height + while pctl.playerCommand != "done": + time.sleep(0.005) - border_colour = colours.box_border + return pctl.time_to_get - 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) +def get_love(track_object: TrackClass) -> bool: + star = star_store.full_get(track_object.index) + if star is None: + return False - current_tab = 0 - tab_height = round(24 * gui.scale) # 30 + if "L" in star[1]: + return True + return False - 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) +def get_love_index(index: int) -> bool: + star = star_store.full_get(index) + if star is None: + return False - if top_mode: + if "L" in star[1]: + return True + return False - xx = x - yy = y - tab_width = 90 * gui.scale +def get_love_timestamp_index(index: int): + star = star_store.full_get(index) + if star is None: + return 0 + return star[3] - ddt.rect_a((x, y), (full_width, header_width), tab_bg) +def love(set=True, track_id=None, no_delay=False, notify=False, sync=True): + if len(pctl.track_queue) < 1: + return False - for item in self.tabs: + if track_id is not None and track_id < 0: + return False - if self.click and gui.message_box: - gui.message_box = False + if track_id is None: + track_id = pctl.track_queue[pctl.queue_step] - box = [xx, yy, tab_width, tab_height] - box2 = [xx, yy, tab_width, tab_height - 1] - fields.add(box2) + loved = False + star = star_store.full_get(track_id) - if self.click and coll(box2): - self.tab_active = current_tab - self.lyrics_panel = False + if star is not None: + if "L" in star[1]: + loved = True - 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 set is False: + return loved - if coll(box2): - ddt.rect(box, tab_over) + # 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 - alpha = 100 - if current_tab == self.tab_active: - alpha = 240 + if star is None: + star = star_store.new_object() - ddt.text((xx + (tab_width // 2), yy + 4 * gui.scale, 2), item[0], tab_text, 212) + loved ^= True - current_tab += 1 - xx += tab_width - if current_tab == 6: - yy += round(24 * gui.scale) # 30 - xx = x + 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) - else: + delay = 0.3 + if no_delay or not sync or not lastfm.details_ready(): + delay = 0 - ddt.rect_a((x, y), (tab_width, full_height), tab_bg) + star[3] = time.time() - for item in self.tabs: + 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.click and gui.message_box: - if not coll(message_box.get_rect()): - gui.message_box = False - else: - inp.mouse_click = True - self.click = False + if pctl.master_library[track_id].file_ext == "JELY": + jellyfin.favorite(pctl.master_library[track_id]) - 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) + 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) - if self.click and coll(box2): - self.tab_active = current_tab - self.lyrics_panel = False + gui.pl_update = 2 + gui.update += 1 + if sync and pctl.mpris is not None: + pctl.mpris.update(force=True) - 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) +def maloja_get_scrobble_counts(): + if lastfm.scanning_scrobbles is True or not prefs.maloja_url: + return - if coll(box2): - ddt.rect(box, tab_over) + 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) - yy = box[1] + 4 * gui.scale + 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 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) + try: + data = json.loads(r.text) + l = data["list"] - current_tab += 1 + counts = {} - # ddt.line(x + 110, self.box_y + 1, self.box_x + 110, self.box_y + self.h, colours.grey(50)) + 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 - self.tabs[self.tab_active][1](x + side_width, y + header_width, content_width, content_height) + touched = [] - self.click = False - self.right_click = False + 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") - ddt.text_background_colour = colours.box_background + except Exception: + logging.exception("There was an error parsing the data") + show_message(_("There was an error parsing the data"), mode="warning") + gui.pl_update += 1 + lastfm.scanning_scrobbles = False + tauon.bg_save() -class Fields: - def __init__(self): +def maloja_scrobble(track: TrackClass, timestamp: int = int(time.time())) -> bool | None: + url = prefs.maloja_url - self.id = [] - self.last_id = [] + if not track.artist or not track.title: + return None - self.field_array = [] - self.force = False + if not url.endswith("/newscrobble"): + if not url.endswith("/"): + url += "/" + url += "apis/mlj_1/newscrobble" - def add(self, rect, callback=None): + d = {} + d["artists"] = [track.artist] # let Maloja parse/fix artists + d["title"] = track.title - self.field_array.append((rect, callback)) + if track.album: + d["album"] = track.album + if track.album_artist: + d["albumartists"] = [track.album_artist] # let Maloja parse/fix artists - def test(self): + d["length"] = int(track.length) + d["time"] = timestamp + d["key"] = prefs.maloja_key - if self.force: - self.force = False - return True + 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 - self.last_id = self.id - #logging.info(len(self.id)) - self.id = [] +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] - 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" +def encode_folder_name(track_object: TrackClass) -> str: + folder_name = track_object.artist + " - " + track_object.album - if self.last_id == self.id: - return False + if folder_name == " - ": + folder_name = track_object.parent_folder_name - return True + folder_name = filename_safe(folder_name).strip() - def clear(self): + if not folder_name: + folder_name = str(track_object.index) - self.field_array = [] + 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) + return folder_name -fields = Fields() +def signal_handler(signum, frame): + signal.signal(signum, signal.SIG_IGN) # ignore additional signals + tauon.exit(reason="SIGINT recieved") +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 -def update_playlist_call(): - gui.update + 2 - gui.pl_update = 2 + 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() -pref_box = Over() +def jellyfin_get_library_thread() -> None: + pref_box.close() + save_prefs(bag=bag, cf=cf) + if jellyfin.scanning: + inp.mouse_click = False + show_message(_("Job already in progress!")) + return -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) + jellyfin.scanning = True + shoot_dl = threading.Thread(target=jellyfin.ingest_library) + shoot_dl.daemon = True + shoot_dl.start() +def plex_get_album_thread() -> None: + pref_box.close() + save_prefs(bag=bag, cf=cf) + if plex.scanning: + inp.mouse_click = False + show_message(_("Already scanning!")) + return + plex.scanning = 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]) + shoot_dl = threading.Thread(target=plex.get_albums) + shoot_dl.daemon = True + shoot_dl.start() -def clear_gen(id: int) -> None: - del pctl.gen_codes[id] - show_message(_("Okay, it's a normal playlist now."), mode="done") +def sub_get_album_thread() -> None: + # if prefs.backend != 1: + # show_message("This feature is currently only available with the BASS backend") + # return -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, ""): + pref_box.close() + save_prefs(bag=bag, cf=cf) + if subsonic.scanning: + inp.mouse_click = False + show_message(_("Already scanning!")) return - if "tmix\"" in pctl.gen_codes.get(id, ""): + subsonic.scanning = True + + shoot_dl = threading.Thread(target=subsonic.get_music3) + shoot_dl.daemon = True + shoot_dl.start() + +def koel_get_album_thread() -> None: + # if prefs.backend != 1: + # show_message("This feature is currently only available with the BASS backend") + # return + + pref_box.close() + save_prefs(bag=bag, cf=cf) + if koel.scanning: + inp.mouse_click = False + show_message(_("Already scanning!")) 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") + koel.scanning = True + shoot_dl = threading.Thread(target=koel.get_albums) + shoot_dl.daemon = True + shoot_dl.start() -class TopPanel: - def __init__(self): +def do_exit_button() -> None: + if inp.mouse_up or ab_click: + if gui.tray_active and prefs.min_to_tray: + if inp.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") - self.height = gui.panelY - self.ty = 0 +def do_maximize_button() -> None: + 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) - self.start_space_left = round(46 * gui.scale) - self.start_space_compact_left = 46 * gui.scale + inp.mouse_down = False + inp.mouse_click = False + inp.drag_mode = False - 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 +def do_minimize_button(): + if macos: + # hack + SDL_SetWindowBordered(t_window, True) + SDL_MinimizeWindow(t_window) + SDL_SetWindowBordered(t_window, False) + else: + SDL_MinimizeWindow(t_window) - self.ini_menu_space = 17 * gui.scale # 17 - self.menu_space = 17 * gui.scale - self.click_buffer = 4 * gui.scale + inp.mouse_down = False + inp.mouse_click = False + inp.drag_mode = False - self.tabs_right_x = 0 # computed for drag and drop code elsewhere (hacky) - self.tabs_left_x = 1 +def draw_window_tools(tauon: Tauon) -> None: + bag = tauon.bag + gui = tauon.gui + colours = tauon.bag.colours + window_size = tauon.bag.window_size + ddt = tauon.bag.ddt + prefs = tauon.prefs - self.prime_tab = gui.saved_prime_tab - self.prime_side = gui.saved_prime_direction # 0=left, 1=right - self.shown_tabs = [] + # rect = (window_size[0] - 55 * gui.scale, window_size[1] - 35 * gui.scale, 53 * gui.scale, 33 * gui.scale) + # tauon.fields.add(rect) + # prefs.left_window_control = not inp.key_shift_down + macstyle = gui.macstyle - # --- - 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) + 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 - self.drag_slide_timer = Timer(100) - self.tab_d_click_timer = Timer(10) - self.tab_d_click_ref = None + h = round(28 * gui.scale) + y = round(1 * gui.scale) + if macstyle: + y = round(9 * gui.scale) - self.adds = [] + 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 - def left_overflow_switch_playlist(self, pl): - self.prime_side = 0 - self.prime_tab = pl - switch_playlist(pl) + xx = 0 + l = prefs.left_window_control + r = not l + focused = window_is_focused(tauon.t_window) - def right_overflow_switch_playlist(self, pl): - self.prime_side = 1 - self.prime_tab = pl - switch_playlist(pl) + # Close + if r: + xx = window_size[0] - x_width + xx -= round(2 * gui.scale) - def render(self): + 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) + tauon.fields.add(rect) + colour = mac_close + if not focused: + colour = (86, 85, 86, 255) + mac_circle.render(xx + 6 * gui.scale, y, colour) + if tauon.coll(rect) and not gui.mouse_unknown: + if coll_point(inp.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) + tauon.fields.add(rect) + if tauon.coll(rect) and not gui.mouse_unknown: + ddt.rect((rect[0], rect[1], rect[2], rect[3]), bg_on) + tauon.top_panel.exit_button.render(rect[0] + 8 * gui.scale, rect[1] + 8 * gui.scale, x_on) + if coll_point(inp.last_click_location, rect): + do_exit_button() + else: + tauon.top_panel.exit_button.render(rect[0] + 8 * gui.scale, rect[1] + 8 * gui.scale, x_off) - # C-TD - global quick_drag - global update_layout + # 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) - hh = gui.panelY2 - yy = gui.panelY - hh - self.height = hh + tauon.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 tauon.coll(rect) and not gui.mouse_unknown: + if (inp.mouse_up or ab_click) and coll_point(inp.last_click_location, rect): + restore_full_mode() + gui.update += 2 - if quick_drag is True: - # gui.pl_update = 1 - gui.update_on_drag = True + # maximize - # Draw the background - ddt.rect((0, 0, window_size[0], gui.panelY), colours.top_panel_background) + if bag.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.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) + tauon.fields.add(rect) + colour = mac_maximize + if not focused: + colour = (86, 85, 86, 255) + mac_circle.render(xx + 6 * gui.scale, y, colour) + if tauon.coll(rect) and not gui.mouse_unknown: + if (inp.mouse_up or ab_click) and coll_point(inp.last_click_location, rect): + do_minimize_button() - 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 + 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) + tauon.fields.add(rect) + if tauon.coll(rect): + ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_on) + tauon.top_panel.maximize_button.render(rect[0] + 10 * gui.scale, rect[1] + 10 * gui.scale, fg_on) + if (inp.mouse_up or ab_click) and coll_point(inp.last_click_location, rect): + do_maximize_button() + else: + tauon.top_panel.maximize_button.render(rect[0] + 10 * gui.scale, rect[1] + 10 * gui.scale, fg_off) - if pctl.playing_state == 3 and not radiobox.dummy_track.title: - title = pctl.tag_meta - artist = radiobox.loaded_url # pctl.url + # minimize - ddt.text_background_colour = colours.top_panel_background + if bag.draw_min_button: + # 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) - 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) + tauon.fields.add(rect) + colour = mac_minimize + if not focused: + colour = (86, 85, 86, 255) + mac_circle.render(xx + 6 * gui.scale, y, colour) + if tauon.coll(rect) and not gui.mouse_unknown: + if (inp.mouse_up or ab_click) and coll_point(inp.last_click_location, rect): + do_maximize_button() + else: + if r: + xx -= mi_width + if l: + xx += last_width - 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) + rect = (xx, y, mi_width, h) + last_width = mi_width + ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_off) + tauon.fields.add(rect) + if tauon.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 (inp.mouse_up or ab_click) and coll_point(inp.last_click_location, rect): + do_minimize_button() 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) + ddt.rect_a( + (rect[0] + 11 * gui.scale, rect[1] + 16 * gui.scale), (14 * gui.scale, 3 * gui.scale), fg_off) - rect = (wwx + 9 * gui.scale, yy + 4 * gui.scale, 34 * gui.scale, 25 * gui.scale) - fields.add(rect) + # restore - if coll(rect) and not prefs.shuffle_lock: - if inp.mouse_click: + if gui.mode == 3: - if gui.combo_mode: - gui.switch_showcase_off = True - else: - gui.lsp ^= True + # 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 - update_layout = True - gui.update += 1 - if mouse_down and quick_drag: - gui.lsp = True - update_layout = True - gui.update += 1 + rect = (xx, y, re_width, h) + ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_off) + tauon.fields.add(rect) + if tauon.coll(rect): + ddt.rect_a((rect[0], rect[1]), (rect[2], rect[3]), bg_on) + tauon.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(inp.click_location, rect): + restore_full_mode() + gui.update += 2 + else: + tauon.top_panel.restore_button.render(rect[0] + 8 * gui.scale, rect[1] + 9 * gui.scale, fg_off) - if middle_click: - toggle_left_last() - update_layout = True - gui.update += 1 +def draw_window_border(): + corner_icon.render(window_size[0] - corner_icon.w, window_size[1] - corner_icon.h, colours.corner_icon) - if right_click: - # prefs.artist_list ^= True - lsp_menu.activate(position=(5 * gui.scale, gui.panelY)) - update_layout_do() + corner_rect = (window_size[0] - 20 * gui.scale, window_size[1] - 20 * gui.scale, 20, 20) + tauon.fields.add(corner_rect) - colour = colours.corner_button # [230, 230, 230, 255] + right_rect = (window_size[0] - 3 * gui.scale, 20 * gui.scale, 10, window_size[1] - 40 * gui.scale) + tauon.fields.add(right_rect) - if gui.lsp: - colour = colours.corner_button_active - if gui.combo_mode: - colour = colours.corner_button - if coll(rect): - colour = colours.corner_button_active + # top_rect = (20 * gui.scale, 0, window_size[0] - 40 * gui.scale, 2 * gui.scale) + # tauon.fields.add(top_rect) - 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) + left_rect = (0, 10 * gui.scale, 4 * gui.scale, window_size[1] - 50 * gui.scale) + tauon.fields.add(left_rect) - # 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) + bottom_rect = (20 * gui.scale, window_size[1] - 4, window_size[0] - 40 * gui.scale, 7 * gui.scale) + tauon.fields.add(bottom_rect) - if playlist_box.drag: - drag_mode = False + if tauon.coll(corner_rect): + gui.cursor_want = 4 + elif tauon.coll(right_rect): + gui.cursor_want = 8 + # elif tauon.coll(top_rect): + # gui.cursor_want = 9 + elif tauon.coll(left_rect): + gui.cursor_want = 10 + elif tauon.coll(bottom_rect): + gui.cursor_want = 11 - # Need to test length - self.tab_text_spaces = [] + colour = colours.window_frame - 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) + 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) - x = self.start_space_left + wwx - y = yy # self.ty +def bass_player_thread(player): + # logging.basicConfig(filename=user_directory + '/crash.log', level=logging.ERROR, + # format='%(asctime)s %(levelname)s %(name)s %(message)s') - # 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 + 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 - p_text_len = 180 * gui.scale - right_space_es = p_text_len + offset +def coll_point(l: list[int], r: list[int]) -> bool: + # rect point collision detection + return r[0] < l[0] <= r[0] + r[2] and r[1] <= l[1] <= r[1] + r[3] - x_start = x +def prime_fonts(bag: Bag) -> None: + standard_font = bag.prefs.linux_font + ddt = bag.ddt + # 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) - 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 + ddt.prime_font(standard_font, 9, 412) + ddt.prime_font(standard_font, 10, 413) - # List all tabs eligible to be shown - #logging.info("-------------") - ready_tabs = [] - show_tabs = [] + standard_font = bag.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" - 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)) + 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) - left_tabs = [] - right_tabs = [] - if prefs.shuffle_lock: - for p in ready_tabs: - left_tabs.append(p) + standard_font = bag.prefs.linux_font_bold + # if msys: + # standard_font = prefs.linux_font_bold + ", Noto Sans, Sans Bold" - else: - for p in ready_tabs: - if p < self.prime_tab: - left_tabs.append(p) + 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) - for p in ready_tabs: - if p > self.prime_tab: - right_tabs.append(p) - left_tabs.reverse() + standard_font = bag.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) - run = max_w + standard_font = bag.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) - 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 +def find_synced_lyric_data(track: TrackClass) -> list[str] | None: + if track.is_network: + return None - 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 + direc = track.parent_folder_path + name = os.path.splitext(track.filename)[0] + ".lrc" - # 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 len(track.lyrics) > 20 and track.lyrics[0] == "[" and ":" in track.lyrics[:20] and "." in track.lyrics[:20]: + return track.lyrics.splitlines() - 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) + 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 - x += 17 * gui.scale - x_start = x + return data - 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])) +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) - xx = x + (max_w - run) # + round(6 * gui.scale) - self.tabs_left_x = x_start +def draw_internel_link(x, y, text, colour, font): + tweak = font + while tweak > 100: + tweak -= 100 - 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.scale == 2: + tweak *= 2 + tweak += 4 + if gui.scale == 1.25: + tweak = round(tweak * 1.25) + tweak += 1 - 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 + sp = ddt.text((x, y), text, colour, font) - 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() + rect = [x - 5 * gui.scale, y - 2 * gui.scale, sp + 11 * gui.scale, 23 * gui.scale] + tauon.fields.add(rect) - # TAB INPUT PROCESSING - target = pctl.multi_playlist - if gui.radio_view: - target = pctl.radio_playlists - for i, tab in enumerate(target): + if tauon.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 not gui.radio_view: - if not prefs.tabs_on_top or prefs.shuffle_lock: - break +def draw_linked_text(location, text, colour, font, force=False, replace=""): + base = "" + link_text = "" + rest = "" + on_base = True - if len(pctl.multi_playlist) != len(self.tab_text_spaces): - break + 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 i not in show_tabs: - continue + target_link = link_text + if replace: + link_text = replace - # Determine the tab width - tab_width = self.tab_text_spaces[i] + self.tab_extra_width + left = ddt.get_text_w(base, font) + right = ddt.get_text_w(base + link_text, font) - # Save the far right boundary of the tabs (hacky) - self.tabs_right_x = x + tab_width + x = location[0] + y = location[1] - # 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) + 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) - # 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: + tweak = font + while tweak > 100: + tweak -= 100 - 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) + if gui.scale == 2: + tweak *= 2 + tweak += 4 + elif gui.scale != 1: + tweak = round(tweak * gui.scale) + tweak += 2 - # 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() + if system == "Windows": + tweak += 1 - # Drag to move playlist - if mouse_up and playlist_box.drag and coll_point(mouse_up_position, f_rect): + # 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)) - 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 + return left, right - left, target_link + +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 tauon.coll(link_rect): + if not click: + gui.cursor_want = 3 + if click: + webbrowser.open(link_pa[2], new=2, autoraise=True) + tauon.fields.add(link_rect) - if i != playlist_box.drag_on: +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] - # # Reveal the tab in case it has been hidden - # pctl.multi_playlist[playlist_box.drag_on].hidden = False + if click is None: + click = inp.mouse_click - 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) + tauon.fields.add(link_rect) + if tauon.coll(link_rect): + if not click: + gui.cursor_want = 3 + if click: + webbrowser.open(link_pa[2], new=2, autoraise=True) + track_box = True - playlist_box.drag = False - gui.update += 1 +def pixel_to_logical(x): + return round((x / window_size[0]) * logical_size[0]) - # Delete playlist on wheel click - elif tab_menu.active is False and middle_click: - # delete_playlist(i) - delete_playlist_ask(i) - break +def img_slide_update_gall(value, pause: bool = True) -> None: + gui.halt_image_rendering = True - # 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 + bag.album_mode_art_size = value - # 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 + clear_img_cache(False) + if pause: + gallery_load_delay.set() + gui.frame_callback_list.append(TestTimer(0.6)) + gui.halt_image_rendering = False - 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 + # Update sizes + tauon.gall_ren.size = bag.album_mode_art_size - 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 bag.album_mode_art_size > 150: + prefs.thin_gallery_borders = False - 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"]) +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 = [] - radio_view.drag = None + i = 0 + while len(tauon.gall_ren.queue) > 0: + time.sleep(0.01) + i += 1 + if i > 5 / 0.01: + break - x += tab_width + self.tab_spacing + for key, value in tauon.gall_ren.gall.items(): + SDL_DestroyTexture(value[2]) + tauon.gall_ren.gall = {} - # Test dupelicate tab function - if playlist_box.drag: - rect = (0, x, self.height, window_size[0]) - fields.add(rect) + 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 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) + 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: - if playlist_box.drag_source == 1: - pctl.multi_playlist[playlist_box.drag_on].hidden = False +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) - move_playlist(playlist_box.drag_on, i) - playlist_box.drag = False + 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)) - # Need to test length again - # Need to test length - self.tab_text_spaces = [] + 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) - 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) + gui.halt_image_rendering = False + album_art_gen.clear_cache() - # Reset X draw position - x = x_start - bar_highlight_size = round(2 * gui.scale) +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 - # TAB DRAWING - shown = [] - for i, tab in enumerate(target): + 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 - if not gui.radio_view: - if not prefs.tabs_on_top or prefs.shuffle_lock: - break + while ddt.get_text_w(line, font) > px: - if len(pctl.multi_playlist) != len(self.tab_text_spaces): - break + line = line[:-1] + if len(line) < 2: + break - # if tab.hidden is True: - # continue + return line - if i not in show_tabs: - continue +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 window_size[0] - x - (self.tab_text_spaces[i] + self.tab_extra_width) < right_space_es: - # break + 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(" ") - shown.append(i) + 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 - tab_width = self.tab_text_spaces[i] + self.tab_extra_width - rect = [x, y, tab_width, self.height] +# 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 - # 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 +def fix_encoding(index, mode, enc): + global enc_field - # 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 + todo = [] - # Draw tab background - ddt.rect(rect, bg) - if playing_hint: - ddt.rect(rect, [255, 255, 255, 7]) + if mode == 1: + todo = [index] + elif mode == 0: + for b in range(len(pctl.default_playlist)): + if pctl.master_library[pctl.default_playlist[b]].parent_folder_name == pctl.master_library[ + index].parent_folder_name: + todo.append(pctl.default_playlist[b]) - # Determine text colour - if active: - fg = colours.tab_text_active - else: - fg = colours.tab_text + for q in range(len(todo)): - # 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) + # 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]) - # 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 + 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 - # Drag to move playlist - if tab_hit: - if mouse_down and i != playlist_box.drag_on and playlist_box.drag is True: + 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 - 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]) + 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 - 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 old_star != None: + star_store.insert(todo[q], old_star) - 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 + # 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] - ddt.text( - (x + tab_width - 3, int(round(ay)), 1), "+" + str(self.adds[k][1]), colours.pluse_colour, 212, bg=bg) - gui.update += 1 +def transfer_tracks(index, mode, to): + todo = [] - x += tab_width + self.tab_spacing + if mode == 0: + todo = [index] + elif mode == 1: + for b in range(len(pctl.default_playlist)): + if pctl.master_library[pctl.default_playlist[b]].parent_folder_name == pctl.master_library[ + index].parent_folder_name: + todo.append(pctl.default_playlist[b]) + elif mode == 2: + todo = pctl.default_playlist - # 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]) + pctl.multi_playlist[to].playlist_ids += todo - if mouse_up: - drop_tracks_to_new_playlist(shift_selection) +def prep_gal(): + global albums + albums = [] - # 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]) + folder = "" - if prefs.tabs_on_top and right_overflow: - x += 24 * gui.scale - self.tabs_right_x += 24 * gui.scale + for index in pctl.default_playlist: - # ------------- - # Other input - if mouse_up: - quick_drag = False - playlist_box.drag = False - radio_view.drag = None + if folder != pctl.master_library[index].parent_folder_name: + albums.append([index, 0]) + folder = pctl.master_library[index].parent_folder_name - # 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: +def add_stations(tauon: Tauon, stations: list[RadioStation], name: str) -> None: + if len(stations) == 1: + for i, playlist in enumerate(pctl.radio_playlists): + if playlist.name == "Default": + playlist.stations.insert(0, stations[0]) + playlist.scroll = 0 + pctl.radio_playlist_viewing = i + break + else: + pctl.radio_playlists.append(RadioPlaylist(uid=uid_gen(), name="Default", stations=stations, scroll=0)) + pctl.radio_playlist_viewing = len(pctl.radio_playlists) - 1 + else: + pctl.radio_playlists.append(RadioPlaylist(uid=uid_gen(), name=name, stations=stations, scroll=0)) + pctl.radio_playlist_viewing = len(pctl.radio_playlists) - 1 + if not gui.radio_view: + enter_radio_view(tauon=tauon) - cycle_playlist_pinned(mouse_wheel) +def load_m3u(path: str) -> None: + name = os.path.basename(path)[:-4] + playlist = [] + stations = [] - 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 + location_dict = {} + titles = {} - x += self.ini_menu_space - y += 7 * gui.scale - ddt.text_background_colour = colours.top_panel_background + if not os.path.isfile(path): + return - # MENU ----------------------------- + with Path(path).open(encoding="utf-8") as file: + lines = file.readlines() - 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) + for i, line in enumerate(lines): + line = line.strip("\r\n").strip() + if not line.startswith("#"): # line.startswith("http"): - 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) + # 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() - 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 line.startswith("http"): + radio: RadioStation = RadioStation( + stream_url=line, + title=line_title if line_title else os.path.splitext(os.path.basename(path))[0].strip()) + stations.append(radio) - # 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)) - # + 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) - dl = len(dl_mon.ready) - watching = len(dl_mon.watching) + # 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 - 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) + # 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") - 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 + if playlist: + pctl.multi_playlist.append( + pl_gen(title=name, playlist_ids=playlist)) + if stations: + add_stations(stations, name) - 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 + gui.update = 1 - load_orders.append(copy.deepcopy(load_order)) +def read_pls(lines: list[str], path: str, followed: bool = False) -> None: + ids = [] + urls = {} + titles = {} - if len(dl_mon.ready) > 0: - dl_mon.ready.clear() - switch_playlist(pln) + 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() - 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") + 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() + stations: list[RadioStation] = [] + for id in ids: + if id in urls: + radio = RadioPlaylist( + stream_url=titles[id] if id in titles else urls[id], + title=os.path.splitext(os.path.basename(path))[0], + scroll=0) + 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: - 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] + 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)) - 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] +def load_pls(path: str) -> None: + if os.path.isfile(path): + f = open(path) + lines = f.readlines() + read_pls(lines, path) + f.close() - # LAYOUT -------------------------------- - x += self.menu_space + word_length +def load_xspf(path: str) -> None: + global to_got - self.drag_zone_start_x = x - 5 * gui.scale - status = True + name = os.path.basename(path)[:-5] + # tauon.log("Importing XSPF playlist: " + path, title=True) + logging.info("Importing XSPF playlist: " + path) - if loading_in_progress: + try: + parser = ET.XMLParser(encoding="utf-8") + e = ET.parse(path, parser).getroot() - 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] + a = [] + b = {} + info = "" - 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 + for top in e: - text = _("{N} listening along").format(N=len(tauon.listen_alongers)) - bg = [40, 190, 235, 255] - else: - status = False + 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 - 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)) + 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 - w = 100 * gui.scale - x += ddt.text((x, y), _("Transcoding"), bg, 311) + 8 * gui.scale + b["info"] = info + b["name"] = name + a.append(copy.deepcopy(b)) + b = {} - if gui.transcoding_batch_total: + except Exception: + logging.exception("Error importing/parsing XSPF playlist") + show_message(_("Error importing XSPF playlist."), _("Sorry about that."), mode="warning") + return - # 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] + # Extract internet streams first + stations: list[RadioStation] = [] + for i in reversed(range(len(a))): + item = a[i] + if item["location"].startswith("http"): + radio = RadioStation( + stream_url=item["location"], + title=item["name"]) +# radio.scroll = 0 # TODO(Martin): This was here wrong as scrolling is meant to be for RadioPlaylist? + if item["info"].startswith("http"): + radio.website_url = item["info"] - c1 = [40, 40, 40, 255] - c2 = [100, 59, 200, 200] - c3 = [150, 70, 200, 255] + stations.append(radio) - if colours.lm: - c1 = [100, 100, 100, 255] - c2 = [170, 140, 255, 255] - c3 = [230, 170, 255, 255] + if gui.auto_play_import: + gui.auto_play_import = False + radiobox.start(radio) - 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) + del a[i] + if stations: + add_stations(stations, os.path.basename(path)) + playlist = [] + missing = 0 - done = round(gui.transcoding_bach_done / gui.transcoding_batch_total * 100) - doing = round(core_use / gui.transcoding_batch_total * 100) + if len(a) > 5000: + to_got = "xspfl" - ddt.rect([x, yy, done, h], c3) - ddt.rect([x + done, yy, doing, h], c2) + # 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 - x += w + 8 * gui.scale + for track in a: + found = False - 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) + # Check if we already have a track with full file path in database + if not found and "location" in track: - x += ddt.text((x, y), text, bg, 311) + 8 * gui.scale + location = track["location"] + if location in location_dict: + playlist.append(location_dict[location]) + if not os.path.isfile(location): + missing += 1 + found = True + if found is True: + continue - 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) + # 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 + # 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 -top_panel = TopPanel() + if (not found and "location" in track) or "title" in track: + nt = TrackClass() + nt.index = pctl.master_count + nt.found = False + 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) -class BottomBarType1: - def __init__(self): + pctl.master_library[pctl.master_count] = nt + playlist.append(pctl.master_count) + pctl.master_count += 1 + if nt.found: + continue - self.mode = 0 + 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"]) - self.seek_time = 0 + 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 - 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 + # tauon.log("Finished importing XSPF") - 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] +def ex_tool_tip(x, y, text1_width, text, font): + text2_width = ddt.get_text_w(text, font) + if text2_width == text1_width: + return - 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) + y -= 10 * gui.scale - self.buffer_shard = asset_loader(scaled_asset_directory, loaded_asset_dc, "shard.png", True) + w = ddt.get_text_w(text, 312) + 24 * gui.scale + h = 24 * gui.scale - self.scrob_stick = 0 + x -= int(w / 2) - def update(self): + 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 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 +def close_all_menus(): + for menu in Menu.instances: + menu.active = False + Menu.active = False - seek_bar_x = 300 * gui.scale - if window_size[0] < 600 * gui.scale: - seek_bar_x = 250 * gui.scale +def menu_standard_or_grey(bool: bool): + if bool: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - self.seek_bar_size[0] = window_size[0] - seek_bar_x - self.seek_bar_position[0] = seek_bar_x + return [line_colour, colours.menu_background, None] - # if gui.bb_show_art: - # self.seek_bar_position[0] = 300 + gui.panelBY - # self.seek_bar_size[0] = window_size[0] - 300 - gui.panelBY +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() - # self.seek_bar_position[0] = 0 - # self.seek_bar_size[0] = window_size[0] +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() - def render(self): +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() - global volume_store - global clicked - global right_click +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() - ddt.rect_a((0, window_size[1] - gui.panelBY), (window_size[0], gui.panelBY), colours.bottom_panel_colour) +def lsp_menu_test_queue(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "queue" - ddt.rect_a(self.seek_bar_position, self.seek_bar_size, colours.seek_bar_background) +def lsp_menu_test_playlist(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "playlist" - right_offset = 0 - if gui.display_time_mode >= 2: - right_offset = 22 * gui.scale +def lsp_menu_test_tree(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "folder view" - if window_size[0] < 670 * gui.scale: - right_offset -= 90 * gui.scale - # Scrobble marker +def lsp_menu_test_artist(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "artist list" - 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 +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 - 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)) +def toggle_repeat() -> None: + gui.update += 1 + pctl.repeat_mode ^= True + if pctl.mpris is not None: + pctl.mpris.update_loop() - 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]) +def menu_repeat_off() -> None: + pctl.repeat_mode = False + pctl.album_repeat_mode = False + if pctl.mpris is not None: + pctl.mpris.update_loop() - # # 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])) +def menu_set_repeat() -> None: + pctl.repeat_mode = True + pctl.album_repeat_mode = False + if pctl.mpris is not None: + pctl.mpris.update_loop() - # ddt.rect_r(rect, [255, 255, 255, 20]) +def menu_album_repeat() -> None: + pctl.repeat_mode = True + pctl.album_repeat_mode = True + if pctl.mpris is not None: + pctl.mpris.update_loop() - # SEEK BAR------------------ - if pctl.playing_time < 1: - self.seek_time = 0 +def toggle_random(): + gui.update += 1 + pctl.random_mode ^= True + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - 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() +def toggle_random_on(): + pctl.random_mode = True + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - fields.add(self.seek_bar_position + self.seek_bar_size) - if coll(self.seek_bar_position + self.seek_bar_size): +def toggle_random_off(): + pctl.random_mode = False + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - if middle_click and pctl.playing_state > 0: - gui.seek_cur_show = True +def menu_shuffle_off(): + pctl.random_mode = False + pctl.album_shuffle_mode = False + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - clicked = True - if mouse_wheel != 0: - pctl.seek_time(pctl.playing_time + (mouse_wheel * 3)) +def menu_set_random(): + pctl.random_mode = True + pctl.album_shuffle_mode = False + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - if gui.seek_cur_show: - gui.update += 1 +def menu_album_random(): + pctl.random_mode = True + pctl.album_shuffle_mode = True + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - # 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) +def toggle_shuffle_layout(albums: bool = False): + prefs.shuffle_lock ^= True + if prefs.shuffle_lock: - 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) + gui.shuffle_was_showcase = gui.showcase_mode + gui.shuffle_was_random = pctl.random_mode + gui.shuffle_was_repeat = pctl.repeat_mode - if self.seek_down is True: - if mouse_position[0] == 0: - self.seek_down = False - self.seek_hit = True + 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() - 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: +def toggle_shuffle_layout_albums(): + toggle_shuffle_layout(albums=True) - self.volume_hit = False - self.seek_down = False - self.seek_hit = False +def exit_shuffle_layout(_): + return prefs.shuffle_lock - 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] +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) - pctl.seek_decimal(seek) - #logging.info(seek) +def bio_set_small(): + # gui.artist_panel_height = 200 * gui.scale + prefs.bio_large = False + update_layout_do(tauon=tauon) + if gui.artist_info_panel: + artist_info_box.get_data(artist_info_box.artist_on) - self.seek_time = pctl.playing_time +def artist_info_panel_close(): + gui.artist_info_panel ^= True + gui.update_layout() - 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) +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] - # 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] +def toggle_bio_size(): + if prefs.bio_large: + prefs.bio_large = False + update_layout_do(tauon=tauon) + # bio_set_small() + else: + prefs.bio_large = True + update_layout_do(tauon=tauon) + # bio_set_large() + # gui.update_layout() - self.buffer_shard.render(x + offset, y, colour) - x += self.buffer_shard.w +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 - ddt.rect( - (self.seek_bar_position[0] - self.buffer_shard.w, y, self.buffer_shard.w, self.buffer_shard.h), - colours.bottom_panel_colour) +def test_shift(_): + return inp.key_shift_down or inp.key_shiftr_down - if pctl.playing_length > 0: +def test_artist_dl(_): + return not prefs.auto_dl_artist_data - if pctl.download_time != 0: +def show_in_playlist(tauon: Tauon): + if prefs.album_mode and window_size[0] < 750 * gui.scale: + toggle_album_mode(tauon=tauon) - if pctl.download_time == -1: - pctl.download_time = pctl.playing_length + 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() - colour = (255, 255, 255, 10) - if gui.theme_name == "Lavender Light" or gui.theme_name == "Carbon": - colour = (255, 255, 255, 40) +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]) - 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) +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) - 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) +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 - if gui.seek_cur_show: + 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]) - 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)) +def tag_to_new_playlist(tag_item): + path_stem_to_playlist(tag_item.path, tag_item.name) - ddt.rect( - [mouse_position[0], self.seek_bar_position[1], 2, self.seek_bar_size[1]], - [100, 100, 20, 255]) +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) - else: - gui.seek_cur_show = False +def stem_to_new_playlist(path: str) -> None: + path_stem_to_playlist(path, os.path.basename(path)) - 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): +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.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 +def move_playing_folder_to_stem(tauon: Tauon, path: str, pl_id: int | None = None) -> None: + if not pl_id: + pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() + track = pctl.playing_object() - # 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) + if not track or pctl.playing_state == 0: + show_message(_("No item is currently playing")) + return - 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 + move_folder = track.parent_folder_path - 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 + # 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) - step = round(1 * gui.scale) - min_h = round(4 * gui.scale) - spacing = round(5 * gui.scale) + target_base = path - 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() + # Determine name for artist folder + artist = track.artist + if track.album_artist: + artist = track.album_artist - for bar in range(8): + # Make filename friendly + artist = filename_safe(artist) + if not artist: + artist = "unknown artist" - 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) + # Sanity checks + if track.is_network: + show_message(_("This track is a networked track."), mode="error") + return - if coll(h_rect): - if mouse_down or mouse_up: - gui.update_on_drag = True + if not os.path.isdir(move_folder): + show_message(_("The source folder does not exist."), mode="error") + return - 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 + if not os.path.isdir(target_base): + show_message(_("The destination folder does not exist."), mode="error") + return - pctl.set_volume() + if os.path.normpath(target_base) == os.path.normpath(move_folder): + show_message(_("The destination and source folders are the same."), mode="error") + return - colour = colours.mode_button_off + if len(target_base) < 4: + show_message(_("Safety interupt! The source path seems oddly short."), target_base, mode="error") + return - 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 + 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 - ddt.rect(rect, colour) - x += spacing + if directory_size(move_folder) > 3000000000: + show_message(_("Folder size safety limit reached! (3GB)"), move_folder, mode="warning") + return - # 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 + # Use target folder if it already is an artist folder + if os.path.basename(target_base).lower() == artist.lower(): + artist_folder = target_base - if inp.mouse_click is True or self.volume_bar_being_dragged is True: - gui.update = 2 + # 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) - 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 + # 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] - time.sleep(0.02) + # Find insert location + pl = pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids - if mouse_down is False: - self.volume_bar_being_dragged = False - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume(True) + matches = [] + insert = 0 - if mouse_down: - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume(False) + for i, item in enumerate(pl): + if pctl.get_track(item).fullpath.startswith(target_base): + insert = i - 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)): + for i, item in enumerate(pl): + if pctl.get_track(item).fullpath.startswith(artist_folder): + insert = i - if pctl.player_volume > 0: - volume_store = pctl.player_volume - pctl.player_volume = 0 - else: - pctl.player_volume = volume_store + 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 - pctl.set_volume() + logging.info(artist_folder) + logging.info(os.path.join(artist_folder, track.parent_folder_name)) + tauon.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") - ddt.rect_a( - (self.volume_bar_position[0] - right_offset, self.volume_bar_position[1]), - self.volume_bar_size, colours.volume_bar_background) # 22 +def move_playing_folder_to_tag(tag_item): + move_playing_folder_to_stem(tag_item.path) - 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]) +def re_import4(id): + p = None + for i, idd in enumerate(pctl.default_playlist): + if idd == id: + p = i + break - ddt.rect(gui.volume_bar_rect, colours.volume_bar_fill) + load_order = LoadClass() - 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 p is not None: + load_order.playlist_position = p - 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) + 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") - gui.show_bottom_title = gui.showed_title ^ True - if not prefs.hide_bottom_title: - gui.show_bottom_title = True +def re_import3(stem): + p = None + for i, id in enumerate(pctl.default_playlist): + if pctl.get_track(id).fullpath.startswith(stem + "/"): + p = i + break - if gui.show_bottom_title and pctl.playing_state > 0 and window_size[0] > 820 * gui.scale: - line = pctl.title_text() + load_order = LoadClass() - 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 + if p is not None: + load_order.playlist_position = p - # 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) + 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") - 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() +def collapse_tree_deco(): + pl_id = tree_view_box.get_pl_id() - if pctl.playing_ready() and not gui.fullscreen: + 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] - if right_click: - mode_menu.activate() +def collapse_tree(): + tree_view_box.collapse_all() - if d_click_timer.get() < 0.3 and inp.mouse_click: - set_mini_mode() - gui.update += 1 - return - d_click_timer.set() +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 - # TIME---------------------- +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")] - x = window_size[0] - 57 * gui.scale - y = window_size[1] - 29 * gui.scale +def finish_current(): + playing_object = pctl.playing_object() + if playing_object is None: + show_message("") - 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 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)) - 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: +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) - # colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) + 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 - 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) + 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 - offset1 = 10 * gui.scale +def add_album_to_queue_fc(ref): + playing_object = pctl.playing_object() + if playing_object is None: + show_message("") - if system == "Windows": - offset1 += 2 * gui.scale + queue_item = None - offset2 = offset1 + 7 * gui.scale + 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 - 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) + 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 - elif gui.display_time_mode == 3: + # fixme for network tracks - # colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) + for i, item in enumerate(pctl.force_queue): - track = pctl.playing_object() - if track and track.index != gui.dtm3_index: + 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 - 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 +def cancel_import(): + if transcode_list: + del transcode_list[1:] + gui.tc_cancel = True + if pctl.loading_in_progress: + gui.im_cancel = True + if gui.sync_progress: + gui.stop_sync = True + gui.sync_progress = _("Aborting Sync") - x -= 4 - text_time = get_display_time(gui.dtm3_cum + pctl.playing_time) +def toggle_lyrics_show(a): + return not gui.combo_mode - ddt.text( - (x - 25 * gui.scale, y), text_time, colours.time_playing, - fonts.bottom_panel_time) +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") - offset1 = 10 * gui.scale - if system == "Windows": - offset1 += 2 * gui.scale - offset2 = offset1 + 7 * gui.scale + if gui.combo_mode: + colour = colours.menu_text_disabled - 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) + return [colour, colours.menu_background, line] - # BUTTONS - # bottom buttons +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 gui.mode == 1: + if gui.combo_mode or not prefs.show_side_lyrics_art_panel: + colour = colours.menu_text_disabled - # PLAY--- - buttons_x_offset = 0 - compact = False - if window_size[0] < 650 * gui.scale: - compact = True + return [colour, colours.menu_background, line] - 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 +def toggle_lyrics_panel_position(): + prefs.lyric_metadata_panel_top ^= True - if pctl.playing_state == 1: - play_colour = colours.media_buttons_active +def lyrics_in_side_show(track_object: TrackClass): + if gui.combo_mode or not prefs.show_lyrics_side: + return False + return True - if pctl.auto_stop: - stop_colour = colours.media_buttons_active +def toggle_side_art(): + prefs.show_side_lyrics_art_panel ^= True - 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] +def toggle_lyrics_deco(track_object: TrackClass): + colour = colours.menu_text - 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 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 right_click: - pctl.show_current(highlight=True) + if prefs.side_panel_layout == 1: # and prefs.show_side_art: - 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) + 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] - # PAUSE--- - if compact: - buttons_x_offset = -46 * gui.scale + 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] - x = (75 * gui.scale) + buttons_x_offset - y = window_size[1] - self.control_line_bottom +def toggle_lyrics(track_object: TrackClass): + if not track_object: + return - if not compact or (compact and pctl.playing_state == 1): + 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 - 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")) + 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") - # 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) +def get_lyric_fire(track_object: TrackClass, silent: bool = False) -> str | None: + lyrics_ren.lyrics_position = 0 - # 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")) + 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 - ddt.rect_a((x, y + 0), (13 * gui.scale, 13 * gui.scale), stop_colour) - # ddt.rect_r(rect,[255,0,0,255], True) + 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 compact: - buttons_x_offset -= 5 * gui.scale + if t < -65: + show_message(_("Stop requesting lyrics AAAAAA."), mode="error") - # 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 + # If the user keeps pressing, lets mess with them haha + lyrics_fetch_timer.force_set(t - 5) - self.forward_button.render( - buttons_x_offset + 240 * gui.scale, 1 + window_size[1] - self.control_line_bottom, forward_colour) + return "later" - # ddt.rect_r(rect,[255,0,0,255], True) + if t > 0: + lyrics_fetch_timer.set() + t = 0 - # 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 + lyrics_fetch_timer.force_set(t - 10) - 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) + if not silent: + show_message(_("Searching...")) - # menu button + s_artist = track_object.artist + s_title = track_object.title - 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 + 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] - 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) + logging.info(f"Searching for lyrics: {s_artist} - {s_title}") - if self.mode == 0 and window_size[0] > 530 * gui.scale: + found = False + for name in prefs.lyrics_enables: - # shuffle button - x = window_size[0] - 318 * gui.scale - right_offset - y = window_size[1] - 27 * gui.scale + if name in lyric_sources.keys(): + func = lyric_sources[name] - rect = (x - 5 * gui.scale, y - 5 * gui.scale, 60 * gui.scale, 25 * gui.scale) - fields.add(rect) + 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") - rpbc = colours.mode_button_off - off = True - if (inp.mouse_click or right_click) and coll(rect): + if not found: + logging.error(f"Could not find lyrics from source {name}") - 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 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 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 +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 - # Keep hover highlight on if menu is open - if shuffle_menu.active and not pctl.random_mode: - rpbc = colours.mode_button_over + shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object])) + shoot_dl.daemon = True + shoot_dl.start() - #self.shuffle_button.render(x + round(1 * gui.scale), y + round(1 * gui.scale), rpbc) + logging.info("..Done") - #y += round(3 * gui.scale) - #ddt.rect_a((x, y), (25 * gui.scale, 3 * gui.scale), rpbc) +def get_lyric_wiki_silent(track_object: TrackClass): + logging.info("Searching for lyrics...") - 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) + if track_object.artist == "" or track_object.title == "": + return - #ddt.rect_a((x + 25 * gui.scale, y), (23 * gui.scale, 3 * gui.scale), rpbc) + shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object, True])) + shoot_dl.daemon = True + shoot_dl.start() - #y += round(5 * gui.scale) - #ddt.rect_a((x, y), (48 * gui.scale, 3 * gui.scale), rpbc) + logging.info("..Done") - # REPEAT - x = window_size[0] - round(380 * gui.scale) - right_offset - y = window_size[1] - round(27 * gui.scale) +def test_auto_lyrics(track_object: TrackClass): + if not track_object: + return - rpbc = colours.mode_button_off - off = True + 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) - 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): +def get_bio(track_object: TrackClass): + if track_object.artist != "": + lastfm.get_bio(track_object.artist) - 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 +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 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): + return [line_colour, colours.menu_background, None] - # 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")) +def toggle_synced_lyrics(tr): + prefs.prefer_synced_lyrics ^= True - 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 +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") - # Keep hover highlight on if menu is open - if repeat_menu.active and not pctl.repeat_mode: - rpbc = colours.mode_button_over + return [line_colour, colours.menu_background, text] - rpbc = alpha_blend(rpbc, colours.bottom_panel_colour) # bake in alpha in case of overlap +def paste_lyrics_deco(): + if SDL_HasClipboardText(): + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - y += round(3 * gui.scale) - w = round(3 * gui.scale) - y = round(y) - x = round(x) + return [line_colour, colours.menu_background, None] - ar = x + round(50 * gui.scale) - h = round(5 * gui.scale) +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 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) +#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)) - # 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) +#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)) + +def copy_lyrics_deco(track_object: TrackClass): + if track_object.lyrics: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled + return [line_colour, colours.menu_background, None] -bottom_bar1 = BottomBarType1() +def copy_lyrics(track_object: TrackClass): + copy_to_clipboard(track_object.lyrics) +def clear_lyrics(track_object: TrackClass): + track_object.lyrics = "" -class BottomBarType_ao1: - def __init__(self): +def clear_lyrics_deco(track_object: TrackClass): + if track_object.lyrics: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - self.mode = 0 + return [line_colour, colours.menu_background, None] - self.seek_time = 0 +def split_lyrics(track_object: TrackClass): + if track_object.lyrics != "": + track_object.lyrics = track_object.lyrics.replace(". ", ". \n") - 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 +def show_sub_search(track_object: TrackClass): + sub_lyrics_box.activate(track_object) - 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] +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 - 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) +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 - self.scrob_stick = 0 + if save_embed_img_disable_test(track_object): + show_message(_("Saving network images not implemented")) + return - def update(self): + try: + pic = album_art_gen.get_embed(track_object) - 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 + if not pic: + show_message(_("Image save error."), _("No embedded album art found file."), mode="warning") + return - seek_bar_x = 300 * gui.scale - if window_size[0] < 600 * gui.scale: - seek_bar_x = 250 * gui.scale + source_image = io.BytesIO(pic) + im = Image.open(source_image) - self.seek_bar_size[0] = window_size[0] - seek_bar_x - self.seek_bar_position[0] = seek_bar_x + source_image.close() - # if gui.bb_show_art: - # self.seek_bar_position[0] = 300 + gui.panelBY - # self.seek_bar_size[0] = window_size[0] - 300 - gui.panelBY + ext = "." + im.format.lower() + if im.format == "JPEG": + ext = ".jpg" - # self.seek_bar_position[0] = 0 - # self.seek_bar_size[0] = window_size[0] + target = os.path.join(folder, "embed-" + str(im.height) + "px-" + str(track_object.index) + ext) - def render(self): + if len(pic) > 30: + with open(target, "wb") as w: + w.write(pic) - global volume_store - global clicked - global right_click + open_folder(track_object.index) - ddt.rect_a((0, window_size[1] - gui.panelBY), (window_size[0], gui.panelBY), colours.bottom_panel_colour) + except Exception: + logging.exception("Unknown error trying to save an image") + show_message(_("Image save error."), _("A mysterious error occurred"), mode="error") - right_offset = 0 - if gui.display_time_mode >= 2: - right_offset = 22 * gui.scale +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) - if window_size[0] < 670 * gui.scale: - right_offset -= 90 * gui.scale + if info is None: + return [colours.menu_text_disabled, colours.menu_background, None] - # # 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])) + line_colour = colours.menu_text + return [line_colour, colours.menu_background, None] - # ddt.rect_r(rect, [255, 255, 255, 20]) +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 - # 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): +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) - 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 +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) - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() + if info is None: + return [colours.menu_text_disabled, colours.menu_background, None] - # 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() + if info[0] == 1: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - # Volume Bar 2 ------------------------------------------------ - if True: - x = window_size[0] - right_offset - 120 * gui.scale - y = window_size[1] - round(21 * gui.scale) + return [line_colour, colours.menu_background, None] - if gui.compact_bar: - x -= 90 * gui.scale +def cycle_image_deco(track_object: TrackClass): + info = album_art_gen.get_info(track_object) - 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 + 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 - 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 + return [line_colour, colours.menu_background, None] - step = round(1 * gui.scale) - min_h = round(4 * gui.scale) - spacing = round(5 * gui.scale) +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 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 + if info is not None and info[1] > 1: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - pctl.set_volume() + return [line_colour, colours.menu_background, None] - for bar in range(8): +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) - 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) +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) - if coll(h_rect): - if mouse_down: - gui.update_on_drag = True +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] - 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 +def download_art1(tr): + if tr.is_network: + show_message(_("Cannot download art for network tracks.")) + return - pctl.set_volume() + # Determine noise of folder ---------------- + siblings = [] + parent = tr.parent_folder_path - colour = colours.mode_button_off + 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) - 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 + album_tags = [] + date_tags = [] - ddt.rect(rect, colour) - x += spacing + for tr in siblings: + album_tags.append(tr.album) + date_tags.append(tr.date) - # TIME---------------------- + album_tags = set(album_tags) + date_tags = set(date_tags) - x = window_size[0] - 57 * gui.scale - y = window_size[1] - 35 * gui.scale + 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 - 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: + if not os.path.isdir(tr.parent_folder_path): + show_message(_("Directory missing.")) + return - colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) + try: + show_message(_("Looking up MusicBrainz ID...")) - 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) + if "musicbrainz_releasegroupid" not in tr.misc or "musicbrainz_artistids" not in tr.misc or not tr.misc[ + "musicbrainz_artistids"]: - offset1 = 10 * gui.scale + logging.info("MusicBrainz ID lookup...") - if system == "Windows": - offset1 += 2 * gui.scale + artist = tr.album_artist + if not tr.album: + return + if not artist: + artist = tr.artist - offset2 = offset1 + 7 * gui.scale + s = musicbrainzngs.search_release_groups(tr.album, artist=artist, limit=1) - 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) + album_id = s["release-group-list"][0]["id"] + artist_id = s["release-group-list"][0]["artist-credit"][0]["artist"]["id"] - elif gui.display_time_mode == 3: + logging.info("Found release group ID: " + album_id) + logging.info("Found artist ID: " + artist_id) - colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) + else: - track = pctl.playing_object() - if track and track.index != gui.dtm3_index: + album_id = tr.misc["musicbrainz_releasegroupid"] + artist_id = tr.misc["musicbrainz_artistids"][0] - 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 + logging.info("Using tagged release group ID: " + album_id) + logging.info("Using tagged artist ID: " + artist_id) - x -= 4 - text_time = get_display_time(gui.dtm3_cum + pctl.playing_time) + if prefs.enable_fanart_cover: + try: + show_message(_("Searching fanart.tv for cover art...")) - ddt.text((x - 25 * gui.scale, y), text_time, colours.time_playing, fonts.bottom_panel_time) + r = requests.get("https://webservice.fanart.tv/v3/music/albums/" \ + + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) - offset1 = 10 * gui.scale - if system == "Windows": - offset1 += 2 * gui.scale - offset2 = offset1 + 7 * gui.scale + artlink = r.json()["albums"][album_id]["albumcover"][0]["url"] + id = r.json()["albums"][album_id]["albumcover"][0]["id"] - 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) + response = urllib.request.urlopen(artlink, context=tls_context) + info = response.info() - # BUTTONS - # bottom buttons + t = io.BytesIO() + t.seek(0) + t.write(response.read()) + t.seek(0, 2) + l = t.tell() + t.seek(0) - if gui.mode == 1: + if info.get_content_maintype() == "image" and l > 1000: - # PLAY--- - buttons_x_offset = 0 - compact = False - if window_size[0] < 650 * gui.scale: - compact = True + 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 - 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 + f = open(filepath, "wb") + f.write(t.read()) + f.close() - if pctl.playing_state == 1: - play_colour = colours.media_buttons_active + show_message(_("Cover art downloaded from fanart.tv"), mode="done") + # clear_img_cache() + for track_id in pctl.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 pctl.auto_stop: - stop_colour = colours.media_buttons_active + 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() - 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] + show_message(_("Cover art downloaded from MusicBrainz"), mode="done") + # clear_img_cache() + clear_track_image_cache(tr) - 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")) + for track_id in pctl.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 right_click: - pctl.show_current(highlight=True) + return - 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) + except Exception: + logging.exception("Matching cover art or ID could not be found.") + show_message(_("Matching cover art or ID could not be found.")) - # PAUSE--- - if compact: - buttons_x_offset = -46 * 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 - x = (75 * gui.scale) + buttons_x_offset - y = window_size[1] - self.control_line_bottom +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 not compact or (compact and pctl.playing_state == 2): +def remove_embed_picture(track_object: TrackClass, dry: bool = True) -> int | None: + """Return amount of removed objects or None""" + index = track_object.index - 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")) + if inp.key_shift_down or inp.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 pctl.default_playlist: + tr = pctl.get_track(k) + if original_parent_folder == tr.parent_folder_name: + tracks.append(k) - # 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) + removed = 0 + if not dry: + pr = pctl.stop(True) + try: + for item in tracks: - # 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 + tr = pctl.get_track(item) - self.forward_button.render( - buttons_x_offset + 125 * gui.scale, - 1 + window_size[1] - self.control_line_bottom, forward_colour) + if tr.is_cue: + continue + if tr.is_network: + continue -bottom_bar_ao1 = BottomBarType_ao1() + 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 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") -class MiniMode: - def __init__(self): - self.save_position = None - self.was_borderless = True - self.volume_timer = Timer() - self.volume_timer.force_set(100) + 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") - 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) + 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") - self.shuffle_fade_timer = Timer(100) - self.repeat_fade_timer = Timer(100) + clear_track_image_cache(tr) - 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 + except Exception: + logging.exception("Image remove error") + show_message(_("Image remove error"), mode="error") + return None - w = window_size[0] - h = window_size[1] + if dry: + return removed - y1 = w - if w == h: - y1 -= 79 * gui.scale + 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() - h1 = h - y1 +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") - # Draw background - bg = colours.mini_mode_background - # bg = [250, 250, 250, 255] +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) - ddt.rect((0, 0, w, h), bg) - ddt.text_background_colour = bg + text = _("Delete Image File") + line_colour = colours.menu_text - detect_mouse_rect = (3, 3, w - 6, h - 6) - fields.add(detect_mouse_rect) - mouse_in = coll(detect_mouse_rect) + if info is None or track_object.is_network: + return [colours.menu_text_disabled, colours.menu_background, None] - # Play / Pause when right clicking below art - if right_click: # and mouse_position[1] > y1: - pctl.play_pause() + if info and info[0] == 0: + text = _("Delete Image File") - # Volume change on scroll - if mouse_wheel != 0: - self.volume_timer.set() + 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 - 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 + text = _("Delete Embedded | Folder") + if inp.key_shift_down or inp.key_shiftr_down: + text = _("Delete Embedded | Track") + return [line_colour, colours.menu_background, text] - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() +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") - track = pctl.playing_object() +def toggle_gimage(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_gimage + prefs.show_gimage ^= True + return None - 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) +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 - ddt.rect((0, 0, w, w), (0, 0, 0, 45)) - if track is not None: + return [line_colour, colours.menu_background, None] - # Render album art - album_art_gen.display(track, (0, 0), (w, w)) +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) - line1c = colours.mini_mode_text_1 - line2c = colours.mini_mode_text_2 +def append_here(): + global cargo + pctl.default_playlist += cargo - 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] +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") - # Double click bottom text to return to full window - text_hit_area = (60 * gui.scale, y1 + 4, 230 * gui.scale, 50 * gui.scale) + if active: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - 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() + return [line_colour, colours.menu_background, line] - # Draw title texts - line1 = track.artist - line2 = track.title +def lightning_move_test(discard): + return gui.lightning_copy and prefs.show_transfer - # Calculate seek bar position - seek_w = int(w * 0.70) +# def copy_deco(): +# line = "Copy" +# if inp.key_shift_down: +# line = "Copy" #Folder From Library" +# else: +# line = "Copy" +# +# +# return [colours.menu_text, colours.menu_background, line] - 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] +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 - if w != h or mouse_in_area: +def re_template_word(word, tr): + if word == "aa" or word == "albumartist": - 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: + if tr.album_artist: + return tr.album_artist + return tr.artist - ddt.text((w // 2, y1 + 10 * gui.scale, 2), line1, line2c, 514, window_size[0] - 30 * gui.scale) + if word == "a" or word == "artist": + return tr.artist - ddt.text((w // 2, y1 + 31 * gui.scale, 2), line2, line1c, 414, window_size[0] - 30 * gui.scale) + if word == "t" or word == "title": + return tr.title - # Test click to seek - if mouse_up and coll(seek_r_hit): + 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) - 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 word == "sn" or word == "singlenumber" or word == "singletracknumber" or word == "s": + return str(tr.track_number) - if click_x < 6 * gui.scale: - click_x = 0 - seek = click_x / seek_r[2] + if word == "d" or word == "date" or word == "year": + return str(tr.date) - pctl.seek_decimal(seek) + if word == "b" or "album" in word: + return str(tr.album) - # Draw progress bar background - ddt.rect(seek_r, [255, 255, 255, 32]) + if word == "g" or word == "genre": + return tr.genre - # 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 word == "x" or "ext" in word or "file" in word: + return tr.file_ext.lower() - if pctl.playing_state != 1: - seek_colour = [210, 40, 100, 255] + if word == "ux" or "upper" in word: + return tr.file_ext.upper() - seek_r[2] = progress_w + if word == "c" or "composer" in word: + return tr.composer - 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 + if "comment" in word: + return tr.comment.replace("\n", "").replace("\r", "") + return "" - ddt.rect(seek_r, seek_colour) +def parse_template2(string: str, track_object: TrackClass, strict: bool = False): + temp = "" + out = "" - 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) + mode = 0 - fields.add(left_area) - fields.add(right_area) + for c in string: - 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]) + if mode == 0: - 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]) + if c == "<": + mode = 1 + else: + out += c - # Shuffle + else: - 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 c == ">": - 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] + test = re_template_word(temp, track_object) + if strict: + assert test + out += test - sx = seek_r[0] + seek_w + 12 * gui.scale - sy = seek_r[1] - 2 * gui.scale - self.shuffle.render(sx, sy, colour) + mode = 0 + temp = "" + else: - # 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) + temp += c - 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] + if "<und" in string: + out = out.replace(" ", "_") + return parse_template(out, track_object, strict=strict) +def parse_template(string, track_object: TrackClass, up_ext: bool = False, strict: bool = False): + set = 0 + underscore = False + output = "" - sx = seek_r[0] - 36 * gui.scale - sy = seek_r[1] - 1 * gui.scale - self.repeat.render(sx, sy, colour) + 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 + output = output.rstrip(" -").lstrip(" -") - # sx = seek_r[0] - 39 * gui.scale - # sy = seek_r[1] - 1 * gui.scale + if underscore: + output = output.replace(" ", "_") - #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) + # Attempt to ensure the output text is filename safe + return filename_safe(output) +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 = inp.mouse_position[0] + rename_playlist_box.y = inp.mouse_position[1] - # 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() + 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) - # 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() + rename_playlist_box.y = min(rename_playlist_box.y, round(350 * gui.scale)) - 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) + if rename_playlist_box.y < gui.panelY: + rename_playlist_box.y = gui.panelY + 10 * gui.scale + 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 -mini_mode = MiniMode() + if generator: + rename_playlist_box.toggle_edit_gen() +def edit_generator_box(index: int) -> None: + rename_playlist(index, generator=True) -class MiniMode2: +def pin_playlist_toggle(pl: int) -> None: + pctl.multi_playlist[pl].hidden ^= True - def __init__(self): +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")] - self.save_position = None - self.was_borderless = True - self.volume_timer = Timer() - self.volume_timer.force_set(100) +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")] - 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 view_pl_is_locked(_) -> bool: + return pctl.multi_playlist[pctl.active_playlist_viewing].locked - def render(self): +def pl_is_locked(pl: int) -> bool: + if not pctl.multi_playlist: + return False + return pctl.multi_playlist[pl].locked - w = window_size[0] - h = window_size[1] +def lock_playlist_toggle(pl: int) -> None: + pctl.multi_playlist[pl].locked ^= True - x1 = h +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 - # Draw background - ddt.rect((0, 0, w, h), colours.mini_mode_background) - ddt.text_background_colour = colours.mini_mode_background +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 - detect_mouse_rect = (2, 2, w - 4, h - 4) - fields.add(detect_mouse_rect) - mouse_in = coll(detect_mouse_rect) + 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") - # Play / Pause when right clicking below art - if right_click: # and mouse_position[1] > y1: - pctl.play_pause() + 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 - # Volume change on scroll - if mouse_wheel != 0: - self.volume_timer.set() + 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() - 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 + 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 - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() +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 - track = pctl.playing_object() + if not direc: + direc = str(user_directory / "playlists") + if not os.path.exists(direc): + os.makedirs(direc) - if track is not None: + target = os.path.join(direc, pctl.multi_playlist[pl].title + ".xspf") - # Render album art - album_art_gen.display(track, (0, 0), (h, h)) + xspf_root = ET.Element("playlist", version="1", xmlns="http://xspf.org/ns/0/") + xspf_tracklist_tag = ET.SubElement(xspf_root, "trackList") - text_hit_area = (x1, 0, w, h) + 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 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() + 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) - # Draw title texts - line1 = track.artist - line2 = track.title + ET.SubElement(xspf_track_tag, "duration").text = str(int(track.length * 1000)) - if not line1 and not line2: + xspf_tree = ET.ElementTree(xspf_root) + ET.indent(xspf_tree, space=' ', level=0) + xspf_tree.write(target, encoding='UTF-8', xml_declaration=True) - ddt.text( - (x1 + 15 * gui.scale, 44 * gui.scale), track.filename, colours.grey(150), 315, - window_size[0] - x1 - 30 * gui.scale) - else: + 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]) - # 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: + return target - ddt.text( - (x1 + 15 * gui.scale, 18 * gui.scale), line2, colours.grey(249), 514, - window_size[0] - x1 - 30 * gui.scale) +def reload(): + if prefs.album_mode: + reload_albums(quiet=True) - ddt.text( - (x1 + 15 * gui.scale, 43 * gui.scale), line1, colours.grey(110), 514, - window_size[0] - x1 - 30 * gui.scale) + # tree_view_box.clear_all() + # elif gui.combo_mode: + # reload_albums(quiet=True) + # combo_pl_render.prep() - # 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() +def clear_playlist(tauon: Tauon, index: int) -> None: + if pl_is_locked(index): + show_message(_("Playlist is locked to prevent accidental erasure")) + return - # Seek bar - bg_rect = (h, h - round(5 * gui.scale), w - h, round(5 * gui.scale)) - ddt.rect(bg_rect, [255, 255, 255, 18]) + tauon.pctl.multi_playlist[index].last_folder.clear() # clear import folder list # TODO(Martin): This was actually a string not a list wth? - if pctl.playing_state > 0: + if not pctl.multi_playlist[index].playlist_ids: + logging.info("Playlist is already empty") + return - hit_rect = h - 5 * gui.scale, h - 12 * gui.scale, w - h + 5 * gui.scale, 13 * gui.scale + li = [] + for i, ref in enumerate(pctl.multi_playlist[index].playlist_ids): + li.append((i, ref)) - if coll(hit_rect) and mouse_up: - p = (mouse_position[0] - h) / (w - h) + undo.bk_tracks(index, list(reversed(li))) - 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) + del pctl.multi_playlist[index].playlist_ids[:] + if pctl.active_playlist_viewing == index: + pctl.default_playlist = tauon.pctl.multi_playlist[index].playlist_ids + reload() - 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) + # pctl.playlist_playing = 0 + pctl.multi_playlist[index].position = 0 + if index == pctl.active_playlist_viewing: + pctl.playlist_view_position = 0 + gui.pl_update = 1 -mini_mode2 = MiniMode2() +def convert_playlist(pl: int, get_list: bool = False) -> list[list[int]]| None: + global transcode_list + if not tauon.test_ffmpeg(): + return None + paths: list[str] = [] + folders: list[list[int]] = [] -class MiniMode3: + 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 __init__(self): + 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!")) - self.save_position = None - self.was_borderless = True - self.volume_timer = Timer() - self.volume_timer.force_set(100) + folders.append(folder) - 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) + if get_list: + return folders - self.shuffle_fade_timer = Timer(100) - self.repeat_fade_timer = Timer(100) + transcode_list.extend(folders) - 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 +def get_folder_tracks_local(pl_in: int) -> list[int]: + selection = [] + parent = os.path.normpath(pctl.master_library[pctl.default_playlist[pl_in]].parent_folder_path) + while pl_in < len(pctl.default_playlist) and parent == os.path.normpath( + pctl.master_library[pctl.default_playlist[pl_in]].parent_folder_path): + selection.append(pl_in) + pl_in += 1 + return selection - w = window_size[0] - h = window_size[1] +def test_pl_tab_locked(pl: int) -> bool: + if gui.radio_view: + return False + return pctl.multi_playlist[pl].locked - y1 = w #+ 10 * gui.scale - # if w == h: - # y1 -= 79 * gui.scale +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") - h1 = h - y1 +def move_playlist(tauon: Tauon, source: int, dest: int) -> None: + pctl = tauon.pctl + if dest > source: + dest += 1 + try: + active = pctl.multi_playlist[pctl.active_playlist_playing] + view = pctl.multi_playlist[pctl.active_playlist_viewing] - # Draw background - bg = colours.mini_mode_background - bg = [0, 0, 0, 0] - # bg = [250, 250, 250, 255] + temp = pctl.multi_playlist[source] + pctl.multi_playlist[source] = "old" + pctl.multi_playlist.insert(dest, temp) + pctl.multi_playlist.remove("old") - ddt.rect((0, 0, w, h), bg) + pctl.active_playlist_playing = pctl.multi_playlist.index(active) + pctl.active_playlist_viewing = pctl.multi_playlist.index(view) + pctl.default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + except Exception: + logging.exception("Playlist move error") - style_overlay.display() +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 = [RadioPlaylist(uid=uid_gen(),name="Default", stations=[])] + return - 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 + if check_lock and pl_is_locked(index): + show_message(_("Playlist is locked to prevent accidental deletion")) + return - detect_mouse_rect = (3, 3, w - 6, h - 6) - fields.add(detect_mouse_rect) - mouse_in = coll(detect_mouse_rect) + if not force: + if pl_is_locked(index): + show_message(_("Playlist is locked to prevent accidental deletion")) + return - # Play / Pause when right clicking below art - if right_click: # and mouse_position[1] > y1: - pctl.play_pause() + if gui.rename_playlist_box: + return - # Volume change on scroll - if mouse_wheel != 0: - self.volume_timer.set() + # Set screen to be redrawn + gui.pl_update = 1 + gui.update += 1 - 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 + # 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) - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() + # 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()) + pctl.default_playlist = pctl.multi_playlist[0].playlist_ids + pctl.active_playlist_playing = 0 + return - track = pctl.playing_object() + # Take note of the id of the playing playlist + old_playing_id = pctl.multi_playlist[pctl.active_playlist_playing].uuid_int - 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) + # Take note of the id of the viewed open playlist + old_view_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - #ddt.rect((0, 0, w, w), (0, 0, 0, 45)) - if track is not None: + # Delete the requested playlist + del pctl.multi_playlist[index] - # Render album art + # Re-set the open viewed playlist number by uid + for i, pl in enumerate(pctl.multi_playlist): - wid = (w // 2) + round(60 * gui.scale) - ins = (window_size[0] - wid) / 2 - off = round(4 * gui.scale) + 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 - 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)) + # Re-initiate the now viewed playlist + if old_view_id != pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int: + pctl.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] - line1c = [255, 255, 255, 255] #colours.mini_mode_text_1 - line2c = [255, 255, 255, 255] #colours.mini_mode_text_2 + if prefs.album_mode: + reload_albums(True) + goto_album(pctl.playlist_view_position) - # 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] + # Re-set the playing playlist number by uid + for i, pl in enumerate(pctl.multi_playlist): - # Double click bottom text to return to full window - text_hit_area = (60 * gui.scale, y1 + 4, 230 * gui.scale, 50 * gui.scale) + 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 - 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() + test_show_add_home_music(tauon=tauon) - # 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: + # Cleanup + ids = [] + for p in pctl.multi_playlist: + ids.append(p.uuid_int) - 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) + 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] - y1 += round(10 * gui.scale) + pctl.db_inc += 1 - # Calculate seek bar position - seek_w = int(w * 0.80) +def delete_playlist_force(index: int): + delete_playlist(index, force=True, check_lock=True) - 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] +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) - if w != h or mouse_in_area: +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 + 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") - # Test click to seek - if mouse_up and coll(seek_r_hit): +def rescan_tags(tauon: Tauon, pl: int) -> None: + for track in tauon.pctl.multi_playlist[pl].playlist_ids: + if tauon.pctl.master_library[track].is_cue is False: + tauon.to_scan.append(track) + tauon.thread_manager.ready("worker") - 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] +# 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 click_x < 6 * gui.scale: - click_x = 0 - seek = click_x / seek_r[2] +def re_import2(pl: int) -> None: + paths = pctl.multi_playlist[pl].last_folder - pctl.seek_decimal(seek) + reduce_paths(paths) - # Draw progress bar background - ddt.rect(seek_r, [255, 255, 255, 32]) + 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)) - # 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 paths: + show_message(_("Rescanning folders..."), mode="info") - if pctl.playing_state != 1: - seek_colour = [210, 40, 100, 255] +def rescan_all_folders(): + for i, p in enumerate(pctl.multi_playlist): + re_import2(i) - seek_r[2] = progress_w +def s_append(index: int): + paste(playlist_no=index) - ddt.rect(seek_r, seek_colour) +def append_playlist(index: int): + global cargo + pctl.multi_playlist[index].playlist_ids += cargo + gui.pl_update = 1 + reload() +def index_key(index: int): + tr = pctl.master_library[index] + s = str(tr.track_number) + d = str(tr.disc_number) - 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] + if "/" in d: + d = d.split("/")[0] - # 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] + # 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 = "" - if click_x < 6 * gui.scale: - click_x = 0 - volume = click_x / volume_r[2] - pctl.player_volume = int(volume * 100) - pctl.set_volume() + # 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 - ddt.rect(volume_r, [255, 255, 255, 32]) + # 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 - #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 + if (not tr.disc_number or tr.disc_number == "0") and tr.is_cue: + s = tr.filename + "-" + s - ddt.rect(volume_r, volume_colour) + # 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" +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 - 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) + 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) - fields.add(left_area) - fields.add(right_area) + 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) - 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]) + gui.pl_update += 1 - 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]) +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 - # 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) + 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) - 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] + 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) - sx = volume_r[0] + volume_w + 12 * gui.scale - sy = volume_r[1] - 3 * gui.scale - mini_mode.shuffle.render(sx, sy, colour) + gui.pl_update += 1 - # - # 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) +def key_filepath(index: int): + track = pctl.master_library[index] + return track.parent_folder_path.lower(), track.filename - 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] +def key_fullpath(index: int): + return pctl.master_library[index].fullpath - sx = volume_r[0] - 39 * gui.scale - sy = volume_r[1] - 1 * gui.scale - mini_mode.repeat.render(sx, sy, colour) +def key_filename(index: int): + track = pctl.master_library[index] + return track.filename - # 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) +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 - # 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() + if use_natsort and False: + target[:] = natsort.os_sorted(target, key=key_fullpath) + else: + target.sort(key=key_filepath) - search_over.render() +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 - # 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() +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) + seen_files = {} + seen_types = {} - # 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 + mp3_bitrates = {} + ogg_bitrates = {} + m4a_bitrates = {} -mini_mode3 = MiniMode3() + are_cue = 0 -def set_mini_mode(): - if gui.fullscreen: - return + for index in pctl.multi_playlist[pl].playlist_ids: + track = pctl.get_track(index) - global mouse_down - global mouse_up - global old_window_position - mouse_down = False - mouse_up = False - inp.mouse_click = False + playlist_time += int(track.length) + play_time += star_store.get(index) - if gui.maximized: - SDL_RestoreWindow(t_window) - update_layout_do() + if track.is_cue: + are_cue += 1 - if gui.mode < 3: - old_window_position = get_window_position() + 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 - if prefs.mini_mode_on_top: - SDL_SetWindowAlwaysOnTop(t_window, True) + type = track.file_ext + if type == "OGA": + type = "OGG" + seen_types[type] = seen_types.get(type, 0) + 1 - gui.mode = 3 - gui.vis = 0 - gui.turbo = False - gui.draw_vis4_top = False - gui.level_update = 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 - 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) + total_size = sum(seen_files.values()) - mini_mode.was_borderless = draw_border - SDL_SetWindowBordered(t_window, False) + 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" - 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") + 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" - if logical_size == window_size: - size = (int(size[0] * gui.scale), int(size[1] * gui.scale)) + 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) - logical_size[0] = size[0] - logical_size[1] = size[1] + line += str(perc) + "%" + line += "\n\n" - SDL_SetWindowMinimumSize(t_window, 100, 100) + 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}%); " - SDL_SetWindowResizable(t_window, False) - SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) + if others: + others = round(others, 1) + if others < 0.1: + others = "<0.1" + line += _("Others") + f"({others}%);" + line = line.rstrip("; ") + line += "\n\n" - if mini_mode.save_position: - SDL_SetWindowPosition(t_window, mini_mode.save_position[0], mini_mode.save_position[1]) + 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}%); " - 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 + if others: + others = round(others, 1) + if others < 0.1: + others = "<0.1" + line += _("Others") + f"({others}%);" + line = line.rstrip("; ") + line += "\n\n" - gui.update += 3 + # 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" + line += "\n" + f"-------------- {_('Top Artists')} --------------------" + "\n\n" -restore_ignore_timer = Timer() -restore_ignore_timer.force_set(100) + ls = stats_gen.artist_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 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" -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] + 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]) - if not mini_mode.was_borderless: - SDL_SetWindowBordered(t_window, True) +def imported_sort(pl: int) -> None: + if pl_is_locked(pl): + show_message(_("Playlist is locked")) + return - logical_size[0] = gui.save_size[0] - logical_size[1] = gui.save_size[1] + og = pctl.multi_playlist[pl].playlist_ids + og.sort(key=lambda x: pctl.get_track(x).index) - SDL_SetWindowPosition(t_window, gui.save_position[0], gui.save_position[1]) + reload_albums() + tree_view_box.clear_target_pl(pl) +def imported_sort_folders(pl: int) -> None: + if pl_is_locked(pl): + show_message(_("Playlist is locked")) + return - SDL_SetWindowResizable(t_window, True) - SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - SDL_SetWindowAlwaysOnTop(t_window, False) + og = pctl.multi_playlist[pl].playlist_ids + og.sort(key=lambda x: pctl.get_track(x).index) - # if macos: - # SDL_SetWindowMinimumSize(t_window, 560, 330) - # else: - SDL_SetWindowMinimumSize(t_window, 560, 330) + 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 - restore_ignore_timer.set() # Hacky + og.sort(key=lambda x: first_occurrences[pctl.get_track(x).parent_folder_path]) - gui.mode = 1 + reload_albums() + tree_view_box.clear_target_pl(pl) - global mouse_down - global mouse_up - mouse_down = False - mouse_up = False - inp.mouse_click = False +def standard_sort(pl: int) -> None: + if pl_is_locked(pl): + show_message(_("Playlist is locked")) + return - 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 + sort_path_pl(pl) + sort_track_2(pl) + reload_albums() + tree_view_box.clear_target_pl(pl) - #logging.info(window_size) +def year_s(plt): + sorted_temp = sorted(plt, key=lambda x: x[1]) + temp = [] - 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 + for album in sorted_temp: + temp += album[0] + return temp - gui.update_layout() - if prefs.art_bg: - tauon.thread_manager.ready("style") +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 = "" + p = 0 + while p < len(playlist): -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 + track = get_object(playlist[p]) - 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 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 = [] - 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 track.album_artist: + album_artist = track.album_artist - artistoffset = 0 - indexLine = "" + if p > len(playlist) - 1: + break - offset_font_extra = 0 - if gui.row_font_size > 14: - offset_font_extra = 8 + album = [] + on = get_object(playlist[p]).parent_folder_path + album.append(playlist[p]) + t = 1 - # In windows (arial?) draws numbers too high (hack fix) - num_y_offset = 0 - # if system == 'Windows': - # num_y_offset = 1 + while t + p < len(playlist) - 1 and get_object(playlist[p + t]).parent_folder_path == on: + album.append(playlist[p + t]) + t += 1 - if True or style == 1: + date = get_object(playlist[p]).date - # if not gui.rsp and not gui.combo_mode: - # width -= 10 * gui.scale + # 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:] - dash = False - if n_track.artist and colours.artist_text == colours.title_text: - dash = True + # 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 n_track.title: + plt.append((album, date, artist + " " + get_object(playlist[p]).album)) + p += len(album) + #logging.info(album) - line = track_number_process(n_track.track_number) + if plt: + pl2 += year_s(plt) + plt = [] - indexLine = line + if custom_list is not None: + return pl2 - 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 += " " + # We can't just assign the playlist because it may disconnect the 'pointer' pctl.default_playlist + pctl.multi_playlist[pl].playlist_ids[:] = pl2[:] + reload_albums() + tree_view_box.clear_target_pl(pl) - line = "" +def pl_toggle_playlist_break(ref): + pctl.multi_playlist[ref].hide_title ^= 1 + gui.pl_update = 1 - if n_track.artist != "" and not dash: - line0 = n_track.artist +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 - artistoffset = ddt.text( - (start_x + 27 * gui.scale, y), - line0, - alpha_mod(artistc, album_fade), - gui.row_font_size, - int(width / 2)) +def new_playlist(switch: bool = True) -> int | None: + if gui.radio_view: + pctl.radio_playlists.append(RadioPlaylist(uid=uid_gen(), name=_("New Radio List"), stations=[], scroll=0)) + return None - line = n_track.title - else: - line += n_track.title - else: - line = \ - os.path.splitext(n_track.filename)[ - 0] + title = gen_unique_pl_title(_("New Playlist")) - if p_track >= len(default_playlist): - gui.pl_update += 1 - return + top_panel.prime_side = 1 + top_panel.prime_tab = len(pctl.multi_playlist) - index = default_playlist[p_track] - star_x = 0 - total = star_store.get(index) + 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 gui.star_mode == "line" and total > 0 and pctl.master_library[index].length > 0: +def append_deco(): + if pctl.playing_state > 0: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - 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 + text = None + if tauon.spot_ctl.coasting: + text = _("Add Spotify Album") - lh = 1 - if gui.scale != 1: - lh = 2 + return [line_colour, colours.menu_background, text] - colour = colours.star_line - if this_line_playing and colours.star_line_playing is not None: - colour = colours.star_line_playing +def rescan_deco(pl: int): + if pctl.multi_playlist[pl].last_folder: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - 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)) + # base = os.path.basename(pctl.multi_playlist[pl].last_folder) + return [line_colour, colours.menu_background, None] - star_x += 6 * gui.scale +def regenerate_deco(pl: int): + id = pl_to_id(pl) + value = pctl.gen_codes.get(id) - 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) + if value: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - draw_rating_widget(sx, sy, n_track) + return [line_colour, colours.menu_background, None] - star_x += round(70 * 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 - if gui.star_mode == "star" and total > 0 and pctl.master_library[ - index].length != 0: + current += cha - 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 inquote and cha != "\"": + q_string += cha - sx2 = sx - selected_star = -2 - rated_star = -1 + if current: + cmds.append(current) + quotes.append(q_string) - # if key_ctrl_down: + return cmds, quotes, inquote - c = 60 - d = 6 +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 - colour = [70, 70, 70, 255] - if colours.lm: - colour = [90, 90, 90, 255] - # colour = alpha_mod(indexc, album_fade) + urls = [] + playlist = pctl.multi_playlist[pl].playlist_ids - for count in range(8): + 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) - if selected_star < count and playtime_stars < count and rated_star < count: - break + if warn: + show_message(_("Playlist contains non-Spotify tracks"), mode="error") + return - 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) + 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) - # 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) +def regenerate_playlist(pl: int = -1, silent: bool = False, id: int | None = None) -> None: + if id is None and pl == -1: + return - # if selected_star > -2: - # if selected_star >= count: - # colour = (220, 200, 60, 255) - # else: - # if rated_star >= count: - # colour = (220, 200, 60, 255) + if id is None: + id = pl_to_id(pl) - star_pc_icon.render(sx, sy, colour) + if pl == -1: + pl = id_to_pl(id) + if pl is None: + return - if gui.show_hearts: + source_playlist = pctl.multi_playlist[pl].playlist_ids - xxx = star_x + string = pctl.gen_codes.get(id) + if not string: + if not silent: + show_message(_("This playlist has no generator")) + return - count = 0 - spacing = 6 * gui.scale + cmds, quotes, inquote = parse_generator(string) - 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 inquote: + gui.gen_code_errors = "close" + return - if love(False, index): - count = 1 + playlist = [] + selections = [] + errors = False + selections_searched = 0 - x = width + start_x - 52 * gui.scale - offset_font_extra - xxx + def is_source_type(code: str | None) -> bool: + return \ + code is None or \ + code == "" or \ + code.startswith(("self", "jelly", "plex", "koel", "tau", "air", "sal")) - f_store.store(display_you_heart, (x, yy)) + #logging.info(cmds) + #logging.info(quotes) - star_x += 18 * gui.scale + pctl.regen_in_progress = True - if "spotify-liked" in pctl.master_library[index].misc: + for i, cm in enumerate(cmds): - x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx + quote = quotes[i] - f_store.store(display_spot_heart, (x, yy)) + if cm.startswith("\"") and (cm.endswith((">", "<"))): + cm_found = False - star_x += heart_row_icon.w + spacing + 2 + for col in column_names: - for name in pctl.master_library[index].lfm_friend_likes: + if quote.lower() == col.lower() or _(quote).lower() == col.lower(): + cm_found = True - # Limit to number of hears to display - if gui.star_mode == "none": - if count > 6: - break - elif count > 4: + 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 - x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx + elif cm == "self": + selections.append(pctl.multi_playlist[pl].playlist_ids) - f_store.store(display_friend_heart, (x, yy, name)) + elif cm == "auto": + pass - count += 1 + elif cm.startswith("spl\""): + playlist.extend(tauon.spot_ctl.playlist(quote, return_list=True)) - star_x += heart_row_icon.w + spacing + 2 + elif cm.startswith("tpl\""): + playlist.extend(tauon.tidal.playlist(quote, return_list=True)) - # Draw track number/index - display_queue = False + elif cm == "tfa": + playlist.extend(tauon.tidal.fav_albums(return_list=True)) - if pctl.force_queue: + elif cm == "tft": + playlist.extend(tauon.tidal.fav_tracks(return_list=True)) - 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) + elif cm.startswith("tar\""): + playlist.extend(tauon.tidal.artist(quote, return_list=True)) - if marks: - display_queue = True + elif cm.startswith("tmix\""): + playlist.extend(tauon.tidal.mix(quote, return_list=True)) - if display_queue: + elif cm == "sal": + playlist.extend(tauon.spot_ctl.get_library_albums(return_list=True)) - 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" + elif cm == "slt": + playlist.extend(tauon.spot_ctl.get_library_likes(return_list=True)) - # 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] + elif cm == "plex": + if not plex.scanning: + playlist.extend(plex.get_albums(return_list=True)) - # if album_type: - # li += "🠗" + elif cm.startswith("jelly\""): + if not jellyfin.scanning: + playlist.extend(jellyfin.get_playlist(quote, return_list=True)) - colour = [244, 200, 66, 255] - if colours.lm: - colour = [220, 40, 40, 255] + elif cm == "jelly": + if not jellyfin.scanning: + playlist.extend(jellyfin.ingest_library(return_list=True)) + + 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)) - ddt.text( - (start_x + 5 * gui.scale, y, 2), - li, colour, gui.row_font_size + 200 - 1) + elif cm == "air": + if not subsonic.scanning: + playlist.extend(subsonic.get_music3(return_list=True)) - elif len(indexLine) > 2: + 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) - ddt.text( - (start_x + 5 * gui.scale, y, 2), indexLine, - alpha_mod(indexc, album_fade), gui.row_font_size) - else: + temp = [] + for selection in selections: + temp += selection - ddt.text( - (start_x, y), indexLine, - alpha_mod(indexc, album_fade), gui.row_font_size) + playlist += list(OrderedDict.fromkeys(temp)) + selections.clear() - if dash and n_track.artist and n_track.title: - line = n_track.artist + " - " + n_track.title + elif cm == "cue": - 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) + 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 = get_display_time(n_track.length) + 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)) - 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) + 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)) - f_store.recall_all() + elif cm.startswith("ext"): + value = quote.upper() + if value: + if not selections: + for plist in pctl.multi_playlist: + selections.append(plist.playlist_ids) + temp = [] + for selection in selections: + for track in selection: + tr = pctl.get_track(track) + if tr.file_ext == value: + temp.append(track) -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 += list(OrderedDict.fromkeys(temp)) + elif cm == "ypa": + playlist = year_sort(0, playlist) -class StandardPlaylist: - def __init__(self): - pass + elif cm == "tn": + sort_track_2(0, playlist) - def full_render(self): + elif cm == "ia>": + playlist = gen_last_imported_folders(0, playlist) - global highlight_left - global highlight_right + elif cm == "ia<": + playlist = gen_last_imported_folders(0, playlist, reverse=True) - global playlist_hold - global playlist_hold_position - global shift_selection + elif cm == "m>": + playlist = gen_last_modified(0, playlist) - global click_time - global quick_drag - global mouse_down - global mouse_up - global selection_stage + elif cm == "m<": + playlist = gen_last_modified(0, playlist, reverse=False) - global r_menu_index - global r_menu_position + elif cm == "ly" or cm == "lyrics": + playlist = gen_lyrics(0, playlist) - left = gui.playlist_left - width = gui.plw + elif cm == "l" or cm == "love" or cm == "loved": + playlist = gen_love(0, playlist) - 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 + elif cm == "clr": + selections.clear() - w = 0 - gui.row_extra = 0 - cv = 0 # update gui.playlist_current_visible_tracks + elif cm == "rv" or cm == "reverse": + playlist = gen_reverse(0, playlist) - # Draw the background - SDL_SetRenderTarget(renderer, gui.tracklist_texture) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_RenderClear(renderer) + elif cm == "rva": + playlist = gen_folder_reverse(0, playlist) - rect = (left, gui.panelY, width, window_size[1]) - ddt.rect(rect, colours.playlist_panel_background) + elif cm == "rata>": - # 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) + playlist = gen_folder_top_rating(0, custom_list=playlist) - # 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: + elif cm == "rat>": - # Set scroll speed - mx = 4 + def rat_key(track_id): + return star_store.get_rating(track_id) - if gui.playlist_view_length < 25: - mx = 3 - if gui.playlist_view_length < 10: - mx = 2 - pctl.playlist_view_position -= mouse_wheel * mx + playlist = sorted(playlist, key=rat_key, reverse=True) - if gui.playlist_view_length > 40: - pctl.playlist_view_position -= mouse_wheel + elif cm == "rat<": - #if mouse_wheel: - #logging.debug("Position changed by mouse wheel scroll: " + str(mouse_wheel)) + def rat_key(track_id): + return star_store.get_rating(track_id) - 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() + playlist = sorted(playlist, key=rat_key) - scroll_hide_timer.set() - gui.frame_callback_list.append(TestTimer(0.9)) + 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 - # Show notice if playlist empty - if len(default_playlist) == 0: - colour = alpha_mod(colours.index_text, 200) # colours.playlist_text_missing + 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 - top_a = gui.panelY - if gui.artist_info_panel: - top_a += gui.artist_panel_height + 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 - b = window_size[1] - top_a - gui.panelBY - half = int(top_a + (b * 0.60)) + 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 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 + elif cm == "norat": + temp = [] + for item in playlist: + if star_store.get_rating(item) == 0: + temp.append(item) + playlist = temp - 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) + elif cm == "d>": + playlist = gen_sort_len(0, custom_list=playlist) - ddt.pretty_rect = None - ddt.alpha_bg = False + elif cm == "d<": + playlist = gen_sort_len(0, custom_list=playlist) + playlist = list(reversed(playlist)) - # Show notice if at end of playlist - elif pctl.playlist_view_position > len(default_playlist) - 1: - colour = alpha_mod(colours.index_text, 200) + 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] - top_a = gui.panelY - if gui.artist_info_panel: - top_a += gui.artist_panel_height + 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] - b = window_size[1] - top_a - gui.panelBY - half = int(top_a + (b * 0.17)) + elif cm == "path": + sort_path_pl(0, custom_list=playlist) - 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 + elif cm == "pa>": + playlist = gen_folder_top(0, custom_list=playlist) - ddt.text( - (left + int(width / 2) + 10 * gui.scale, half, 2), _("End of Playlist"), - colour, 213) + elif cm == "pa<": + playlist = gen_folder_top(0, custom_list=playlist) + playlist = gen_folder_reverse(0, playlist) - ddt.pretty_rect = None - ddt.alpha_bg = False + elif cm == "pt>" or cm == "pc>": + playlist = gen_top_100(0, custom_list=playlist) - # 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) + elif cm == "pt<" or cm == "pc<": + playlist = gen_top_100(0, custom_list=playlist) + playlist = list(reversed(playlist)) - # Process Input + 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] - # type (0 is track, 1 is fold title), track_position, track_object, box, input_box, - list_items = [] - number = 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] - for i in range(gui.playlist_view_length + 1): + 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] - track_position = i + pctl.playlist_view_position + 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] - # Make sure the view position is valid - pctl.playlist_view_position = max(pctl.playlist_view_position, 0) + elif cm == "y<": + playlist = gen_sort_date(0, False, playlist) - # Break if we are at end of playlist - if len(default_playlist) <= track_position or number > gui.playlist_view_length: - break + elif cm == "y>": + playlist = gen_sort_date(0, True, playlist) - track_object = pctl.get_track(default_playlist[track_position]) - track_id = track_object.index - move_on_title = False + 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 - line_y = gui.playlist_top + gui.playlist_row_height * number + 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 - track_box = ( - left + highlight_left, line_y, highlight_width, - gui.playlist_row_height - 1) + 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 - input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) + 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 - # 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 + 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 - highlight = False - drag_highlight = False + elif cm == "st" or cm == "rt" or cm == "r": + random.shuffle(playlist) - # Shift selection highlight - if (track_position in shift_selection and len(shift_selection) > 1): - highlight = True + elif cm == "sf" or cm == "rf" or cm == "ra" or cm == "sa": + playlist = gen_folder_shuffle(0, custom_list=playlist) - # Tracks have been dropped? - if playlist_hold is True and coll(input_box): - if mouse_up: - move_on_title = True + elif cm.startswith("n"): + value = cm[1:] + if value.isdigit(): + playlist = playlist[:int(value)] - # 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 + # SEARCH FOLDER + elif cm.startswith("p\"") and len(cm) > 3: - # Detect folder title click - if click_title and mouse_position[1] < window_size[1] - gui.panelBY: + 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) - 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 + search = quote + tauon.search_over.all_folders = True + tauon.search_over.sip = True + tauon.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 tauon.search_over.sip: + time.sleep(0.01) - 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 + found_name = "" - # 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 + for result in tauon.search_over.results: + if result[0] == 5: + found_name = result[1] + break + else: + logging.info("No folder search result found") + continue - if album_mode: - goto_album(pctl.playlist_playing_position) + tauon.search_over.clear() - # 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 + playlist += tauon.search_over.click_meta(found_name, get_list=True, search_lists=selections) - 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 + # SEARCH GENRE + elif (cm.startswith(('g"', 'gm"', 'g="'))) and len(cm) > 3: - # 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: + 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) - quick_drag = True - set_drag_source() + g_search = quote.lower().replace("-", "") # .replace(" ", "") - if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: - playlist_hold = True + search = g_search + tauon.search_over.sip = True + tauon.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 tauon.search_over.sip: + time.sleep(0.01) - selection_stage = 1 - temp = get_folder_tracks_local(track_position) - pctl.selected_in_playlist = track_position + found_name = "" - 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) + if cm.startswith("g=\""): + for result in tauon.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 tauon.search_over.results: + if result[0] == 3: + found_name = result[1] + break + elif cm.startswith("gm\""): + for result in tauon.search_over.results: + if result[0] == 3 and result[1].endswith("+"): + found_name = result[1] + break - else: - shift_selection = copy.copy(temp) + if not found_name: + logging.warning("No genre search result found") + continue - # Should draw drag highlight? + tauon.search_over.clear() - if mouse_down and playlist_hold and coll(input_box) and track_position not in shift_selection: + playlist += tauon.search_over.click_genre(found_name, get_list=True, search_lists=selections) - if len(shift_selection) < 2 and not key_shift_down: - pass - else: - drag_highlight = True + # 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) - # Something to do with quick search, I forgot - if pctl.selected_in_playlist > track_position + 1: - gui.row_extra += 1 + search = quote + tauon.search_over.sip = True + tauon.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 tauon.search_over.sip: + time.sleep(0.01) - list_items.append( - (1, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, False)) - number += 1 + found_name = "" - if number > gui.playlist_view_length: - break + for result in tauon.search_over.results: + if result[0] == 0: + found_name = result[1] + break + else: + logging.warning("No artist search result found") + continue - # Standard track --------------------------------------------------------------------- - playing = False + tauon.search_over.clear() + # for item in tauon.search_over.click_artist(found_name, get_list=True, search_lists=selections): + # playlist.append(item) + playlist += tauon.search_over.click_artist(found_name, get_list=True, search_lists=selections) - highlight = False - drag_highlight = False - line_y = gui.playlist_top + gui.playlist_row_height * number + elif cm.startswith("ff\""): - track_box = ( - left + highlight_left, line_y, highlight_width, - gui.playlist_row_height - 1) + 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() - input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) + if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): + line = str(unidecode(line)) - # 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 + if not search_magic(quote.lower(), line): + del playlist[i] - else: - line_hit = False - else: - line_hit = False - line_over = False + playlist = list(OrderedDict.fromkeys(playlist)) - # Prevent click if near scroll bar - if scroll_enable and mouse_position[0] < 30: - line_hit = False + elif cm.startswith("fx\""): - # 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): + 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)) - pctl.jump(track_id, track_position) + if search_magic(quote.lower(), line): + del playlist[i] - 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) + elif cm.startswith(('find"', 'f"', 'fs"')): - 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 + 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) - # 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 + cooldown = 0 + dones = {} + for selection in selections: + for track_id in selection: + if track_id not in dones: + tr = pctl.get_track(track_id) - # 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 + 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) - # # 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 + else: + line = " ".join([tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() - # Begin drag single track - if inp.mouse_click and line_hit and not side_drag: - quick_drag = True - set_drag_source() + # if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): + # line = str(unidecode(line)) - # 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 search_magic(quote.lower(), line): + playlist.append(track_id) - if len(shift_selection) > 1 or key_shift_down: - if track_position not in shift_selection: # p_track != playlist_hold_position and + cooldown += 1 + if cooldown > 300: + time.sleep(0.005) + cooldown = 0 - if len(shift_selection) == 0: + dones[track_id] = None - 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 + playlist = list(OrderedDict.fromkeys(playlist)) - gui.pl_update = 1 + 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 - else: - ref = [] - selection_stage = 2 - for item in shift_selection: - ref.append(default_playlist[item]) + 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] - for item in shift_selection: - default_playlist[item] = "old" + else: + errors = True - for item in shift_selection: - if move_on_title: - default_playlist.insert(track_position, "new") - else: - default_playlist.insert(track_position + 1, "new") + gui.gen_code_errors = errors + if not playlist and not errors: + gui.gen_code_errors = "empty" - 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 gui.rename_playlist_box and (not playlist or cmds.count("a") > 1): + pass + else: + source_playlist[:] = playlist[:] - pctl.selected_in_playlist = shift_selection[0] - gui.pl_update += 1 + tree_view_box.clear_target_pl(0, id) + pctl.regen_in_progress = False + gui.pl_update = 1 + reload() + pctl.notify_change() - reload_albums(True) - pctl.notify_change() + #logging.info(cmds) - # 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 +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") - # Right click menu activation - if right_click and line_hit and mouse_position[0] > gui.playlist_left + 10: +def spotify_show_test(_): + return prefs.spot_mode - 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 +def jellyfin_show_test(_): + return prefs.jelly_password and prefs.jelly_username - if track_position not in shift_selection: - pctl.selected_in_playlist = track_position - shift_selection = [pctl.selected_in_playlist] +def upload_jellyfin_playlist(pl: TauonPlaylist) -> None: + if jellyfin.scanning: + return + shooter(jellyfin.upload_playlist, [pl]) - if line_over and inp.mouse_click: +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() - 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] +def forget_pl_import_folder(pl: int) -> None: + pctl.multi_playlist[pl].last_folder = [] - if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: - playlist_hold = True - playlist_hold_position = track_position +def remove_duplicates(pl: int) -> None: + playlist = [] - # 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 + for item in pctl.multi_playlist[pl].playlist_ids: + if item not in playlist: + playlist.append(item) - # Multi Select Highlight - if track_position in shift_selection or track_position == pctl.selected_in_playlist: - highlight = True + 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 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 + pctl.multi_playlist[pl].playlist_ids[:] = playlist[:] - list_items.append( - (0, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, playing)) - number += 1 +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")) - if number > gui.playlist_view_length: - break - # --------------------------------------------------------------------------------------- +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 - # 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 +def auto_sync_thread(pl: int) -> None: + if prefs.transcode_inplace: + show_message(_("Cannot sync when in transcode inplace mode")) + return - for type, track_position, tr, track_box, input_box, highlight, number, drag_highlight, playing in list_items: + # Find target path + gui.sync_progress = "Starting Sync..." + gui.update += 1 - line_y = gui.playlist_top + gui.playlist_row_height * number + 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 - ddt.text_background_colour = colours.playlist_panel_background + prefs.sync_target = str(path) - if type == 1: + # Get list of folder names on device + logging.info("Getting folder list from device...") + d_folder_names = path.iterdir() + logging.info("Got list") - # Is type ALBUM TITLE - separator = " - " - if prefs.row_title_separator_type == 1: - separator = " ‒ " - if prefs.row_title_separator_type == 2: - separator = " ⦁ " + # Get list of folders we want + folders = convert_playlist(pl, get_list=True) + folder_names: list[str] = [] + folder_dict = {} - date = "" - duration = "" + if gui.stop_sync: + gui.sync_progress = "" + gui.stop_sync = False + gui.update += 1 - line = tr.parent_folder_name + # 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 - # 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: + # ------ + # 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) - if tr.album_artist != "" and tr.album != "": - line = tr.album_artist + separator + tr.album + # ------- + # 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 prefs.left_align_album_artist_title and not True: - album_artist_mode = True - line = tr.album + 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 len(line) < 6 and "CD" in line: - line = tr.album + 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 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 prefs.bypass_transcode or (prefs.smart_bypass and 0 < pctl.get_track(folder_dict[item][0]).bitrate <= 128): + logging.info("Smart bypass...") - if line.endswith(")"): - b = line.split("(") - if len(b) > 1 and len(b[1]) <= 11: + 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 - match = year_search.search(b[1]) + (path / item).mkdir() + encode_done = source_parent + else: + show_message(_("One or more folders is missing")) + continue - if match: - line = b[0] - date = "(" + b[1] + else: - elif line.startswith("("): + 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") - b = line.split(")") - if len(b) > 1 and len(b[0]) <= 11: + if encode_done.exists(): - match = year_search.search(b[0]) + if (path / item).exists(): + show_message( + _("Sync warning"), _("One or more folders to sync has the same name. Skipping."), mode="warning") + continue - if match: - line = b[1] - date = b[0] + ")" + (path / item).mkdir() - if "(" in line and year_search.search(line): - date = "" + for file in encode_done.iterdir(): + file = file.name + logging.info(f"Copy file {file} to {path / item}…") + # gui.sync_progress += "." + gui.update += 1 - line = line.replace(" - ", separator) + 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") - qq = 0 - d_date = date - title_line = line + 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" - # Calculate folder duration + logging.info("Finished copying folder") - q = track_position + gui.sync_speed = 0 + gui.sync_progress = "" + gui.stop_sync = False + gui.update += 1 + show_message(_("Sync completed"), mode="done") - total_time = 0 - while q < len(default_playlist): +def auto_sync(pl: int) -> None: + shoot_dl = threading.Thread(target=auto_sync_thread, args=([pl])) + shoot_dl.daemon = True + shoot_dl.start() - if pctl.get_track(default_playlist[q]).parent_folder_path != tr.parent_folder_path: - break +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) - total_time += pctl.get_track(default_playlist[q]).length +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] - q += 1 - qq += 1 +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) - if qq > 1: - duration = " [ " + get_display_time(total_time) + " ]" # Hair space inside brackets for better visual spacing +def set_podcast_playlist(pl: int) -> None: + pctl.multi_playlist[pl].persist_time_positioning ^= True - if prefs.append_total_time: - date += duration +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] - ex = left + highlight_left + highlight_width - 7 * gui.scale +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] - height = line_y + gui.playlist_row_height - 19 * gui.scale # gui.pl_title_y_offset +def csv_string(item): + item = str(item) + item.replace("\"", "\"\"") + return f"\"{item}\"" - 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) +def export_playlist_albums(pl: int) -> None: + p = pctl.multi_playlist[pl] + name = p.title + playlist = p.playlist_ids - light_offset = 0 - if colours.lm: - light_offset = 3 * gui.scale - ex -= light_offset + 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 qq > 1: - ex += 1 * gui.scale + playtimes[last_folder] = playtimes.get(last_folder, 0) + int(star_store.get(id)) - ddt.text_background_colour = colours.playlist_panel_background + filename = f"{user_directory}/{name}.csv" + xport = open(filename, "w") - if gui.scale == 2: - height += 1 + xport.write("Album name;Artist;Release date;Genre;Rating;Playtime;Folder path") - 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) + 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)) - #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) + 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)) +def key_rating(index: int): + return star_store.get_rating(index) - if prefs.row_title_format == 2: +def key_scrobbles(index: int): + return pctl.get_track(index).lfm_scrobbles - separator = " | " +def key_disc(index: int): + return pctl.get_track(index).disc_number - start_offset = round(15 * gui.scale) - xx = left + highlight_left + start_offset - ww = highlight_width +def key_cue(index: int): + return pctl.get_track(index).is_cue - 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)] +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 - 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) +def add_pl_tag(text): + return f" <{text}>" +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) - 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))) + if custom_list is not None: + return playlist + 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)) + 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) - 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 + if custom_list is not None: + return playlist - aa = 0 + 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)) - ft_width = ddt.get_text_w(line, gui.row_font_size + gui.pl_title_font_offset) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a pt>" - left_align = highlight_width - date_w - 13 * gui.scale - light_offset +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 - left_align -= star_offset + if len(source) < 3: + return [] - extra = aa + 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)) - left_align -= extra + 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 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) + if get_sets: + r = [] + for item in sets: + r.append((item, best(item))) + return r - else: - ddt.text( - (ex - date_w, height, 1), line, - colours.folder_title, - gui.row_font_size + gui.pl_title_font_offset) + sets = sorted(sets, key=best, reverse=True) - # ----- + playlist = [] - # 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) + for se in sets: + playlist += se - # 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]) + # 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 - continue + 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)) - # 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) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a pa>" - 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]) +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 [] - # Blue drop line - if drag_highlight: # playlist_hold_position != p_track: + 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)) - ddt.rect( - [left + highlight_left, line_y + gui.playlist_row_height - 1 * gui.scale, highlight_width, - 3 * gui.scale], [125, 105, 215, 255]) + def best(folder): + return album_star_store.get_rating(pctl.get_track(folder[0])) - # Highlight - if highlight: - ddt.rect_a( - (left + highlight_left, line_y), (highlight_width, gui.playlist_row_height), - colours.row_select_highlight) + if get_sets: + r = [] + for item in sets: + r.append((item, best(item))) + return r - ddt.text_background_colour = alpha_blend(colours.row_select_highlight, ddt.text_background_colour) + sets = sorted(sets, key=best, reverse=True) - 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) + playlist = [] - if not gui.set_mode: + for se in sets: + playlist += se - line_render( - tr, track_position, gui.playlist_text_offset + line_y, - playing, 255, left + inset_left, inset_width, 1, line_y) + if custom_list is not None: + return playlist - else: - # NEE --------------------------------------------------------- - n_track = tr - p_track = track_position - this_line_playing = playing + 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)) - start = 18 * gui.scale + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a rata>" - if center_mode: - start = inset_left +def gen_lyrics(plpl: int, custom_list=None): + playlist = [] - elif gui.lsp: - start += gui.lspw + source = custom_list + if source is None: + source = pctl.multi_playlist[pl].playlist_ids - run = start - end = start + gui.plw + for item in source: + if pctl.master_library[item].lyrics != "": + playlist.append(item) - if center_mode: - end = highlight_width + start + if custom_list is not None: + return playlist - # 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) + if len(playlist) > 0: + pctl.multi_playlist.append( + pl_gen( + title=_("Tracks with lyrics"), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - for h, item in enumerate(gui.pl_st): + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a ly" - 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 + else: + show_message(_("No tracks with lyrics were found.")) - if run > end - 50 * gui.scale: - break +def gen_incomplete(plpl: int, custom_list=None): + playlist = [] - if len(gui.pl_st) == h + 1: - wid -= 6 * gui.scale + source = custom_list + if source is None: + source = pctl.multi_playlist[pl].playlist_ids - 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) + albums = {} + nums = {} + for id in source: + track = pctl.get_track(id) + if track.album and track.track_number: - if item[0] == "Starline": + if type(track.track_number) is str and not track.track_number.isdigit(): + continue - total = star_store.get_by_object(n_track) + if track.album not in albums: + albums[track.album] = [] + nums[track.album] = [] - if total > 0 and n_track.length != 0 and wid > 0: - if gui.star_mode == "star": + if track not in albums[track.album]: + albums[track.album].append(track) + nums[track.album].append(int(track.track_number)) - 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) + 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) - 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 + if custom_list is not None: + return playlist - else: + 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)) - ratio = total / n_track.length - if ratio > 0.55: - star_x = int(ratio * (4 * gui.scale)) - star_x = min(star_x, wid) + # pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a ly" - colour = colours.star_line - if playing and colours.star_line_playing is not None: - colour = colours.star_line_playing + else: + show_message(_("No incomplete albums were found.")) - 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) +def gen_codec_pl(codec): + playlist = [] - 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 + 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) - 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) + if len(playlist) > 0: + pctl.multi_playlist.append( + pl_gen( + title=_("Codec: ") + codec, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "#": +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 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) + a_cache = {} - 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 = "" + def key_import(index: int): - 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 + track = pctl.master_library[index] + cached = a_cache.get((track.album, track.parent_folder_name)) + if cached is not None: + return cached - if get_love(n_track): + if track.album: + a_cache[(track.album, track.parent_folder_name)] = index + return index - 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 + playlist = copy.deepcopy(source) + playlist = sorted(playlist, key=key_import, reverse=reverse) + sort_track_2(0, playlist) - 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 + if custom_list is not None: + return playlist - 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 +def gen_last_modified(index, custom_list=None, reverse=True): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - x = run + u + (heart_row_icon.w + spacing) * count + a_cache = {} - j = 0 # justify right - if run < start + 100 * gui.scale: - j = 1 # justify left + def key_modified(index: int): - display_friend_heart(x, yy, name, j) - count += 1 + track = pctl.master_library[index] + cached = a_cache.get((track.album, track.parent_folder_name)) + if cached is not None: + return cached - # 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) + 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) - 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) + if custom_list is not None: + return playlist - 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 + 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 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 + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a m>" - if text: - if item[0] in colours.column_colours: - colour = colours.column_colours[item[0]] +def gen_love(pl: int, custom_list=None): + playlist = [] - if this_line_playing and item[0] in colours.column_colours_playing: - colour = colours.column_colours_playing[item[0]] + source = custom_list + if source is None: + source = pctl.multi_playlist[pl].playlist_ids - if run + 6 * gui.scale + wid > end: - wid = end - run - 40 * gui.scale - if center_mode: - wid += 25 * gui.scale + for item in source: + if get_love_index(item): + playlist.append(item) - wid = max(0, wid) + playlist.sort(key=lambda x: get_love_timestamp_index(x), reverse=True) - # # 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) + if custom_list is not None: + return playlist - ddt.text( - (run + 6 * gui.scale, y + y_off), - text, - colour, - font, - max_w=wid) + 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.")) - if ddt.was_truncated: - #logging.info(text) - rect = (run, y, wid - 1, gui.playlist_row_height - 1) - gui.heart_fields.append(rect) +def gen_comment(pl: int) -> None: + playlist = [] - if coll(rect): - columns_tool_tip.set(run - 7 * gui.scale, y, text, font, rect) + 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) - run += item[1] + 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.")) - # ----------------------------------------------------------------- - # 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 +def gen_replay(pl: int) -> None: + playlist = [] - # w += 1 - # if w > gui.playlist_view_length: - # break + for item in pctl.multi_playlist[pl].playlist_ids: + if pctl.master_library[item].misc.get("replaygain_track_gain"): + playlist.append(item) - # 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 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.")) - 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() +def gen_sort_len(index: int, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - SDL_SetRenderTarget(renderer, gui.main_texture) - SDL_RenderCopy(renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) + def length(index: int) -> int: - if mouse_down is False: - playlist_hold = False + if pctl.master_library[index].length < 1: + return 0 + return int(pctl.master_library[index].length) - ddt.pretty_rect = None - ddt.alpha_bg = False + playlist = copy.deepcopy(source) + playlist = sorted(playlist, key=length, reverse=True) - def cache_render(self): + if custom_list is not None: + return playlist - SDL_RenderCopy(renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) + # 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)) -playlist_render = StandardPlaylist() + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a d>" +def gen_folder_duration(pl: int, get_sets: bool = False): + if len(pctl.multi_playlist[pl].playlist_ids) < 3: + return None -class ArtBox: + 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 __init__(self): - pass + def best(folder): + total_duration = 0 + for item in folder: + total_duration += pctl.master_library[item].length + return total_duration - def draw(self, x, y, w, h, target_track=None, tight_border=False, default_border=None): + if get_sets: + r = [] + for item in sets: + r.append((item, best(item))) + return r - # 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) + sets = sorted(sets, key=best, reverse=True) + playlist = [] - # We need to find the size of the inner square for the artwork - # box = min(w, h) + for se in sets: + playlist += se - box_w = w - box_h = h + 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)) - box_w -= 17 * gui.scale # Inset the square a bit - box_h -= 17 * gui.scale # Inset the square a bit +def gen_sort_date(index: int, rev: bool = False, custom_list=None): + def g_date(index: int): - box_x = x + ((w - box_w) // 2) - box_y = y + ((h - box_h) // 2) + if pctl.master_library[index].date != "": + return str(pctl.master_library[index].date) + return "z" - # And position the square - rect = (box_x, box_y, box_w, box_h) - gui.main_art_box = rect + playlist = [] + lowest = 0 + highest = 0 + first = True - # Draw the album art. If side bar is being dragged set quick draw flag - showc = None - result = 1 + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - 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) + 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) - # 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 + 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: - ddt.rect_s(rect, colours.art_box, 1 * gui.scale) - border = rect + line = " <" + str(lowest) + "-" + str(highest) + ">" - fields.add(border) + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + line, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - # 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 + 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<" - # Input for album art - if target_track: +def gen_sort_date_new(index: int): + gen_sort_date(index, True) - # Cycle images on click +def gen_500_random(index: int): + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - if coll(gui.main_art_box) and inp.mouse_click is True and key_focused == 0: + random.shuffle(playlist) - album_art_gen.cycle_offset(target_track) + 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)) - if pctl.mpris: - pctl.mpris.update(force=True) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a st" - # 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) +def gen_folder_shuffle(index, custom_list=None): + folders = [] + dick = {} - # 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: + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - padding = 6 * gui.scale + 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) - 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] + random.shuffle(folders) + playlist = [] - art_metadata_overlay(xw, yh, showc) + 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" + +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) -art_box = ArtBox() + 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" -class ScrollBox: +def gen_reverse(index, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - def __init__(self): + playlist = list(reversed(source)) - 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 + if custom_list is not None: + return playlist - 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): + 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 max_value < 2: - return 0 + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rv" - if click is None: - click = inp.mouse_click +def gen_folder_reverse(index: int, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - bar_height = round(90 * gui.scale) + 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) - if h > 400 * gui.scale and max_value < 20: - bar_height = round(180 * gui.scale) + folders = list(reversed(folders)) + playlist = [] - bg = [255, 255, 255, 7] - fg = [255, 255, 255, 30] - fg_h = [255, 255, 255, 40] - fg_off = [255, 255, 255, 15] + for folder in folders: + playlist += dick[folder] - 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] + if custom_list is not None: + return playlist - ddt.rect((x, y, w, h), bg) + 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)) - half = bar_height // 2 + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rva" - ratio = value / max_value +def gen_dupe(index: int) -> None: + playlist = pctl.multi_playlist[index].playlist_ids - mi = y + half - mo = y + h - half - distance = mo - mi - position = int(round(distance * ratio)) + 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)) - fw = w + extend_field - fx = x - extend_field +def gen_sort_path(index: int) -> None: + def path(index: int) -> str: + return pctl.master_library[index].fullpath - if coll((fx, y, fw, h)): + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) + playlist = sorted(playlist, key=path) - if mouse_down: - gui.update += 1 + 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)) - if r_click: - p = mouse_position[1] - half - y - p = max(0, p) +def gen_sort_artist(index: int) -> None: + def artist(index: int) -> str: + return pctl.master_library[index].artist - range = h - bar_height - p = min(p, range) + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) + playlist = sorted(playlist, key=artist) - per = p / range + 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)) - value = int(round(max_value * per)) +def gen_sort_album(index: int) -> None: + def album(index: int) -> None: + return pctl.master_library[index].album - ratio = value / max_value + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) + playlist = sorted(playlist, key=album) - mi = y + half - mo = y + h - half - distance = mo - mi - position = int(round(distance * ratio)) + 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)) - in_bar = False - if coll((x, mi + position - half, w, bar_height)): - in_bar = True - if click: - self.held = 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" - # 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 +def reload_config_file(): + if transcode_list: + show_message(_("Cannot reload while a transcode is in progress!"), mode="error") + return - if pctl.playlist_view_position < 0: - pctl.playlist_view_position = 0 + 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() - elif mouse_down and not self.held: +def open_config_file(): + save_prefs(bag=bag, cf=cf) + 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 - if click and not in_bar: - self.slide_hold = True - self.direction_lock = 1 - if mouse_position[1] - y < position: - self.direction_lock = 0 +def open_keymap_file(): + target = str(config_directory / "input.txt") - self.d_position = value / max_value + if not os.path.isfile(target): + show_message(_("Input file missing")) + return - 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: + if system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", target]) + else: + subprocess.call(["xdg-open", target]) - tt = scroll_timer.hit() - if tt > 0.1: - tt = 0 +def open_file(target): + if not os.path.isfile(target): + show_message(_("Input file missing")) + return - flip = -1 - if self.direction_lock: - flip = 1 + if system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", target]) + else: + subprocess.call(["xdg-open", target]) - self.d_position = min(max(self.d_position + (((tt * jump_distance) / max_value) * flip), 0), 1) +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]) - else: - self.slide_hold = False +def remove_folder(index: int): + for b in range(len(pctl.default_playlist) - 1, -1, -1): + r_folder = pctl.master_library[index].parent_folder_name + if pctl.master_library[pctl.default_playlist[b]].parent_folder_name == r_folder: + del pctl.default_playlist[b] + reload() - if (self.held and mouse_up) or not mouse_down: - self.held = False +def convert_folder(index: int): + global transcode_list - if self.held and not window_is_focused(): - self.held = False + if not tauon.test_ffmpeg(): + return - if self.held: - get_sdl_input.mouse_capture_want = True - new_y = mouse_position[1] - gui.update += 1 + folder = [] + if inp.key_shift_down or inp.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] - offset = new_y - self.source_click_y + 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") - position = self.source_bar_y + offset + return + folder = [index] - position = max(position, 0) - position = min(position, distance) + else: + r_folder = pctl.master_library[index].parent_folder_path + for item in pctl.default_playlist: + if r_folder == pctl.master_library[item].parent_folder_path: - ratio = position / distance - value = int(round(max_value * ratio)) + track_object = pctl.get_track(item) + if track_object.file_ext == "SPOT": # track_object.is_network: + show_message(_("Transcoding spotify tracks not possible")) + return - 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 + 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") - ddt.rect(rect, colour) + return - if self.slide_hold: - return round(max_value * self.d_position) + #logging.info(folder) + transcode_list.append(folder) + tauon.thread_manager.ready("worker") - return value +def transfer(index: int, args) -> None: + global cargo + 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 pctl.default_playlist[pctl.selected_in_playlist] -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() + elif args[1] == 2: # folder + for b in range(len(pctl.default_playlist)): + if pctl.master_library[pctl.default_playlist[b]].parent_folder_name == pctl.master_library[ + index].parent_folder_name: + cargo.append(pctl.default_playlist[b]) + if args[0] == 0: # cut + for b in reversed(range(len(pctl.default_playlist))): + if pctl.master_library[pctl.default_playlist[b]].parent_folder_name == pctl.master_library[ + index].parent_folder_name: + del pctl.default_playlist[b] + elif args[1] == 3: # playlist + cargo += pctl.default_playlist + if args[0] == 0: # cut + pctl.default_playlist = [] -class RadioBox: + elif args[0] == 2: # Drop + if args[1] == 1: # Before - def __init__(self): + insert = pctl.selected_in_playlist + while insert > 0 and pctl.master_library[pctl.default_playlist[insert]].parent_folder_name == \ + pctl.master_library[index].parent_folder_name: + insert -= 1 + if insert == 0: + break + else: + insert += 1 - 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() + while len(cargo) > 0: + pctl.default_playlist.insert(insert, cargo.pop()) - self.x = 1 - self.y = 1 - self.w = 1 - self.h = 1 - self.center = False + elif args[1] == 2: # After + insert = pctl.selected_in_playlist - self.scroll_position = 0 - self.scroll = ScrollBox() + while insert < len(pctl.default_playlist) and pctl.master_library[pctl.default_playlist[insert]].parent_folder_name == \ + pctl.master_library[index].parent_folder_name: + insert += 1 - 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 = "" + while len(cargo) > 0: + pctl.default_playlist.insert(insert, cargo.pop()) + elif args[1] == 3: # End + pctl.default_playlist += cargo + # cargo = [] - 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) + cargo = old_cargo + reload() - self.song_key = "" +def temp_copy_folder(ref): + global cargo + cargo = [] + transfer(ref, args=[1, 2]) - self.drag = None +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() - self.tab = 0 - self.temp_list = [] +def menu_paste(position): + paste(None, position) - self.hosts = None - self.host = None +def s_copy(): + # Copy tracks to internal clipboard + # gui.lightning_copy = False + # if inp.key_shift_down: + gui.lightning_copy = True - 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)) + clip = copy_from_clipboard() + if "file://" in clip: + copy_to_clipboard("") - 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 + global cargo + cargo = [] + if pctl.default_playlist: + for item in shift_selection: + cargo.append(pctl.default_playlist[item]) - 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 + if not cargo and -1 < pctl.selected_in_playlist < len(pctl.default_playlist): + cargo.append(pctl.default_playlist[pctl.selected_in_playlist]) - def search_country(self, text): + tauon.copied_track = None - 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") + if len(cargo) == 1: + tauon.copied_track = cargo[0] - def search_tag(self, text): +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 - text = text.lower() - self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&tag=" + text) +def lightning_paste(): + move = True + # if not inp.key_shift_down: + # move = False - def search_title(self, text): + move_track = pctl.get_track(cargo[0]) + move_path = move_track.parent_folder_path - text = text.lower() - self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&name=" + text) + 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 - def is_m3u(self, url): - return url.lower().endswith(".m3u") or url.lower().endswith(".m3u8") + match_track = pctl.get_track(pctl.default_playlist[shift_selection[0]]) + match_path = match_track.parent_folder_path - def extract_stream_m3u(self, url, recursion_limit=5): - if recursion_limit <= 0: - return None - logging.info("Fetching M3U...") + if pctl.playing_state > 0 and move: + if pctl.playing_object().parent_folder_path == move_path: + pctl.stop(True) - try: - response = requests.get(url, timeout=10) - if response.status_code != 200: - logging.error(f"M3U Fetch error code: {response.status_code}") - return None + p = Path(match_path) + s = list(p.parts) + base = s[0] + c = base + del s[0] - content = response.text - lines = content.strip().split("\n") + 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]) - 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) + to_move = list(set(to_move)) - return None + for level in s: + upper = c + c = os.path.join(c, level) - except Exception: - logging.exception("Failed to extract M3U") - return None + t_artist = match_track.artist + ta_artist = match_track.album_artist - 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 + t_artist = filename_safe(t_artist) + ta_artist = filename_safe(ta_artist) - if self.load_connecting: - return + if (len(t_artist) > 0 and t_artist in level) or \ + (len(ta_artist) > 0 and ta_artist in level): - if tauon.spot_ctl.playing or tauon.spot_ctl.coasting: - tauon.spot_ctl.control("stop") + logging.info("found target artist level") + logging.info(t_artist) + logging.info("Upper folder is: " + upper) - try: - self.websocket.close() - logging.info("Websocket closed") - except Exception: - logging.exception("No socket to close?") + if len(move_path) < 4: + show_message(_("Safety interupt! The source path seems oddly short."), move_path, mode="error") + return - 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 = "" + if not os.path.isdir(upper): + show_message(_("The target directory is missing!"), upper, mode="warning") + return - album_art_gen.clear_cache() + if not os.path.isdir(move_path): + show_message(_("The source directory is missing!"), move_path, mode="warning") + return - if not tauon.test_ffmpeg(): - prefs.auto_rec = False - 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 - self.run_proxy = True - if url.endswith(".ts"): - self.run_proxy = False + if directory_size(move_path) > 3000000000: + show_message(_("Folder size safety limit reached! (3GB)"), move_path, mode="warning") + return - 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 + 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 - # 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 + artist = move_track.artist + if move_track.album_artist != "": + artist = move_track.album_artist - if tauon.stream_proxy.download_running: - tauon.stream_proxy.abort = True + artist = filename_safe(artist) - self.load_connecting = True - self.load_failed = False + if artist == "": + show_message(_("The track needs to have an artist name.")) + return - shoot = threading.Thread(target=self.start2, args=[url]) - shoot.daemon = True - shoot.start() + artist_folder = os.path.join(upper, artist) - def start2(self, url): + logging.info("Target will be: " + artist_folder) - 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 + 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) - 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() + 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 - if tauon.update_play_lock is not None: - tauon.update_play_lock() + insert = shift_selection[0] + old_insert = insert + while insert < len(pctl.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 - time.sleep(0.1) - self.load_connecting = False - self.load_failed = False - gui.update += 1 + load_order.playlist_position = insert - 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 + tauon.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] - def send_heartbeat(ws): - #logging.info(self.ws_interval) - time.sleep(self.ws_interval) - ws.send("{\"op\":9}") - logging.info("Send heatbeat") + break + else: + show_message(_("Could not find a folder with the artist's name to match level at.")) + return - 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() + # for file in os.listdir(artist_folder): - if d["op"] == 0: - self.ws_interval = d["d"]["heartbeat"] / 1000 - ws.send("{\"op\":9}") + if prefs.album_mode: + prep_gal() + reload_albums(True) - if d["op"] == 1: - try: + cargo.clear() + gui.lightning_copy = False - 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 +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 - pctl.found_tags = found_tags - pctl.tag_meta = line + elif "tidal.com/playlist/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + tauon.tidal.playlist(num) + clip = False - filename = d["d"]["song"]["albums"][0]["image"] - fulllink = "https://cdn.listen.moe/covers/" + filename + elif "tidal.com/mix/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + tauon.tidal.mix(num) + clip = False - #logging.info(fulllink) - art_response = requests.get(fulllink, timeout=10) - #logging.info(art_response.status_code) + elif "tidal.com/browse/track/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + tauon.tidal.track(num) + clip = False - 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") + 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 prefs.album_mode: + reload_albums() + gui.pl_update += 1 + clip = False - 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 + 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 - def on_error(ws, error): -# pass - logging.error(error) + 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 - def on_close(ws): -# pass - logging.info("### closed ###") + load_orders.append(copy.deepcopy(load_order)) + found = True - 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...") + if not found: - th.start_new_thread(run, ()) + 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) - # 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() + gui.pl_update += 1 - 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 s_cut(): + s_copy() + del_selected() - def delete_radio_entry_after(self, item): - p = radiobox.right_clicked_station_p - del prefs.radio_urls[p + 1:] +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: + pctl.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 edit_entry(self, item): - radio = item - self.radio_field_title.text = radio["title"] - self.radio_field.text = radio["stream_url"] +def paste_playlist_coast_album(): + shoot_dl = threading.Thread(target=paste_playlist_coast_fire) + shoot_dl.daemon = True + shoot_dl.start() - def browser_get_hosts(self): +def paste_playlist_coast_track(): + shoot_dl = threading.Thread(target=paste_playlist_track_coast_fire) + shoot_dl.daemon = True + shoot_dl.start() - import socket - """ - Get all base urls of all currently available radiobrowser servers +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 - Returns: - list: a list of strings + return [line_colour, colours.menu_background, None] - """ - 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] +def refind_playing(): + # Refind playing index + if pctl.playing_ready(): + for i, n in enumerate(pctl.default_playlist): + if pctl.track_queue[pctl.queue_step] == n: + pctl.playlist_playing_position = i + break - # 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") +def del_selected(force_delete: bool = False): + global shift_selection - # sort list of names - hosts.sort() - # add "https://" in front to make it an url - return list(map(lambda x: "https://" + x, hosts)) + gui.update += 1 + gui.pl_update = 1 - def search_page(self): + if not shift_selection: + shift_selection = [pctl.selected_in_playlist] - y = self.y - x = self.x - w = self.w - h = self.h + if not pctl.default_playlist: + return - yy = y + round(40 * gui.scale) + li = [] - 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) + for item in reversed(shift_selection): + if item > len(pctl.default_playlist) - 1: + return - ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) + li.append((item, pctl.default_playlist[item])) # take note for force delete - 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: + # 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 - 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") + del pctl.default_playlist[item] - 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() + if force_delete: + for item in li: - 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) + 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")) - for station in primary_stations: - self.temp_list.append(station) + 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) - 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() + reload() + tree_view_box.clear_target_pl(pctl.active_playlist_viewing) - def search_radio_browser2(self, param): + pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(pctl.default_playlist) - 1) - if not self.hosts: - self.hosts = self.browser_get_hosts() - if not self.host: - self.host = random.choice(self.hosts) + shift_selection = [pctl.selected_in_playlist] + gui.pl_update += 1 + refind_playing() + pctl.notify_change() - 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 force_del_selected(): + del_selected(force_delete=True) - def parse_data(self, data): +def test_show(tauon:Tauon, dummy) -> bool: + return prefs.album_mode - self.temp_list.clear() +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() - 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 last_fm_test(ignore): + if lastfm.connected: + return True + return False - def render(self) -> None: +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 - if self.edit_mode: - w = round(510 * gui.scale) - h = round(120 * gui.scale) # + sh +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 - 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 +def love_decox(): + global r_menu_index - 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) + 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")] - self.saved() - return +def love_index(): + global r_menu_index - 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) + notify = False + if not gui.show_hearts: + notify = True - self.w = w - self.h = h - self.x = x - self.y = y + # 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() - yy = y +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() - 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) +def toggle_spotify_like3(): + toggle_spotify_like_active2(pctl.get_track(r_menu_index)) - ddt.text_background_colour = colours.box_background +def toggle_spotify_like_row_deco(): + tr = pctl.get_track(r_menu_index) + text = _("Spotify Like Track") - if key_esc_press or (gui.level_2_click and not coll((x, y, w, h))): - self.active = False + # 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") - ddt.text((x + 10 * gui.scale, yy + 8 * gui.scale), _("Station Browser"), colours.box_title_text, 213) + return [colours.menu_text, colours.menu_background, text] - # --- - 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 +def spot_like_show_test(x): + return spotify_show_test and pctl.get_track(r_menu_index).file_ext == "SPTY" - 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: +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 - 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") +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 - 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) +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(pctl.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(pctl.default_playlist[pctl.selected_in_playlist], + pctl.selected_in_playlist, + pl_to_id(pctl.active_playlist_viewing))) + queue_timer_set() - # --- ---------------------------------------------------------------------- - if self.tab == 1: - self.search_page() - elif self.tab == 0: - self.saved() - self.draw_list() - # self.footer() - return +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(pctl.default_playlist[index], + index, + pl_to_id(pctl.active_playlist_viewing))) - def saved(self): - y = self.y - x = self.x - w = self.w - h = self.h +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] - yy = y + round(40 * gui.scale) +def split_queue_album(id: int) -> int | None: + item = pctl.force_queue[0] - width = round(370 * gui.scale) + pl = id_to_pl(item.playlist_id) + if pl is None: + return None - 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) + playlist = pctl.multi_playlist[pl].playlist_ids - ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) + i = pctl.playlist_playing_position + 1 + parts = [] + album_parent_path = pctl.get_track(item.track_id).parent_folder_path - yy += round(30 * gui.scale) + while i < len(playlist): + if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: + break - 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 + parts.append((playlist[i], i)) + i += 1 - 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) + del pctl.force_queue[0] - if draw.button(_("Save"), x + width + round(21 * gui.scale), yy - round(20 * gui.scale), press=gui.level_2_click): + for part in reversed(parts): + pctl.force_queue.insert(0, queue_item_gen(part[0], part[1], item.type)) + return (len(parts)) - 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"] = "" +def add_to_queue_next(ref: int) -> None: + if pctl.force_queue and pctl.force_queue[0].album_stage == 1: + split_queue_album(None) - if self.add_mode: - pctl.radio_playlists[pctl.radio_playlist_viewing]["items"].append(radio) - self.active = False + pctl.force_queue.insert(0, queue_item_gen(ref, r_menu_position, pl_to_id(pctl.active_playlist_viewing))) - else: - show_message(_("Could not validate URL. Must start with https:// or http://")) +def delete_track(track_ref): + tr = pctl.get_track(track_ref) + fullpath = tr.fullpath - def draw_list(self): + if system == "Windows" or msys: + fullpath = fullpath.replace("/", "\\") - x = self.x - y = self.y - w = self.w - h = self.h + if tr.is_network: + show_message(_("Cannot delete a network track")) + return - if self.drag: - gui.update_on_drag = True + while track_ref in pctl.default_playlist: + pctl.default_playlist.remove(track_ref) - yy = y + round(100 * gui.scale) - x += round(10 * gui.scale) + try: + send2trash(fullpath) - radio_list = prefs.radio_urls - if self.tab == 1: - radio_list = self.temp_list + 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")) - 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) + 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") - 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) + reload() + refind_playing() + pctl.notify_change() - self.scroll_position = max(self.scroll_position, 0) +def rename_tracks_deco(track_id: int): + if inp.key_shift_down or inp.key_shiftr_down: + return [colours.menu_text, colours.menu_background, _("Rename (Single track)")] + return [colours.menu_text, colours.menu_background, _("Rename Tracks…")] - p = self.scroll_position * 2 - offset = 0 - to_delete = None - swap = None +def activate_trans_editor(): + trans_edit_box.active = True - while True: +def delete_folder(index, force=False): + track = pctl.master_library[index] - if p > len(radio_list) - 1: - break + if track.is_network: + show_message(_("Cannot physically delete"), _("One or more tracks is from a network location!"), mode="info") + return - xx = x + offset - item = radio_list[p] + old = track.parent_folder_path - rect = (xx, yy, round(233 * gui.scale), round(40 * gui.scale)) - fields.add(rect) + if len(old) < 5: + show_message(_("This folder path seems short, I don't wanna try delete that"), mode="warning") + return - bg = colours.box_background - text_colour = colours.box_input_text + 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 - playing = pctl.playing_state == 3 and self.loaded_url == item["stream_url"] + protect = ("", "Documents", "Music", "Desktop", "Downloads") - if playing: - # bg = colours.box_sub_highlight - # ddt.rect(rect, bg, True) + 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 - bg = colours.tab_background_active - text_colour = colours.tab_text_active - ddt.rect(rect, bg) + if directory_size(old) > 1500000000: + show_message(_("Delete size safety limit reached! (1.5GB)"), old, mode="warning") + return - 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) + 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 coll(rect): + if force: + shutil.rmtree(old) + elif system == "Windows" or msys: + send2trash(old.replace("/", "\\")) + else: + send2trash(old) - 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) + for i in reversed(range(len(pctl.default_playlist))): - bg = alpha_blend(bg, colours.box_background) + if old == pctl.master_library[pctl.default_playlist[i]].parent_folder_path: + del pctl.default_playlist[i] - 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)) + if not os.path.exists(old): + if force: + show_message(_("Folder deleted."), old, mode="done") 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)) + show_message(_("Folder sent to trash."), old, mode="done") + else: + show_message(_("Hmm, its still there"), old, mode="error") - 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)) + if prefs.album_mode: + prep_gal() + reload_albums() - 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]) + 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") - if offset == 0: - offset = rect[2] + round(4 * gui.scale) - else: - offset = 0 - yy += round(43 * gui.scale) + tree_view_box.clear_target_pl(pctl.active_playlist_viewing) + gui.pl_update += 1 + pctl.notify_change() - if yy > y + 300 * gui.scale: - break +def rename_parent(index: int, template: str) -> None: + # template = prefs.rename_folder_template + template = template.strip("/\\") + track = pctl.master_library[index] - p += 1 + if track.is_network: + show_message(_("Cannot rename"), _("One or more tracks is from a network location!"), mode="info") + return - # 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) + old = track.parent_folder_path + #logging.info(old) - # 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)) + new = parse_template2(template, track) - # 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 len(new) < 1: + show_message(_("Rename error."), _("The generated name is too short"), mode="warning") + return - # if not mouse_down: - # self.drag = None + if len(old) < 5: + show_message(_("Rename error."), _("This folder path seems short, I don't wanna try rename that"), mode="warning") + return - def footer(self): + if not os.path.exists(old): + show_message(_("Rename Failed. The original folder is missing."), mode="warning") + return - y = self.y - x = self.x + round(15 * gui.scale) - w = self.w - h = self.h + protect = ("", "Documents", "Music", "Desktop", "Downloads") - 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")) + 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 - 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") + 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) - 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 + pre_state = 0 + + for key, object in pctl.master_library.items(): + if object.fullpath == "": + continue -radiobox = RadioBox() -tauon.radiobox = radiobox -tauon.dummy_track = radiobox.dummy_track + if old == object.parent_folder_path: + new_fullpath = os.path.join(new_parent_path, object.filename) -# def visit_radio_site_show_test(p): -# return "website_url" in prefs.radio_urls[p] and prefs.radio_urls[p]["website_url"] -# + if os.path.normpath(new_parent_path) == os.path.normpath(old): + show_message(_("The folder already has that name.")) + return -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] + 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) -def visit_radio_station_site_deco(item): - return visit_radio_site_deco(item[1]) + 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) -def visit_radio_site(item): - if "website_url" in item and item["website_url"]: - webbrowser.open(item["website_url"], new=2, autoraise=True) + # 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) -def visit_radio_station(item): - visit_radio_site(item[1]) + 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") -def radio_saved_panel_test(_): - return radiobox.tab == 0 + if pre_state == 1: + pctl.revert() + tree_view_box.clear_target_pl(pctl.active_playlist_viewing) + pctl.notify_change() -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"]) +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 -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)) + track_box = False + rename_index = index + if rename_folders_disable_test(index): + show_message(_("Not applicable for a network track.")) + return -class RenamePlaylistBox: + gui.rename_folder_box = True + input_text = "" + shift_selection.clear() - def __init__(self): + global playlist_hold + inp.quick_drag = False + playlist_hold = False - self.x = 300 - self.y = 300 - self.playlist_index = 0 +def move_folder_up(index: int, do: bool = False) -> bool | None: + track = pctl.master_library[index] - self.edit_generator = False + if track.is_network: + show_message(_("Cannot move"), _("One or more tracks is from a network location!"), mode="info") + return None - def toggle_edit_gen(self): + 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) - self.edit_generator ^= True - if self.edit_generator: + 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(rename_text_area.text) > 0: - pctl.multi_playlist[self.playlist_index].title = rename_text_area.text + if len(os.listdir(parent_folder)) > 1: + return False - pl = self.playlist_index - id = pl_to_id(pl) + if do is False: + return True - text = pctl.gen_codes.get(id) - if not text: - text = "" + 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) - rename_text_area.set_text(text) - rename_text_area.highlight_none() + try: - gui.regen_single = rename_playlist_box.playlist_index - tauon.thread_manager.ready("worker") + # 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) - else: - rename_text_area.set_text(pctl.multi_playlist[self.playlist_index].title) - rename_text_area.highlight_none() - # rename_text_area.highlight_all() + # Delete the old directory that contained the original folder + shutil.rmtree(parent_folder) - def render(self): + # Rename the moved folder back to its original name + os.rename(os.path.join(upper_folder, "RMTEMP000"), os.path.join(upper_folder, folder_name)) - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False + except Exception as e: + logging.exception("System Error!") + show_message(_("System Error!"), str(e), mode="error") - if inp.key_tab_press: - self.toggle_edit_gen() + # 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(): - text_w = ddt.get_text_w(rename_text_area.text, 315) - min_w = max(250 * gui.scale, text_w + 50 * gui.scale) + 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("\\/")) - 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 + search_string_cache.pop(object.index, None) + search_dia_string_cache.pop(object.index, None) - # Draw background - ddt.rect(rect, bg) + logging.info(object.fullpath) + logging.info(object.parent_folder_path) - # 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) + if pre_state == 1: + pctl.revert() - # Draw accent - rect2 = [self.x, self.y + rect[3] - 4 * gui.scale, min_w, 4 * gui.scale] - ddt.rect(rect2, [255, 255, 255, 60]) +def clean_folder(index: int, do: bool = False) -> int | None: + track = pctl.master_library[index] - if self.edit_generator: - pl = self.playlist_index - id = pl_to_id(pl) - pctl.gen_codes[id] = rename_text_area.text + if track.is_network: + show_message(_("Cannot clean"), _("One or more tracks is from a network location!"), mode="info") + return None - if input_text or key_backspace_press: - gui.regen_single = rename_playlist_box.playlist_index - tauon.thread_manager.ready("worker") + 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": - # 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)] + 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 hint_rect[0] + hint_rect[2] > window_size[0]: - hint_rect[0] = window_size[0] - hint_rect[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() - 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) + for track_id in pctl.default_playlist: + if pctl.get_track(track_id).parent_folder_path == folder: + clear_track_image_cache(pctl.get_track(track_id)) - text_colour = [150, 150, 150, 255] - title_colour = text_colour - code_colour = [250, 250, 250, 255] - hint_colour = [110, 110, 110, 255] + 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 - title_font = 311 - code_font = 311 - hint_font = 310 + return found - # ddt.pretty_rect = hint_rect +def reset_play_count(index: int): + star_store.remove(index) - 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) +def vacuum_playtimes(index: int): + todo = [] + for k in pctl.default_playlist: + if pctl.master_library[index].parent_folder_name == pctl.master_library[k].parent_folder_name: + todo.append(k) - 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) + for track in todo: - 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) + tr = pctl.get_track(track) - 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) + total_playtime = 0 + flags = "" - # 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) + to_del = [] - 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) + 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])) - 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) + for key in to_del: + del star_store.db[key] - # 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) + 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?") - 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) +def reload_metadata(input, keep_star: bool = True) -> None: + global todo - # yy += round(16 * gui.scale) - ddt.text((xx0, yy), _("Sorters"), title_colour, title_font) - yy += round(14 * gui.scale) + # vacuum_playtimes(index) + # return + todo = [] - 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) + if isinstance(input, list): + todo = input - 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) + else: + for k in pctl.default_playlist: + if pctl.master_library[input].parent_folder_path == pctl.master_library[k].parent_folder_path: + todo.append(pctl.master_library[k]) - 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) + for i in reversed(range(len(todo))): + if todo[i].is_cue: + del todo[i] - # ddt.pretty_rect = None + for track in todo: - # 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 + search_string_cache.pop(track.index, None) + search_dia_string_cache.pop(track.index, None) - 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 + #logging.info('Reloading Metadata for ' + track.filename) + if keep_star: + tauon.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) -rename_playlist_box = RenamePlaylistBox() + # 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() -class PlaylistBox: + gui.pl_update += 1 + tauon.thread_manager.ready("worker") - def recalc(self): - self.tab_h = round(25 * gui.scale) - self.gap = round(2 * gui.scale) +def reload_metadata_selection(tauon: Tauon) -> None: + cargo = [] + for item in shift_selection: + cargo.append(pctl.default_playlist[item]) - self.text_offset = 2 * gui.scale - if gui.scale == 1.25: - self.text_offset = 3 + for k in cargo: + if tauon.pctl.master_library[k].is_cue == False: + tauon.to_scan.append(k) + tauon.thread_manager.ready("worker") - def __init__(self): +def editor(index: int | None) -> None: + todo = [] + obs = [] - self.scroll_on = prefs.old_playlist_box_position - self.drag = False - self.drag_source = 0 - self.drag_on = -1 + if inp.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(pctl.default_playlist[item]) + obs.append(pctl.master_library[pctl.default_playlist[item]]) + if len(todo) > 0: + index = todo[0] + else: + for k in pctl.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]) - self.adds = [] + # 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) - self.indicate_w = round(2 * gui.scale) + file_line = "" + for track in todo: + file_line += ' "' + file_line += pctl.master_library[track].fullpath + file_line += '"' - 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 system == "Windows" or msys: + file_line = file_line.replace("/", "\\") + prefix = "" + app = prefs.tag_editor_target - # if gui.scale == 1.25: - self.tab_h = 0 - self.gap = 0 + if (system == "Windows" or msys) and app: + if app[0] != '"': + app = '"' + app + if app[-1] != '"': + app = app + '"' - self.text_offset = 2 * gui.scale - self.recalc() + app_switch = "" - def draw(self, x, y, w, h): + ok = False - global quick_drag + prefix = launch_prefix - # 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 + if system == "Linux": + ok = whicher(prefs.tag_editor_target, flatpak_mode) + else: - max_tabs = (h - 10 * gui.scale) // (self.gap + self.tab_h) + 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 - tab_title_colour = [230, 230, 230, 255] + ok = True - bg_lumi = test_lumi(colours.playlist_box_background) - light_mode = False + if not ok: + show_message(_("Tag editor app does not appear to be installed."), mode="warning") - if bg_lumi < 0.55: - light_mode = True - tab_title_colour = [20, 20, 20, 255] + 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") - dark_mode = False - if bg_lumi > 0.8: - dark_mode = True + return - if light_mode: - indicate_w = round(3 * gui.scale) - else: - indicate_w = round(2 * gui.scale) + if "picard" in prefs.tag_editor_target: + app_switch = " --d " - show_scroll = False - tab_start = x + 10 * gui.scale + line = prefix + app + app_switch + file_line - if window_size[0] < 700 * gui.scale: - tab_start = x + 4 * gui.scale + show_message( + prefs.tag_editor_name + " launched.", "Fields will be updated once application is closed.", mode="arrow") + gui.update = 1 - if mouse_wheel != 0 and coll((x, y, w, h)): - self.scroll_on -= mouse_wheel + complete = subprocess.run(shlex.split(line), stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) - self.scroll_on = min(self.scroll_on, len(pctl.multi_playlist) - max_tabs + 1) + 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('"') - self.scroll_on = max(self.scroll_on, 0) + 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.") - if len(pctl.multi_playlist) > max_tabs: - show_scroll = True - else: - self.scroll_on = 0 + gui.message_box = False + reload_metadata(obs, keep_star=False) - if show_scroll: - tab_start += 15 * gui.scale + # Re apply playtime data in case file names change + for item in old_stars: - if colours.lm: - w -= round(6 * gui.scale) - tab_width = w - tab_start # - 0 * gui.scale + old_key = item[1] + old_value = item[2] - # 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) + if not old_value: # ignore if there was no old playcount metadata + continue - draw_pin_indicator = False # prefs.tabs_on_top + new_key = star_store.object_key(item[0]) + new_value = star_store.full_get(item[0].index) - # 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 + if old_key == new_key: + continue - # Process inputs - delete_pl = None - tab_on = 0 - yy = y + 5 * gui.scale - for i, pl in enumerate(pctl.multi_playlist): + if new_value is None: + new_value = [0, "", 0] - if tab_on >= max_tabs: - break - if i < self.scroll_on: - continue + new_value[0] += old_value[0] + new_value[1] = "".join(set(new_value[1] + old_value[1])) - # if not pl.hidden and i in tabs_on_top: - # continue + if old_key in star_store.db: + del star_store.db[old_key] - tab_on += 1 + star_store.db[new_key] = new_value - 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 + gui.pl_update = 1 + gui.update = 1 + pctl.notify_change() - if tab_menu.active is False and middle_click: - delete_pl = i - # delete_playlist(i) - # break +def launch_editor(index: int): + if snap_mode: + show_message(_("Sorry, this feature isn't (yet) available with Snap.")) + return - if mouse_up and self.drag and coll_point(mouse_up_position, (tab_start, yy - 1, tab_width, (self.tab_h + 1))): + if launch_editor_disable_test(index): + show_message(_("Cannot edit tags of a network track.")) + return - # 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 + mini_t = threading.Thread(target=editor, args=[index]) + mini_t.daemon = True + mini_t.start() - # 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) +def launch_editor_selection_disable_test(index: int): + for position in shift_selection: + if pctl.get_track(pctl.default_playlist[position]).is_network: + return True + return False - gui.update += 1 +def launch_editor_selection(index: int): + if launch_editor_selection_disable_test(index): + show_message(_("Cannot edit tags of a network track.")) + return - # 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: + mini_t = threading.Thread(target=editor, args=[None]) + mini_t.daemon = True + mini_t.start() - 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) +def edit_deco(index: int): + if inp.key_shift_down or inp.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] - 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() +def launch_editor_disable_test(index: int): + return pctl.get_track(index).is_network - # 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 +def show_lyrics_menu(index: int): + global track_box + track_box = False + enter_showcase_view(track_id=r_menu_index) + inp.mouse_click = False - 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) +def recode(text, enc): + return text.encode("Latin-1", "ignore").decode(enc, "ignore") - # 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 +def intel_moji(index: int): + gui.pl_update += 1 + gui.update += 1 - yy += self.tab_h + self.gap + track = pctl.master_library[index] - # Draw tabs - # delete_pl = None - tab_on = 0 - yy = y + 5 * gui.scale - for i, pl in enumerate(pctl.multi_playlist): + lot = [] - # if yy + self.tab_h > y + h: - # break - if tab_on >= max_tabs: - break - if i < self.scroll_on: - continue + for item in pctl.default_playlist: - tab_on += 1 + if track.album == pctl.master_library[item].album and \ + track.parent_folder_name == pctl.master_library[item].parent_folder_name: + lot.append(item) - name = pl.title - hidden = pl.hidden + lot = set(lot) - # Background is insivible by default (for hightlighting if selected) - bg = [0, 0, 0, 0] + l_artist = track.artist.encode("Latin-1", "ignore") + l_album = track.album.encode("Latin-1", "ignore") + detect = None - # 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] + 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 - # 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] + 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 - # 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] + for item in lot: + t_track = pctl.master_library[item] - # Get actual bg from blend for text bg - real_bg = alpha_blend(bg, colours.playlist_box_background) + 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 - # Draw highlight - ddt.rect((tab_start, yy - round(1 * gui.scale), tab_width, self.tab_h), bg) + 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 - # Draw title text - text_start = 10 * gui.scale - if draw_pin_indicator: - # text_start = 40 * gui.scale - text_start = 32 * gui.scale + 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) - 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)) + 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 not pl.hidden and prefs.tabs_on_top: - cl = [255, 255, 255, 25] + if key != None: + star_store.insert(item, key) - if light_mode: - cl = [0, 0, 0, 40] + search_string_cache.pop(track.index, None) + search_dia_string_cache.pop(track.index, None) - xx = tab_start + tab_width - self.lock_icon.w - self.lock_icon.render(xx, yy, cl) + else: + show_message(_("Autodetect failed")) - 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) +def sel_to_car(): + cargo = [] - # Is mouse collided with tab? - hit = coll((tab_start + 50 * gui.scale, yy - 1, tab_width - 50 * gui.scale, (self.tab_h + 1))) + for item in shift_selection: + cargo.append(pctl.default_playlist[item]) - # if not prefs.tabs_on_top: - if i == pctl.active_playlist_playing: +def cut_selection(): + sel_to_car() + del_selected() - indicator_colour = colours.title_playing - if colours.lm: - indicator_colour = colours.seek_bar_fill +def clip_ar_al(index: int): + line = pctl.master_library[index].artist + " - " + pctl.master_library[index].album + SDL_SetClipboardText(line.encode("utf-8")) - ddt.rect((tab_start + 0 - 2 * gui.scale, yy - round(1 * gui.scale), indicate_w, self.tab_h), indicator_colour) +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")) - # # 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]) +def clip_title(index: int): + n_track = pctl.master_library[index] - # 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]) + 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")) - 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]) +def lightning_copy(): + s_copy() + gui.lightning_copy = True - # 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 +def toggle_transcode(mode: int = 0) -> bool: + if mode == 1: + return prefs.enable_transcode + prefs.enable_transcode ^= True + return None - 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 +def toggle_chromecast(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_chromecast + prefs.show_chromecast ^= True + return None - 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]) +def toggle_transfer(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_transfer + prefs.show_transfer ^= True - yy += self.tab_h + self.gap + 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 - if delete_pl is not None: - # delete_playlist(delete_pl) - delete_playlist_ask(delete_pl) - gui.update += 1 +def transcode_deco(): + if inp.key_shift_down or inp.key_shiftr_down: + return [colours.menu_text, colours.menu_background, _("Transcode Single")] + return [colours.menu_text, colours.menu_background, _("Transcode Folder")] - # Create new playlist if drag in blank space after tabs - rect = (x, yy, w - 10 * gui.scale, h - (yy - y)) - fields.add(rect) +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")) - 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) +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] - if right_click: - extra_tab_menu.activate(pctl.active_playlist_viewing) +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) - # 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 + 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) - 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]) +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) -playlist_box = PlaylistBox() +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() -def create_artist_pl(artist: str, replace: bool = False): - source_pl = pctl.active_playlist_viewing - this_pl = pctl.active_playlist_viewing +def selection_queue_deco(): + total = 0 + for item in shift_selection: + total += pctl.get_track(pctl.default_playlist[item]).length - 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 + total = get_hms_time(total) - playlist = [] + text = (_("Queue {N}").format(N=len(shift_selection))) + f" [{total}]" - 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) + return [colours.menu_text, colours.menu_background, text] - if replace: - pctl.multi_playlist[this_pl].playlist_ids[:] = playlist[:] - pctl.multi_playlist[this_pl].title = _("Artist: ") + artist - if album_mode: - reload_albums() +def toggle_rym(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_rym + prefs.show_rym ^= True + return None - # 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) +def toggle_band(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_band + prefs.show_band ^= True + return None - pctl.gen_codes[pl_to_id(this_pl)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" +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")) - pctl.multi_playlist.append( - pl_gen( - title=_("Artist: ") + artist, - playlist_ids=playlist, - hide_title=False, - parent=pl_to_id(source_pl))) +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...")) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" +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) - switch_playlist(len(pctl.multi_playlist) - 1) +def copy_to_clipboard(text: str) -> None: + SDL_SetClipboardText(text.encode(errors="surrogateescape")) +def copy_from_clipboard(): + return SDL_GetClipboardText().decode() -artist_list_menu.add(MenuItem(_("Filter to New Playlist"), create_artist_pl, pass_ref=True, icon=filter_icon)) +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")) -artist_list_menu.add_sub(_("View..."), 140) +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] -def aa_sort_alpha(): - prefs.artist_list_sort_mode = "alpha" - artist_list_box.saves.clear() + line = genius(s_artist, s_title, return_url=True) + r = requests.head(line, timeout=10) -def aa_sort_popular(): - prefs.artist_list_sort_mode = "popular" - artist_list_box.saves.clear() + 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 -def aa_sort_play(): - prefs.artist_list_sort_mode = "play" - artist_list_box.saves.clear() + show_message(_("Searching...")) + shoot = threading.Thread(target=ser_gen_thread, args=[tr]) + shoot.daemon = True + shoot.start() -def toggle_artist_list_style(): - if prefs.artist_list_style == 1: - prefs.artist_list_style = 2 - else: - prefs.artist_list_style = 1 +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) +def clip_ar_tr(index: int) -> None: + line = pctl.master_library[index].artist + " - " + pctl.master_library[index].title + SDL_SetClipboardText(line.encode("utf-8")) -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 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 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")] +def is_tidal_track(_) -> bool: + return pctl.master_library[r_menu_index].file_ext == "TIDAL" + +# def get_track_spot_url_show_test(_): +# if pctl.get_track(r_menu_index).misc.get("spotify-track-url"): +# return True +# return False -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 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] -def verify_discogs(): - return len(prefs.discogs_pat) == 40 +def get_spot_artist_track(index: int) -> None: + get_artist_spot(pctl.get_track(index)) +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 save_discogs_artist_thumb(artist, filepath): - logging.info("Searching discogs for artist image...") +def get_spot_album_track(index: int): + get_album_spot_active(pctl.get_track(index)) - # Make artist name url safe - artist = artist.replace("/", "").replace("\\", "").replace(":", "") +# 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)) - # 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"] +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(pctl.default_playlist[item]).album) + artists.append(pctl.get_track(pctl.default_playlist[item]).artist) + pctl.multi_playlist[pl].playlist_ids.append(pctl.default_playlist[item]) - # 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"] + if len(track_list) > 1: + if len(albums) > 0 and albums.count(albums[0]) == len(albums): + track = pctl.get_track(pctl.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] - # Respect rate limit - rate_remaining = r.headers["X-Discogs-Ratelimit-Remaining"] - if int(rate_remaining) < 30: - time.sleep(5) + elif len(track_list) == 1 and artists: + pctl.multi_playlist[pl].title = artists[0] - # 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"] + if tree_view_box.dragging_name: + pctl.multi_playlist[pl].title = tree_view_box.dragging_name - response = urllib.request.urlopen(url, context=ssl_context) - im = Image.open(response) + pctl.notify_change() - width, height = im.size - if width > height: - delta = width - height - left = int(delta / 2) - upper = 0 - right = height + left - lower = height +def queue_deco(): + if len(pctl.force_queue) > 0: + line_colour = colours.menu_text 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") + line_colour = colours.menu_text_disabled + return [line_colour, colours.menu_background, None] +def bass_test(_) -> bool: + # return True + return prefs.backend == 1 -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") +def gstreamer_test(_) -> bool: + # return True + return prefs.backend == 2 - response = urllib.request.urlopen(thumblink, timeout=10, context=ssl_context) - info = response.info() +def field_copy(text_field) -> None: + text_field.copy() - t = io.BytesIO() - t.seek(0) - t.write(response.read()) - l = 0 - t.seek(0, 2) - l = t.tell() - t.seek(0) +def field_paste(text_field) -> None: + text_field.paste() - if info.get_content_maintype() == "image" and l > 1000: - f = open(filepath, "wb") - f.write(t.read()) - f.close() +def field_clear(text_field) -> None: + text_field.clear() - 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") +def vis_off() -> None: + gui.vis_want = 0 + gui.update_layout() + # gui.turbo = False +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 -class ArtistList: + gui.vis_want = 1 + gui.update_layout() + # if prefs.backend == 2: + # show_message("Visualisers not implemented in GStreamer mode") + # gui.turbo = True - def __init__(self): +def spec_on() -> None: + gui.vis_want = 2 + # if prefs.backend == 2: + # show_message("Not implemented") + gui.update_layout() - self.tab_h = round(60 * gui.scale) - self.thumb_size = round(55 * gui.scale) +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 - self.current_artists = [] - self.current_album_counts = {} - self.current_artist_track_counts = {} + gui.vis_want = 3 + if prefs.backend == 2: + show_message(_("Not implemented")) + # gui.turbo = True + prefs.spec2_colour_setting = "custom" + gui.update_layout() - self.thumb_cache = {} +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.")) - self.to_fetch = "" - self.to_fetch_mbid_a = "" +def sa_artist() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Artist", 220, False]) + gui.update_layout() - self.scroll_position = 0 +def sa_album_artist() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Album Artist", 220, False]) + gui.update_layout() - self.id_to_load = "" +def sa_composer() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Composer", 220, False]) + gui.update_layout() - self.d_click_timer = Timer() - self.d_click_ref = -1 +def sa_title() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Title", 220, False]) + gui.update_layout() - self.click_ref = -1 - self.click_highlight_timer = Timer() +def sa_album() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Album", 220, False]) + gui.update_layout() - self.saves = {} +def sa_comment() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Comment", 300, False]) + gui.update_layout() - self.load = False +def sa_track() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["#", 25, True]) + gui.update_layout() - self.shown_letters = [] +def sa_count() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["P", 25, True]) + gui.update_layout() - self.hover_on = "NONE" - self.hover_timer = Timer(10) +def sa_scrobbles() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["S", 25, True]) + gui.update_layout() - self.sample_tracks = {} +def sa_time() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Time", 55, True]) + gui.update_layout() - def load_img(self, artist): +def sa_date() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Date", 55, True]) + gui.update_layout() - filepath = artist_info_box.get_data(artist, get_img_path=True) +def sa_genre() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Genre", 150, False]) + gui.update_layout() - if filepath and os.path.isfile(filepath): +def sa_file() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Filepath", 350, False]) + gui.update_layout() - try: - g = io.BytesIO() - g.seek(0) +def sa_filename() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Filename", 300, False]) + gui.update_layout() - im = Image.open(filepath) +def sa_codec() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Codec", 65, True]) + gui.update_layout() - 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), - )) +def sa_bitrate() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Bitrate", 65, True]) + gui.update_layout() - im.thumbnail((self.thumb_size, self.thumb_size), Image.Resampling.LANCZOS) +def sa_lyrics() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Lyrics", 50, True]) + gui.update_layout() - im.save(g, "PNG") - g.seek(0) +def sa_cue() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["CUE", 50, True]) + gui.update_layout() - 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) +def sa_star() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Starline", 80, True]) + gui.update_layout() - self.thumb_cache[artist] = [texture, sdl_rect] - except Exception: - logging.exception("Artist thumbnail processing error") - self.thumb_cache[artist] = None +def sa_disc() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Disc", 50, True]) + gui.update_layout() - elif artist in prefs.failed_artists: - self.thumb_cache[artist] = None - elif not self.to_fetch: +def sa_rating() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Rating", 80, True]) + gui.update_layout() - if prefs.auto_dl_artist_data: - self.to_fetch = artist - tauon.thread_manager.ready("worker") +def sa_love() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["❤", 25, True]) + # gui.pl_st.append(["❤", 25, True]) + gui.update_layout() - else: - self.thumb_cache[artist] = None +def key_love(index: int) -> bool: + return get_love_index(index) - def worker(self): +def key_artist(index: int) -> str: + return pctl.master_library[index].artist.lower() - if self.load: +def key_album_artist(index: int) -> str: + return pctl.master_library[index].album_artist.lower() - if after_scan: - return +def key_composer(index: int) -> str: + return pctl.master_library[index].composer.lower() - self.prep() - self.load = False - return +def key_comment(index: int) -> str: + return pctl.master_library[index].comment - if self.to_fetch: +def key_title(index: int) -> str: + return pctl.master_library[index].title.lower() - if get_lfm_wait_timer.get() < 2: - return +def key_album(index: int) -> str: + return pctl.master_library[index].album.lower() - 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() +def key_duration(index: int) -> int: + return pctl.master_library[index].length - if mbid and prefs.enable_fanart_artist: - save_fanart_artist_thumb(mbid, filepath3, preview=True) - got_image = True +def key_date(index: int) -> str: + return pctl.master_library[index].date - except Exception: - logging.exception("Failed to find image from fanart.tv") +def key_genre(index: int) -> str: + return pctl.master_library[index].genre.lower() - if not got_image and verify_discogs(): - try: - save_discogs_artist_thumb(artist, filepath4) - except Exception: - logging.exception("Failed to find image from discogs") +def key_t(index: int): + # return str(pctl.master_library[index].track_number) + return index_key(index) - 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) +def key_codec(index: int) -> str: + return pctl.master_library[index].file_ext - self.to_fetch = "" +def key_bitrate(index: int) -> int: + return pctl.master_library[index].bitrate - def prep(self): - self.scroll_position = 0 +def key_hl(index: int) -> int: + if len(pctl.master_library[index].lyrics) > 5: + return 0 + return 1 - curren_pl_no = id_to_pl(self.id_to_load) - if curren_pl_no is None: +def sort_ass(h, invert=False, custom_list=None, custom_name=""): + if custom_list is None: + if pl_is_locked(pctl.active_playlist_viewing): + show_message(_("Playlist is locked")) return - current_pl = pctl.multi_playlist[curren_pl_no] - all = [] - artist_parents = {} - counts = {} - play_time = {} - filtered = 0 - b = 0 + name = gui.pl_st[h][0] + playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + else: + name = custom_name + playlist = custom_list - try: + key = None + ns = False - for item in current_pl.playlist_ids: - b += 1 - if b % 100 == 0: - time.sleep(0.001) + 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 - track = pctl.get_track(item) + if custom_list is None: + if key is not None: + if ns: + key = natsort.natsort_keygen(key=key, alg=natsort.PATH) - 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) + playlist.sort(key=key, reverse=invert) - artists = [x.strip() for x in artists.split(";")] + pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids = playlist + pctl.default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - pp = 0 - if prefs.artist_list_sort_mode == "play": - pp = star_store.get(item) + 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() - for artist in artists: +def sort_dec(h): + sort_ass(h, True) - if artist: +def hide_set_bar(): + gui.set_bar = False + gui.update_layout() + gui.pl_update = 1 - # Add play time - if prefs.artist_list_sort_mode == "play": - p = play_time.get(artist, 0) - play_time[artist] = p + pp +def show_set_bar(): + gui.set_bar = True + gui.update_layout() + gui.pl_update = 1 - # Get a sample track for fallback art - if artist not in self.sample_tracks: - self.sample_tracks[artist] = track +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] - # 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 +def toggle_dim_albums(mode: int = 0) -> bool: + if mode == 1: + return prefs.dim_art - 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) + prefs.dim_art ^= True + gui.pl_update = 1 + gui.update += 1 - current_album_counts = artist_parents +def toggle_gallery_combine(mode: int = 0) -> bool: + if mode == 1: + return prefs.gallery_combine_disc - 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 ")) + prefs.gallery_combine_disc ^= True + reload_albums() - except Exception: - logging.exception("Album scan failure") - time.sleep(4) - return +def toggle_gallery_click(mode: int = 0) -> bool: + if mode == 1: + return prefs.gallery_single_click - # Artist-list, album-counts, scroll-position, playlist-length, number ignored - save = [all, current_album_counts, 0, len(current_pl.playlist_ids), counts, filtered] + prefs.gallery_single_click ^= True - # 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 +def toggle_gallery_thin(mode: int = 0) -> bool: + if mode == 1: + return prefs.thin_gallery_borders - 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 + prefs.thin_gallery_borders ^= True + gui.update += 1 + update_layout_do(tauon=tauon) - self.saves[current_pl.uuid_int] = save - gui.update += 1 +def toggle_gallery_row_space(mode: int = 0) -> bool: + if mode == 1: + return prefs.increase_gallery_row_spacing - def locate_artist_letter(self, text): + prefs.increase_gallery_row_spacing ^= True + gui.update += 1 + update_layout_do(tauon=tauon) - if not text or prefs.artist_list_sort_mode != "alpha": - return +def toggle_galler_text(mode: int = 0) -> bool: + if mode == 1: + return gui.gallery_show_text - 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 + gui.gallery_show_text ^= True + gui.update += 1 + update_layout_do(tauon=tauon) - 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 + # Jump to playing album + if prefs.album_mode and gui.first_in_grid is not None: - def locate_artist(self, track: TrackClass): + if gui.first_in_grid < len(pctl.default_playlist): + goto_album(gui.first_in_grid, force=True) - 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 +def toggle_card_style(mode: int = 0) -> bool: + if mode == 1: + return prefs.use_card_style - 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 + prefs.use_card_style ^= True + gui.update += 1 - def draw_card_text_only(self, artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg): +def toggle_side_panel(tauon: Tauon, mode: int = 0) -> bool: + global update_layout - album_mode = False - for albums in self.current_album_counts.values(): - if len(albums) > 1: - album_mode = True - break + if mode == 1: + return prefs.prefer_side - 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)) + prefs.prefer_side ^= True + update_layout = True - if gui.preview_artist_loading == artist: - # . Max 20 chars. Alt: Downloading image, Loading image - text = _("Downloading data...") + if prefs.album_mode or prefs.prefer_side is True: + gui.rsp = True + else: + gui.rsp = False - 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) + if prefs.prefer_side: + gui.rspw = gui.pref_rspw - def draw_card_with_thumbnail(self, artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg): +def force_album_view(tauon: Tauon) -> None: + toggle_album_mode(tauon=tauon, force_on=True) + +def enter_combo(tauon: Tauon) -> None: + if not tauon.gui.combo_mode: + tauon.gui.combo_was_album = tauon.prefs.album_mode + tauon.gui.showcase_mode = False + tauon.gui.radio_view = False + if tauon.prefs.album_mode: + toggle_album_mode(tauon=tauon) + if tauon.gui.rsp: + tauon.gui.rsp = False + tauon.gui.combo_mode = True + tauon.gui.update_layout() + +def exit_combo(restore: bool = False) -> None: + 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 - if artist not in self.thumb_cache: - self.load_img(artist) +def enter_showcase_view(track_id=None) -> 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() - 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) +def enter_radio_view(tauon: Tauon) -> None: + if not tauon.gui.combo_mode: + enter_combo(tauon=tauon) + tauon.gui.showcase_mode = False + tauon.gui.radio_view = True + tauon.gui.inp.mouse_click = False + tauon.gui.update_layout() - 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) +def standard_size(tauon: Tauon) -> None: + global window_size + global update_layout - 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)) + prefs.album_mode = False + tauon.gui.rsp = True + window_size = window_default_size + SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - 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) + gui.rspw = 80 + int(window_size[0] * 0.18) + update_layout = True + bag.album_mode_art_size = 130 + # clear_img_cache() - ddt.rect(tab_rect, back_colour_2) - bg = back_colour_2 +def path_stem_to_playlist(path: str, title: str) -> None: + """Used with gallery power bar""" + playlist = [] - ddt.rect(rect, back_colour) - ddt.rect(rect, border_colour) + # 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) - fields.add(rect) - if coll(rect) and is_level_zero(True): - self.hover_any = True + pctl.multi_playlist.append(pl_gen( + title=os.path.basename(title).upper(), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - hover_delay = 0.5 - if gui.compact_artist_list: - hover_delay = 2 + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pctl.active_playlist_viewing].title + "\" f\"" + path + "\"" - 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() + switch_playlist(len(pctl.multi_playlist) - 1) - if path: - set_artist_preview(path, artist, round(thumb_x + self.thumb_size), round(y)) +def goto_album(playlist_no: int, down: bool = False, force: bool = False) -> list | int | None: + logging.debug("Postion set by album locate") - if inp.mouse_click: - self.hover_timer.force_set(-2) - gui.delay_frame(2 + hover_delay) + if core_timer.get() < 0.5: + return None - 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) + global album_dex - 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) + # ---- + 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) / (bag.album_mode_art_size + album_h_gap)) + global last_row + last_row = row_len + # ---- - # Draw labels - if not thin_mode or (coll(area) and is_level_zero() and y + self.tab_h < window_size[1] - gui.panelBY): + px = 0 + row = 0 + re = 0 - album_mode = False - for albums in self.current_album_counts.values(): - if len(albums) > 1: - album_mode = True - break + 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 += bag.album_mode_art_size + album_v_gap - 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 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]: - if gui.preview_artist_loading == artist: - # . Max 20 chars. Alt: Downloading image, Loading image - text = _("Downloading data...") + # 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] > (bag.album_mode_art_size + album_v_gap) * 2: + while not gui.album_scroll_px - 20 < px + (bag.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 - 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) + gui.album_scroll_px = max(gui.album_scroll_px, 0 - album_v_slide_value) - def draw_card(self, artist, x, y, w): + if len(album_dex) > 0: + return album_dex[re] + return 0 - 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) + gui.update += 1 # TODO(Martin): WTF Unreachable?? - fields.add(area) +def toggle_album_mode(tauon: Tauon, force_on: bool = False) -> None: + global update_layout + global album_playlist_width + global old_album_pos + gui = tauon.gui + pctl = tauon.pctl + gui.gall_tab_enter = False - light_mode = False - line1_colour = [235, 235, 235, 255] - line2_colour = [255, 255, 255, 120] - fade_max = 50 + if prefs.album_mode is True: + prefs.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: + prefs.album_mode = True + if gui.combo_mode: + exit_combo() - thin_mode = False - if gui.compact_artist_list: - thin_mode = True - line2_colour = [115, 115, 115, 255] + gui.rsp = True + gui.rspw = gui.pref_gallery_w - 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] + space = tauon.bag.window_size[0] - gui.rspw + if gui.lsp: + space -= gui.lspw - # Fade on click - bg = colours.side_panel_background - if not thin_mode: + if prefs.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 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: + reload_albums(quiet=True) - fade = 0 - t = self.click_highlight_timer.get() - if self.click_ref == artist and (t < 2.2 or artist_list_menu.active): + # if pctl.active_playlist_playing == pctl.active_playlist_viewing: + # goto_album(pctl.playlist_playing_position) - if t < 1.9 or artist_list_menu.active: - fade = fade_max - else: - fade = fade_max - round((t - 1.9) / 0.3 * fade_max) + if prefs.album_mode: + if pctl.selected_in_playlist < len(pctl.playing_playlist()): + goto_album(pctl.selected_in_playlist) - gui.update += 1 - ddt.rect(area, [50, 50, 50, fade]) +def toggle_gallery_keycontrol(tauon: Tauon, always_exit: bool = False) -> None: + if is_level_zero(): + if not prefs.album_mode: + toggle_album_mode(tauon=tauon) + 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(tauon=tauon) + else: + gui.album_tab_mode ^= True + if gui.album_tab_mode: + show_in_gal(pctl.selected_in_playlist, silent=True) - bg = alpha_blend([50, 50, 50, fade], colours.side_panel_background) +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) - 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) +def switch_playlist(number, cycle=False, quiet=False): + 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: - self.draw_card_text_only(artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg) + pctl.radio_playlist_viewing = number + if pctl.radio_playlist_viewing > len(pctl.radio_playlists) - 1: + pctl.radio_playlist_viewing = 0 + return - 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 + gui.previous_playlist_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - double_click = False - if self.d_click_timer.get() < 0.4 and self.d_click_ref == artist: - double_click = True + 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 - self.click_highlight_timer.set() + # if pl_follow: + # pctl.multi_playlist[pctl.playlist_active][1] = copy.deepcopy(pctl.playlist_playing) - 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) + if gui.showcase_mode and gui.combo_mode and not quiet: + view_standard() + pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids = pctl.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 - blocks = [] - current_block = [] + if gall_pl_switch_timer.get() > 240: + gui.gallery_positions.clear() + gall_pl_switch_timer.set() - in_artist = False - this_artist = artist.casefold() - last_ref = None - on = 0 + gui.gallery_positions[gui.previous_playlist_id] = gui.album_scroll_px - 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) + if cycle: + pctl.active_playlist_viewing += number + else: + pctl.active_playlist_viewing = number - 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 + 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 current_block: - blocks.append(current_block) - current_block = [] + pctl.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 prefs.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) - #logging.info(blocks) - # return + if prefs.auto_goto_playing: + pctl.show_current(this_only=True, playing=False, highlight=True, no_switch=True) - # 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 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 blocks: - return +def cycle_playlist_pinned(step): + if gui.radio_view: - #select = block_starts[0] + 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 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] + 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 - gui.pl_update += 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 - self.click_highlight_timer.set() +def activate_info_box(): + fader.rise() + pref_box.enabled = True - select = blocks[0][0] +def activate_radio_box(): + radiobox.active = True + radiobox.radio_field.clear() + radiobox.radio_field_title.clear() - if double_click: - # Stat first artist track in playlist +def new_playlist_colour_callback(): + if gui.radio_view: + return [120, 90, 245, 255] + return [237, 80, 221, 255] - 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 +def new_playlist_deco(): + if gui.radio_view: + text = _("New Radio List") + else: + text = _("New Playlist") + return [colours.menu_text, colours.menu_background, text] - 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))) +def clean_db_show_test(_): + return gui.suggest_clean_db - 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) +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 - if middle_click: - self.click_ref = artist - self.click_highlight_timer.set() - create_artist_pl(artist) +def clean_db_deco(): + return [colours.menu_text, [30, 150, 120, 255], _("Clean Database!")] - if right_click: - self.click_ref = artist - self.click_highlight_timer.set() +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) - artist_list_menu.activate(in_reference=artist) + if prefs.album_mode: + reload_albums() + gui.pl_update += 1 - def render(self, x, y, w, h): +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] - if prefs.artist_list_style == 1: - self.tab_h = round(60 * gui.scale) - else: - self.tab_h = round(22 * gui.scale) +def show_import_music(_): + return gui.add_music_folder_ready - viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int +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 - # use parent playlst is set - if pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id: +def stt2(sec): + days, rem = divmod(sec, 86400) + hours, rem = divmod(rem, 3600) + min, sec = divmod(rem, 60) - # 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 + s_day = str(days) + "d" + if s_day == "0d": + s_day = " " - 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] + s_hours = str(hours) + "h" + if s_hours == "0h" and s_day == " ": + s_hours = " " - 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 + s_min = str(min) + "m" + return s_day.rjust(3) + " " + s_hours.rjust(3) + " " + s_min.rjust(3) - else: +def export_database(): + path = str(user_directory / "DatabaseExport.csv") + xport = open(path, "w") - # 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") + xport.write("Artist;Title;Album;Album artist;Track number;Type;Duration;Release date;Genre;Playtime;File path") - area = (x, y, w, h) - area2 = (x + 1, y, w - 3, h) + for index, track in pctl.master_library.items(): - ddt.rect(area, colours.side_panel_background) - ddt.text_background_colour = colours.side_panel_background + xport.write("\n") - 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) + 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)) - range = (h // self.tab_h) - 1 + xport.close() + show_message(_("Export complete."), _("Saved as: ") + path, mode="done") - whole_rage = math.floor(h // self.tab_h) +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)) - if range > 4 and self.scroll_position > len(self.current_artists) - range: - self.scroll_position = len(self.current_artists) - range +def clean_db(tauon: Tauon) -> None: + tauon.prefs.remove_network_tracks = False + tauon.cm_clean_db = True + tauon.thread_manager.ready("worker") - if len(self.current_artists) <= whole_rage: - self.scroll_position = 0 +def clean_db2(tauon: Tauon) -> None: + tauon.prefs.remove_network_tracks = True + tauon.cm_clean_db = True + tauon.thread_manager.ready("worker") - 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) +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) - if not self.current_artists: - text = _("No artists in playlist") + show_message(_("{N} ratings imported").format(N=str(len(unique))), mode="done") - 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...") + gui.pl_update += 1 - ddt.text( - (x + w // 2, y + (h // 7), 2), text, alpha_mod(colours.side_bar_line2, 100), 212, - max_w=w - 17 * gui.scale) +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 - yy = y + 12 * gui.scale + 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) - i = int(self.scroll_position) + s = str(len(unique)) + " ratings imported" + if len(skipped) > 0: + s += f", {len(skipped)} skipped" + show_message(s, mode="done") - if viewing_pl_id in self.saves: - self.saves[viewing_pl_id][2] = self.scroll_position + gui.pl_update += 1 - prefetch_mode = False - prefetch_distance = 22 +def clear_ratings() -> None: + if not inp.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 - self.shown_letters.clear() +def find_incomplete() -> None: + gen_incomplete(pctl.active_playlist_viewing) - self.hover_any = False +def cast_deco(tauon: Tauon) -> list: + 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] - for i, artist in enumerate(self.current_artists[i:], start=i): +def cast_search2(tauon: Tauon) -> None: + tauon.chrome.rescan() - if not prefetch_mode: - self.draw_card(artist, x, round(yy), w) +def cast_search(tauon: Tauon) -> None: + if tauon.chrome_mode: + tauon.pctl.stop() + tauon.chrome.end() + else: + if not tauon.chrome: + show_message(_("pychromecast not found")) + return + show_message(_("Searching for Chomecasts...")) + shooter(cast_search2, [tauon]) - yy += self.tab_h +def clear_queue() -> None: + pctl.force_queue = [] + gui.pl_update = 1 + pctl.pause_queue = False - if yy - y > h - 24 * gui.scale: - prefetch_mode = True - continue +def set_mini_mode_A1() -> None: + prefs.mini_mode_mode = 0 + set_mini_mode() - 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 +def set_mini_mode_B1() -> None: + prefs.mini_mode_mode = 1 + set_mini_mode() - if not self.hover_any: - gui.preview_artist = "" - self.hover_timer.force_set(10) - artist_preview_render.show = False - self.hover_on = False +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() -artist_list_box = ArtistList() +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() -class TreeView: +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 - def __init__(self): +def stop() -> None: + pctl.stop() - self.trees = {} # Per playlist tree - self.rows = [] # For display (parsed from tree) - self.rows_id = "" +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() - self.opens = {} # Folders clicks to show per playlist +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) - self.scroll_positions = {} + key = random.choice(list(folders.keys())) + result = folders[key] + pctl.jump(*result) + pctl.show_current() - # Recursive gen_rows vars - self.count = 0 - self.depth = 0 +def radio_random() -> None: + pctl.advance(rr=True) - self.background_processing = False - self.d_click_timer = Timer(100) - self.d_click_id = "" +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 - self.menu_selected = "" - self.folder_colour_cache = {} - self.dragging_name = "" +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) - self.force_opens = [] - self.click_drag_source = None + rect = (x - round(5 * gui.scale), y - round(4 * gui.scale), round(80 * gui.scale), round(16 * gui.scale)) + gui.heart_fields.append(rect) - self.tooltip_on = "" - self.tooltip_timer = Timer(10) + if tauon.coll(rect) and (inp.mouse_click or (is_level_zero() and not inp.quick_drag)): + gui.pl_update = 2 + pp = inp.mouse_position[0] - x - self.lock_pl = None + if pp < 5 * gui.scale: + rat = 0 + elif pp > 70 * gui.scale: + rat = 10 + else: + rat = pp // (star_row_icon.w // 2) - # self.bold_colours = ColourGenCache(0.6, 0.7) + 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) - def clear_all(self): - self.rows_id = "" - self.trees.clear() + # bg = colours.grey(40) + bg = [255, 255, 255, 17] + fg = colours.grey(210) - def collapse_all(self): - pl_id = pl_to_id(pctl.active_playlist_viewing) + if gui.tracklist_bg_is_light: + bg = [0, 0, 0, 25] + fg = colours.grey(70) - if self.lock_pl: - pl_id = self.lock_pl + 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) - opens = self.opens.get(pl_id) - if opens is None: - opens = [] - self.opens[pl_id] = opens + for ss in range(5): - opens.clear() - self.rows_id = "" + xx = x + ss * star_row_icon.w - def clear_target_pl(self, pl_number, pl_id=None): + 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 pl_id is None: - pl_id = pl_to_id(pl_number) + 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) - if gui.lsp and prefs.left_panel_mode == "folder view": +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")] - 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 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 show_track(self, track: TrackClass) -> None: +def bar_love_notify() -> None: + bar_love(notify=True) - if track is None: - return +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] - # 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 + shoot_love = threading.Thread(target=love, args=[True, track_id, False, notify]) + shoot_love.daemon = True + shoot_love.start() - tree = self.trees.get(pl_id) - if not tree: - return +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 - scroll_position = self.scroll_positions.get(pl_id) - if scroll_position is None: - scroll_position = 0 +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() - # Clear all opened folders - opens.clear() +def toggle_spotify_like_active_deco(): + tr = pctl.playing_object() + text = _("Spotify Like Track") - # Set every folder in path as opened - path = "" - crumbs = track.parent_folder_path.split("/")[1:] - for c in crumbs: - path += "/" + c - opens.append(path) + 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") - # Regenerate row display - self.gen_rows(tree, opens) + return [colours.menu_text, colours.menu_background, text] - # Locate and set scroll position to playing folder - for i, row in enumerate(self.rows): - if row[1] + "/" + row[0] == track.parent_folder_path: +def locate_artist() -> None: + track = pctl.playing_object() + if not track: + return - scroll_position = i - 5 - scroll_position = max(scroll_position, 0) - break + artist = track.artist + if track.album_artist: + artist = track.album_artist - 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) + block_starts = [] + current = False + for i in range(len(pctl.default_playlist)): + track = pctl.get_track(pctl.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 - self.scroll_positions[pl_id] = scroll_position + if block_starts: - gui.update_layout() - gui.update += 1 + next = False + for start in block_starts: - def get_pl_id(self): - if self.lock_pl: - return self.lock_pl - return pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + if next: + pctl.selected_in_playlist = start + pctl.playlist_view_position = start + shift_selection.clear() + break - def render(self, x, y, w, h): + if pctl.selected_in_playlist == start: + next = True + continue - global quick_drag + else: + pctl.selected_in_playlist = block_starts[0] + pctl.playlist_view_position = block_starts[0] + shift_selection.clear() - pl_id = self.get_pl_id() + tree_view_box.show_track(pctl.get_track(pctl.default_playlist[pctl.selected_in_playlist])) + else: + show_message(_("No exact matching artist could be found in this playlist")) - tree = self.trees.get(pl_id) + logging.debug("Position changed by artist locate") - # 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() + gui.pl_update += 1 - self.playlist_id_on = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int +def activate_search_overlay(tauon: Tauon) -> None: + if tauon.cm_clean_db: + show_message(_("Please wait for cleaning process to finish")) + return + tauon.search_over.active = True + tauon.search_over.delay_enter = False + tauon.search_over.search_text.selection = 0 + tauon.search_over.search_text.cursor_position = 0 + tauon.search_over.spotify_mode = False - opens = self.opens.get(pl_id) - if opens is None: - opens = [] - self.opens[pl_id] = opens +def get_album_spot_url_active() -> None: + tr = pctl.playing_object() + if tr: + url = tauon.spot_ctl.get_album_url_from_local(tr) - scroll_position = self.scroll_positions.get(pl_id) - if scroll_position is None: - scroll_position = 0 + if url: + copy_to_clipboard(url) + show_message(_("URL copied to clipboard"), mode="done") + else: + show_message(_("No results found")) - area = (x, y, w, h) - fields.add(area) - ddt.rect(area, colours.side_panel_background) - ddt.text_background_colour = colours.side_panel_background +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") - 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 + return [colours.menu_text, colours.menu_background, text] - # 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 +def goto_playing_extra() -> None: + pctl.show_current(highlight=True) - 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) +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] - else: - return +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 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 +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) - yy = y + round(11 * gui.scale) - xx = x + round(22 * 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) - spacing = round(21 * gui.scale) - max_scroll = len(self.rows) - (h // round(22 * gui.scale)) +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")) - mouse_in = coll(area) +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")) - # 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) +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")) - focused = is_level_zero() +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])) - # 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) + spotify_playlist_menu.add(MenuItem(_("> Import All Playlists"), spot_import_playlists)) + spotify_playlist_menu.activate(position=(tauon.extra_menu.pos[0], window_size[1] - gui.panelBY)) + else: + show_message(_("Please wait until current job is finished")) - self.scroll_positions[pl_id] = scroll_position +def spot_import_context() -> None: + shooter(tauon.spot_ctl.import_context) - # Draw folder rows - playing_track = pctl.playing_object() - max_w = w - round(45 * gui.scale) +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] - light_mode = test_lumi(colours.side_panel_background) < 0.5 - semilight_mode = test_lumi(colours.side_panel_background) < 0.8 +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,)) - for i, item in enumerate(self.rows): +# 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] - if i < scroll_position: - continue +def toggle_auto_theme(mode: int = 0) -> None: + if mode == 1: + return prefs.colour_from_image - if yy > y + h - spacing: - break + prefs.colour_from_image ^= True + gui.theme_temp_current = -1 - target = item[1] + "/" + item[0] + gui.reload_theme = True - 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) + # if prefs.colour_from_image and prefs.art_bg and not inp.key_shift_down: + # toggle_auto_bg() - # text_colour = [255, 255, 255, 100] - text_colour = rgb_add_hls(colours.side_panel_background, 0, 0.35, -0.15) +def toggle_auto_bg(mode: int= 0) -> bool | None: + if mode == 1: + return prefs.art_bg + prefs.art_bg ^= True - box_colour = [200, 100, 50, 255] + if prefs.art_bg: + gui.update = 60 - if semilight_mode: - text_colour = [255, 255, 255, 180] + style_overlay.flush() + tauon.thread_manager.ready("style") + # if prefs.colour_from_image and prefs.art_bg and not inp.key_shift_down: + # toggle_auto_theme() + return None - if light_mode: - text_colour = [0, 0, 0, 200] +def toggle_auto_bg_strong(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.art_bg_stronger == 2 - full_folder_path = item[1] + "/" + item[0] + if prefs.art_bg_stronger == 2: + prefs.art_bg_stronger = 1 + else: + prefs.art_bg_stronger = 2 + gui.update_layout() + return None - # 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] +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 - # 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] +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 - # 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] +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 - 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)) +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 mouse_in and not tree_view_scroll.held: +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 - if middle_click: - stem_to_new_playlist(full_folder_path) +def toggle_notifications(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.show_notifications - elif right_click: + prefs.show_notifications ^= True - if item[3]: + if prefs.show_notifications: + if not de_notify_support: + show_message(_("Notifications for this DE not supported"), "", mode="warning") + return None - 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 +# 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 - elif inp.mouse_click: - # quick_drag = True +def toggle_mini_lyrics(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.show_lyrics_side + prefs.show_lyrics_side ^= True + return None - if not self.click_drag_source: - self.click_drag_source = item - set_drag_source() +def toggle_showcase_vis(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.showcase_vis - elif mouse_up and self.click_drag_source == item: - # Click tree level folder to open/close branch + prefs.showcase_vis ^= True + gui.update_layout() + return None - if target not in opens: - opens.append(target) - else: - for s in reversed(range(len(opens))): - if opens[s].startswith(target): - del opens[s] +def toggle_level_meter(mode: int = 0) -> bool | None: + if mode == 1: + return gui.vis_want != 0 - if item[3]: + if gui.vis_want == 0: + gui.vis_want = 1 + else: + gui.vis_want = 0 - # 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 + gui.update_layout() + return None - 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 +# 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() - # Double click base folder to play - else: - pctl.jump(track_id) +def level_meter_special_2(): + gui.level_meter_colour_mode = 2 - # Regenerate display rows after clicking - self.gen_rows(tree, opens) +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") - # 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] + bg = colours.menu_background - # 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 + return [colours.menu_text, bg, line] - if mouse_in: - tw = ddt.get_text_w(item[0], font) +def lastfm_colour() -> list[int] | None: + if not prefs.scrobble_hold: + return [250, 50, 50, 255] + return None - if self.tooltip_on != item: - self.tooltip_on = item - self.tooltip_timer.set() - gui.frame_callback_list.append(TestTimer(0.6)) +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 - 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) +def lb_mode() -> bool: + return prefs.enable_lb - # # 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) +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 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) + 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 - # 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) + if not release_group_id: + release_group_id = tr.misc.get("musicbrainz_releasegroupid") - 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) + if not release_id: + release_id = tr.misc.get("musicbrainz_albumid") - yy += spacing + 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 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 + 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 - self.dragging_name = self.click_drag_source[0] - logging.info(self.dragging_name) + image_data = None + final_id = None + if release_group_id: + url = pctl.mbid_image_url_cache.get(release_group_id) + if url: + return url - if "/" in self.dragging_name: - self.dragging_name = os.path.basename(self.dragging_name) + base_url = "https://coverartarchive.org/release-group/" + url = f"{base_url}{release_group_id}" - 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 + 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 self.dragging_name and not quick_drag: - self.dragging_name = "" - if not mouse_down: - self.click_drag_source = None + if release_id and not image_data: + url = pctl.mbid_image_url_cache.get(release_id) + if url: + return url - def gen_row(self, tree_point, path, opens): + base_url = "https://coverartarchive.org/release/" + url = f"{base_url}{release_id}" - 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 + 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 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 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 - # 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])]) + url = image["thumbnails"].get("250") + if url is None: + url = image["thumbnails"].get("small") - # 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 url: + logging.info("got mb image url for discord") + pctl.mbid_image_url_cache[final_id] = url + return url - # 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]) + pctl.album_mbid_release_cache[(artist, tr.album)] = None + pctl.album_mbid_release_group_cache[(artist, tr.album)] = None - self.depth += 1 - enter_level = True + return None - self.gen_row(item[0], p, opens) +def discord_loop() -> None: + prefs.discord_active = True - if enter_level: - self.depth -= 1 + try: + if not pctl.playing_ready(): + return + asyncio.set_event_loop(asyncio.new_event_loop()) - def gen_rows(self, tree, opens): - self.count = 0 - self.depth = 0 - self.rows.clear() - self.force_opens.clear() + # logging.info("Attempting to connect to Discord...") + client_id = "954253873160286278" + RPC = Presence(client_id) + RPC.connect() - self.gen_row(tree, "", opens) + logging.info("Discord RPC connection successful.") + time.sleep(1) + start_time = time.time() + idle_time = Timer() - gui.update_layout() + state = 0 + index = -1 + br = False + gui.discord_status = "Connected" gui.update += 1 + current_state = 0 - def gen_tree(self, pl_id): - pl_no = id_to_pl(pl_id) - if pl_no is None: - return + while True: + while True: - playlist = pctl.multi_playlist[pl_no].playlist_ids - # Generate list of all unique folder paths - paths = [] - z = 5000 - for p in playlist: + current_index = pctl.playing_object().index + if pctl.playing_state == 3: + current_index = radiobox.song_key - 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) + 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 - # 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() + 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 -tree_view_box = TreeView() + time.sleep(2) + if prefs.disconnect_discord: + RPC.clear(pid) + RPC.close() + prefs.disconnect_discord = False + gui.discord_status = "Not connected" + br = True + break -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")] + 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") -# 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] + if tr.album: + album = tr.album + else: + album = _("Unknown Album") + if pctl.playing_state == 3: + album = radiobox.loaded_station["title"] -class QueueBox: + if len(album) == 1: + album += " " - def recalc(self): - self.tab_h = 34 * gui.scale - def __init__(self): + if state == 1: + #logging.info("PLAYING: " + title) + #logging.info(start_time) + url = get_album_art_url(pctl.playing_object()) - 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() + 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) - 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)) + else: + #logging.info("Discord RPC - Stop") + RPC.update( + pid=pid, + state="Idle", + large_image="tauon-standard") - 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")) + time.sleep(5) - queue_menu.add(MenuItem(_("↳ Except for This"), self.clear_queue_crop, show_test=self.except_for_this_show_test)) + if prefs.disconnect_discord: + RPC.clear(pid) + RPC.close() + prefs.disconnect_discord = False + break - 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) + 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 - def except_for_this_show_test(self, _): - return self.queue_remove_show(_) and test_shift(_) + finally: + loop = asyncio.get_event_loop() + if not loop.is_closed(): + loop.close() + prefs.discord_active = False - def make_as_playlist(self): +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() - if pctl.force_queue: - playlist = [] - for item in pctl.force_queue: +def open_donate_link() -> None: + webbrowser.open("https://github.com/sponsors/Taiko2k", new=2, autoraise=True) - if item.type == 0: - playlist.append(item.track_id) - else: +def stop_quick_add() -> None: + pctl.quick_add_target = None - pl = id_to_pl(item.playlist_id) - if pl is None: - logging.info("Lost the target playlist") - continue +def show_stop_quick_add(_) -> bool: + return pctl.quick_add_target is not None + +def view_tracks(tauon: Tauon) -> None: + # if gui.show_playlist is False: + # gui.show_playlist = True + if prefs.album_mode: + toggle_album_mode(tauon=tauon) + 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 prefs.album_mode: +# toggle_album_mode(tauon=tauon) +# if gui.combo_mode: +# toggle_combo_view(off=True) +# if not gui.rsp: +# toggle_side_panel() +# update_layout = True +# gui.rspw = window_size[0] + +def view_standard_meta(tauon: Tauon) -> None: + # if gui.show_playlist is False: + # gui.show_playlist = True + if prefs.album_mode: + toggle_album_mode(tauon=tauon) - pp = pctl.multi_playlist[pl].playlist_ids + if gui.combo_mode: + exit_combo() - i = item.position # = pctl.playlist_playing_position + 1 + if not gui.rsp: + toggle_side_panel() - parts = [] - album_parent_path = pctl.get_track(item.track_id).parent_folder_path + global update_layout + update_layout = True + # gui.rspw = 80 + int(window_size[0] * 0.18) - while i < len(pp): - if pctl.get_track(pp[i]).parent_folder_path != album_parent_path: - break +def view_standard(tauon: Tauon) -> None: + # if gui.show_playlist is False: + # gui.show_playlist = True + if prefs.album_mode: + toggle_album_mode(tauon=tauon) + if gui.combo_mode: + exit_combo() + if not gui.rsp: + toggle_side_panel() - parts.append((pp[i], i)) - i += 1 +def standard_view_deco(): + if prefs.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] - for part in parts: - playlist.append(part[0]) +# def gallery_only_view(): +# if gui.show_playlist is False: +# return +# if not prefs.album_mode: +# toggle_album_mode(tauon=tauon) +# gui.show_playlist = False +# update_layout = True +# gui.rspw = window_size[0] +# album_playlist_width = gui.playlist_width +# #gui.playlist_width = -19 - pctl.multi_playlist.append( - pl_gen( - title=_("Queued Tracks"), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) +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 drop_tracks_insert(self, insert_position): +def library_deco(): + tc = colours.menu_text + if gui.combo_mode or (gui.show_playlist is False and prefs.album_mode): + tc = colours.menu_text_disabled - global quick_drag + if gui.set_mode: + return [tc, colours.menu_background, _("Disable Columns")] + return [tc, colours.menu_background, _("Enable Columns")] - if not shift_selection: - return +def break_deco(): + tex = colours.menu_text + if gui.combo_mode or (gui.show_playlist is False and prefs.album_mode): + tex = colours.menu_text_disabled + if not break_enable: + tex = colours.menu_text_disabled - # 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) + 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")] - playlist_index = pctl.active_playlist_viewing - playlist_id = pl_to_id(pctl.active_playlist_viewing) +def toggle_playlist_break() -> None: + pctl.multi_playlist[pctl.active_playlist_viewing].hide_title ^= 1 + gui.pl_update = 1 - main_track_position = shift_selection[0] - main_track_id = default_playlist[main_track_position] - quick_drag = False +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 len(shift_selection) > 1: + 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 - # 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 + t = pctl.master_library[track] - 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)) + path = t.fullpath + cleanup = False - def clear_queue_crop(self): + 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 - save = False - for item in pctl.force_queue: - if item.uuid_int == self.right_click_id: - save = item - break + if not os.path.isfile(path): + show_message(_("Encoding warning: Missing one or more files")) + core_use -= 1 + return - clear_queue() - if save: - pctl.force_queue.append(save) + out_line = encode_track_name(t) - def play_now(self): + if not (output / _("output")).exists(): + (output / _("output")).mkdir() + target_out = str(output / _("output") / (str(track) + "." + codec)) - 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 + command = tauon.get_ffmpeg() + " " - del pctl.force_queue[queue_index] - # [trackid, position, pl_id, type, album_stage, uid_gen(), auto_stop] + if not t.is_cue: + command += '-i "' + else: + command += "-ss " + str(t.start_time) + command += " -t " + str(t.length) - if pctl.force_queue and pctl.force_queue[0].album_stage == 1: - split_queue_album(None) + command += ' -i "' - target_track_id = queue_item.track_id + command += path.replace('"', '\\"') - pl = id_to_pl(queue_item.playlist_id) - if pl is not None: - pctl.active_playlist_playing = pl + 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 target_track_id not in pctl.playing_playlist(): - pctl.advance() - return + if codec != "flac": + command += " -b:a " + str(bitrate) + "k -vn " - pctl.jump(target_track_id, queue_item.position) + command += '"' + target_out.replace('"', '\\"') + '"' - if queue_item.type == 1: # is album type - queue_item.album_stage = 1 # set as partway playing - pctl.force_queue.insert(0, queue_item) + # logging.info(shlex.split(command)) + startupinfo = None + if system == "Windows" or msys: + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - def toggle_auto_stop(self) -> None: + if not msys: + command = shlex.split(command) - for item in pctl.force_queue: - if item.uuid_int == self.right_click_id: - item.auto_stop ^= True - break + subprocess.call(command, stdout=subprocess.PIPE, shell=False, startupinfo=startupinfo) - def toggle_auto_stop_deco(self): + logging.info("FFmpeg finished") + if codec == "opus" and prefs.transcode_opus_as: + codec = "ogg" - enabled = False - for item in pctl.force_queue: - if item.uuid_int == self.right_click_id: - if item.auto_stop: - enabled = True - break + # logging.info(target_out) - if enabled: - return [colours.menu_text, colours.menu_background, _("Cancel Auto-Stop")] - return [colours.menu_text, colours.menu_background, _("Auto-Stop")] + 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 queue_remove_show(self, id: int) -> bool: + 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) - if self.right_click_id is not None: - return True - return False + old_key = star_store.key(track) + old_star = star_store.full_get(track) - def right_remove_item(self) -> None: + try: + send2trash(pctl.master_library[track].fullpath) + except Exception: + logging.exception("File trash error") - if self.right_click_id is None: - show_message(_("Eh?")) + if os.path.isfile(pctl.master_library[track].fullpath): + try: + os.remove(pctl.master_library[track].fullpath) + except Exception: + logging.exception("File delete error") - 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")) + pctl.master_library[track].fullpath = new_name + pctl.master_library[track].file_ext = codec.upper() - def toggle_pause(self) -> None: - pctl.pause_queue ^= True + # Update and merge playtimes + new_key = star_store.key(track) + if old_star and (new_key != old_key): - 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: + new_star = star_store.full_get(track) + if new_star is None: + new_star = star_store.new_object() - # text_colour = [230, 230, 230, 255] - bg = colours.queue_background + 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 fq[i].type == 0: + if old_key in star_store.db: + del star_store.db[old_key] - rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h) + star_store.db[new_key] = new_star - if draw_back: - ddt.rect(rect, colours.queue_card_background) - bg = colours.queue_card_background + gui.transcoding_bach_done += 1 + if cleanup: + os.remove(path) + core_use -= 1 + gui.update += 1 - 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] +def cue_scan(content: str, tn: TrackClass) -> int | None: + # Get length from backend - tauon.gall_ren.render(track, (rect[0] + 4 * gui.scale, rect[1] + 4 * gui.scale), round(28 * gui.scale)) + lasttime = tn.length - ddt.rect((rect[0] + 4 * gui.scale, rect[1] + 4 * gui.scale, 26, 26), [0, 0, 0, 6]) + content = content.replace("\r", "") + content = content.split("\n") - line = track.album - if fqo.type == 0: - line = track.title + #logging.info(content) - if not line: - line = clean_string(track.filename) + global added - line2y = yy + 14 * gui.scale + cued = [] - artist_line = track.artist - if fqo.type == 1 and track.album_artist: - artist_line = track.album_artist + LENGTH = 0 + PERFORMER = "" + TITLE = "" + START = 0 + DATE = "" + ALBUM = "" + GENRE = "" + MAIN_PERFORMER = "" - if fqo.type == 0 and not artist_line: - line2y -= 7 * gui.scale + for LINE in content: + if 'TITLE "' in LINE: + ALBUM = LINE[7:len(LINE) - 2] - 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) + if 'PERFORMER "' in LINE: + while LINE[0] != "P": + LINE = LINE[1:] - ddt.text( - (rect[0] + (40 * gui.scale), line2y), line, text_colour2, 211, - max_w=rect[2] - 60 * gui.scale, bg=bg) + MAIN_PERFORMER = LINE[11:len(LINE) - 2] - 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 "REM DATE" in LINE: + DATE = LINE[9:len(LINE) - 1] - 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]) + if "REM GENRE" in LINE: + GENRE = LINE[10:len(LINE) - 1] - def draw(self, x: int, y: int, w: int, h: int): + if "TRACK " in LINE: + break - yy = y + 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 - yy += round(4 * gui.scale) + START = int(LINE[pos - 2:pos]) + (int(LINE[pos - 5:pos - 3]) * 60) + LENGTH = int(lasttime) - START + lasttime = START - sep_colour = alpha_blend([255, 255, 255, 11], colours.queue_background) + 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 - 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) + elif 'TITLE "' in LINE: - yy += round(3 * gui.scale) + 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 - box_rect = (x, yy - 6 * gui.scale, w, h) - ddt.rect(box_rect, colours.queue_background) - ddt.text_background_colour = colours.queue_background + elif "TRACK " in LINE: - 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) + pos = 0 + while LINE[pos] != "K": + pos += 1 + if pos > 15: + return 1 + TN = LINE[pos + 2:pos + 4] - # if y < gui.panelY * 2: - # ddt.rect((x, y - 3 * gui.scale, w, 30 * gui.scale), colours.queue_background, True) + TN = int(TN) - 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) + # try: + # bitrate = audio.info.bitrate + # except Exception: + # logging.exception("Failed to set audio bitrate") + # bitrate = 0 - qb_right_click = 0 + if PERFORMER == "": + PERFORMER = MAIN_PERFORMER - if coll(box_rect): - # Update scroll position - self.scroll_position += mouse_wheel * -1 - self.scroll_position = max(self.scroll_position, 0) + nt = copy.deepcopy(tn) - if right_click: - qb_right_click = 1 + nt.cue_sheet = "" + nt.is_embed_cue = True - # 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] + 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) - line = _("Up Next:") - if pctl.force_queue: - # line = "Queue" - ddt.text((x + (10 * gui.scale), yy + 2 * gui.scale), line, text_colour, 211) + pctl.master_library[pctl.master_count] = nt - yy += 7 * gui.scale + cued.append(pctl.master_count) + # loaded_pathes_cache[filepath.replace('\\', '/')] = pctl.master_count + # added.append(pctl.master_count) - if len(pctl.force_queue) < 3: - self.scroll_position = 0 + pctl.master_count += 1 + LENGTH = 0 + PERFORMER = "" + TITLE = "" + START = 0 + TN = 0 - # Draw square dots to indicate view has been scrolled down - if self.scroll_position > 0: - ds = 3 * gui.scale - gp = 4 * gui.scale + added += reversed(cued) - 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]) + # cue_list.append(filepath) - # 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]) +def get_album_from_first_track(track_position, track_id=None, pl_number=None, pl_id: int | None = None): + if pl_number is None: - yy += 6 * gui.scale + if pl_id: + pl_number = id_to_pl(pl_id) + else: + pl_number = pctl.active_playlist_viewing - yy += 10 * gui.scale + playlist = pctl.multi_playlist[pl_number].playlist_ids - i = 0 + if track_id is None: + track_id = playlist[track_position] - # 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 + if playlist[track_position] != track_id: + return [] + + tracks = [] + album_parent_path = pctl.get_track(track_id).parent_folder_path - # End drag if mouse not in correct state for it - if not mouse_down and not mouse_up: - self.dragging = None + i = track_position - if not queue_menu.active: - self.right_click_id = None + while i < len(playlist): + if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: + break - fq = self.fq + tracks.append(playlist[i]) + i += 1 - list_top = yy + return tracks - i = self.scroll_position +def worker3(tauon: Tauon) -> None: + while True: + # time.sleep(0.04) - # Limit scroll distance - if i > len(fq): - self.scroll_position = len(fq) - i = self.scroll_position + # if tauon.thread_manager.exit_worker3: + # tauon.thread_manager.exit_worker3 = False + # return + # time.sleep(1) - showed_indicator = False - list_extends = False - x1 = x + 13 * gui.scale # highlight position - w1 = w - 28 * gui.scale - 10 * gui.scale + tauon.gall_ren.worker_render() - while i < len(fq) + 1: +def worker4(tauon: Tauon) -> None: + gui = tauon.gui + prefs = tauon.prefs + pctl = tauon.pctl + gui.style_worker_timer.set() + while True: + if prefs.art_bg or (gui.mode == 3 and prefs.mini_mode_mode == 5): + style_overlay.worker() - # Stop drawing if past window - if yy > window_size[1] - gui.panelBY - gui.panelY - (50 * gui.scale): - list_extends = True - break + 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 - # 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) +def worker2(tauon: Tauon) -> None: + search_over = tauon.search_over + while True: + tauon.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 = [] - if self.dragging is not None and coll(h_rect) and mouse_up: + search_over.searched_text = search_over.search_text.text - ob = None - for u in reversed(range(len(pctl.force_queue))): + artists = {} + albums = {} + genres = {} + metas = {} + composers = {} + years = {} - if pctl.force_queue[u].uuid_int == self.dragging: - ob = pctl.force_queue[u] - pctl.force_queue[u] = None - break + tracks = set() - else: - self.dragging = None + br = 0 - if self.dragging: - pctl.force_queue.insert(i, ob) - self.dragging = None + if search_over.searched_text in ("the", "and"): + continue - 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 + search_over.sip = True + gui.update += 1 - # 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 + o_text = search_over.search_text.text.lower().replace("-", "") - inp.mouse_click = False - self.draw(x, y, w, h) - return + dia_mode = False + if all([ord(c) < 128 for c in o_text]): + dia_mode = True - if i > len(fq) - 1: - break + artist_mode = False + if o_text.startswith("artist "): + o_text = o_text[7:] + artist_mode = True - track = pctl.get_track(fq[i].track_id) + prefs.album_mode = False + if o_text.startswith("album "): + o_text = o_text[6:] + prefs.album_mode = True - rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h) + composer_mode = False + if o_text.startswith("composer "): + o_text = o_text[9:] + composer_mode = True - if inp.mouse_click and coll(rect): + year_mode = False + if o_text.startswith("year "): + o_text = o_text[5:] + year_mode = True - self.dragging = fq[i].uuid_int - self.drag_start_y = mouse_position[1] - self.drag_start_top = yy + 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 - if d_click_timer.get() < 1: + s_text = o_text - if self.d_click_ref == fq[i].uuid_int: + searched = set() - pl = id_to_pl(fq[i].uuid_int) - if pl is not None: - switch_playlist(pl) + for playlist in pctl.multi_playlist: + # if "<" in playlist.title: + # #logging.info("Skipping search on derivative playlist: " + playlist.title) + # continue - 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 + for track in playlist.playlist_ids: + if track in searched: + continue + searched.add(track) - d_click_timer.set() - if self.dragging and coll(h_rect): - yy += self.tab_h - yy += 4 * gui.scale + 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 qb_right_click and coll(rect): - self.right_click_id = fq[i].uuid_int - qb_right_click = 2 + 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 middle_click and coll(rect): - pctl.force_queue.remove(fq[i]) - gui.pl_update += 1 + t = pctl.master_library[track] - if fq[i].uuid_int == self.dragging: - # ddt.rect_r(rect, [22, 22, 22, 255], True) - pass - else: + 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() - db = False - if fq[i].uuid_int == self.right_click_id: - db = True + if cache_string is None: + if not dia_mode: + search_string_cache[ + track] = title + artist + album_artist + composer + date + album + genre + sartist + filename + stem - self.draw_card(x, y, w, h, yy, track, fq[i], db) + 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 - # Drag tracks from main playlist and insert ------------ - if quick_drag: + if dia_mode: + title = unidecode(title) - if x < mouse_position[0] < x + w: + artist = unidecode(artist) + album_artist = unidecode(album_artist) + composer = unidecode(composer) + album = unidecode(album) + filename = unidecode(filename) + sartist = unidecode(sartist) - 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 + if cache_string is None: + search_dia_string_cache[ + track] = title + artist + album_artist + composer + date + album + genre + sartist + filename + stem - insert_position = None + stem = os.path.dirname(t.parent_folder_path) - 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 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 mouse_up: - insert_position = i + 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 - 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 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 mouse_up: - insert_position = i + 1 + 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 insert_position is not None: - self.drop_tracks_insert(insert_position) + 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()): - # ----------------------------------------- - yy += self.tab_h - yy += 4 * gui.scale + value = 1 + if a.lower().startswith(s_text): + value = 5 - i += 1 + # Add artist + if a in artists: + artists[a] += value + else: + temp_results.append([0, a, track, playlist.uuid_int, 0]) + artists[a] = value - # 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 + 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 - yy += 15 * gui.scale - if fq: - ddt.rect((x, yy, w, 3 * gui.scale), sep_colour) - yy += 11 * gui.scale + elif search_magic(s_text, artist + sartist): + value = 1 + if artist.startswith(s_text): + value = 10 - # Calculate total queue duration - duration = 0 - tracks = 0 + # 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 - 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 + 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 - album_parent_path = pctl.get_track(item.track_id).parent_folder_path + elif search_magic(s_text, album_artist): + # Add album artist + value = 1 + if t.album_artist.startswith(s_text): + value = 5 - playing_track = pctl.playing_object() + 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 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 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 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) + if s_text in album: - while i < len(playlist): - if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: - break + value = 1 + if s_text == album: + value = 3 - duration += pctl.get_track(playlist[i]).length - tracks += 1 - i += 1 + 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 - # 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 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 self.dragging: + 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 - fqo = None - for item in fq: - if item.uuid_int == self.dragging: - fqo = item - break - else: - self.dragging = False + if s_text in title: - 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) + if t not in tracks: - # 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)) + value = 50 + if s_text == title: + value = 200 - # 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) + temp_results.append([2, t.title, track, playlist.uuid_int, value]) + tracks.add(t) -queue_box = QueueBox() + elif t not in tracks: + temp_results.append([2, t.title, track, playlist.uuid_int, 1]) + tracks.add(t) -def art_metadata_overlay(right, bottom, showc): - if not showc: - return + br += 1 + if br > 800: + time.sleep(0.005) # Throttle thread + br = 0 + if search_over.searched_text != search_over.search_text.text: + break - padding = 6 * gui.scale + search_over.sip = False + search_over.on = 0 + gui.update += 1 - if not key_shift_down: + # Remove results not matching any filter keyword - line = "" - if showc[0] == 1: - line += "E " - elif showc[0] == 2: - line += "N " - else: - line += "F " + if artist_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 0: + del temp_results[i] - line += str(showc[2] + 1) + "/" + str(showc[1]) + elif prefs.album_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 1: + del temp_results[i] - y = bottom - 40 * gui.scale + elif composer_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 6: + del temp_results[i] - 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 year_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 7: + del temp_results[i] - else: # Extended metadata + # 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 - line = "" - if showc[0] == 1: - line += "Embedded" - elif showc[0] == 2: - line += "Network" - else: - line += "File" + 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 - y = bottom - 76 * gui.scale + search_over.on = 0 + search_over.force_select = 0 + #logging.info(perf_timer.get()) - 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]) +def worker1(tauon: Tauon) -> None: + global cue_list + global home + global added + global to_get + global to_got - y += 18 * gui.scale + gui = tauon.gui + pctl = tauon.pctl + loaded_pathes_cache = {} + loaded_cue_cache = {} + added = [] - line = "" - line += showc[4] - line += " " + str(showc[3][0]) + "×" + str(showc[3][1]) + def get_quoted_from_line(line: str) -> str: + """Extract quoted or unquoted string from a line - 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]) + e.g., 'FILE "01 - Track01.wav" WAVE' or 'TITLE Track01' or "PERFORMER 'Artist Name'" + """ + parts = line.split(None, 1) + if len(parts) < 2: + return "" - y += 18 * gui.scale + content = parts[1].strip() - line = "" - line += str(showc[2] + 1) + "/" + str(showc[1]) + 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] - 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]) + def add_from_cue(path: str): + global added + if not msys: # Windows terminal doesn't like unicode + logging.info("Reading CUE file: " + path) -class MetaBox: + 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") - def l_panel(self, x, y, w, h, track, top_border=True): + except Exception: + logging.exception("Abort: Can't detect encoding of CUE file") + return 1 - if not track: - return + f.close() - 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] + # 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 - rect = (x, y, w, h) + 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 - 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) + if files == 1: + pass + elif files_with_subtracks > 1: + pass + else: + return 1 - ddt.text_background_colour = colours.gallery_background + cue_performer = "" + cue_date = "" + cue_album = "" + cue_genre = "" + cue_main_performer = "" + cue_songwriter = "" + cue_disc = 0 + cue_disc_total = 0 - insert = round(9 * gui.scale) - border = round(2 * gui.scale) + cd = [] + cds = [] - compact_mode = False - if w < h * 1.9: - compact_mode = True + file_name = "" + file_path = "" - art_rect = [x + insert - 2 * gui.scale, y + insert, h - insert * 2 + 1 * gui.scale, - h - insert * 2 + 1 * gui.scale] + in_header = True - if compact_mode: - art_rect[0] = x + round(w / 2 - art_rect[2] / 2) - round(1 * gui.scale) # - border + i = -1 + while True: + i += 1 - border_rect = ( - art_rect[0] - border, art_rect[1] - border, art_rect[2] + (border * 2), art_rect[3] + (border * 2)) + if i > len(content) - 1: + break - 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) + line = content[i].strip() - 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])) + if in_header: + if line.startswith("REM "): + line = line[4:] - 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 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 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) + if line.startswith("FILE "): + in_header = False + else: + continue - 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) + if line.startswith("FILE "): - gui.showed_title = True + if cd: + cds.append(cd) + cd = [] - def lyrics(self, x, y, w, h, track: TrackClass): + file_name = get_quoted_from_line(line) + file_path = os.path.join(os.path.dirname(path), file_name) - ddt.rect((x, y, w, h), colours.side_panel_background) - ddt.text_background_colour = colours.side_panel_background + 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 bag.formats.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 not track: - return + if line.startswith("TRACK "): + line = line[6:] + if line.endswith("AUDIO"): + line = line[:-5] - # 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) + 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 - # 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() + 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 - gui.update += 1 + 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) - tw, th = ddt.get_text_wh(track.lyrics + "\n", 15, w - 50 * gui.scale, True) + while True: + i += 1 + if i > len(content) - 1 or content[i].startswith("FILE ") or content[i].strip().startswith( + "TRACK"): + break - oth = th + line = content[i] + line = line.strip() - th -= h - th += 25 * gui.scale # Empty space buffer at end + 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 - if lyrics_ren_mini.lyrics_position * -1 > th: - lyrics_ren_mini.lyrics_position = th * -1 - if oth > h: - lyric_side_bottom_pulse.pulse() + i -= 1 + cd.append(nt) - scroll_w = 15 * gui.scale - if gui.maximized: - scroll_w = 17 * gui.scale + if cd: + cds.append(cd) - 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 + for cdn, cd in enumerate(cds): - margin = 10 * gui.scale - if colours.lm: - margin += 1 * gui.scale + last_end = None + end_track = TrackClass() + end_track.fullpath = cd[-1].fullpath + tag_scan(end_track) - 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) + # 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] - ddt.rect((x, y + h - 1, w, 1), colours.side_panel_background) + # Update with proper length + for track in reversed(cd): - 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) + if last_end == None: + last_end = end_track.length - def draw(self, x, y, w, h, track=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 - ddt.rect((x, y, w, h), colours.side_panel_background) + # 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 not track: - return + if cue_disc: + track.disc_number = cue_disc + elif len(cds) == 0: + track.disc_number = "" + else: + track.disc_number = str(cdn) - # 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 cue_disc_total: + track.disc_total = cue_disc_total + elif len(cds) == 0: + track.disc_total = "" + else: + track.disc_total = str(len(cds)) - 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 + # 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) - # Check for lyrics if auto setting - test_auto_lyrics(track) + except Exception: + logging.exception("Internal error processing CUE file") - # # 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) + def add_file(path, force_scan: bool = False) -> int | None: + # bm.get("add file start") + global to_got - # Draw standard metadata - if len(pctl.track_queue) > 0: + if not os.path.isfile(path): + logging.error("File to import missing") + return 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 + if os.path.splitext(path)[1][1:] in {"CUE", "cue"}: + add_from_cue(path) + return 0 - ddt.text_background_colour = colours.side_panel_background + if path.lower().endswith(".xspf"): + logging.info("Found XSPF file at: " + path) + load_xspf(path) + return 0 - 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 + if path.lower().endswith(".m3u") or path.lower().endswith(".m3u8"): + load_m3u(path) + return 0 - title = "" - album = "" - artist = "" - ext = "" - date = "" - genre = "" + if path.endswith(".pls"): + load_pls(path) + return 0 - margin = x + 10 * gui.scale - if colours.lm: - margin += 2 * gui.scale + if os.path.splitext(path)[1][1:].lower() not in bag.formats.DA_Formats: + if os.path.splitext(path)[1][1:].lower() in bag.formats.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") - text_width = w - 25 * gui.scale - tr = None + 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 - # if pctl.playing_state < 3: + 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") - 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: + return 1 - 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]) + 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") - 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]) + return 1 - if tr is None: - tr = pctl.playing_object() - if tr is None: - return + 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") - 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 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") - if not title and not artist: - title = pctl.tag_meta + to_got = b + gets(target_dir) + quick_import_done.append(target_dir) + # gets(target_dir) - if h > 58 * gui.scale: + return 1 - block_y = y + 7 * gui.scale + to_got += 1 + gui.update = 1 - if not prefs.show_side_art: - block_y += 3 * gui.scale + path = path.replace("\\", "/") - 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) + if path in loaded_pathes_cache: + de = loaded_pathes_cache[path] - gui.showed_title = True + if pctl.master_library[de].fullpath in cue_list: + logging.info("File has an associated .cue file... Skipping") + return None - if h > 140 * gui.scale: + if pctl.master_library[de].file_ext.lower() in bag.formats.GME_Formats: + # Skip cache for subtrack formats + pass + else: + added.append(de) + return None - 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) + time.sleep(0.002) - if not genre == date == "": - line = date - if genre != "": - if line != "": - line += " | " - line += genre + # audio = auto.File(path) - ddt.text( - (margin, block_y + 20 * gui.scale), line, colours.side_bar_line2, - fonts.side_panel_line2, max_w=text_width) + nt = TrackClass() - 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) + nt.index = pctl.master_count + set_path(nt, path) - 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) + 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: + tauon.after_scan.append(nt) + tauon.thread_manager.ready("worker") -meta_box = MetaBox() + pctl.master_count += 1 + # nt = tag_scan(nt) + if nt.cue_sheet != "": + tag_scan(nt) + cue_scan(nt.cue_sheet, nt) + del nt -class PictureRender: + elif nt.file_ext.lower() in bag.formats.GME_Formats and gme: - def __init__(self): - self.show = False - self.path = "" + 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) - self.image_data = None - self.texture = None - self.sdl_rect = None - self.size = (0, 0) + gme.gme_delete(emu) - def load(self, path, box_size=None): + else: - if not os.path.isfile(path): - logging.warning("NO PICTURE FILE TO LOAD") - return + commit_track(nt) - g = io.BytesIO() - g.seek(0) + # bm.get("fill entry") + if gui.auto_play_import: + pctl.jump(pctl.master_count - 1) + gui.auto_play_import = False - im = Image.open(path) - if box_size is not None: - im.thumbnail(box_size, Image.Resampling.LANCZOS) + # Count the approx number of files to be imported + def pre_get(direc): - 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] + global to_get - def draw(self, x, y): + to_get = 0 + for root, dirs, files in os.walk(direc): + to_get += len(files) + if gui.im_cancel: + return + gui.update = 3 - if self.show is False: + def gets(direc, force_scan=False): + if os.path.basename(direc) == "__MACOSX": return - if self.image_data is not None: - if self.texture is not None: - SDL_DestroyTexture(self.texture) + 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") - # 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 + return + except Exception: + logging.exception("Unknown error accessing one or more files") + return - 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) + 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: -artist_picture_render = PictureRender() -artist_preview_render = PictureRender() + if os.path.splitext(items_in_dir[q])[1][1:].lower() in bag.formats.DA_Formats: + if len(items_in_dir[q]) > 2 and items_in_dir[q][0:2] == "._": + continue -class ArtistInfoBox: + add_file(os.path.join(direc, items_in_dir[q]).replace("\\", "/"), force_scan) - def __init__(self): - self.artist_on = None - self.min_rq_timer = Timer() - self.min_rq_timer.force_set(10) + 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("\\", "/")) - self.text = "" + if gui.im_cancel: + return - self.status = "" + def cache_paths() -> tuple[dict, dict]: + 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 - self.scroll_y = 0 - self.process_text_artist = "" - self.processed_text = "" - self.th = 0 - self.w = 0 - self.lock = False + #logging.info(pctl.master_library) - self.mini_box = asset_loader(scaled_asset_directory, loaded_asset_dc, "mini-box.png", True) + global album_art_gen + global to_got + global to_get - def manual_dl(self): + active_timer = Timer() + while True: + if not tauon.after_scan: + time.sleep(0.1) - track = pctl.playing_object() - if track is None or not track.artist: - show_message(_("No artist name found"), mode="warning") + if tauon.after_scan \ + or tauon.bag.load_orders \ + or tauon.artist_list_box.load \ + or tauon.artist_list_box.to_fetch \ + or tauon.gui.regen_single_id \ + or tauon.gui.regen_single > -1 \ + or tauon.pctl.after_import_flag \ + or tauon.worker_save_state \ + or tauon.move_jobs \ + or tauon.cm_clean_db \ + or tauon.transcode_list \ + or tauon.to_scan \ + or tauon.loaderCommandReady: + active_timer.set() + elif active_timer.get() > 5: return - # Check if the artist has changed - self.artist_on = track.artist + if tauon.after_scan: + i = 0 + while tauon.after_scan: + i += 1 - if not self.lock and self.artist_on: - self.lock = True - # self.min_rq_timer.set() + if i > 123: + break - self.scroll_y = 0 - self.status = _("Looking up...") - self.process_text_artist = "" + tag_scan(tauon.after_scan[0]) - shoot_dl = threading.Thread(target=self.get_data, args=([self.artist_on, False, True])) - shoot_dl.daemon = True - shoot_dl.start() + gui.update = 2 + gui.pl_update = 1 + # time.sleep(0.001) + if pctl.running: + del tauon.after_scan[0] + else: + break - def draw(self, x, y, w, h): + album_artist_dict.clear() - if gui.artist_panel_height > 300 and w < 500 * gui.scale: - bio_set_small() + tauon.artist_list_box.worker() - if w < 300 * gui.scale: - gui.artist_info_panel = False - gui.update_layout() - return + # 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 - track = pctl.playing_object() - if track is None: - return + # Update smart playlists + if gui.regen_single > -1: + target = gui.regen_single + gui.regen_single = -1 + regenerate_playlist(target, silent=True) - # Check if the artist has changed - artist = track.artist - wait = False + if pctl.after_import_flag and not tauon.after_scan and not tauon.search_over.active and not pctl.loading_in_progress: + pctl.after_import_flag = False - # Activate menu - if right_click and coll((x, y, w, h)): - artist_info_menu.activate(in_reference=artist) + 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") - background = colours.artist_bio_background - text_colour = colours.artist_bio_text - ddt.rect((x + 10, y + 5, w - 15, h - 5), background) + tree_view_box.clear_all() - if artist != self.artist_on: + if tauon.worker_save_state and \ + not gui.pl_pulse and \ + not pctl.loading_in_progress and \ + not tauon.to_scan and not tauon.after_scan and \ + not plex.scanning and \ + not jellyfin.scanning and \ + not tauon.cm_clean_db and \ + not lastfm.scanning_friends and \ + not tauon.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 - if artist == "": - return + # Folder moving + if len(tauon.move_jobs) > 0: + gui.update += 1 + tauon.move_in_progress = True + job = tauon.move_jobs[0] + del tauon.move_jobs[0] - 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 job[0].strip("\\/") == job[1].strip("\\/"): + show_message(_("Folder copy error."), _("The target and source are the same."), mode="info") + gui.update += 1 + tauon.move_in_progress = False + continue - if pctl.playing_time < 2: - if os.path.isfile(os.path.join(a_cache_dir, artist + "-lfm.txt")): - pass - else: - self.status = "..." - wait = True + try: + shutil.copytree(job[0], job[1]) + except Exception: + logging.exception("Failed to copy directory") + tauon.move_in_progress = False + gui.update += 1 + show_message(_("The folder copy has failed!"), _("Some files may have been written."), mode="warning") + continue - if not wait and not self.lock: - self.lock = True - # self.min_rq_timer.set() + if job[2] == True: + try: + shutil.rmtree(job[0]) - self.scroll_y = 0 - self.status = _("Loading...") + 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 + tauon.move_in_progress = False + return - shoot_dl = threading.Thread(target=self.get_data, args=([artist])) - shoot_dl.daemon = True - shoot_dl.start() + 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") - if self.process_text_artist != self.artist_on: - self.process_text_artist = self.artist_on + tauon.move_in_progress = False + load_orders.append(job[4]) + gui.update += 1 - text = self.text - lic = "" - link = "" + # Clean database + if tauon.cm_clean_db is True: + items_removed = 0 - if "<a" in text: - text, ex = text.split('<a href="', 1) + # old_db = copy.deepcopy(pctl.master_library) + to_got = 0 + to_get = len(pctl.master_library) + tauon.search_over.results.clear() - link, ex = ex.split('">', 1) + keys = set(pctl.master_library.keys()) + for index in keys: + time.sleep(0.0001) + track = pctl.master_library[index] + to_got += 1 - lic = ex.split("</a>. ", 1)[1] + if to_got % 100 == 0: + gui.update = 1 - text += "\n" + if not prefs.remove_network_tracks and track.file_ext == "SPTY": - 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")) + for playlist in pctl.multi_playlist: + if index in playlist.playlist_ids: + break else: - self.urls.append((word.strip(), [120, 200, 60, 255], "W")) + pctl.purge_track(index) + items_removed += 1 - self.processed_text = text - self.w = -1 # trigger text recalc + continue - if self.status == "Ready": + 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 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) + if track.is_network and track.file_ext == "SPTY": + continue - text_max_w = w - (round(55 * gui.scale) + p_off) + pctl.purge_track(index) + items_removed += 1 - 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 + tauon.cm_clean_db = False + show_message( + _("Cleaning complete."), + _("{N} items were removed from the database.").format(N=str(items_removed)), mode="done") + if prefs.album_mode: + reload_albums(True) + if gui.combo_mode: + reload_albums() - scroll_max = self.th - (h - 26) + gui.update = 1 + gui.pl_update = 1 + pctl.notify_change() - 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) + search_dia_string_cache.clear() + search_string_cache.clear() + tauon.search_over.results.clear() - right = x + w - 25 * gui.scale + pctl.notify_change() - 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 + # FOLDER ENC + if tauon.transcode_list: + try: + tauon.transcode_state = "" + gui.update += 1 - 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) + folder_items = transcode_list[0] - yy = y + 12 - for item in self.urls: + ref_track_object = pctl.master_library[folder_items[0]] + ref_album = ref_track_object.album - rect = (right - 2, yy - 2, 16, 16) + # Generate a folder name based on artist and album of first track in batch + folder_name = encode_folder_name(ref_track_object) - 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]) + # 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 - 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) + 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) - yy += 19 * gui.scale + 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() - else: - ddt.text((x + w // 2, y + h // 2 - 7 * gui.scale, 2), self.status, [255, 255, 255, 60], 313, bg=background) + cache_dir = tmp_cache_dir() + if not os.path.isdir(cache_dir): + os.makedirs(cache_dir) - def get_data(self, artist: str, get_img_path: bool = False, force_dl: bool = False) -> str | None: + if prefs.transcode_codec in ("opus", "ogg", "flac", "mp3"): + global core_use + cores = os.cpu_count() - if not get_img_path: - logging.info("Load Bio Data") + total = len(folder_items) + gui.transcoding_batch_total = total + gui.transcoding_bach_done = 0 + dones = [] - if artist is None and not get_img_path: - self.artist_on = artist - self.lock = False - return "" + 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() - f_artist = filename_safe(artist) + 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") - 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) + 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")) - 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"), - ] + #logging.info(transcode_list[0]) - if get_img_path: - for path in image_paths: - if os.path.isfile(path): - return path - return "" + del transcode_list[0] + tauon.transcode_state = "" + gui.update += 1 + except Exception: + logging.exception("Transcode failed") + tauon.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] - # Check for cache - box_size = ( - round(gui.artist_panel_height - 20 * gui.scale) * 2, round(gui.artist_panel_height - 20 * gui.scale)) - try: + 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 os.path.isfile(text_filepath): - logging.info("Load cached bio and image") + if tauon.to_scan: + while tauon.to_scan: + track = tauon.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 tauon.to_scan[0] + gui.update += 1 + album_artist_dict.clear() + pctl.notify_change() + gui.pl_update += 1 - artist_picture_render.show = False + if tauon.loaderCommandReady is True: + for order in load_orders: + if order.stage == 1: + if tauon.loaderCommand == tauon.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 tauon.loaderCommand == tauon.LC_File: + loaded_pathes_cache, loaded_cue_cache = cache_paths() + add_file(order.target) - for path in image_paths: - if os.path.isfile(path): - filepath = path - artist_picture_render.load(filepath, box_size) - artist_picture_render.show = True + if gui.im_cancel: + gui.im_cancel = False + to_get = 0 + to_got = 0 + load_orders.clear() + added = [] + tauon.loaderCommand = LC_Done + tauon.loaderCommandReady = False 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 + tauon.loaderCommand = LC_Done + #logging.info("LOAD ORDER") + order.tracks = added - return "" + # 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 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 + added = [] + order.stage = 2 + tauon.loaderCommandReady = False + #logging.info("DONE LOADING") + break - # 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") +def get_album_info(position, pl: int | None = None): + playlist = pctl.default_playlist + if pl is not None: + playlist = pctl.multi_playlist[pl].playlist_ids - 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) + global album_info_cache_key - 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) + 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()) - 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") + if position in album_info_cache: + return album_info_cache[position] - # 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 + if album_dex and prefs.album_mode and (pl is None or pl == pctl.active_playlist_viewing): + dex = album_dex + else: + dex = reload_albums(custom_list=playlist) - self.status = "Ready" - gui.update = 2 + end = len(playlist) + start = 0 - # 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" + for i, p in enumerate(reversed(dex)): + if p <= position: + start = p + break + end = p + album = list(range(start, end)) - except Exception: - logging.exception("Failed to load bio") - self.status = _("Load Failed") + playing = 0 + select = False - self.artist_on = artist - self.processed_text = "" - self.process_text_artist = "" - self.min_rq_timer.set() - self.lock = False - gui.update = 2 - return "" + 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 -# artist info box def -artist_info_box = ArtistInfoBox() + album_info_cache[position] = playing, album, select + return playing, album, select +def get_folder_list(index: int): + 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] + for item in pctl.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 -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)) + if not pctl.default_playlist: + return -class RadioThumbGen: - def __init__(self): - self.cache = {} - self.requests = [] - self.size = 100 + on = pctl.selected_in_playlist + if on > len(pctl.default_playlist) - 1: + on = 0 + pctl.selected_in_playlist = 0 - def loader(self): + if up is False: - 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"]) + while num > 0: + while pctl.master_library[ + pctl.default_playlist[on]].parent_folder_name == pctl.master_library[ + pctl.default_playlist[pctl.selected_in_playlist]].parent_folder_name: + on += 1 - 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 on > len(pctl.default_playlist) - 1: + pctl.selected_in_playlist = old_selected + return - 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 + pctl.selected_in_playlist = on + num -= 1 + else: + if num > 1: + if pctl.selected_in_playlist > len(pctl.default_playlist) - 1: + pctl.selected_in_playlist = old_selected + return - 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() + alb = get_album_info(pctl.selected_in_playlist) + if alb[1][0] in album_dex[:num]: + pctl.selected_in_playlist = old_selected + return - 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 + while num > 0: + alb = get_album_info(pctl.selected_in_playlist) - def draw(self, station, x, y, w): - if not station.get("title"): - return 0 - key = (station["title"], w) + if alb[1][0] > -1: + on = alb[1][0] - 1 - 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 + 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 = [] -radio_thumb_gen = RadioThumbGen() + last = "a" + noise = 0 + def key(tag): + return tags[tag][1] -def station_browse(): - radiobox.active = True - radiobox.edit_mode = False - radiobox.add_mode = False - radiobox.center = True - radiobox.tab = 1 + for position in album_dex: + index = pctl.default_playlist[position] + track = pctl.get_track(index) -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 + crumbs = track.parent_folder_path.split("/") + for i, b in enumerate(crumbs): -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 + 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) -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)) + max_tags = round((window_size[1] - gui.panelY - gui.panelBY - 10) // 30 * gui.scale) + tag_list_sort = tag_list_sort[:max_tags] -def remove_station(item): - index = item[0] - del pctl.radio_playlists[pctl.radio_playlist_viewing]["items"][index] + for i in reversed(range(len(tag_list))): + if tag_list[i] not in tag_list_sort: + del tag_list[i] + h = [] -radio_context_menu.add(MenuItem(_("Remove"), remove_station, pass_ref=True)) + 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) -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) + cc = random.random() + cj = 0.03 + if len(h) < 5: + cj = 0.11 - 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) + cj = 0.5 / max(len(h), 2) - # 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) + for item in h: + item.colour = hsl_to_rgb(cc, 0.8, 0.7) + cc += cj + + return h - # 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] +def reload_albums(tauon: Tauon, quiet: bool = False, return_playlist: int = -1, custom_list=None) -> list[int] | None: + global album_dex + global update_layout + global old_album_pos - if coll(rect): - colour = b_colour - if inp.mouse_click: - add_station() - else: - colour = a_colour + if tauon.cm_clean_db: + # Doing reload while things are being removed may cause crash + return None - self.add_icon.render(rect[0] + round(4 * gui.scale), rect[1] + round(4 * gui.scale), colour) + dex = [] + current_folder = "" + current_album = "" + current_artist = "" + current_date = "" + current_title = "" - y += round(33 * gui.scale) - rect = (x, y, round(25 * gui.scale), round(25 * gui.scale)) - fields.add(rect) + 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 - 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) + playlist = pctl.multi_playlist[target_pl_no].playlist_ids - 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"] + for i in range(len(playlist)): + tr = pctl.master_library[playlist[i]] - 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) + 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 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"]) + 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 - self.save_icon.render(rect[0] + round(3 * gui.scale), rect[1] + round(4 * gui.scale), colour) + if return_playlist > -1 or custom_list: + return dex - x = round(30 * gui.scale) - y = gui.panelY + round(30 * gui.scale) - yy = y + album_dex = dex + album_info_cache.clear() + gui.update += 2 + gui.pl_update = 1 + update_layout = True - 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] + if not quiet: + goto_album(pctl.playlist_playing_position) - w = round(400 * gui.scale) - h = round(55 * gui.scale) - gap = round(7 * gui.scale) + # Generate POWER BAR + gui.power_bar = gen_power2() + gui.pt = 0 - mm = (window_size[1] - (gui.panelBY + yy + h + round(15 * gui.scale))) // (h + gap) + 1 +def star_line_toggle(mode: int= 0) -> bool | None: + if mode == 1: + return gui.star_mode == "line" - 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 + if gui.star_mode == "line": + gui.star_mode = "none" + else: + gui.star_mode = "line" - pctl.radio_playlists[pctl.radio_playlist_viewing]["scroll"] = scroll - insert = None + gui.show_ratings = False - 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]) + gui.update += 1 + gui.pl_update = 1 + return None - 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] +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 - 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) + else: + if mode == 1: + return gui.star_mode == "star" - 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) + if gui.star_mode == "star": + gui.star_mode = "none" + else: + gui.star_mode = "star" - bottom_bar1.play_button.render(x + (w - round(30 * gui.scale)), yy + round(23 * gui.scale), colour) + # gui.show_ratings = False + gui.update += 1 + gui.pl_update = 1 + return None - 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))) +def heart_toggle(mode: int = 0) -> bool | None: + if mode == 1: + return gui.show_hearts - self.menu_icon.render(x + (w - round(75 * gui.scale)), yy + round(26 * gui.scale), colour) + gui.show_hearts ^= True + # gui.show_ratings = False - # 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) + gui.update += 1 + gui.pl_update = 1 + return None - yy += round(h + gap) +def album_rating_toggle(mode: int = 0) -> bool | None: + if mode == 1: + return gui.show_album_ratings - 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) + gui.show_album_ratings ^= True - 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) + gui.update += 1 + gui.pl_update = 1 + return None - 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]) +def rating_toggle(mode: int = 0) -> bool | None: + if mode == 1: + return gui.show_ratings - yy = window_size[1] / 3 - boxx / 2 - yy += boxx + round(30 * gui.scale) + gui.show_ratings ^= True - 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 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.")) - if self.drag: - gui.update_on_drag = True + gui.update += 1 + gui.pl_update = 1 + return None - 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"]) +def toggle_titlebar_line(mode: int = 0) -> bool | None: + global update_title + if mode == 1: + return update_title - radios[radios.index("New")] = self.drag - self.drag = None - gui.update += 1 + 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 -radio_view = RadioView() +def toggle_side_panel_layout(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.side_panel_layout == 1 -class Showcase: + if prefs.side_panel_layout == 1: + prefs.side_panel_layout = 0 + else: + prefs.side_panel_layout = 1 + return None - def __init__(self): +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 - self.lastfm_artist = None - self.artist_mode = False +def scale1(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.ui_scale == 1: + return True + return False - def render(self): + prefs.ui_scale = 1 + pref_box.large_preset() - global right_click + if prefs.ui_scale != gui.scale: + show_message(_("Change will be applied on restart.")) + return None - box = int(window_size[1] * 0.4 + 120 * gui.scale) - box = min(window_size[0] // 2, box) +def scale125(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.ui_scale == 1.25: + return True + return False + return None - hide_art = False - if window_size[0] < 900 * gui.scale: - hide_art = True + prefs.ui_scale = 1.25 + pref_box.large_preset() - x = int(window_size[0] * 0.15) - y = int((window_size[1] / 2) - (box / 2)) - 10 * gui.scale + if prefs.ui_scale != gui.scale: + show_message(_("Change will be applied on restart.")) + return None - if hide_art: - box = 45 * gui.scale - elif window_size[1] / window_size[0] > 0.7: - x = int(window_size[0] * 0.07) +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 - 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) +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 - t1 = colours.grey(250) +def toggle_min_tray(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.min_to_tray + prefs.min_to_tray ^= True + return None - 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] +def scale2(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.ui_scale == 2: + return True + return False - if test_lumi(colours.playlist_panel_background) < 0.7: - light_mode = True - t1 = colours.grey(30) - gui.vis_4_colour = [40, 40, 40, 255] + prefs.ui_scale = 2 + pref_box.large_preset() - ddt.rect((0, gui.panelY, window_size[0], window_size[1] - gui.panelY), colours.playlist_panel_background) + if prefs.ui_scale != gui.scale: + show_message(_("Change will be applied on restart.")) + return None - if prefs.bg_showcase_only and prefs.art_bg: - style_overlay.display() +def toggle_borderless(mode: int = 0) -> bool | None: + global update_layout - # 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 + if mode == 1: + return draw_border - 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 + update_layout = True + tauon.draw_border ^= True - if prefs.bg_showcase_only and prefs.art_bg: - ddt.alpha_bg = True - ddt.force_gray = True + if tauon.draw_border: + SDL_SetWindowBordered(t_window, False) + else: + SDL_SetWindowBordered(t_window, True) + return None - # 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() +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 - # ddt.force_gray = True +def toggle_scroll(mode: int = 0) -> bool | None: + global scroll_enable + global update_layout - if pctl.playing_state == 3 and not radiobox.dummy_track.title: + if mode == 1: + if scroll_enable: + return False + return True - 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) + scroll_enable ^= True + gui.pl_update = 1 + update_layout = True + return None - y = int(window_size[1] / 2) - 60 - gui.scale - ddt.text((x, y, 2), pctl.tag_meta, colours.side_bar_line1, 216, w) +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 - else: +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 - if len(pctl.track_queue) < 1: - ddt.alpha_bg = False - return +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 - # if draw.button("Return", 20, gui.panelY + 5, bg=colours.grey(30)): - # pass +def toggle_true_shuffle(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.true_shuffle + prefs.true_shuffle ^= True + return None - if prefs.bg_showcase_only and prefs.art_bg: - ddt.alpha_bg = True - ddt.force_gray = True +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 - 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 +def toggle_enable_web(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.enable_web - if gui.force_showcase_index >= 0: - index = gui.force_showcase_index - track = pctl.master_library[index] - else: + prefs.enable_web ^= True - if pctl.playing_state == 3: - track = radiobox.dummy_track - else: - index = pctl.track_queue[pctl.queue_step] - track = pctl.master_library[index] + 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 not hide_art: + elif prefs.enable_web is False: + if tauon.radio_server is not None: + tauon.radio_server.shutdown() + gui.web_running = False - # 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) + time.sleep(0.25) + return None - # Draw album art in box - album_art_gen.display(track, (x, y), (box, box)) +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 - # 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 +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 - # Check for lyrics if auto setting - test_auto_lyrics(track) +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 - gui.draw_vis4_top = False +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 - 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) +def toggle_dl_mon(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.monitor_downloads + prefs.monitor_downloads ^= True + return None - gcx = x + box + int(window_size[0] * 0.15) + 10 * gui.scale - gcx -= 100 * gui.scale +def toggle_music_ex(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.extract_to_music + prefs.extract_to_music ^= True + return None - timed_ready = False - if True and prefs.show_lyrics_showcase: - timed_ready = timed_lyrics_ren.generate(track) +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 timed_ready and track.lyrics: +def toggle_top_tabs(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.tabs_on_top + prefs.tabs_on_top ^= True + return None - # 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 +#def toggle_guitar_chords(mode: int = 0) -> bool | None: +# if mode == 1: +# return prefs.guitar_chords +# prefs.guitar_chords ^= True +# return None - timed_ready = prefs.prefer_synced_lyrics +# def toggle_auto_lyrics(mode: int = 0) -> bool | None: +# if mode == 1: +# return prefs.auto_lyrics +# prefs.auto_lyrics ^= True -# 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 +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 - 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) +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 - elif track.lyrics == "" or not prefs.show_lyrics_showcase: +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 - w = window_size[0] - (x + box) - round(30 * gui.scale) - x = int(x + box + (window_size[0] - x - box) / 2) +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 - if hide_art: - x = window_size[0] // 2 +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 - # x = int((window_size[0]) / 2) - y = int(window_size[1] / 2) - round(60 * gui.scale) +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 - if prefs.showcase_vis and prefs.backend == 1: - y -= round(30 * gui.scale) +def toggle_transcode_inplace(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.transcode_inplace: + return True + return False - if track.artist == "" and track.title == "": + if gui.sync_progress: + prefs.transcode_inplace = False + return None - ddt.text((x, y, 2), clean_string(track.filename), t1, 216, w) + 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 - else: +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 - ddt.text((x, y, 2), track.artist, t1, 20, w) +def toggle_sbt(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.prefer_bottom_title + prefs.prefer_bottom_title ^= True + return None - y += round(48 * gui.scale) +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 - 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) +def toggle_use_title(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.use_title + prefs.use_title ^= True + return None - 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) +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 - gui.spec4_rec.x = x - (gui.spec4_rec.w // 2) - gui.spec4_rec.y = y + round(50 * gui.scale) +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 - 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): +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 - if gui.message_box or not is_level_zero(include_menus=True): - self.render_vis() - else: - gui.draw_vis4_top = True +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 - else: - x += box + int(window_size[0] * 0.15) + 10 * gui.scale - x -= 100 * gui.scale - w = window_size[0] - x - 30 * gui.scale +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 - 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 +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 - lyrics_ren.test_update(track) - tw, th = ddt.get_text_wh(lyrics_ren.text + "\n", 17, w, True) +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 - 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) +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 - 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 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 render_vis(self, top=False): +def reload_backend() -> None: + gui.backend_reloading = True + logging.info("Reload backend...") + wait = 0 + pre_state = pctl.stop(True) - SDL_SetRenderTarget(renderer, gui.spec4_tex) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_RenderClear(renderer) + 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") - bx = 0 - by = 50 * gui.scale + pctl.playerCommand = "unload" + pctl.playerCommandReady = True - 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]) + wait = 0 + while pctl.playerCommand != "done": + time.sleep(0.01) + wait += 1 + if wait > 200: + break - 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 + tauon.thread_manager.ready_playback() - 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 pre_state == 1: + pctl.revert() + gui.backend_reloading = False - if not top and (pctl.playing_state == 1 or pctl.playing_state == 3): - gui.update = 2 +def gen_chart() -> None: + try: + topchart = t_topchart.TopChart(tauon, album_art_gen) - slide = 0.7 - for i, bar in enumerate(gui.spec4_array): + tracks = [] - # We wont draw higher bars that may not move - if i > 40: - break + source_tracks = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - # 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 + 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) - # 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)))) + for item in dex: + tracks.append(pctl.get_track(source_tracks[item])) - # 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) + 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)) - SDL_RenderFillRect(renderer, gui.bar4) + path = topchart.generate( + tracks, prefs.chart_bg, prefs.chart_rows, prefs.chart_columns, prefs.chart_text, + prefs.chart_font, prefs.chart_tile, cascade) - # Set distance between bars - bx += 8 * gui.scale + 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 - if top: - SDL_SetRenderTarget(renderer, None) - else: - SDL_SetRenderTarget(renderer, gui.main_texture) + gui.generating_chart = False - # SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) - SDL_RenderCopy(renderer, gui.spec4_tex, None, gui.spec4_rec) + 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") -showcase = Showcase() +def update_playlist_call(): + gui.update + 2 + gui.pl_update = 2 +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]) -# Animates colour between two colours -class ColourPulse2: +def clear_gen(id: int) -> None: + del pctl.gen_codes[id] + show_message(_("Okay, it's a normal playlist now."), mode="done") - def __init__(self): +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") - self.timer = Timer() - self.in_timer = Timer() - self.out_timer = Timer() - self.out_timer.start = 0 - self.active = False +def set_mini_mode(): + if gui.fullscreen: + return - def get(self, hit, on, off, low_hls, high_hls): + global old_window_position + inp.mouse_down = False + inp.mouse_up = False + inp.mouse_click = False - 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] + if gui.maximized: + SDL_RestoreWindow(t_window) + update_layout_do(tauon=tauon) - ani_time = 0.15 + if gui.mode < 3: + old_window_position = get_window_position() - if hit is True and self.active is False: - self.active = True - self.in_timer.set() + if prefs.mini_mode_on_top: + SDL_SetWindowAlwaysOnTop(t_window, True) - out_time = self.out_timer.get() - if out_time < ani_time: - self.in_timer.force_set(ani_time - out_time) + gui.mode = 3 + gui.vis = 0 + gui.turbo = False + gui.draw_vis4_top = False + gui.level_update = False - elif hit is False and self.active is True: - self.active = False - self.out_timer.set() + 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) - in_time = self.in_timer.get() - if in_time < ani_time: - self.out_timer.force_set(ani_time - in_time) + mini_mode.was_borderless = tauon.draw_border + SDL_SetWindowBordered(t_window, False) - 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 + 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") - return colour_slide(low_hls, high_hls, pro, 1) + 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] -cctest = ColourPulse2() + SDL_SetWindowMinimumSize(t_window, 100, 100) + SDL_SetWindowResizable(t_window, False) + SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) -class ViewBox: + if mini_mode.save_position: + SDL_SetWindowPosition(t_window, mini_mode.save_position[0], mini_mode.save_position[1]) - 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 + 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 - self.border = 3 * gui.scale + gui.update += 3 - 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) +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] - # _ .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) + if not mini_mode.was_borderless: + SDL_SetWindowBordered(t_window, True) - self.on_colour = [255, 190, 50, 255] - self.over_colour = [255, 190, 50, 255] - self.off_colour = colours.grey(40) + logical_size[0] = gui.save_size[0] + logical_size[1] = gui.save_size[1] - if not reload: - gui.combo_was_album = False + SDL_SetWindowPosition(t_window, gui.save_position[0], gui.save_position[1]) - 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) + SDL_SetWindowResizable(t_window, True) + SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) + SDL_SetWindowAlwaysOnTop(t_window, False) - 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 + # if macos: + # SDL_SetWindowMinimumSize(t_window, 560, 330) + # else: + SDL_SetWindowMinimumSize(t_window, 560, 330) - self.col_force_off = False + restore_ignore_timer.set() # Hacky - # gui.level_2_click = False - gui.update = 2 + gui.mode = 1 - def button(self, x, y, asset, test, colour_get=None, name="Unknown", animate=True, low=0, high=0): + inp.mouse_down = False + inp.mouse_up = False + inp.mouse_click = False - 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 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 - if on: - colour = self.on_colour + #logging.info(window_size) - else: - colour = self.off_colour + 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 - fun = None - col = False - if coll(rect): + gui.update_layout() + if prefs.art_bg: + tauon.thread_manager.ready("style") - tool_tip.test(x + asset.w + 10 * gui.scale, y - 15 * gui.scale, name) +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 - col = True - if gui.level_2_click: - fun = test - if colour_get is None: - colour = self.over_colour + 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 - colour = colour_get.get(col, on, not on and not animate, low, high) + 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 "+" in name: - # - # colour = cctest.get(col, on, [0, 0.2, 0.0], [0, 0.8, 0.8]) + artistoffset = 0 + indexLine = "" - # if not on and not animate: - # colour = self.off_colour + offset_font_extra = 0 + if gui.row_font_size > 14: + offset_font_extra = 8 - asset.render(x, y, colour) + # In windows (arial?) draws numbers too high (hack fix) + num_y_offset = 0 + # if system == 'Windows': + # num_y_offset = 1 - return fun + if True or style == 1: - def tracks(self, hit=False): + # if not gui.rsp and not gui.combo_mode: + # width -= 10 * gui.scale - if hit is False: - return album_mode is False and \ - gui.combo_mode is False and \ - gui.rsp is False + dash = False + if n_track.artist and colours.artist_text == colours.title_text: + dash = True - 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 + if n_track.title: - view_tracks() + line = track_number_process(n_track.track_number) - def side(self, hit=False): + indexLine = line - 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 + 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 += " " - view_standard_meta() + line = "" - def gallery1(self, hit: bool = False) -> bool | None: + if n_track.artist != "" and not dash: + line0 = n_track.artist - if hit is False: - return album_mode is True # and gui.show_playlist is True + artistoffset = ddt.text( + (start_x + 27 * gui.scale, y), + line0, + alpha_mod(artistc, album_fade), + gui.row_font_size, + int(width / 2)) - 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 + line = n_track.title + else: + line += n_track.title + else: + line = \ + os.path.splitext(n_track.filename)[ + 0] - if x_menu.active: - x_menu.close_next_frame = True + if p_track >= len(pctl.default_playlist): + gui.pl_update += 1 + return - force_album_view() + index = pctl.default_playlist[p_track] + star_x = 0 + total = star_store.get(index) - def radio(self, hit=False): + if gui.star_mode == "line" and total > 0 and pctl.master_library[index].length > 0: - if hit is False: - return gui.radio_view + 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 - if not gui.radio_view: - enter_radio_view() - else: - exit_combo(restore=True) + lh = 1 + if gui.scale != 1: + lh = 2 - if x_menu.active: - x_menu.close_next_frame = True + colour = colours.star_line + if this_line_playing and colours.star_line_playing is not None: + colour = colours.star_line_playing - def lyrics(self, hit=False): + 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)) - if hit is False: - return gui.showcase_mode + star_x += 6 * gui.scale - if not gui.showcase_mode: - if gui.radio_view: - gui.was_radio = True - enter_showcase_view() + 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) - elif gui.was_radio: - enter_radio_view() - else: - exit_combo(restore=True) - if x_menu.active: - x_menu.close_next_frame = True + draw_rating_widget(sx, sy, n_track) - def col(self, hit=False): + star_x += round(70 * gui.scale) - if hit is False: - return gui.set_mode + if gui.star_mode == "star" and total > 0 and pctl.master_library[ + index].length != 0: - if not gui.set_mode: - if gui.combo_mode: - exit_combo() + 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 album_mode and gui.plw < 550 * gui.scale: - toggle_album_mode() + sx2 = sx + selected_star = -2 + rated_star = -1 - toggle_library_mode() + # if inp.key_ctrl_down: - def artist_info(self, hit=False): + c = 60 + d = 6 - if hit is False: - return gui.artist_info_panel + colour = [70, 70, 70, 255] + if colours.lm: + colour = [90, 90, 90, 255] + # colour = alpha_mod(indexc, album_fade) - gui.artist_info_panel ^= True - gui.update_layout() + for count in range(8): - def render(self): + if selected_star < count and playtime_stars < count and rated_star < count: + break - if prefs.shuffle_lock: - self.active = False - self.clicked = False - return + 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 not self.active: - return + # 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) - # 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 + # if selected_star > -2: + # if selected_star >= count: + # colour = (220, 200, 60, 255) + # else: + # if rated_star >= count: + # colour = (220, 200, 60, 255) - x = self.x - 40 * gui.scale + star_pc_icon.render(sx, sy, colour) - vr = [x, gui.panelY, self.w, self.h] - # vr = [x, gui.panelY, 52 * gui.scale, 220 * gui.scale] + if gui.show_hearts: - 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) + xxx = star_x - x += 7 * gui.scale - y = gui.panelY + 14 * gui.scale + count = 0 + spacing = 6 * gui.scale - func = None + yy = ry + (gui.playlist_row_height // 2) - (5 * gui.scale) + if gui.scale == 1.25: + yy += 1 + if xxx > 0: + xxx += 3 * gui.scale - # 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 love(False, index): + count = 1 - # if colours.lm: - # low = (0, 0.5, 0) + x = width + start_x - 52 * gui.scale - offset_font_extra - xxx - # ---- - #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] + f_store.store(display_you_heart, (x, yy)) - 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 + star_x += 18 * gui.scale - # ---- + if "spotify-liked" in pctl.master_library[index].misc: - y += 40 * gui.scale + x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx - high = [76, 137, 229, 255] # (.6, .6, .75) - if colours.lm: - # high = (.6, .80, .85) - high = [63, 63, 63, 255] + f_store.store(display_spot_heart, (x, yy)) - 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 + star_x += heart_row_icon.w + spacing + 2 - # --- + for name in pctl.master_library[index].lfm_friend_likes: - y += 40 * gui.scale + # Limit to number of hears to display + if gui.star_mode == "none": + if count > 6: + break + elif count > 4: + break - high = [76, 229, 229, 255] - if colours.lm: - # high = (.5, .7, .65) - high = [63, 63, 63, 255] + x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx - 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 + f_store.store(display_friend_heart, (x, yy, name)) - # --- + count += 1 - y += 45 * gui.scale + star_x += heart_row_icon.w + spacing + 2 - high = [107, 76, 229, 255] - if colours.lm: - # high = (.7, .75, .75) - high = [63, 63, 63, 255] + # Draw track number/index + display_queue = False - 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 + 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) - y += 40 * gui.scale + if marks: + display_queue = True - high = [92, 86, 255, 255] - if colours.lm: - # high = (.7, .75, .75) - high = [63, 63, 63, 255] + if display_queue: - 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 + 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] - y += 45 * gui.scale + # if album_type: + # li += "🠗" - high = [229, 205, 76, 255] - if colours.lm: - # high = (.9, .75, .65) - high = [63, 63, 63, 255] + colour = [244, 200, 66, 255] + if colours.lm: + colour = [220, 40, 40, 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 + ddt.text( + (start_x + 5 * gui.scale, y, 2), + li, colour, gui.row_font_size + 200 - 1) - # -- + elif len(indexLine) > 2: - # 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 + ddt.text( + (start_x + 5 * gui.scale, y, 2), indexLine, + alpha_mod(indexc, album_fade), gui.row_font_size) + else: - if func is not None: - func(True) + ddt.text( + (start_x, y), indexLine, + alpha_mod(indexc, album_fade), gui.row_font_size) - if gui.level_2_click and coll(vr): - x_menu.clicked = False + if dash and n_track.artist and n_track.title: + line = n_track.artist + " - " + n_track.title - gui.level_2_click = False - if not x_menu.active: - self.active = False + 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) -view_box = ViewBox() + 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() -class DLMon: +# def visit_radio_site_show_test(p): +# return "website_url" in prefs.radio_urls[p] and prefs.radio_urls[p].["website_url"] - def __init__(self): +def visit_radio_site_deco(station: RadioStation): + if station.website_url: + return [colours.menu_text, colours.menu_background, None] + return [colours.menu_text_disabled, colours.menu_background, None] - self.ticker = Timer() - self.ticker.force_set(8) +def visit_radio_station_site_deco(item: tuple[int, RadioStation]): + return visit_radio_site_deco(item[1]) - self.watching = {} - self.ready = set() - self.done = set() +def visit_radio_site(station: RadioStation): + if station.website_url: + webbrowser.open(station.website_url, new=2, autoraise=True) - def scan(self): +def visit_radio_station(item: tuple[int, RadioStation]): + visit_radio_site(item[1]) - if len(self.watching) == 0: - if self.ticker.get() < 10: - return - elif self.ticker.get() < 2: - return +def radio_saved_panel_test(_): + return radiobox.tab == 0 - self.ticker.set() +def save_to_radios(station: RadioStation): + pctl.radio_playlists[pctl.radio_playlist_viewing].stations.append(station) + toast(_("Added station to: ") + pctl.radio_playlists[pctl.radio_playlist_viewing].name) - for downloads in download_directories: +def create_artist_pl(artist: str, replace: bool = False): + source_pl = pctl.active_playlist_viewing + this_pl = pctl.active_playlist_viewing - for item in os.listdir(downloads): + 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 - path = os.path.join(downloads, item) + playlist = [] - if path in self.done: - continue + 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 path in self.ready and not os.path.exists(path): - del self.ready[path] - continue + if replace: + pctl.multi_playlist[this_pl].playlist_ids[:] = playlist[:] + pctl.multi_playlist[this_pl].title = _("Artist: ") + artist + if prefs.album_mode: + reload_albums() - if path in self.watching and not os.path.exists(path): - del self.watching[path] - continue + # 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) - # 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 + pctl.gen_codes[pl_to_id(this_pl)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" - min_age = (time.time() - stamp) / 60 - ext = os.path.splitext(path)[1][1:].lower() + else: - if msys and "TauonMusicBox" in path: - continue + pctl.multi_playlist.append( + pl_gen( + title=_("Artist: ") + artist, + playlist_ids=playlist, + hide_title=False, + parent=pl_to_id(source_pl))) - 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] + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" - # 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)) + switch_playlist(len(pctl.multi_playlist) - 1) - if os.path.exists(target_dir): - pass - #logging.info("Target folder for archive already exists") +def aa_sort_alpha(): + prefs.artist_list_sort_mode = "alpha" + artist_list_box.saves.clear() - 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.") +def aa_sort_popular(): + prefs.artist_list_sort_mode = "popular" + artist_list_box.saves.clear() - 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: +def aa_sort_play(): + prefs.artist_list_sort_mode = "play" + artist_list_box.saves.clear() - # 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) +def toggle_artist_list_style(): + if prefs.artist_list_style == 1: + prefs.artist_list_style = 2 + else: + prefs.artist_list_style = 1 - 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 +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() - if len(self.watching) > 0: - gui.update += 1 +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")] +def verify_discogs(): + return len(prefs.discogs_pat) == 40 -dl_mon = DLMon() -tauon.dl_mon = dl_mon +def save_discogs_artist_thumb(artist, filepath): + logging.info("Searching discogs for artist image...") + # Make artist name url safe + artist = artist.replace("/", "").replace("\\", "").replace(":", "") -def dismiss_dl(): - dl_mon.ready.clear() - dl_mon.done.update(dl_mon.watching) - dl_mon.watching.clear() + # 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"] -dl_menu.add(MenuItem("Dismiss", dismiss_dl)) + # 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"] -class Fader: + response = urllib.request.urlopen(url, context=tls_context) + im = Image.open(response) - def __init__(self): + 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 - 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) + im = im.crop((left, upper, right, lower)) + im.save(filepath, "JPEG", quality=90) + im.close() + logging.info("Found artist image from Discogs") - def render(self): +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") - 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) + response = urllib.request.urlopen(thumblink, timeout=10, context=tls_context) + info = response.info() - rect = [0, 0, window_size[0], window_size[1]] - ddt.rect(rect, [0, 0, 0, int(110 * self.a)]) + t = io.BytesIO() + t.seek(0) + t.write(response.read()) + l = 0 + t.seek(0, 2) + l = t.tell() + t.seek(0) - if not (self.a == 0 or self.a == 1): - gui.update += 1 + if info.get_content_maintype() == "image" and l > 1000: + f = open(filepath, "wb") + f.write(t.read()) + f.close() - def rise(self): + 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") - self.state = 1 - self.timer.hit() - self.total_timer.set() +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 fall(self): +# 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] - self.state = 0 - self.timer.hit() - self.total_timer.set() +def art_metadata_overlay(right, bottom, showc): + if not showc: + return + padding = 6 * gui.scale -fader = Fader() + if not inp.key_shift_down: + line = "" + if showc[0] == 1: + line += "E " + elif showc[0] == 2: + line += "N " + else: + line += "F " -class EdgePulse: + line += str(showc[2] + 1) + "/" + str(showc[1]) - def __init__(self): + y = bottom - 40 * gui.scale - self.timer = Timer() - self.timer.force_set(10) - self.ani_duration = 0.5 + 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]) - 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 + else: # Extended metadata - def pulse(self): - self.timer.set() + line = "" + if showc[0] == 1: + line += "Embedded" + elif showc[0] == 2: + line += "Network" + else: + line += "File" + y = bottom - 76 * gui.scale -class EdgePulse2: + 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]) - def __init__(self): + y += 18 * gui.scale - self.timer = Timer() - self.timer.force_set(10) - self.ani_duration = 0.22 + line = "" + line += showc[4] + line += " " + str(showc[3][0]) + "×" + str(showc[3][1]) - def render(self, x, y, w, h, bottom=False) -> bool | None: + 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]) - time = self.timer.get() - if time < self.ani_duration: + y += 18 * gui.scale - if bottom: - if mouse_wheel > 0: - self.timer.force_set(10) - return None - elif mouse_wheel < 0: - self.timer.force_set(10) - return None + line = "" + line += str(showc[2] + 1) + "/" + str(showc[1]) - alpha = 30 - int(25 * (time / self.ani_duration)) - h_off = (h // 5) * (time / self.ani_duration) * 4 + 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]) - if colours.lm: - colour = (0, 0, 0, alpha) - else: - colour = (255, 255, 255, alpha) +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] - 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 station_browse(): + radiobox.active = True + radiobox.edit_mode = False + radiobox.add_mode = False + radiobox.center = True + radiobox.tab = 1 - def pulse(self): - self.timer.set() +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: tuple[int, RadioStation]): + 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.title if station.title is not None else "" + radiobox.station_editing = station -edge_playlist2 = EdgePulse2() -bottom_playlist2 = EdgePulse2() -gallery_pulse_top = EdgePulse2() -tab_pulse = EdgePulse() -lyric_side_top_pulse = EdgePulse2() -lyric_side_bottom_pulse = EdgePulse2() +def remove_station(item: tuple[int, RadioStation]): + index = item[0] + del pctl.radio_playlists[pctl.radio_playlist_viewing].stations[index] +def dismiss_dl(): + dl_mon.ready.clear() + dl_mon.done.update(dl_mon.watching) + dl_mon.watching.clear() def download_img(link: str, target_folder: str, track: TrackClass) -> None: try: - response = urllib.request.urlopen(link, context=ssl_context) + response = urllib.request.urlopen(link, context=tls_context) info = response.info() if info.get_content_maintype() == "image": if info.get_content_subtype() == "jpeg": @@ -41492,12 +37821,11 @@ def download_img(link: str, target_folder: str, track: TrackClass) -> None: 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: + tauon.fields.add(rect, update_playlist_call) + if tauon.coll(rect) and not track_box: gui.pl_update += 1 w = ddt.get_text_w(_("You"), 13) xx = (x - w) - 5 * gui.scale @@ -41521,8 +37849,8 @@ def display_you_heart(x: int, yy: int, just: int = 0) -> None: 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: + tauon.fields.add(rect, update_playlist_call) + if tauon.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 @@ -41548,8 +37876,8 @@ def display_friend_heart(x: int, yy: int, name: str, just: int = 0) -> None: 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: + tauon.fields.add(rect, update_playlist_call) + if tauon.coll(rect) and not track_box: gui.pl_update += 1 w = ddt.get_text_w(name, 13) xx = (x - w) - 5 * gui.scale @@ -41567,10 +37895,6 @@ def display_friend_heart(x: int, yy: int, name: str, just: int = 0) -> None: 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] @@ -41578,7 +37902,7 @@ def hit_callback(win, point, data): # Special layout modes if gui.mode == 3: - if key_shift_down or key_shiftr_down: + if inp.key_shift_down or inp.key_shiftr_down: return SDL_HITTEST_NORMAL # if prefs.mini_mode_mode == 5: @@ -41594,224 +37918,81 @@ def hit_callback(win, point, data): # 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() - - if job[0] == "playlist": - pctl.multi_playlist.append(job[1]) - switch_playlist(len(pctl.multi_playlist) - 1) - elif job[0] == "tracks": + # 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 tauon.search_over.active: + return SDL_HITTEST_DRAGGABLE - uid = job[1] - li = job[2] + return SDL_HITTEST_NORMAL - 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 + # Standard player mode + if not gui.maximized: + if y < 0 and x > window_size[0]: + return SDL_HITTEST_RESIZE_TOPRIGHT - for i, ref in reversed(li): + if y < 0 and x < 1: + return SDL_HITTEST_RESIZE_TOPLEFT - if i > len(pl): - logging.error("restore track error - playlist not correct length") - continue - pl.insert(i, ref) + # 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 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 + if y < gui.panelY: - gui.pl_update = 1 + if gui.top_bar_mode2: - def bk_playlist(self, pl_index: int) -> None: + if y < gui.panelY - gui.panelY2: + if prefs.left_window_control and x < 100 * gui.scale: + return SDL_HITTEST_NORMAL - self.e.append(("playlist", pctl.multi_playlist[pl_index])) + 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 - def bk_tracks(self, pl_index: int, indis) -> None: + if top_panel.drag_zone_start_x < x < window_size[0] - (gui.offset_extra + 5): - uid = pctl.multi_playlist[pl_index].uuid_int - self.e.append(("tracks", uid, indis)) + if tab_menu.active or inp.mouse_up or inp.mouse_down: # mouse up/down is workaround for Wayland + return SDL_HITTEST_NORMAL - 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)) + 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 -undo = Undo() + 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 -def reload_scale(): - auto_scale() +def reload_scale(bag: Bag): + auto_scale(bag) scale = prefs.scale_want gui.scale = scale ddt.scale = gui.scale - prime_fonts() + prime_fonts(bag) ddt.clear_text_cache() - scale_assets(scale_want=scale, force=True) - img_slide_update_gall(album_mode_art_size) + scale_assets(bag=bag, scale_want=scale, force=True) + img_slide_update_gall(bag.album_mode_art_size) for item in WhiteModImageAsset.assets: item.reload() @@ -41824,11 +38005,16 @@ def reload_scale(): top_panel.__init__() view_box.__init__(reload=True) queue_box.recalc() - playlist_box.recalc() - -def update_layout_do(): + tauon.playlist_box.recalc() + +def update_layout_do(tauon: Tauon): + window_size = tauon.bag.window_size + prefs = tauon.bag.prefs + dirs = tauon.bag.dirs + ddt = tauon.bag.ddt + gui = tauon.gui if prefs.scale_want != gui.scale: - reload_scale() + reload_scale(bag) w = window_size[0] h = window_size[1] @@ -41838,28 +38024,25 @@ def update_layout_do(): gui.switch_showcase_off = False exit_combo(restore=True) - global draw_max_button - if draw_max_button and prefs.force_hide_max_button: - draw_max_button = False + if tauon.bag.draw_max_button and prefs.force_hide_max_button: + tauon.bag.draw_max_button = False if gui.theme_name != prefs.theme_name: gui.reload_theme = True - global theme - theme = get_theme_number(prefs.theme_name) + prefs.theme = get_theme_number(dirs, prefs.theme_name) #logging.info("Config reload theme...") # Restore in case of error if gui.rspw < 30 * gui.scale: - gui.rspw = 100 * gui.scale # Lock right side panel to full size if fully extended ----- - if prefs.side_panel_layout == 0 and not album_mode: + if prefs.side_panel_layout == 0 and not prefs.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 - if not album_mode and gui.rspw > max_w - 12 * gui.scale and side_drag: + if not prefs.album_mode and gui.rspw > max_w - 12 * gui.scale and gui.side_drag: gui.rsp_full_lock = True # ---------------------------------------------------------- @@ -41919,12 +38102,11 @@ def update_layout_do(): # ----- # 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: + if (prefs.always_art_header or (w < 600 * gui.scale and not gui.rsp and prefs.art_in_top_panel)) and not prefs.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 - else: gui.top_bar_mode2 = False gui.panelY = round(30 * gui.scale) @@ -41932,7 +38114,7 @@ def update_layout_do(): gui.playlist_top_bk = gui.playlist_top gui.show_playlist = True - if w < 750 * gui.scale and album_mode: + if w < 750 * gui.scale and prefs.album_mode: gui.show_playlist = False # Set bio panel size according to setting @@ -41940,7 +38122,6 @@ def update_layout_do(): gui.artist_panel_height = 320 * gui.scale if window_size[0] < 600 * gui.scale: gui.artist_panel_height = 200 * gui.scale - else: gui.artist_panel_height = 200 * gui.scale if window_size[0] < 600 * gui.scale: @@ -41999,10 +38180,10 @@ def update_layout_do(): 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] + gui.save_size[0] = tauon.bag.logical_size[0] + gui.save_size[1] = tauon.bag.logical_size[1] - bottom_bar1.update() + tauon.bottom_bar1.update() # if system != 'windows': # if draw_border: @@ -42021,18 +38202,17 @@ def update_layout_do(): gui.playlist_top += gui.artist_panel_height gui.offset_extra = 0 - if draw_border and not prefs.left_window_control: - + if tauon.draw_border and not prefs.left_window_control: offset = 61 * gui.scale - if not draw_min_button: + if not tauon.bag.draw_min_button: offset -= 35 * gui.scale - if draw_max_button: + if tauon.bag.draw_max_button: offset += 33 * gui.scale if gui.macstyle: offset = 24 - if draw_min_button: + if tauon.bag.draw_min_button: offset += 20 - if draw_max_button: + if tauon.bag.draw_max_button: offset += 20 offset = round(offset * gui.scale) gui.offset_extra = offset @@ -42050,7 +38230,6 @@ def update_layout_do(): album_v_gap = 25 * gui.scale if prefs.thin_gallery_borders: - if gui.gallery_show_text: album_h_gap = 20 * gui.scale album_v_gap = 55 * gui.scale @@ -42083,7 +38262,6 @@ def update_layout_do(): 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 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: @@ -42115,23 +38293,23 @@ def update_layout_do(): gui.art_unlock_ratio = False gui.art_max_ratio_lock = 1 - if side_drag and key_shift_down: + if gui.side_drag and inp.key_shift_down: gui.art_unlock_ratio = True gui.art_max_ratio_lock = 5 gui.rspw = gui.pref_rspw - if album_mode: + if prefs.album_mode: gui.rspw = gui.pref_gallery_w # Limit the right side panel width to height of area if gui.rsp and prefs.side_panel_layout == 0: - if album_mode: + if prefs.album_mode: pass else: if not gui.art_unlock_ratio: - if gui.rsp_full_lock and not side_drag: + if gui.rsp_full_lock and not gui.side_drag: gui.rspw = window_size[0] gui.rspw = min(gui.rspw, window_size[1] - gui.panelY - gui.panelBY) @@ -42157,7 +38335,7 @@ def update_layout_do(): l = gui.lspw gui.rspw = max(window_size[0] - l - 300, 110) - # if album_mode and window_size[0] > 750 * gui.scale: + # if prefs.album_mode and window_size[0] > 750 * gui.scale: # gui.pref_gallery_w = gui.rspw # Determine how wide the playlist need to be (again) @@ -42223,7 +38401,7 @@ def update_layout_do(): gui.tracklist_highlight_left = highlight_left gui.tracklist_highlight_width = highlight_width - if album_mode and gui.hide_tracklist_in_gallery: + if prefs.album_mode and gui.hide_tracklist_in_gallery: gui.show_playlist = False gui.rspw = window_size[0] - 20 * gui.scale if gui.lsp: @@ -42277,76 +38455,17 @@ def update_layout_do(): SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) SDL_RenderClear(renderer) - update_set() + update_set(tauon=tauon) if prefs.art_bg: tauon.thread_manager.ready("style") -# 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) - -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 - - -gal_up = False -gal_down = False -gal_left = False -gal_right = False - -get_sdl_input = GetSDLInput() - - -def window_is_focused() -> bool: +def window_is_focused(t_window) -> bool: """Thread safe?""" if SDL_GetWindowFlags(t_window) & SDL_WINDOW_INPUT_FOCUS: return True return False - def save_state() -> None: if should_save_state: logging.info("Writing database to disk... ") @@ -42366,19 +38485,15 @@ def save_state() -> None: view_prefs["append-date"] = prefs.append_date tauonplaylist_jar = [] + radioplaylist_jar = [] tauonqueueitem_jar = [] -# if db_version > 68: + trackclass_jar = [] for v in pctl.multi_playlist: -# logging.warning(f"Playlist: {v}") tauonplaylist_jar.append(v.__dict__) + for v in pctl.radio_playlists: + radioplaylist_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__) @@ -42392,7 +38507,7 @@ def save_state() -> None: pctl.player_volume, pctl.track_queue, pctl.queue_step, - default_playlist, + pctl.default_playlist, None, # pctl.playlist_playing_position, None, # Was cue list "", # radio_field.text, @@ -42407,8 +38522,8 @@ def save_state() -> None: 0, # save time (unused) gui.vis_want, # gui.vis pctl.selected_in_playlist, - album_mode_art_size, - draw_border, + bag.album_mode_art_size, + tauon.draw_border, prefs.enable_web, prefs.allow_remote, prefs.expose_web, @@ -42514,7 +38629,7 @@ def save_state() -> None: prefs.bg_showcase_only, None, # prefs.discogs_pat, prefs.mini_mode_mode, - after_scan, + tauon.after_scan, gui.gallery_positions, prefs.chart_bg, prefs.left_panel_mode, @@ -42538,7 +38653,7 @@ def save_state() -> None: prefs.auto_rec, prefs.spotify_token, prefs.use_libre_fm, - playlist_box.scroll_on, + tauon.playlist_box.scroll_on, prefs.artist_list_sort_mode, prefs.phazor_device_selected, prefs.failed_background_artists, @@ -42548,7 +38663,7 @@ def save_state() -> None: trackclass_jar, prefs.premium, gui.radio_view, - pctl.radio_playlists, + radioplaylist_jar, # pctl.radio_playlists, pctl.radio_playlist_viewing, prefs.radio_thumb_bans, prefs.playlist_exports, @@ -42568,979 +38683,3386 @@ def save_state() -> None: prefs.gallery_combine_disc, ] - 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) + 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) + + old_position = old_window_position + if not prefs.save_window_position: + old_position = None + + save = [ + tauon.draw_border, + gui.save_size, + prefs.window_opacity, + gui.scale, + gui.maximized, + old_position, + ] + + if not fs_mode: + with (user_directory / "window.p").open("wb") as file: + pickle.dump(save, file, protocol=pickle.HIGHEST_PROTOCOL) + + tauon.spot_ctl.save_token() + + with (user_directory / "lyrics_substitutions.json").open("w") as file: + json.dump(prefs.lyrics_subs, file) + + save_prefs(bag=bag, cf=cf) + + 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) + + logging.info("Done writing database") + + 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 test_show_add_home_music(tauon: Tauon) -> None: + gui = tauon.gui + pctl = tauon.pctl + music_directory = tauon.bag.dirs.music_directory + gui.add_music_folder_ready = True + + if music_directory is None: + gui.add_music_folder_ready = False + return + + for item in pctl.multi_playlist: + if item.last_folder == str(music_directory): + gui.add_music_folder_ready = False + break + +def menu_is_open() -> bool: + for menu in Menu.instances: + if menu.active: + return True + return False + +def is_level_zero(include_menus: bool = True) -> bool: + if include_menus: + for menu in Menu.instances: + if menu.active: + return False + + 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 tauon.search_over.active \ + and not gui.box_over \ + and not trans_edit_box.active + +def drop_file(target): + global new_playlist_cooldown + + 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] + + #logging.info((i_x, i_y)) + gui.drop_playlist_target = 0 + #logging.info(event.drop) + + 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 + + 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 + + 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 + + elif gui.lsp and gui.panelY < i_y < window_size[1] - gui.panelBY and i_x < gui.lspw and gui.mode == 1: + + y = gui.panelY + y += 5 * gui.scale + y += tauon.playlist_box.tab_h + tauon.playlist_box.gap + + 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 += tauon.playlist_box.tab_h + tauon.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 + + + else: + gui.drop_playlist_target = pctl.active_playlist_viewing + + 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") + + load_order = LoadClass() + load_order.target = target.replace("\\", "/") + + 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) + + 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 + inp.mouse_down = False + inp.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 + + 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}") + 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.") + + 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 + + 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 + + tls_context = setup_tls(holder) + + # 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") + + + # 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.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}") + + # Detect what desktop environment we are in to enable specific features + desktop = os.environ.get("XDG_CURRENT_DESKTOP") + # de_notify_support = desktop == 'GNOME' or desktop == 'KDE' + de_notify_support = False + draw_min_button = True + draw_max_button = True + left_window_control = False + xdpi = 0 + + detect_macstyle = False + gtk_settings: Settings | None = None + mac_close = (253, 70, 70, 255) + mac_maximize = (254, 176, 36, 255) + mac_minimize = (42, 189, 49, 255) + try: + # TODO(Martin): Bump to 4.0 - https://github.com/Taiko2k/Tauon/issues/1316 + gi.require_version("Gtk", "3.0") + from gi.repository import Gtk + + gtk_settings = Gtk.Settings().get_default() + xdpi = gtk_settings.get_property("gtk-xft-dpi") / 1024 + if "minimize" not in str(gtk_settings.get_property("gtk-decoration-layout")): + draw_min_button = False + if "maximize" not in str(gtk_settings.get_property("gtk-decoration-layout")): + draw_max_button = False + if "close" in str(gtk_settings.get_property("gtk-decoration-layout")).split(":")[0]: + left_window_control = True + gtk_theme = str(gtk_settings.get_property("gtk-theme-name")).lower() + #logging.info(f"GTK theme is: {gtk_theme}") + for k, v in mac_styles.items(): + if k in gtk_theme: + detect_macstyle = True + if v is not None: + mac_close = v[0] + mac_maximize = v[1] + mac_minimize = v[2] + except Exception: + logging.exception("Error accessing GTK settings") + + # TODO(Martin): Move this one to a separate dir func? + + 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) + + sss = SDL_SysWMinfo() + SDL_GetWindowWMInfo(t_window, sss) + + loaded_asset_dc: dict[str, WhiteModImageAsset | LoadImageAsset] = {} + # loading_image = asset_loader(bag, 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") + + # Detect locale for translations + try: + py_locale.setlocale(py_locale.LC_ALL, "") + except Exception: + logging.exception("SET LOCALE ERROR") + + 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 + + wayland = True + if os.environ.get("SDL_VIDEODRIVER") != "wayland": + wayland = False + os.environ["GDK_BACKEND"] = "x11" + + + # Setting various timers + message_box_min_timer = Timer() + cursor_blink_timer = Timer() + animate_monitor_timer = Timer() + min_render_timer = Timer() + check_file_timer = Timer() + vis_rate_timer = Timer() + vis_decay_timer = Timer() + scroll_timer = Timer() + perf_timer = Timer() + quick_d_timer = Timer() + core_timer = Timer() + sleep_timer = Timer() + gallery_select_animate_timer = Timer() + gallery_select_animate_timer.force_set(10) + search_clear_timer = Timer() + gall_pl_switch_timer = Timer() + gall_pl_switch_timer.force_set(999) + d_click_timer = Timer() + d_click_timer.force_set(10) + lyrics_check_timer = Timer() + scroll_hide_timer = Timer(100) + scroll_gallery_hide_timer = Timer(100) + get_lfm_wait_timer = Timer(10) + lyrics_fetch_timer = Timer(10) + gallery_load_delay = Timer(10) + queue_add_timer = Timer(100) + toast_love_timer = Timer(100) + toast_mode_timer = Timer(100) + scrobble_warning_timer = Timer(1000) + sync_file_timer = Timer(1000) + sync_file_update_timer = Timer(1000) + sync_get_device_click_timer = Timer(100) + + f_store = FunctionStore() + + search_string_cache = {} + search_dia_string_cache = {} + + vis_update = False + + + # GUI Variables ------------------------------------------------------------------------------------------- + + # Variables now go in the GuiVar, PlayerCtl, Input, Prefs and Bag class instances. + # The following just haven't been moved yet: + console = DConsole() + + spot_cache_saved_albums = [] + + resize_mode = False + + side_panel_text_align = 0 + + spec_smoothing = True + + # gui.offset_extra = 0 + + old_album_pos = -55 + + album_dex = [] + album_artist_dict = {} + row_len = 5 + last_row = 0 + album_v_gap = 66 + album_h_gap = 30 + album_v_slide_value: int = 50 + + time_last_save = 0 + + b_info_y = int(window_size[1] * 0.7) # For future possible panel below playlist + + volume_store = 50 # Used to save the previous volume when muted + + # row_alt = False + + to_get = 0 # Used to store temporary import count display + to_got = 0 + + editline = "" + # gui.rsp = True + + # Playlist Panel + pl_view_offset = 0 + pl_rect = (2, 12, 10, 10) + + theme = 7 + scroll_enable = True + scroll_timer = Timer() + scroll_timer.set() + scroll_opacity = 0 + break_enable = True + + source = None + + album_playlist_width = 430 + + update_title = False + + playlist_hold_position = 0 + playlist_hold = False + selection_stage = 0 + + selected_in_playlist = -1 + + shift_selection = [] + + gen_codes: dict[int, str] = {} + # Control Variables-------------------------------------------------------------------------- + + + # Player Variables---------------------------------------------------------------------------- + Archive_Formats = {"zip"} + + if whicher("unrar", flatpak_mode): + Archive_Formats.add("rar") + + if whicher("7z", flatpak_mode): + Archive_Formats.add("7z") + + MOD_Formats = {"xm", "mod", "s3m", "it", "mptm", "umx", "okt", "mtm", "669", "far", "wow", "dmf", "med", "mt2", "ult"} + GME_Formats = {"ay", "gbs", "gym", "hes", "kss", "nsf", "nsfe", "sap", "spc", "vgm", "vgz"} + formats = Formats( + format_colours = { + "MP3": [255, 130, 80, 255], # Burnt orange + "FLAC": [156, 249, 79, 255], # Bright lime green + "M4A": [81, 220, 225, 255], # Soft cyan + "AIFF": [81, 220, 225, 255], # Soft cyan + "OGG": [244, 244, 78, 255], # Light yellow + "OGA": [244, 244, 78, 255], # Light yellow + "WMA": [213, 79, 247, 255], # Magenta + "APE": [247, 79, 79, 255], # Deep pink + "TTA": [94, 78, 244, 255], # Purple + "OPUS": [247, 79, 146, 255], # Pink + "AAC": [79, 247, 168, 255], # Teal + "WV": [229, 23, 18, 255], # Deep red + "PLEX": [229, 160, 13, 255], # Orange-brown + "KOEL": [111, 98, 190, 255], # Lavender + "TAU": [111, 98, 190, 255], # Lavender + "SUB": [235, 140, 20, 255], # Golden yellow + "SPTY": [30, 215, 96, 255], # Bright green + "TIDAL": [0, 0, 0, 255], # Black + "JELY": [190, 100, 210, 255], # Fuchsia + "XM": [50, 50, 50, 255], # Grey + "MOD": [50, 50, 50, 255], # Grey + "S3M": [50, 50, 50, 255], # Grey + "IT": [50, 50, 50, 255], # Grey + "MPTM": [50, 50, 50, 255], # Grey + "AY": [237, 212, 255, 255], # Pastel purple + "GBS": [255, 165, 0, 255], # Vibrant orange + "GYM": [0, 191, 255, 255], # Bright blue + "HES": [176, 224, 230, 255], # Light blue-green + "KSS": [255, 255, 153, 255], # Bright yellow + "NSF": [255, 140, 0, 255], # Deep orange + "NSFE": [255, 140, 0, 255], # Deep orange + "SAP": [152, 255, 152, 255], # Light green + "SPC": [255, 128, 0, 255], # Bright orange + "VGM": [0, 128, 255, 255], # Deep blue + "VGZ": [0, 128, 255, 255], # Deep blue + }, + VID_Formats = {"mp4", "webm"}, + MOD_Formats = MOD_Formats, + GME_Formats = GME_Formats, + DA_Formats = { + "mp3", "wav", "opus", "flac", "ape", "aiff", + "m4a", "ogg", "oga", "aac", "tta", "wv", "wma", + } | MOD_Formats | GME_Formats, + Archive_Formats = Archive_Formats + ) + + cargo = [] + + # --------------------------------------------------------------------- + # Player variables + + # pl_follow = False + + # List of encodings to check for with the fix mojibake function + encodings = ["cp932", "utf-8", "big5hkscs", "gbk"] # These seem to be the most common for Japanese + + track_box = False + + taskbar_progress = True + track_queue: list[int] = [] + + playing_in_queue: int = 0 + draw_sep_hl = False + + # ------------------------------------------------------------------------------- + # Playlist Variables + playlist_view_position = 0 + playlist_playing = -1 + + core_use = 0 + dl_use = 0 + + random_mode = False + repeat_mode = False + + playlist_active: int = 0 + + quick_search_mode = False + search_index = 0 + + # ---------------------------------------- + # Playlist right click menu + + r_menu_index = 0 + r_menu_position = 0 + + # Library and loader Variables-------------------------------------------------------- + master_library: dict[int, TrackClass] = {} + + cue_list = [] + + master_count = 0 + + load_orders: list[LoadClass] = [] + + volume = 75 + + folder_image_offsets: dict[str, int] = {} + db_version: float = 0.0 + latest_db_version: float = 70 + + albums = [] + album_position = 0 + + # url_saves = [] + rename_files_previous = "" + rename_folder_previous = "" + p_force_queue: list[TauonQueueItem] = [] + + reload_state = None + smtc = False + + radio_playlist_viewing = 0 + radio_playlists = [{"uid": uid_gen(), "name": "Default", "items": []}] + + ddt = TDraw(renderer) + fonts = Fonts() + colours = ColoursClass() + colours.post_config() + + force_subpixel_text = False + if gtk_settings and gtk_settings.get_property("gtk-xft-rgba") == "rgb": + force_subpixel_text = True + dc_device = False # (BASS) Disconnect device on pause + if desktop == "KDE": + dc_device = True + encoder_output = music_directory / "encode-output" + if music_directory is None: + encoder_output = user_directory / "encoder" + power_save = False + if macos or phone: + power_save = True + + multi_playlist: list[TauonPlaylist] = [pl_gen()] + prefs = Prefs( + power_save=power_save, + encoder_output=encoder_output, + # user_directory=user_directory, + # music_directory=music_directory, + # cache_directory=cache_directory, + force_subpixel_text=force_subpixel_text, + dc_device=dc_device, + macos=macos, + # detect_macstyle=detect_macstyle, + macstyle=macos or detect_macstyle, + left_window_control=macos or left_window_control, + phone=phone, + # gtk_settings=gtk_settings, + discord_allow=discord_allow, + flatpak_mode=flatpak_mode, + desktop=desktop, + window_opacity=window_opacity, + ui_scale=scale, + ) + prefs.theme = get_theme_number(dirs, prefs.theme_name) + + bag = Bag( + colours=colours, + console=console, + dirs=dirs, + prefs=prefs, + ddt=ddt, + fonts=fonts, + formats=formats, + renderer=renderer, + sdl_syswminfo=sss, + system=system, + pump=True, + draw_min_button=draw_min_button, + draw_max_button=draw_max_button, + smtc=smtc, + macos=macos, + msys=msys, + phone=phone, + xdpi=xdpi, + desktop=desktop, + launch_prefix=launch_prefix, + load_orders=load_orders, + snap_mode=snap_mode, + master_count=master_count, + playlist_active=playlist_active, + playing_in_queue=playing_in_queue, + playlist_playing=playlist_playing, + playlist_view_position=playlist_view_position, + selected_in_playlist=selected_in_playlist, + album_mode_art_size=int(200 * scale), + tls_context=tls_context, + track_queue=track_queue, + volume=volume, + multi_playlist=multi_playlist, + p_force_queue=p_force_queue, + logical_size=logical_size, + window_size=window_size, + gen_codes=gen_codes, + master_library=master_library, + loaded_asset_dc=loaded_asset_dc, + radio_playlist_viewing=radio_playlist_viewing, + radio_playlists=radio_playlists, + ) + + gui = GuiVar( + bag=bag, + tracklist_texture_rect=tracklist_texture_rect, + tracklist_texture=tracklist_texture, + album_v_slide_value=album_v_slide_value, + console=console, + main_texture_overlay_temp=main_texture_overlay_temp, + main_texture=main_texture, + max_window_tex=max_window_tex, + ) + + # Functions for reading and setting play counts + inp = gui.inp + keymaps = KeyMap(bag=bag) + + # This is legacy. New settings are added straight to the save list (need to overhaul) + view_prefs = { + "split-line": True, + "update-title": False, + "star-lines": False, + "side-panel": True, + "dim-art": False, + "pl-follow": False, + "scroll-enable": True, + } + + + # STATE LOADING + # Loading of program data from previous run + gbc.disable() + ggc = 2 + + + + + perf_timer.set() + + radio_playlists: list[RadioPlaylist] = [RadioPlaylist(uid=uid_gen(), name="Default", stations=[])] + # radio_playlists: list[dict[str, int | str | list[dict[str, str]]]] + + primary_stations: list[RadioStation] = [] + + primary_stations.append(RadioStation( + title="SomaFM Groove Salad", + stream_url="https://ice3.somafm.com/groovesalad-128-mp3", + country="USA", + website_url="https://somafm.com/groovesalad", + icon="https://somafm.com/logos/120/groovesalad120.png")) + + primary_stations.append(RadioStation( + title="SomaFM PopTron", + stream_url="https://ice3.somafm.com/poptron-128-mp3", + country="USA", + website_url="https://somafm.com/poptron/", + icon="https://somafm.com/logos/120/poptron120.jpg")) + + primary_stations.append(RadioStation( + title="SomaFM Vaporwaves", + stream_url="https://ice4.somafm.com/vaporwaves-128-mp3", + country="USA", + website_url="https://somafm.com/vaporwaves", + icon="https://somafm.com/img3/vaporwaves400.png")) + + primary_stations.append(RadioStation( + title="DKFM Shoegaze Radio", + stream_url="https://kathy.torontocast.com:2005/stream", + country="Canada", + website_url="https://decayfm.com", + icon="https://cdn-profiles.tunein.com/s193842/images/logod.png")) + + for station in primary_stations: + radio_playlists[0].stations.append(station) + + 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] + pctl.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: + bag.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: + tauon.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: + if db_version > 69: + radio_playlists = [] + radioplaylist_jar = save[165] + for d in radioplaylist_jar: + nt = RadioPlaylist(**d) + radio_playlists.append(nt) + else: + 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") + logging.info(f"Database loaded in {round(perf_timer.get(), 3)} seconds.") + + shoot_pump = threading.Thread(target=pumper, args=(bag,)) + shoot_pump.daemon = True + shoot_pump.start() + + if window_size is None: + window_size = window_default_size + gui.rspw = 200 + + core_timer.set() + + perf_timer.set() + keys = set(master_library.keys()) + for pl in multi_playlist: + if db_version > 68 or db_version == 0: + keys -= set(pl.playlist_ids) + else: + keys -= set(pl[2]) + if len(keys) > 5000: + gui.suggest_clean_db = True + # logging.info(f"Database scanned in {round(perf_timer.get(), 3)} seconds.") + + bag.pump = False + shoot_pump.join() + + # Run upgrades if we're behind the current DB standard + if db_version > 0 and db_version < latest_db_version: + logging.warning(f"Current DB version {db_version} was lower than latest {latest_db_version}, running migrations!") + try: + master_library, multi_playlist, star_store, p_force_queue, theme, prefs, gui, gen_codes, radio_playlists = database_migrate( + db_version=db_version, + master_library=master_library, + install_mode=install_mode, + multi_playlist=multi_playlist, + star_store=star_store, + install_directory=install_directory, + a_cache_dir=a_cache_dir, + cache_directory=cache_directory, + config_directory=config_directory, + user_directory=user_directory, + gui=gui, + gen_codes=gen_codes, + prefs=prefs, + radio_playlists=radio_playlists, + theme=theme, + p_force_queue=p_force_queue, + ) + except ValueError: + logging.exception("That should not happen") + sys.exit(42) + except Exception: + logging.exception("Unknown error running database migration!") + sys.exit(42) + + playing_in_queue = min(playing_in_queue, len(track_queue) - 1) + + shoot = threading.Thread(target=keymaps.load) + shoot.daemon = True + shoot.start() + + # Loading Config ----------------- + + download_directories: list[str] = [] + cf = Config() + + load_prefs(bag=bag, cf=cf) + save_prefs(bag=bag, cf=cf) + + 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)) + + # 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(bag=bag, cf=cf) + + # 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}'") + + # ---- + if prefs.use_gamepad: + SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) + + + 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!") + auto_scale(bag) + scale_assets(bag=bag, 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 + + mpt = None + try: + p = ctypes.util.find_library("libopenmpt") + if p: + mpt = ctypes.cdll.LoadLibrary(p) + elif msys: + mpt = ctypes.cdll.LoadLibrary("libopenmpt-0.dll") + else: + mpt = ctypes.cdll.LoadLibrary("libopenmpt.so") + + mpt.openmpt_module_create_from_memory.restype = c_void_p + mpt.openmpt_module_get_metadata.restype = c_char_p + mpt.openmpt_module_get_duration_seconds.restype = c_double + except Exception: + logging.exception("Failed to load libopenmpt!") + + gme = None + p = None + try: + p = ctypes.util.find_library("libgme") + if p: + gme = ctypes.cdll.LoadLibrary(p) + elif msys: + gme = ctypes.cdll.LoadLibrary("libgme-0.dll") + else: + gme = ctypes.cdll.LoadLibrary("libgme.so") + + gme.gme_free_info.argtypes = [ctypes.POINTER(GMETrackInfo)] + gme.gme_track_info.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.POINTER(GMETrackInfo)), ctypes.c_int] + gme.gme_track_info.restype = ctypes.c_char_p + gme.gme_open_file.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p), ctypes.c_int] + gme.gme_open_file.restype = ctypes.c_char_p + except Exception: + logging.exception("Cannot find libgme") - old_position = old_window_position - if not prefs.save_window_position: - old_position = None + 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 + except Exception: + logging.exception("Failed init notifications") - save = [ - draw_border, - gui.save_size, - prefs.window_opacity, - gui.scale, - gui.maximized, - old_position, - ] + 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 not fs_mode: - with (user_directory / "window.p").open("wb") as file: - pickle.dump(save, file, protocol=pickle.HIGHEST_PROTOCOL) + QuickThumbnail.renderer = holder.renderer - tauon.spot_ctl.save_token() + strings = Strings() + signal.signal(signal.SIGINT, signal_handler) - with (user_directory / "lyrics_substitutions.json").open("w") as file: - json.dump(prefs.lyrics_subs, file) + if system == "Windows" or msys: + from lynxtray import SysTrayIcon + + tray = STray() + + stats_gen = GStats() + + tauon = Tauon( + holder=holder, + bag=bag, + strings=strings, + gui=gui) + radiobox = tauon.radiobox + tauon.dummy_track = radiobox.dummy_track + star_store=tauon.star_store + pctl = tauon.pctl + pctl.default_playlist = multi_playlist[0].playlist_ids + lb = ListenBrainz(prefs) + deco = tauon.deco + deco.get_themes = get_themes + deco.renderer = renderer + + album_star_store = AlbumStarStore(subsonic=tauon.subsonic) + 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") - save_prefs() - 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) + 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!") - logging.info("Done writing database") + 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") - 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") + if prefs.backend != 4: + prefs.backend = 4 + if system == "Linux" and not macos and not msys: + gnome = Gnome(tauon) -SDL_StartTextInput() + 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) + mac_circle = asset_loader(bag, loaded_asset_dc, "macstyle.png", True) + if not maximized and gui.maximized: + SDL_MaximizeWindow(t_window) + # logging.error(SDL_GetError()) -# SDL_SetHint(SDL_HINT_IME_INTERNAL_EDITING, b"1") -# SDL_EventState(SDL_SYSWMEVENT, 1) + # t_window = SDL_CreateShapedWindow( + # window_title, + # SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + # window_size[0], window_size[1], + # flags) + # logging.error(SDL_GetError()) -def test_show_add_home_music() -> None: - gui.add_music_folder_ready = True + if system == "Windows" or msys: + gui.window_id = sss.info.win.window - if music_directory is None: - gui.add_music_folder_ready = False - return - for item in pctl.multi_playlist: - if item.last_folder == str(music_directory): - gui.add_music_folder_ready = False - break + # ------------------------------------------------------------------------------------------- + # 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), + ] -test_show_add_home_music() + 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) -if gui.restart_album_mode: - toggle_album_mode(True) + except Exception: + logging.exception("Error loading xcursor") -if gui.remember_library_mode: - toggle_library_mode() -quick_import_done = [] + # try: + # SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, b"1") + # + # except Exception: + # logging.exception("old version of SDL detected") -if reload_state: - if reload_state[0] == 1: - pctl.jump_time = reload_state[1] - pctl.play() + # get window surface and set up renderer + # renderer = SDL_CreateRenderer(t_window, 0, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC) -pctl.notify_update() + # 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 -key_focused = 0 + self.taskbar.SetProgressValue(self.window_id, perc, 100) -theme = get_theme_number(prefs.theme_name) + elif pctl.playing_state == 2 and self.updated_state != 2: + self.updated_state = 2 + self.taskbar.SetProgressState(self.window_id, 0x8) -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)] + 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) -def menu_is_open(): - for menu in Menu.instances: - if menu.active: - return True - return False + if (install_directory / "TaskbarLib.tlb").is_file(): + logging.info("Taskbar progress enabled") + pctl.windows_progress = WinTask() + else: + pctl.taskbar_progress = False + logging.warning("Could not find TaskbarLib.tlb") -def is_level_zero(include_menus: bool = True) -> bool: - if include_menus: - for menu in Menu.instances: - if menu.active: - return False + ddt.scale = gui.scale + ddt.force_subpixel_text = prefs.force_subpixel_text - 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 + launch = Launch(tauon, pctl, gui, ddt) + draw = Drawing() + if system == "Linux": + prime_fonts(bag) + 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() + click_time = time.time() + scroll_hold = False + scroll_point = 0 + scroll_bpoint = 0 + sbl = 50 + sbp = 100 + + asbp = 50 + album_scroll_hold = False + + message_info_icon = asset_loader(bag, loaded_asset_dc, "notice.png") + message_warning_icon = asset_loader(bag, loaded_asset_dc, "warning.png") + message_tick_icon = asset_loader(bag, loaded_asset_dc, "done.png") + message_arrow_icon = asset_loader(bag, loaded_asset_dc, "ext.png") + message_error_icon = asset_loader(bag, loaded_asset_dc, "error.png") + message_bubble_icon = asset_loader(bag, loaded_asset_dc, "bubble.png") + message_download_icon = asset_loader(bag, loaded_asset_dc, "ddl.png") + + + + + tool_tip = ToolTip(bag, gui) + tool_tip2 = ToolTip(bag, gui) + tool_tip2.trigger = 1.8 + track_box_path_tool_timer = Timer() + + columns_tool_tip = ToolTip3(bag, gui) + tool_tip_instant = ToolTip3(bag, gui) + + # 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 = tauon.cancel_menu + gallery_menu = Menu(tauon, 175, show_icons=True) + artist_info_menu = Menu(tauon, 135) + queue_menu = Menu(tauon, 150) + repeat_menu = tauon.repeat_menu + shuffle_menu = tauon.shuffle_menu + 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(tauon=tauon) + export_playlist_box = ExportPlaylistBox(bag) + rename_playlist_box = RenamePlaylistBox() + + 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)) + + filter_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "filter.png", True)) + filter_icon.colour = [43, 213, 255, 255] + filter_icon.xoff = 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) + 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)) + + folder_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "folder.png", True)) + info_icon = MenuIcon(asset_loader(bag, 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(bag, loaded_asset_dc, "power.png", True) + + 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(bag, 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(bag, loaded_asset_dc, "lock.png", True) + lock_icon = MenuIcon(lock_asset) + lock_icon.base_asset_mod = asset_loader(bag, 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)) + + 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(bag, loaded_asset_dc, "heart-menu.png", True)) + spot_heartx_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "heart-menu.png", True)) + transcode_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "transcode.png", True)) + mod_folder_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "mod_folder.png", True)) + settings_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "settings2.png", True)) + rename_tracks_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "pen.png", True)) + add_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "new.png", True)) + spot_asset = asset_loader(bag, 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)) + 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)) -# Hold the splash/loading screen for a minimum duration -# while core_timer.get() < 0.5: -# time.sleep(0.01) + # 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) -# Resize menu widths to text length (length can vary due to translations) -for menu in Menu.instances: + 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)) - w = 0 - icon_space = 0 + 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)) - if menu.show_icons: - icon_space = 25 * gui.scale + 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)) - 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]: + 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)) - test_width = ddt.get_text_w(sub_item.title, menu.font) + sub_icon_space + 23 * gui.scale - ww = max(test_width, ww) + # 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) - item.sub_menu_width = max(ww, item.sub_menu_width) + 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)) - menu.w = max(w, menu.w) + 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 drop_file(target): - global new_playlist_cooldown - global mouse_down - global drag_mode + 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)) - 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)) + 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)) - 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] + 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)) - #logging.info((i_x, i_y)) - gui.drop_playlist_target = 0 - #logging.info(event.drop) + 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)) - 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 + # tab_menu.add_to_sub("Filepath", 1, gen_sort_path, pass_ref=True) - 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 + # tab_menu.add_to_sub("Artist → gui.abc", 0, gen_sort_artist, pass_ref=True) - 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 + # 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)) - elif gui.lsp and gui.panelY < i_y < window_size[1] - gui.panelBY and i_x < gui.lspw and gui.mode == 1: + playlist_menu.add(MenuItem("Paste", paste, paste_deco)) - y = gui.panelY - y += 5 * gui.scale - y += playlist_box.tab_h + playlist_box.gap + 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 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 + # Create track context menu + track_menu = Menu(tauon, 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)) - else: - gui.drop_playlist_target = pctl.active_playlist_viewing + heartx_icon.colour = [55, 55, 55, 255] + heartx_icon.xoff = 1 + heartx_icon.yoff = 0 + heartx_icon.colour_callback = heart_xmenu_colour - 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") + 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 - load_order = LoadClass() - load_order.target = target.replace("\\", "/") + # Mark track as 'liked' + track_menu.add(MenuItem("Love", love_index, love_decox, icon=heartx_icon)) - if os.path.isdir(load_order.target): - quick_import_done.append(load_order.target) + heart_spot_icon = MenuIcon(asset_loader(bag, 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 - # 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) + 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)) - load_order.playlist = pctl.multi_playlist[gui.drop_playlist_target].uuid_int - load_orders.append(copy.deepcopy(load_order)) + # def toggle_queue(mode: int = 0) -> bool: + # if mode == 1: + # return prefs.show_queue + # prefs.show_queue ^= True + # prefs.show_queue ^= True - #logging.info('dropped: ' + str(dropped_file)) - gui.update += 1 - mouse_down = False - drag_mode = False + track_menu.add(MenuItem(_("Add to Queue"), add_to_queue, pass_ref=True, hint="MB3")) -if gui.restore_showcase_view: - enter_showcase_view() -if gui.restore_radio_view: - enter_radio_view() + track_menu.add(MenuItem(_("↳ After Current Track"), add_to_queue_next, pass_ref=True, show_test=test_shift)) -# switch_playlist(len(pctl.multi_playlist) - 1) + track_menu.add(MenuItem(_("Show in Gallery"), show_in_gal, pass_ref=True, show_test=test_show)) -SDL_SetRenderTarget(renderer, overlay_texture_texture) + track_menu.add_sub(_("Meta…"), 160) -block_size = 3 + 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)) -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 + # track_menu.add(_('Paste + Transfer Folder'), lightning_paste, pass_ref=False, show_test=lightning_move_test) - # gui.update = 2 + track_menu.add(MenuItem(_("Paste"), menu_paste, paste_deco, pass_ref=True)) - while SDL_PollEvent(ctypes.byref(event)) != 0: + track_menu.add(MenuItem(_("Delete Track File"), delete_track, pass_ref=True, icon=delete_icon, show_test=test_shift)) - # if event.type == SDL_SYSWMEVENT: - # logging.info(event.syswm.msg.contents) # Not implemented by pysdl2 + track_menu.br() - 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 + # 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)) - 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) + track_menu.add_to_sub(0, MenuItem(_("Edit fields…"), activate_trans_editor)) - if event.type == SDL_RENDER_TARGETS_RESET and not msys: - reset_render = True + 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)) - if event.type == SDL_DROPTEXT: - power += 5 + # track_menu.add_to_sub("Reset Track Play Count", 0, reset_play_count, pass_ref=True) - link = event.drop.file.decode() - #logging.info(link) + # track_menu.add('Reload Metadata', reload_metadata, pass_ref=True) + track_menu.add_to_sub(0, MenuItem(_("Rescan Tags"), reload_metadata, pass_ref=True)) - 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)) + mbp_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "mbp-g.png")) + mbp_icon.base_asset = asset_loader(bag, loaded_asset_dc, "mbp-gs.png") - 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] + mbp_icon.xoff = 2 + mbp_icon.yoff = -1 - 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 + if gui.scale == 1.25: + mbp_icon.yoff = 0 - shoot_dl = threading.Thread(target=download_img, args=(link, target_dir, track)) - shoot_dl.daemon = True - shoot_dl.start() + edit_icon = None + if prefs.tag_editor_name == "Picard": + edit_icon = mbp_icon - gui.update = True + 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]) - 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) + selection_menu = Menu(tauon, 200, show_icons=False) + folder_menu = Menu(tauon, 193, show_icons=True) - if event.type == SDL_DROPFILE: + folder_menu.add(MenuItem(_("Open Folder"), open_folder, pass_ref=True, pass_ref_deco=True, icon=folder_icon, disable_test=open_folder_disable_test)) - 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) + 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)) - elif event.type == 8192: - gui.pl_update = 1 - gui.update += 2 + 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)) - elif event.type == SDL_QUIT: - power += 5 + 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)) - 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 + 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)) - elif event.type == SDL_MOUSEMOTION: + 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)) - 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: - k_input = True - focused = True - power += 5 - gui.update += 1 - gui.mouse_in_window = True + # 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() - if ggc == 2: # dont click on first full frame - continue + tauon.spot_ctl.cache_saved_albums = spot_cache_saved_albums - 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") + # 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)) - # if mouse_position[1] > 1 and mouse_position[0] > 1: - # mouse_down = True + 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(bag, loaded_asset_dc, "sonemic-g.png")) + son_icon.base_asset = asset_loader(bag, 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(bag, 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 = tauon.x_menu + set_menu = tauon.set_menu + field_menu = tauon.field_menu + dl_menu = tauon.dl_menu + view_menu = Menu(tauon, 170) + set_menu_hidden = Menu(tauon, 100) + vis_menu = Menu(tauon, 140) + window_menu = Menu(tauon, 140) + + extra_menu = tauon.extra_menu + + 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)) + + # 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 tauon.chrome: + x_menu.add_sub(_("Chromecast…"), 220) + shooter(cast_search2, [tauon]) + + tauon.chrome_menu = x_menu + + #x_menu.add(_("Cast…"), cast_search, cast_deco) + + mode_menu = Menu(tauon, 175) + + 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)) - inp.mouse_click = True + mode_menu.br() + mode_menu.add(MenuItem(_("Copy Title to Clipboard"), copy_bb_metadata)) - 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] - - 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) + extra_menu.add(MenuItem(_("Random Track"), random_track, hint=";")) - 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.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: - key_ctrl_down = True - else: - key_meta = True - key_focused = 1 + radiorandom_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "radiorandom.png", True)) + revert_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "revert.png", True)) - 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: - key_ctrl_down = False - else: - key_meta = False - key_focused = 1 + 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)) - elif event.type == SDL_TEXTINPUT: - k_input = True - power += 5 - input_text += event.text.text.decode("utf-8") + 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)) - gui.update += 1 - #logging.info(input_text) + # extra_menu.add('Toggle Repeat', toggle_repeat, hint='COMMA') - 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) + # extra_menu.add('Toggle Random', toggle_random, hint='PERIOD') + extra_menu.add(MenuItem(_("Clear Queue"), clear_queue, queue_deco, hint="Alt+Shift+Q")) - if event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED: - #logging.info("SDL_WINDOWEVENT_FOCUS_GAINED") + heart_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "heart-menu.png", True)) + heart_row_icon = asset_loader(bag, loaded_asset_dc, "heart-track.png", True) + heart_notify_icon = asset_loader(bag, loaded_asset_dc, "heart-notify.png", True) + heart_notify_break_icon = asset_loader(bag, loaded_asset_dc, "heart-notify-break.png", True) + # spotify_row_icon = asset_loader(bag, loaded_asset_dc, "spotify-row.png", True) + star_pc_icon = asset_loader(bag, loaded_asset_dc, "star-pc.png", True) + star_row_icon = asset_loader(bag, loaded_asset_dc, "star.png", True) + star_half_row_icon = asset_loader(bag, loaded_asset_dc, "star-half.png", True) - if system == "Linux" and not macos and not msys: - gnome.focus() - k_input = True + heart_colours = ColourGenCache(0.7, 0.7) - 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 + heart_icon.colour = [245, 60, 60, 255] + heart_icon.xoff = 3 + heart_icon.yoff = 0 - elif event.window.event == SDL_WINDOWEVENT_FOCUS_LOST: - close_all_menus() - key_focused = 1 - gui.update += 1 + if gui.scale == 1.25: + heart_icon.yoff = 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 + 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)) - logical_size[0] = event.window.data1 - logical_size[1] = event.window.data2 + 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)) - if gui.mode != 3: - logical_size[0] = max(300, logical_size[0]) - logical_size[1] = max(300, logical_size[1]) + extra_menu.add(MenuItem(_("Show Full Artist"), get_artist_spot, + show_test=spotify_show_test, icon=spot_icon)) - 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 + extra_menu.add(MenuItem(_("Start Spotify Remote"), show_spot_playing, show_spot_playing_deco, show_test=spotify_show_test, + icon=spot_icon)) - auto_scale() - update_layout = True + 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() - elif event.window.event == SDL_WINDOWEVENT_ENTER: - #logging.info("ENTER") - mouse_enter_window = True - gui.mouse_in_window = True - gui.update += 1 - # elif event.window.event == SDL_WINDOWEVENT_HIDDEN: - # - elif event.window.event == SDL_WINDOWEVENT_EXPOSED: - #logging.info("expose") - gui.lowered = False + last_fm_icon = asset_loader(bag, loaded_asset_dc, "as.png", True) + lastfm_icon = MenuIcon(last_fm_icon) - elif event.window.event == SDL_WINDOWEVENT_MINIMIZED: - gui.lowered = True - # if prefs.min_to_tray: - # tray.down() - # tauon.thread_manager.sleep() + if gui.scale == 2 or gui.scale == 1.25: + lastfm_icon.xoff = 0 + else: + lastfm_icon.xoff = -1 - elif event.window.event == SDL_WINDOWEVENT_RESTORED: + lastfm_icon.yoff = 1 - gui.lowered = False - gui.maximized = False - gui.pl_update = 1 - gui.update += 2 + lastfm_icon.colour = [249, 70, 70, 255] + lastfm_icon.colour_callback = lastfm_colour - if update_title: - update_title_do() - #logging.info("restore") - elif event.window.event == SDL_WINDOWEVENT_SHOWN: - focused = True - gui.pl_update = 1 - gui.update += 1 + lb_icon = MenuIcon(asset_loader(bag, loaded_asset_dc, "lb-g.png")) + lb_icon.base_asset = asset_loader(bag, loaded_asset_dc, "lb-gs.png") - # elif event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED: - # logging.info("FOCUS GAINED") - # # input.mouse_enter_event = True - # # gui.update += 1 - # # k_input = True - 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 + lb_icon.mode_callback = lb_mode - elif event.window.event == SDL_WINDOWEVENT_LEAVE: - gui.mouse_in_window = False - gui.update += 1 - power = 1000 + lb_icon.xoff = 3 + lb_icon.yoff = -1 - if mouse_moved: - if fields.test(): - gui.update += 1 + if gui.scale == 1.25: + lb_icon.yoff = 0 - 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 prefs.auto_lfm: + listen_icon = lastfm_icon + elif lb.enable: + listen_icon = lb_icon + else: + listen_icon = None - # 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!!!!") + x_menu.add(MenuItem("LFM", tauon.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)) - power += 1 + added = [] + message_box = MessageBox() + nagbox = NagBox() - 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") + spot_search_rate_timer = Timer() - 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 + album_info_cache = {} + perfs = [] + album_info_cache_key = (-1, -1) + tauon.get_album_info = get_album_info - if animate_monitor_timer.get() < 1 or load_orders: + power_tag_colours = ColourGenCache(0.5, 0.8) - if cursor_blink_timer.get() > 0.65: - cursor_blink_timer.set() - TextBox.cursor ^= True - gui.update = 1 + gui.pt_on = Timer() + gui.pt_off = Timer() + gui.pt = 0 - if k_input: - cursor_blink_timer.set() - TextBox.cursor = True + tauon.reload_albums = reload_albums - SDL_Delay(3) - power = 1000 + # ------------------------------------------------------------------------------------ + # 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() - if mouse_wheel or k_input or gui.pl_update or gui.update or top_panel.adds: # or mouse_moved: - power = 1000 + ctlThread = threading.Thread(target=controller, args=[tauon]) + ctlThread.daemon = True + ctlThread.start() - if prefs.art_bg and core_timer.get() < 3: - power = 1000 + if prefs.enable_remote: + tauon.start_remote() + tauon.remote_limited = False + # -------------------------------------------------------------- - 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 + pref_box = Over(bag=bag, gui=gui) - if pctl.wake_past_time: + inc_arrow = asset_loader(bag, loaded_asset_dc, "inc.png", True) + dec_arrow = asset_loader(bag, loaded_asset_dc, "dec.png", True) + corner_icon = asset_loader(bag, loaded_asset_dc, "corner.png", True) - if get_real_time() > pctl.wake_past_time: - pctl.wake_past_time = 0 - power = 1000 - gui.update += 1 + bottom_bar_ao1 = BottomBarType_ao1(bag=bag, gui=gui) + mini_mode = MiniMode(bag=bag, gui=gui) + mini_mode2 = MiniMode2(bag=bag, gui=gui) + mini_mode3 = MiniMode3(bag=bag, gui=gui) - if gui.level_update and not album_scroll_hold and not scroll_hold: - power = 500 + restore_ignore_timer = Timer() + restore_ignore_timer.force_set(100) - # 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) + 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) - if not pctl.running: - break + playlist_render = StandardPlaylist(tauon=tauon, pl_bg=pl_bg) + art_box = ArtBox(tauon=tauon) + 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() - if pctl.playing_state > 0: - power += 400 + tree_view_box = TreeView() - if power < 500: + queue_box = QueueBox(gui=gui, queue_menu=queue_menu) - time.sleep(0.03) + meta_box = MetaBox(tauon=tauon) + artist_picture_render = PictureRender() + artist_preview_render = PictureRender() - 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 + # artist info box def + artist_info_box = ArtistInfoBox(bag=bag) - else: - power = 0 + 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)) - gui.pl_update = min(gui.pl_update, 2) + radio_thumb_gen = RadioThumbGen(tauon=tauon) - new_playlist_cooldown = False + 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)) - if prefs.auto_extract and prefs.monitor_downloads: - dl_mon.scan() + radio_view = RadioView(tauon=tauon) + showcase = Showcase() + cctest = ColourPulse2() + view_box = ViewBox(tauon=tauon) + dl_mon = DLMon() + tauon.dl_mon = dl_mon + dl_menu.add(MenuItem("Dismiss", dismiss_dl)) - 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 + 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() - #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") + 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, [tauon], None] + tauon.thread_manager.d["search"] = [worker2, [tauon], None] + tauon.thread_manager.d["gallery"] = [worker3, [tauon], None] + tauon.thread_manager.d["style"] = [worker4, [tauon], None] + tauon.thread_manager.d["radio-thumb"] = [radio_thumb_gen.loader, [tauon], 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() - if key_meta: - input_text = "" - k_input = False - inp.key_return_press = False - inp.key_tab_press = False - 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) + gui.playlist_view_length = int(((window_size[1] - gui.playlist_top) / 16) - 1) - if key_focused != 0: + 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") + + 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(tauon=tauon) + + if gui.restart_album_mode: + toggle_album_mode(tauon=tauon, force_on=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 + + if pctl.pl_to_id(pctl.active_playlist_viewing) in gui.gallery_positions: + gui.album_scroll_px = gui.gallery_positions[pctl.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(tauon=tauon) + + # 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(dirs) + 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() + + 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 inp.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 + d_mouse_click = False + right_click = False + level_2_right_click = False + inp.mouse_click = False + middle_click = False + inp.mouse_up = False inp.key_return_press = False key_down_press = False key_up_press = False @@ -43553,4414 +42075,5051 @@ def drop_file(target): 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 + inp.mouse_wheel = 0 pref_box.scroll = 0 + new_playlist_cooldown = False 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 + mouse_enter_window = False + gui.mouse_in_window = True + if key_focused: + key_focused -= 1 + + # f not inp.mouse_down: + inp.k_input = False + clicked = False + focused = False + mouse_moved = False + gui.level_2_click = False + + # gui.update = 2 + + while SDL_PollEvent(ctypes.byref(event)) != 0: + + # if event.type == SDL_SYSWMEVENT: + # logging.info(event.syswm.msg.contents) # Not implemented by pysdl2 + + 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 + + if event.type == SDL_CONTROLLERBUTTONDOWN and prefs.use_gamepad: + inp.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 pctl.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: + + power += 5 + + 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)) + + 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] + + 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 + + shoot_dl = threading.Thread(target=download_img, args=(link, target_dir, track)) + shoot_dl.daemon = True + shoot_dl.start() + + gui.update = True + + 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) + + 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 + + if gui.tray_active and prefs.min_to_tray and not inp.key_shift_down: + tauon.min_to_tray() 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: + 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") + inp.k_input = True + gui.update += 1 + + elif event.type == SDL_MOUSEMOTION: + + inp.mouse_position[0] = int(event.motion.x / logical_size[0] * window_size[0]) + inp.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: + + inp.k_input = True + focused = True + power += 5 + gui.update += 1 + gui.mouse_in_window = True + + if ggc == 2: # dont click on first full frame + continue + + if event.button.button == SDL_BUTTON_RIGHT: + right_click = True + inp.right_down = True + #logging.info("RIGHT DOWN") + elif event.button.button == SDL_BUTTON_LEFT: + #logging.info("LEFT DOWN") + + # if inp.mouse_position[1] > 1 and inp.mouse_position[0] > 1: + # inp.mouse_down = True + + inp.mouse_click = True + + inp.mouse_down = True + elif event.button.button == SDL_BUTTON_MIDDLE: + if not tauon.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: + inp.k_input = True + power += 5 + gui.update += 1 + if event.button.button == SDL_BUTTON_RIGHT: + inp.right_down = False + elif event.button.button == SDL_BUTTON_LEFT: + if inp.mouse_down: + inp.mouse_up = True + inp.mouse_up_position[0] = event.motion.x / logical_size[0] * window_size[0] + inp.mouse_up_position[1] = event.motion.y / logical_size[0] * window_size[0] + + inp.mouse_down = False + gui.update += 1 + elif event.type == SDL_KEYDOWN and key_focused == 0: + inp.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.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: + inp.key_ralt = True + elif event.key.keysym.sym == SDLK_LALT: + inp.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: + inp.key_shift_down = True + elif event.key.keysym.sym == SDLK_RSHIFT: + inp.key_shiftr_down = True + elif event.key.keysym.sym == SDLK_LCTRL: + inp.key_ctrl_down = True + elif event.key.keysym.sym == SDLK_RCTRL: + inp.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: + inp.key_ctrl_down = True + else: + inp.key_meta = True + key_focused = 1 + + elif event.type == SDL_KEYUP: + + inp.k_input = True + power += 5 + gui.update += 2 + if event.key.keysym.sym == SDLK_LSHIFT: + inp.key_shift_down = False + elif event.key.keysym.sym == SDLK_LCTRL: + inp.key_ctrl_down = False + elif event.key.keysym.sym == SDLK_RCTRL: + inp.key_rctrl_down = False + elif event.key.keysym.sym == SDLK_RSHIFT: + inp.key_shiftr_down = False + elif event.key.keysym.sym == SDLK_RALT: + gui.album_tab_mode = False + inp.key_ralt = False + elif event.key.keysym.sym == SDLK_LALT: + gui.album_tab_mode = False + inp.key_lalt = False + elif event.key.keysym.sym == SDLK_LGUI: + if macos: + inp.key_ctrl_down = False + else: + inp.key_meta = False + key_focused = 1 + + elif event.type == SDL_TEXTINPUT: + inp.k_input = True + power += 5 + input_text += event.text.text.decode("utf-8") + + gui.update += 1 + #logging.info(input_text) + + elif event.type == SDL_MOUSEWHEEL: + inp.k_input = True + power += 6 + inp.mouse_wheel += event.wheel.y + gui.update += 1 + elif event.type == SDL_WINDOWEVENT: + + power += 5 + #logging.info(event.window.event) + + 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() + inp.k_input = True + + mouse_enter_window = True + focused = True + gui.lowered = False + key_focused = 1 + inp.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_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 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 + 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(bag) + update_layout = True + + + elif event.window.event == SDL_WINDOWEVENT_ENTER: + #logging.info("ENTER") + mouse_enter_window = True + gui.mouse_in_window = True + gui.update += 1 + + # elif event.window.event == SDL_WINDOWEVENT_HIDDEN: + # + elif event.window.event == SDL_WINDOWEVENT_EXPOSED: + #logging.info("expose") + gui.lowered = False + + 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: + + gui.lowered = False + gui.maximized = False + gui.pl_update = 1 + gui.update += 2 + + if update_title: + update_title_do() + #logging.info("restore") + + elif event.window.event == SDL_WINDOWEVENT_SHOWN: + focused = True + gui.pl_update = 1 + gui.update += 1 + + # elif event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED: + # logging.info("FOCUS GAINED") + # # input.mouse_enter_event = True + # # gui.update += 1 + # # inp.k_input = True + + 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 + + elif event.window.event == SDL_WINDOWEVENT_LEAVE: + gui.mouse_in_window = False + gui.update += 1 + power = 1000 - 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 mouse_moved: + if tauon.fields.test(): + gui.update += 1 - if keymaps.test("show-encode-folder"): - open_encode_out() + 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 keymaps.test("toggle-left-panel"): - gui.lsp ^= True - update_layout_do() + 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 - if keymaps.test("toggle-last-left-panel"): - toggle_left_last() - update_layout_do() + if animate_monitor_timer.get() < 1 or load_orders: - if keymaps.test("escape"): - key_esc_press = True + if cursor_blink_timer.get() > 0.65: + cursor_blink_timer.set() + TextBox.cursor ^= True + gui.update = 1 - if key_ctrl_down: - gui.pl_update += 1 + if inp.k_input: + cursor_blink_timer.set() + TextBox.cursor = True - if mouse_enter_window: - inp.key_return_press = False + SDL_Delay(3) + power = 1000 - if gui.fullscreen and key_esc_press: - gui.fullscreen = False - SDL_SetWindowFullscreen(t_window, 0) + if inp.mouse_wheel or inp.k_input or gui.pl_update or gui.update or top_panel.adds: # or mouse_moved: + power = 1000 - # 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 prefs.art_bg and core_timer.get() < 3: + power = 1000 - 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 inp.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 - # 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 pctl.wake_past_time: - 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 get_real_time() > pctl.wake_past_time: + pctl.wake_past_time = 0 + power = 1000 + gui.update += 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 + if gui.level_update and not album_scroll_hold and not scroll_hold: + power = 500 - 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 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 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 pctl.running: + break - if quick_search_mode: - if keymaps.test("add-to-queue") and pctl.selected_ready(): - add_selected_to_queue() - input_text = "" + if pctl.playing_state > 0: + power += 400 + + if power < 500: + time.sleep(0.03) + + 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 tauon.transcode_list and not gui.frame_callback_list: + pass else: + sleep_timer.set() + if sleep_timer.get() > 2: + SDL_WaitEventTimeout(None, 1000) + continue - if key_c_press and key_ctrl_down: - gui.pl_update = 1 - s_copy() + else: + power = 0 - if key_x_press and key_ctrl_down: - gui.pl_update = 1 - s_cut() + gui.pl_update = min(gui.pl_update, 2) - if key_v_press and key_ctrl_down: - gui.pl_update = 1 - paste() + new_playlist_cooldown = False - if keymaps.test("playpause"): - pctl.play_pause() + if prefs.auto_extract and prefs.monitor_downloads: + dl_mon.scan() + if inp.mouse_down and not tauon.coll((2, 2, window_size[0] - 4, window_size[1] - 4)): + #logging.info(SDL_GetMouseState(None, None)) + if SDL_GetGlobalMouseState(None, None) == 0: + inp.mouse_down = False + inp.mouse_up = True + inp.quick_drag = False - if inp.key_return_press and (gui.rename_folder_box or rename_track_box.active or radiobox.active): + #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") + + if inp.key_meta: + input_text = "" + inp.k_input = False inp.key_return_press = False - inp.level_2_enter = True + inp.key_tab_press = False - if key_ctrl_down and key_z_press: - undo.undo() + if inp.k_input: + # TODO(Martin): Check if commenting this out is the correct thing to do + #if inp.mouse_click or right_click or inp.mouse_up: + # last_click_location = copy.deepcopy(inp.click_location) + # click_location = copy.deepcopy(inp.inp.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 + inp.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 + inp.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) + inp.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) + inp.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) + inp.k_input = True + + if inp.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("quit"): - tauon.exit("Quit keyboard shortcut pressed") + 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("testkey"): # F7: test - pass + if keymaps.test("show-encode-folder"): + open_encode_out() - 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("toggle-left-panel"): + gui.lsp ^= True + update_layout_do(tauon=tauon) - 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]) + if keymaps.test("toggle-last-left-panel"): + toggle_left_last() + update_layout_do(tauon=tauon) - fr_s = star_store.full_get(fr.index) - to_s = star_store.full_get(to.index) + if keymaps.test("escape"): + key_esc_press = True - fr_scr = fr.lfm_scrobbles - to_scr = to.lfm_scrobbles + if inp.key_ctrl_down: + gui.pl_update += 1 - undo.bk_playtime_transfer(fr, fr_s, fr_scr, to, to_s, to_scr) + if mouse_enter_window: + inp.key_return_press = False - if to_s is None: - to_s = star_store.new_object() - if fr_s is None: - fr_s = star_store.new_object() + 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 tauon.search_over.active: + if prefs.album_mode and gui.album_tab_mode \ + and not inp.key_ctrl_down \ + and not inp.key_meta \ + and not inp.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 tauon.search_over.active: + if key_del: + close_all_menus() + del_selected() + + # 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 - new = star_store.new_object() + 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 - 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 + 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 - 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) + if keymaps.test("goto-bottom"): + n = len(pctl.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(pctl.default_playlist) - 1 + gui.pl_update = 1 - 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 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 tauon.search_over.active and not gui.box_over and not trans_edit_box.active: - if keymaps.test("toggle-gallery"): - toggle_album_mode() + if quick_search_mode: + if keymaps.test("add-to-queue") and pctl.selected_ready(): + add_selected_to_queue() + input_text = "" - 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("toggle-minimode"): - set_mini_mode() - gui.update += 1 + if key_c_press and inp.key_ctrl_down: + gui.pl_update = 1 + s_copy() - if keymaps.test("cycle-layouts"): + if key_x_press and inp.key_ctrl_down: + gui.pl_update = 1 + s_cut() - 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 key_v_press and inp.key_ctrl_down: + gui.pl_update = 1 + paste() - if keymaps.test("cycle-layouts-reverse"): + if keymaps.test("playpause"): + pctl.play_pause() - 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 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 keymaps.test("toggle-artistinfo"): - view_box.artist_info(True) + if inp.key_ctrl_down and key_z_press: + undo.undo() - if keymaps.test("toggle-showcase"): - view_box.lyrics(True) + if keymaps.test("quit"): + tauon.exit("Quit keyboard shortcut pressed") - if keymaps.test("toggle-gallery-keycontrol"): - toggle_gallery_keycontrol() + if keymaps.test("testkey"): # F7: test + pass - if keymaps.test("toggle-show-art"): - toggle_side_art() + 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( + pctl.default_playlist): + fr = pctl.get_track(tauon.copied_track) + to = pctl.get_track(pctl.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")) - elif gui.mode == 3: - if keymaps.test("toggle-minimode"): - restore_full_mode() - gui.update += 1 + if keymaps.test("toggle-gallery"): + toggle_album_mode(tauon=tauon) - ab_click = False + if keymaps.test("toggle-right-panel"): + if gui.combo_mode: + exit_combo() + elif not prefs.album_mode: + toggle_side_panel() + else: + toggle_album_mode(tauon=tauon) - if keymaps.test("new-playlist"): - new_playlist() + if keymaps.test("toggle-minimode"): + set_mini_mode() + gui.update += 1 - if keymaps.test("edit-generator"): - edit_generator_box(pctl.active_playlist_viewing) + if keymaps.test("cycle-layouts"): - if keymaps.test("new-generator-playlist"): - new_playlist() - edit_generator_box(pctl.active_playlist_viewing) + 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("delete-playlist"): - delete_playlist(pctl.active_playlist_viewing) + if keymaps.test("cycle-layouts-reverse"): - if keymaps.test("delete-playlist-force"): - delete_playlist(pctl.active_playlist_viewing, force=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("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("toggle-columns"): + view_box.col(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 keymaps.test("toggle-artistinfo"): + view_box.artist_info(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]): - inp.mouse_click = False - gui.level_2_click = True + if keymaps.test("toggle-showcase"): + view_box.lyrics(True) - if right_click: - level_2_right_click = True + if keymaps.test("toggle-gallery-keycontrol"): + toggle_gallery_keycontrol() - if pref_box.enabled: + if keymaps.test("toggle-show-art"): + toggle_side_art() - 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 + elif gui.mode == 3: + if keymaps.test("toggle-minimode"): + restore_full_mode() + gui.update += 1 - 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 + ab_click = False - 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("new-playlist"): + new_playlist() - if mouse_wheel != 0: - gui.update += 1 - if mouse_down is True: - gui.update += 1 + if keymaps.test("edit-generator"): + edit_generator_box(pctl.active_playlist_viewing) - 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("new-generator-playlist"): + new_playlist() + edit_generator_box(pctl.active_playlist_viewing) - 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("delete-playlist"): + delete_playlist(pctl.active_playlist_viewing) - 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 keymaps.test("delete-playlist-force"): + delete_playlist(pctl.active_playlist_viewing, force=True) - 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("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 - # 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() + # 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 keymaps.test("previous"): - key_left_press = False - pctl.back() + if inp.mouse_click and ( + prefs.show_nag or gui.box_over or radiobox.active or tauon.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 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 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 tauon.coll([x, y, w, h]): + inp.mouse_click = False + gui.level_2_click = True - if keymaps.test("revert"): - pctl.revert() + if right_click: + level_2_right_click = True - if keymaps.test("random-track-start"): - pctl.advance(rr=True) + if pref_box.enabled: - if keymaps.test("vol-down"): - if pctl.player_volume > 3: - pctl.player_volume -= 3 - else: - pctl.player_volume = 0 - pctl.set_volume() + 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 keymaps.test("toggle-mute"): - pctl.toggle_mute() + pref_box.scroll = inp.mouse_wheel + inp.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 keymaps.test("vol-up"): - pctl.player_volume += 3 - pctl.player_volume = min(pctl.player_volume, 100) - pctl.set_volume() + if right_click and ( + radiobox.active or rename_track_box.active or gui.rename_playlist_box or gui.rename_folder_box or tauon.search_over.active): + right_click = False - 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 inp.mouse_wheel != 0: + gui.update += 1 + if inp.mouse_down is True: + gui.update += 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.append(pctl.selected_in_playlist) - else: - shift_selection.remove(r) + if keymaps.test("pagedown"): # key_PGD: + if len(pctl.default_playlist) > 10: + pctl.playlist_view_position += gui.playlist_view_length - 4 + if pctl.playlist_view_position > len(pctl.default_playlist): + pctl.playlist_view_position = len(pctl.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(pctl.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("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 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 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("info-playing"): + if pctl.selected_in_playlist < len(pctl.default_playlist): + r_menu_index = pctl.get_track(pctl.default_playlist[pctl.selected_in_playlist]).index + track_box = True - if keymaps.test("toggle-shuffle"): - # pctl.random_mode ^= True - toggle_random() + if keymaps.test("info-show"): + if pctl.selected_in_playlist < len(pctl.default_playlist): + r_menu_index = pctl.get_track(pctl.default_playlist[pctl.selected_in_playlist]).index + track_box = True - 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]) + # These need to be disabled when text fields are active + if not tauon.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("toggle-repeat"): - toggle_repeat() + if keymaps.test("previous"): + key_left_press = False + pctl.back() - if keymaps.test("random-track"): - random_track() + if key_a_press and inp.key_ctrl_down: + gui.pl_update = 1 + shift_selection = range(len(pctl.default_playlist)) # TODO(Martin): This can under some circumstances end up doing a range.clear() - if keymaps.test("random-album"): - random_album() + if keymaps.test("revert"): + pctl.revert() - 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-start"): + pctl.advance(rr=True) - 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("vol-down"): + if pctl.player_volume > 3: + pctl.player_volume -= 3 + else: + pctl.player_volume = 0 + pctl.set_volume() - if keymaps.test("seek-forward"): - pctl.seek_time(pctl.playing_time + prefs.seek_interval) + if keymaps.test("toggle-mute"): + pctl.toggle_mute() - if keymaps.test("seek-back"): - pctl.seek_time(pctl.playing_time - prefs.seek_interval) + if keymaps.test("vol-up"): + pctl.player_volume += 3 + pctl.player_volume = min(pctl.player_volume, 100) + pctl.set_volume() - if keymaps.test("play"): - pctl.play() + if keymaps.test("shift-down") and len(pctl.default_playlist) > 0: + gui.pl_update += 1 + if pctl.selected_in_playlist > len(pctl.default_playlist) - 1: + pctl.selected_in_playlist = 0 - if keymaps.test("stop"): - pctl.stop() + if not shift_selection: + shift_selection.append(pctl.selected_in_playlist) + if pctl.selected_in_playlist < len(pctl.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("pause"): - pctl.pause_only() + if keymaps.test("shift-up") and pctl.selected_in_playlist > -1: + gui.pl_update += 1 + if pctl.selected_in_playlist > len(pctl.default_playlist) - 1: + pctl.selected_in_playlist = 0 - if keymaps.test("love-playing"): - bar_love(notify=True) + if not shift_selection: + shift_selection.append(pctl.selected_in_playlist) + if pctl.selected_in_playlist < len(pctl.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("love-selected"): - select_love(notify=True) + if keymaps.test("toggle-shuffle"): + # pctl.random_mode ^= True + toggle_random() - 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("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("substitute-search-selected"): - if pctl.selected_ready(): - show_sub_search(pctl.get_track(default_playlist[pctl.selected_in_playlist])) + if keymaps.test("toggle-repeat"): + toggle_repeat() - if keymaps.test("global-search"): - activate_search_overlay() + if keymaps.test("random-track"): + random_track() - if keymaps.test("add-to-queue") and pctl.selected_ready(): - add_selected_to_queue() + if keymaps.test("random-album"): + random_album() - if keymaps.test("clear-queue"): - clear_queue() + 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("regenerate-playlist"): - regenerate_playlist(pctl.active_playlist_viewing) + 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("cycle-theme"): - gui.reload_theme = True - gui.theme_temp_current = -1 - gui.temp_themes.clear() - theme += 1 + if keymaps.test("seek-forward"): + pctl.seek_time(pctl.playing_time + prefs.seek_interval) - if keymaps.test("cycle-theme-reverse"): - gui.theme_temp_current = -1 - gui.temp_themes.clear() - pref_box.devance_theme() + if keymaps.test("seek-back"): + pctl.seek_time(pctl.playing_time - prefs.seek_interval) - if keymaps.test("reload-theme"): - gui.reload_theme = True + if keymaps.test("play"): + pctl.play() - # if mouse_position[1] < 1: - # mouse_down = False + if keymaps.test("stop"): + pctl.stop() - if mouse_down is False: - scroll_hold = False + if keymaps.test("pause"): + pctl.pause_only() - # if focused is True: - # mouse_down = False + if keymaps.test("love-playing"): + bar_love(notify=True) - 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 keymaps.test("love-selected"): + select_love(notify=True) - # 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: - 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 - else: - to_got = 1 - to_get = 1 - loaderCommandReady = True - tauon.thread_manager.ready("worker") - break + if keymaps.test("search-lyrics-selected"): + if pctl.selected_ready(): + track = pctl.get_track(pctl.default_playlist[pctl.selected_in_playlist]) + if track.lyrics: + show_message(_("Track already has lyrics")) + else: + get_lyric_wiki(track) - elif loading_in_progress is True: - loading_in_progress = False - pctl.notify_change() + if keymaps.test("substitute-search-selected"): + if pctl.selected_ready(): + show_sub_search(pctl.get_track(pctl.default_playlist[pctl.selected_in_playlist])) - 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 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-------------------------------------------------------------------- - - if gui.reload_theme is True: + if keymaps.test("global-search"): + activate_search_overlay() - gui.pl_update = 1 - theme_files = get_themes() + if keymaps.test("add-to-queue") and pctl.selected_ready(): + add_selected_to_queue() - if theme > len(theme_files): # sic - theme = 0 + if keymaps.test("clear-queue"): + clear_queue() - if theme > 0: - theme_number = theme - 1 - try: + if keymaps.test("regenerate-playlist"): + regenerate_playlist(pctl.active_playlist_viewing) - colours.column_colours.clear() - colours.column_colours_playing.clear() + if keymaps.test("cycle-theme"): + gui.reload_theme = True + gui.theme_temp_current = -1 + gui.temp_themes.clear() + theme += 1 - theme_item = theme_files[theme_number] + if keymaps.test("cycle-theme-reverse"): + gui.theme_temp_current = -1 + gui.temp_themes.clear() + pref_box.previous_theme() - gui.theme_name = theme_item[1] - colours.lm = False - colours.__init__() + if keymaps.test("reload-theme"): + gui.reload_theme = True - load_theme(colours, Path(theme_item[0])) - deco.load(colours.deco) - logging.info("Applying theme: " + gui.theme_name) + # if inp.mouse_position[1] < 1: + # inp.mouse_down = False - if colours.lm: - info_icon.colour = [60, 60, 60, 255] - else: - info_icon.colour = [61, 247, 163, 255] + if inp.mouse_down is False: + scroll_hold = False - if colours.lm: - folder_icon.colour = [255, 190, 80, 255] - else: - folder_icon.colour = [244, 220, 66, 255] + # if focused is True: + # inp.mouse_down = False - if colours.lm: - settings_icon.colour = [85, 187, 250, 255] + if inp.media_key: + if inp.media_key == "Play": + if pctl.playing_state == 0: + pctl.play() else: - settings_icon.colour = [232, 200, 96, 255] + 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() - if colours.lm: - radiorandom_icon.colour = [120, 200, 120, 255] - else: - radiorandom_icon.colour = [153, 229, 133, 255] + 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() - 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 + inp.media_key = "" - if gui.pl_update and not gui.update: - gui.update = 1 + if len(load_orders) > 0: + pctl.loading_in_progress = True + pctl.after_import_flag = True + tauon.thread_manager.ready("worker") + if tauon.loaderCommand == tauon.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] - if gui.update > 0 and not resize_mode: - gui.update = min(gui.update, 2) - - 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 - - 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) + # 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): + tauon.loaderCommand = tauon.LC_Folder + else: + tauon.loaderCommand = tauon.LC_File + if order.traget.endswith(".xspf"): + to_got = "xspf" + to_get = 0 + else: + to_got = 1 + to_get = 1 + tauon.loaderCommandReady = True + tauon.thread_manager.ready("worker") + break - # perf_timer.set() - gui.update_on_drag = False - gui.pl_update_on_drag = False + elif pctl.loading_in_progress is True: + pctl.loading_in_progress = False + pctl.notify_change() - # mouse_position[0], mouse_position[1] = get_sdl_input.mouse() - gui.showed_title = False + if tauon.loaderCommand == tauon.LC_Done: + tauon.loaderCommand = tauon.LC_None + gui.update += 1 + # gui.pl_update = 1 + # pctl.loading_in_progress = False + + if update_layout: + update_layout_do(tauon=tauon) + update_layout = False + + # if tauon.worker_save_state and\ + # not gui.pl_pulse and\ + # not pctl.loading_in_progress and\ + # not tauon.to_scan and\ + # not tauon.plex.scanning and\ + # not tauon.cm_clean_db and\ + # not tauon.lastfm.scanning_friends and\ + # not tauon.move_in_progress: + # save_state() + # cue_list.clear() + # tauon.worker_save_state = False + + # ----------------------------------------------------- + # THEME SWITCHER-------------------------------------------------------------------- + + if gui.reload_theme is True: + gui.pl_update = 1 + theme_files = get_themes(dirs=dirs) - 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 + if prefs.theme > len(theme_files): # sic + prefs.theme = 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 prefs.theme > 0: + theme_number = prefs.theme - 1 + try: + colours.column_colours.clear() + colours.column_colours_playing.clear() - fields.clear() - gui.cursor_want = 0 + theme_item = theme_files[theme_number] - gui.layer_focus = 0 + gui.theme_name = theme_item[1] + colours.lm = False + colours.__init__() - if inp.mouse_click or mouse_wheel or right_click: - mouse_position[0], mouse_position[1] = get_sdl_input.mouse() + load_theme(colours, Path(theme_item[0])) + deco.load(colours.deco) + logging.info("Applying theme: " + gui.theme_name) - 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 colours.lm: + info_icon.colour = [60, 60, 60, 255] + else: + info_icon.colour = [61, 247, 163, 255] - # 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 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] + + 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") - # 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 theme == 0: + gui.theme_name = "Mindaro" + logging.info("Applying default theme: Mindaro") + colours.lm = False + colours.__init__() + colours.post_config() + deco.unload() - if gui.mode == 4: - launch.render() - elif gui.mode == 1 or gui.mode == 2: + prefs.theme_name = gui.theme_name + #logging.info("Theme number: " + str(theme)) + gui.reload_theme = False ddt.text_background_colour = colours.playlist_panel_background - # Side Bar Draging---------- - - if not mouse_down: - side_drag = False - - 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 (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 + # --------------------------------------------------------------------------------------------------------- + # GUI DRAWING------ + #logging.info(gui.update) + #logging.info(gui.lowered) + if gui.mode == 3: + gui.pl_update = 0 - if inp.mouse_click: - side_drag = True - gui.side_bar_drag_source = mouse_position[0] - gui.side_bar_drag_original = gui.rspw + if gui.pl_update and not gui.update: + gui.update = 1 - if not quick_drag: - gui.cursor_want = 1 + if gui.update > 0 and not resize_mode: + gui.update = min(gui.update, 2) - # side drag update - if side_drag: + 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 - offset = gui.side_bar_drag_source - mouse_position[0] + 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) - target = gui.side_bar_drag_original + offset + # perf_timer.set() + gui.update_on_drag = False + gui.pl_update_on_drag = 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 + # inp.mouse_position[0], inp.mouse_position[1] = get_sdl_input.mouse() + gui.showed_title = False - # 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 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: + inp.mouse_position[0] = -300 + inp.mouse_position[1] = -300 - 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 + 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") - else: - max_w = window_size[0] + tauon.fields.clear() + gui.cursor_want = 0 - if not album_mode and target > max_w - 12 * gui.scale: - target = max_w - gui.rspw = target - gui.rsp_full_lock = True + gui.layer_focus = 0 - else: - gui.rspw = target - gui.rsp_full_lock = False + if inp.mouse_click or inp.mouse_wheel or right_click: + inp.mouse_position[0], inp.mouse_position[1] = get_sdl_input.mouse() - if album_mode: - pass - # gui.rspw = target + 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 album_mode and gui.rspw < album_mode_art_size + 50 * gui.scale: - target = album_mode_art_size + 50 * gui.scale + # Don't register bottom level click when closing message box + if gui.message_box and pref_box.enabled and not key_focused and not tauon.coll(message_box.get_rect()): + inp.mouse_click = False + gui.message_box = False - # Prevent side bar getting too small - target = max(target, 120 * gui.scale) + # 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") - # Remember size for this view mode - if not album_mode: - gui.pref_rspw = target - else: - gui.pref_gallery_w = target + if gui.mode == 4: + launch.render() + elif gui.mode == 1 or gui.mode == 2: - update_layout_do() + ddt.text_background_colour = colours.playlist_panel_background - # ALBUM GALLERY RENDERING: - # Gallery view - # C-AR + # Side Bar Draging---------- + + if not inp.mouse_down: + gui.side_drag = False + + rect = (window_size[0] - gui.rspw - 5 * gui.scale, gui.panelY, 12 * gui.scale, + window_size[1] - gui.panelY - gui.panelBY) + tauon.fields.add(rect) + + if (tauon.coll(rect) or gui.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 prefs.album_mode) \ + and not artist_info_scroll.held \ + and gui.layer_focus == 0 and gui.show_playlist: + + if gui.side_drag is True: + draw_sep_hl = True + # gui.update += 1 + gui.update_on_drag = True - 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 inp.mouse_click: + gui.side_drag = True + gui.side_bar_drag_source = inp.mouse_position[0] + gui.side_bar_drag_original = gui.rspw - w = gui.rspw + if not inp.quick_drag: + gui.cursor_want = 1 - if window_size[0] < 750 * gui.scale: - w = window_size[0] - 20 * gui.scale - if gui.lsp: - w -= gui.lspw + # side drag update + if gui.side_drag: - x = window_size[0] - w - h = window_size[1] - gui.panelY - gui.panelBY + offset = gui.side_bar_drag_source - inp.mouse_position[0] - if not gui.show_playlist and inp.mouse_click: - left = 0 - if gui.lsp: - left = gui.lspw + target = gui.side_bar_drag_original + offset - 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 + # Snap to album mode position if close + if not prefs.album_mode and prefs.side_panel_layout == 1: + if abs(target - gui.pref_gallery_w) < 35 * gui.scale: + target = gui.pref_gallery_w - rect = [x, gui.panelY, w, h] - ddt.rect(rect, colours.gallery_background) - # ddt.rect_r(rect, [255, 0, 0, 200], True) + # 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 - area_x = w + 38 * gui.scale - # area_x = w - 40 * 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 - row_len = int((area_x - album_h_gap) / (album_mode_art_size + album_h_gap)) + else: + max_w = window_size[0] - #logging.info(row_len) + if not prefs.album_mode and target > max_w - 12 * gui.scale: + target = max_w + gui.rspw = target + gui.rsp_full_lock = True - compact = 40 * gui.scale - a_offset = 7 * gui.scale + else: + gui.rspw = target + gui.rsp_full_lock = False - l_area = x - r_area = w - c_area = r_area // 2 + l_area + if prefs.album_mode: + pass + # gui.rspw = target - ddt.text_background_colour = colours.gallery_background + if prefs.album_mode and gui.rspw < bag.album_mode_art_size + 50 * gui.scale: + target = bag.album_mode_art_size + 50 * gui.scale - line1_colour = colours.gallery_artist_line - line2_colour = colours.grey(240) # colours.side_bar_line1 + # Prevent side bar getting too small + target = max(target, 120 * gui.scale) - if colours.side_panel_background != colours.gallery_background: - line2_colour = [240, 240, 240, 255] - line1_colour = alpha_mod([220, 220, 220, 255], 120) + # Remember size for this view mode + if not prefs.album_mode: + gui.pref_rspw = target + else: + gui.pref_gallery_w = target - 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) + update_layout_do(tauon=tauon) - if row_len == 0: - row_len = 1 + # ALBUM GALLERY RENDERING: + # Gallery view + # C-AR - dev = int((r_area - compact) / (row_len + 0)) + if prefs.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 - render_pos = 0 - album_on = 0 + w = gui.rspw - max_scroll = round( - (math.ceil((len(album_dex)) / row_len) - 1) * (album_mode_art_size + album_v_gap)) - round( - 50 * gui.scale) + if window_size[0] < 750 * gui.scale: + w = window_size[0] - 20 * gui.scale + if gui.lsp: + w -= gui.lspw - # 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: + x = window_size[0] - w + h = window_size[1] - gui.panelY - gui.panelBY - if mouse_wheel != 0: - scroll_gallery_hide_timer.set() - gui.frame_callback_list.append(TestTimer(0.9)) + if not gui.show_playlist and inp.mouse_click: + left = 0 + if gui.lsp: + left = gui.lspw - 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 > 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) - - 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) - - # 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() - - # 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] - if colours.lm: - colour = [250, 100, 0, 255] - if inp.mouse_click: - gui.pt = 1 + if left < inp.mouse_position[0] < left + 20 * gui.scale and window_size[1] - gui.panelBY > \ + inp.mouse_position[1] > gui.panelY: + toggle_album_mode(tauon=tauon) + inp.mouse_click = False + inp.mouse_down = False - power_bar_icon.render(rect[0] + round(5 * gui.scale), rect[1] + round(3 * gui.scale), colour) + rect = [x, gui.panelY, w, h] + ddt.rect(rect, colours.gallery_background) + # ddt.rect_r(rect, [255, 0, 0, 200], True) - # 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 + area_x = w + 38 * gui.scale + # area_x = w - 40 * gui.scale - if last_row != row_len: - last_row = row_len + row_len = int((area_x - album_h_gap) / (bag.album_mode_art_size + album_h_gap)) - if pctl.selected_in_playlist < len(pctl.playing_playlist()): - goto_album(pctl.selected_in_playlist) - # else: - # goto_album(pctl.playlist_playing_position) + #logging.info(row_len) - extend = 0 - if card_mode: # gui.gallery_show_text: - extend = 40 * gui.scale + compact = 40 * gui.scale + a_offset = 7 * 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]: + l_area = x + r_area = w + c_area = r_area // 2 + l_area - if b_info_bar and render_pos > gui.album_scroll_px + b_info_y: - break + ddt.text_background_colour = colours.gallery_background - 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 + line1_colour = colours.gallery_artist_line + line2_colour = colours.grey(240) # colours.side_bar_line1 - x = (l_area + dev * a) - int(album_mode_art_size / 2) + int(dev / 2) + int( - compact / 2) - a_offset + if colours.side_panel_background != colours.gallery_background: + line2_colour = [240, 240, 240, 255] + line1_colour = alpha_mod([220, 220, 220, 255], 120) - if album_dex[album_on] > len(default_playlist): - break + 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) - 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 + if row_len == 0: + row_len = 1 - # 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) + dev = int((r_area - compact) / (row_len + 0)) - # Quick drag and drop - if mouse_up and (playlist_hold and m_in) and not side_drag and shift_selection: + render_pos = 0 + album_on = 0 - info = get_album_info(album_dex[album_on]) - if info[1]: + max_scroll = round( + (math.ceil((len(album_dex)) / row_len) - 1) * (bag.album_mode_art_size + album_v_gap)) - round( + 50 * gui.scale) - track_position = info[1][0] + # Mouse wheel scrolling + if not tauon.search_over.active and not radiobox.active \ + and inp.mouse_position[0] > window_size[0] - w and gui.panelY < inp.mouse_position[1] < window_size[ + 1] - gui.panelBY: - if track_position > shift_selection[0]: - track_position = info[1][-1] + 1 + if inp.mouse_wheel != 0: + scroll_gallery_hide_timer.set() + gui.frame_callback_list.append(TestTimer(0.9)) - ref = [] - for item in shift_selection: - ref.append(default_playlist[item]) + if prefs.gallery_row_scroll: + gui.album_scroll_px -= inp.mouse_wheel * (bag.album_mode_art_size + album_v_gap) # 90 + else: + gui.album_scroll_px -= inp.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 > 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) + + 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) + tauon.fields.add(rect) + + # Show scroll area + if tauon.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() + + # 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) + tauon.fields.add(rect) + colour = [255, 255, 255, 35] + if colours.lm: + colour = [0, 0, 0, 30] + if tauon.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 inp.mouse_down or inp.mouse_up) and pctl.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 - for item in shift_selection: - default_playlist[item] = "old" + if render_pos < gui.album_scroll_px - bag.album_mode_art_size - album_v_gap: + # Skip row + render_pos += bag.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 - for item in shift_selection: - default_playlist.insert(track_position, "new") + x = (l_area + dev * a) - int(bag.album_mode_art_size / 2) + int(dev / 2) + int( + compact / 2) - a_offset - 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 album_dex[album_on] > len(pctl.default_playlist): + break - pctl.selected_in_playlist = shift_selection[0] - gui.pl_update += 1 - playlist_hold = False + rect = (x, y, bag.album_mode_art_size, bag.album_mode_art_size + extend * gui.scale) + # tauon.fields.add(rect) + m_in = tauon.coll(rect) and gui.panelY < inp.mouse_position[1] < window_size[1] - gui.panelBY - reload_albums(True) - pctl.notify_change() + # if m_in: + # ddt.rect_r((x - 7, y - 7, bag.album_mode_art_size + 14, bag.album_mode_art_size + extend + 55), [80, 80, 80, 80], True) - elif not side_drag and is_level_zero(): + # Quick drag and drop + if inp.mouse_up and (playlist_hold and m_in) and not gui.side_drag and shift_selection: - 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 info[1]: - if m_in and mouse_up and prefs.gallery_single_click: + track_position = info[1][0] - 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]) + if track_position > shift_selection[0]: + track_position = info[1][-1] + 1 - pctl.show_current() + ref = [] + for item in shift_selection: + ref.append(pctl.default_playlist[item]) - 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] + for item in shift_selection: + pctl.default_playlist[item] = "old" - if m_in: + for item in shift_selection: + pctl.default_playlist.insert(track_position, "new") - info = get_album_info(album_dex[album_on]) - if inp.mouse_click: + for b in reversed(range(len(pctl.default_playlist))): + if pctl.default_playlist[b] == "old": + del pctl.default_playlist[b] + shift_selection = [] + for b in range(len(pctl.default_playlist)): + if pctl.default_playlist[b] == "new": + shift_selection.append(b) + pctl.default_playlist[b] = ref.pop(0) - if prefs.gallery_single_click: - gui.d_click_ref = album_dex[album_on] + pctl.selected_in_playlist = shift_selection[0] + gui.pl_update += 1 + playlist_hold = False - else: + reload_albums(True) + pctl.notify_change() - if d_click_timer.get() < 0.5 and gui.d_click_ref == album_dex[album_on]: + elif not gui.side_drag and is_level_zero(): + if coll_point(inp.click_location, rect) and gui.panelY < inp.mouse_position[1] < \ + window_size[1] - gui.panelBY: + info = get_album_info(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") + if m_in and inp.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(pctl.default_playlist[album_dex[album_on]], album_dex[album_on]) + pctl.show_current() + elif inp.mouse_down and not m_in: + info = get_album_info(album_dex[album_on]) + inp.quick_drag = True + if not pl_is_locked(pctl.active_playlist_viewing) or inp.key_shift_down: + playlist_hold = True + shift_selection = info[1] + gui.pl_update += 1 + inp.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] + else: + 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(pctl.default_playlist[album_dex[album_on]], album_dex[album_on]) 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 inp.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(pctl.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: - 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 + # Add to queue grouped + add_album_to_queue(pctl.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( + pctl.default_playlist[album_dex[album_on]]).parent_folder_path + # remove from target pl + if pctl.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(pctl.default_playlist)): + if pctl.get_track(pctl.default_playlist[i]).parent_folder_path == parent: + pctl.multi_playlist[pl].playlist_ids.append(pctl.default_playlist[i]) + reload_albums(True) + else: + pctl.selected_in_playlist = album_dex[album_on] + # playlist_position = pctl.playlist_selected + shift_selection = [pctl.selected_in_playlist] + gallery_menu.activate(pctl.default_playlist[pctl.selected_in_playlist]) + r_menu_position = pctl.selected_in_playlist + + shift_selection = [] + u = pctl.selected_in_playlist + while u < len(pctl.default_playlist) and pctl.master_library[ + pctl.default_playlist[u]].parent_folder_path == \ + pctl.master_library[ + pctl.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 += bag.album_mode_art_size + album_v_gap - 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]) + render_pos = 0 + album_on = 0 + album_count = 0 - reload_albums(True) + if not pref_box.enabled or inp.mouse_wheel != 0: + gui.first_in_grid = None - 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 + # Render album grid + while render_pos < gui.album_scroll_px + window_size[1] and pctl.default_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 - bag.album_mode_art_size - album_v_gap: + # Skip row + render_pos += bag.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 - - render_pos = 0 - album_on = 0 - album_count = 0 + # if y > - if not pref_box.enabled or mouse_wheel != 0: - gui.first_in_grid = None - - # Render album grid - while render_pos < gui.album_scroll_px + window_size[1] and default_playlist: + for a in range(row_len): + if album_on > len(album_dex) - 1: + break - if b_info_bar and render_pos > gui.album_scroll_px + b_info_y: - break + x = (l_area + dev * a) - int(bag.album_mode_art_size / 2) + int(dev / 2) + int( + compact / 2) - a_offset - 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 + if album_dex[album_on] > len(pctl.default_playlist): + break - row_x = 0 + track = pctl.master_library[pctl.default_playlist[album_dex[album_on]]] - if y > window_size[1] - gui.panelBY - 30 * gui.scale and window_size[1] < 340 * gui.scale: - break - # if y > + info = get_album_info(album_dex[album_on]) + album = info[1] + # info = (0, 0, 0) - for a in range(row_len): + # rect = (x, y, bag.album_mode_art_size, bag.album_mode_art_size + extend * gui.scale) + # tauon.fields.add(rect) + # m_in = tauon.coll(rect) and gui.panelY < inp.mouse_position[1] < window_size[1] - gui.panelBY - if album_on > len(album_dex) - 1: - break + 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] - x = (l_area + dev * a) - int(album_mode_art_size / 2) + int(dev / 2) + int( - compact / 2) - a_offset + # artisttitle = colours.side_bar_line2 + # albumtitle = colours.side_bar_line1 # grey(220) - if album_dex[album_on] > len(default_playlist): - break + if card_mode: + ddt.text_background_colour = colours.grey(250) + drop_shadow.render( + x + 3 * gui.scale, y + 3 * gui.scale, + bag.album_mode_art_size + 11 * gui.scale, + bag.album_mode_art_size + 45 * gui.scale + 13 * gui.scale) + ddt.rect( + (x, y, bag.album_mode_art_size, bag.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), (bag.album_mode_art_size + 4, bag.album_mode_art_size + 4), colours.grey(200)) + + if a == row_len - 1: + gui.gallery_scroll_field_left = max( + x + bag.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), (bag.album_mode_art_size + 8, bag.album_mode_art_size + 8), + colours.gallery_highlight) + # ddt.rect_a((x, y), (bag.album_mode_art_size, bag.album_mode_art_size), + # colours.gallery_background, True) - track = pctl.master_library[default_playlist[album_dex[album_on]]] + # Draw quick add highlight + if pctl.quick_add_target: + pl = id_to_pl(pctl.quick_add_target) + if pl is not None and pctl.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), (bag.album_mode_art_size + 8, bag.album_mode_art_size + 8), c) + + # Draw transcode highlight + if tauon.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), (bag.album_mode_art_size + 8, bag.album_mode_art_size + 8), c) + # ddt.rect_a((x, y), (bag.album_mode_art_size, bag.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), (bag.album_mode_art_size + 8, bag.album_mode_art_size + 8), c) # [150, 80, 222, 255] + # ddt.rect_a((x, y), (bag.album_mode_art_size, bag.album_mode_art_size), + # colours.gallery_background, True) - info = get_album_info(album_dex[album_on]) - album = info[1] - # info = (0, 0, 0) + # Draw selection animation + if gui.gallery_animate_highlight_on == album_dex[ + album_on] and gallery_select_animate_timer.get() < 1.5: - # 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 + 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)) - 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] + c = [c[1], c[2], c[0], a] + ddt.rect_a((x - 5, y - 5), (bag.album_mode_art_size + 10, bag.album_mode_art_size + 10), c) # [150, 80, 222, 255] - # artisttitle = colours.side_bar_line2 - # albumtitle = colours.side_bar_line1 # grey(220) + gui.update += 1 - 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) + # Draw faint outline 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) - # 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)) - - 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] + (x - 1, y - 1, bag.album_mode_art_size + 2, bag.album_mode_art_size + 2), + [255, 255, 255, 11]) - 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]) + 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), (bag.album_mode_art_size, bag.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: - ddt.rect_a((x, y), (album_mode_art_size, album_mode_art_size), [0, 0, 0, 110]) + elif info[0] != 1 and pctl.playing_state != 0 and prefs.dim_art: + ddt.rect_a((x, y), (bag.album_mode_art_size, bag.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 + # Determine meta info + singles = False + artists = 0 + last_album = "" + last_artist = "" + s = 0 + ones = 0 + for id in album: + tr = pctl.get_track(pctl.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), (bag.album_mode_art_size, bag.album_mode_art_size), back_colour) + + # Draw album art + if singles: + dia = math.sqrt(bag.album_mode_art_size * bag.album_mode_art_size * 2) + ran = dia * 0.25 + off = (dia - ran) / 2 + albs = min(len(album), 5) + spacing = ran / (albs - 1) + size = round(bag.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(pctl.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) - - # 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, bag.album_mode_art_size, bag.album_mode_art_size + extend * gui.scale) + m_in = tauon.coll(rect) and gui.panelY < inp.mouse_position[1] < window_size[1] - gui.panelBY + tauon.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(bag.album_mode_art_size / 2), y + bag.album_mode_art_size - 22 * gui.scale, 2), + pctl.master_library[pctl.default_playlist[album_dex[album_on]]].parent_folder_name, + colours.gallery_artist_line, + 13, + bag.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), bag.album_mode_art_size, bag.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 inp.quick_drag and playlist_hold and inp.mouse_down: + # rect = (x, y, bag.album_mode_art_size, bag.album_mode_art_size + extend * gui.scale) + # m_in = tauon.coll(rect) and gui.panelY < inp.mouse_position[1] < window_size[1] - gui.panelBY + # if m_in: + # ddt.rect_a((x, y), (bag.album_mode_art_size, bag.album_mode_art_size), [120, 10, 255, 100], True) + + if gui.gallery_show_text: + c_index = pctl.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[pctl.default_playlist[i]].album_artist: + album_artist_dict[c_index] = pctl.master_library[ + pctl.default_playlist[i]].album_artist + else: + while i < len(pctl.default_playlist) - 1: + if pctl.master_library[pctl.default_playlist[i]].parent_folder_name != \ + pctl.master_library[ + pctl.default_playlist[album_dex[album_on]]].parent_folder_name: + album_artist_dict[c_index] = pctl.master_library[ + pctl.default_playlist[album_dex[album_on]]].artist + break + if pctl.master_library[pctl.default_playlist[i]].artist != \ + pctl.master_library[ + pctl.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 + pctl.default_playlist[album_dex[album_on]]].artist + + line = album_artist_dict[c_index] + line2 = pctl.master_library[pctl.default_playlist[album_dex[album_on]]].album + if singles: + line2 = pctl.master_library[ + pctl.default_playlist[album_dex[album_on]]].parent_folder_name + if artists > 1: + line = _("Various Artists") + + text_align = 0 + if prefs.center_gallery_text: + x += bag.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 + bag.album_mode_art_size + 8 * gui.scale, text_align), + line, + line1_colour, + 310, + bag.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 + bag.album_mode_art_size + 7 * gui.scale, text_align), + line2, + line2_colour, + 311, + bag.album_mode_art_size - 18 * gui.scale) + + ddt.text( + (x, y + bag.album_mode_art_size + (10 + 14) * gui.scale, text_align), + line, + line1_colour, + 10, + bag.album_mode_art_size - 18 * gui.scale) + elif line2 == "": ddt.text( - (x, y + album_mode_art_size + 8 * gui.scale, text_align), + (x, y + bag.album_mode_art_size + 9 * gui.scale, text_align), line, line1_colour, - 310, - album_mode_art_size - 18 * gui.scale) + 311, + bag.album_mode_art_size - 5 * gui.scale) else: ddt.text( - (x, y + album_mode_art_size + 7 * gui.scale, text_align), + (x, y + bag.album_mode_art_size + 8 * gui.scale, text_align), line2, line2_colour, - 311, - album_mode_art_size - 18 * gui.scale) + 212, + bag.album_mode_art_size) ddt.text( - (x, y + album_mode_art_size + (10 + 14) * gui.scale, text_align), + (x, y + bag.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, + bag.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 += bag.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) + tauon.fields.add(hot_r) - off = 0 + if gui.pt == 0: # mouse moves in + if tauon.coll(hot_r) and window_is_focused(t_window): + gui.pt_on.set() + gui.pt = 1 + elif gui.pt == 1: # wait then trigger if stays, reset if goes out + if not tauon.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 tauon.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 - - # Keep draw loop running while on - if gui.pt > 0: - gui.update = 2 - - # Draw tags - - 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: - - for i, item in enumerate(gui.power_bar): - - if run_y + block_h > top + h: - break + 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 - 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) + # Keep draw loop running while on + if gui.pt > 0: + gui.update = 2 - if (coll(i_rect) or ( - lightning_menu.active and lightning_menu.reference == item)) and item.peak_x == 9 * gui.scale: + # Draw tags - if not lightning_menu.active or lightning_menu.reference == item or right_click: + block_h = round(27 * gui.scale) + block_gap = 1 * gui.scale + if gui.scale == 1.25: + block_gap = 1 - minx = 100 * gui.scale - maxx = minx * 2 + if tauon.coll(hot_r) or gui.pt > 0: - ww = ddt.get_text_w(item.name, 213) + for i, item in enumerate(gui.power_bar): - w = max(minx, ww) - w = min(maxx, w) + if run_y + block_h > top + h: + break - 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 ------------------------ + 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] + tauon.fields.add(i_rect) - # End of gallery view - # -------------------------------------------------------------------------- - # Main Playlist: - if len(load_orders) > 0: + if (tauon.coll(i_rect) or ( + lightning_menu.active and lightning_menu.reference == item)) and item.peak_x == 9 * gui.scale: - for i, order in enumerate(load_orders): - if order.stage == 2: - target_pl = 0 + if not lightning_menu.active or lightning_menu.reference == item or right_click: - # Sort the tracks by track number - sort_track_2(None, order.tracks) + minx = 100 * gui.scale + maxx = minx * 2 - 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 + ww = ddt.get_text_w(item.name, 213) - 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: + w = max(minx, ww) + w = min(maxx, w) - else: - pctl.multi_playlist[target_pl].playlist_ids += order.tracks + 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 ------------------------ - pctl.update_shuffle_pool(pctl.multi_playlist[target_pl].uuid_int) + # End of gallery view + # -------------------------------------------------------------------------- + # Main Playlist: + if len(load_orders) > 0: - 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 i, order in enumerate(load_orders): + if order.stage == 2: + target_pl = 0 - if order.play and order.tracks: + # Sort the tracks by track number + sort_track_2(None, order.tracks) - for p, plst in enumerate(pctl.multi_playlist): - if order.tracks[0] in plst[2]: + 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 - switch_playlist(target_pl) - - pctl.active_playlist_playing = pctl.active_playlist_viewing - - # 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.jump(order.tracks[0], pl_position=default_playlist.index(order.tracks[0])) - - pctl.show_current(True, True, True, True, True) - - del load_orders[i] + 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: - # 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: + else: + pctl.multi_playlist[target_pl].playlist_ids += order.tracks - if _("New Playlist") in pctl.multi_playlist[target_pl].title: - auto_name_pl(target_pl) + pctl.update_shuffle_pool(pctl.multi_playlist[target_pl].uuid_int) - 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 + 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) - if gui.show_playlist: + if order.play and order.tracks: - # 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 + for p, plst in enumerate(pctl.multi_playlist): + if order.tracks[0] in plst.playlist_ids: + target_pl = p + break - if gui.combo_mode and mouse_wheel != 0: - gui.pl_update = 1 + switch_playlist(target_pl) - # MAIN PLAYLIST - # C-PR + pctl.active_playlist_playing = pctl.active_playlist_viewing - top = gui.panelY - if gui.artist_info_panel: - top += gui.artist_panel_height + # If already in playlist, delete latest add + if pctl.multi_playlist[target_pl].title == "Default": + if pctl.default_playlist.count(order.tracks[0]) > 1: + for q in reversed(range(len(pctl.default_playlist))): + if pctl.default_playlist[q] == order.tracks[0]: + del pctl.default_playlist[q] + break - 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 + pctl.jump(order.tracks[0], pl_position=pctl.default_playlist.index(order.tracks[0])) - width = gui.plw - if gui.set_bar and gui.set_mode: - left = 0 - if gui.lsp: - left = gui.lspw + pctl.show_current(True, True, True, True, True) - if gui.tracklist_center_mode: - left = gui.tracklist_inset_left - round(20 * gui.scale) - width = gui.tracklist_inset_width + round(20 * gui.scale) + del load_orders[i] - rect = [left, top, width, gui.set_height] - start = left + 16 * gui.scale - run = 0 - in_grip = False + # 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 not mouse_down and gui.set_hold != -1: - gui.set_hold = -1 + if _("New Playlist") in pctl.multi_playlist[target_pl].title: + auto_name_pl(target_pl) - 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 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: + pctl.loading_in_progress = False + pctl.notify_change() + gui.auto_play_import = False + album_artist_dict.clear() + break - 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 + if gui.show_playlist: - sort_direction = 1 + # playlist hit test + if tauon.coll(( + gui.playlist_left, gui.playlist_top, gui.plw, + window_size[1] - gui.panelY - gui.panelBY)) and not inp.drag_mode and ( + inp.mouse_click or inp.mouse_wheel != 0 or inp.right_click or inp.middle_click or inp.mouse_up or inp.mouse_down): + gui.pl_update = 1 - gui.column_sort_ani_direction = 1 - gui.column_sort_ani_x = start + run + item[1] + if gui.combo_mode and inp.mouse_wheel != 0: + gui.pl_update = 1 - elif gui.column_d_click_on == h: - gui.column_d_click_on = -1 - gui.column_d_click_timer.force_set(10) + # MAIN PLAYLIST + # C-PR - sort_direction = -1 + top = gui.panelY + if gui.artist_info_panel: + top += gui.artist_panel_height - gui.column_sort_ani_direction = -1 - gui.column_sort_ani_x = start + run + item[1] + 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 tauon.coll(rect): + set_menu_hidden.activate() + right_click = False - if sort_direction: + width = gui.plw + if gui.set_bar and gui.set_mode: + left = 0 + if gui.lsp: + left = gui.lspw - if gui.pl_st[h][0] in {"Starline", "Rating", "❤", "P", "S", "Time", "Date"}: - sort_direction *= -1 + if gui.tracklist_center_mode: + left = gui.tracklist_inset_left - round(20 * gui.scale) + width = gui.tracklist_inset_width + round(20 * gui.scale) - if sort_direction == 1: - sort_ass(h) - else: - sort_ass(h, True) - gui.column_sort_ani_timer.set() + rect = [left, top, width, gui.set_height] + start = left + 16 * gui.scale + run = 0 + in_grip = False - 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 not inp.mouse_down and gui.set_hold != -1: + gui.set_hold = -1 - gui.set_label_hold = -1 + 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]) + tauon.fields.add(m_grip) - if inp.mouse_click: - gui.set_label_hold = h - gui.set_label_point = copy.deepcopy(mouse_position) - if right_click: - set_menu.activate(h) + if tauon.coll(l_grip): + if inp.mouse_up and gui.set_label_hold != -1: + if point_distance(inp.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 - 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() + sort_direction = 1 + gui.column_sort_ani_direction = 1 + gui.column_sort_ani_x = start + run + item[1] - # else: - # combo_pl_render.full_render() - else: - gui.heart_fields.clear() - playlist_render.full_render() + elif gui.column_d_click_on == h: + gui.column_d_click_on = -1 + gui.column_d_click_timer.force_set(10) - 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() + sort_direction = -1 - if gui.combo_mode and key_esc_press and is_level_zero(): - exit_combo() + gui.column_sort_ani_direction = -1 + gui.column_sort_ani_x = start + run + item[1] - 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 + if sort_direction: - width = gui.plw + if gui.pl_st[h][0] in {"Starline", "Rating", "❤", "P", "S", "Time", "Date"}: + sort_direction *= -1 - if gui.tracklist_center_mode: - x = gui.tracklist_highlight_left - width = gui.tracklist_highlight_width + if sort_direction == 1: + sort_ass(h) + else: + sort_ass(h, True) + gui.column_sort_ani_timer.set() - rect = [x, top, width, gui.set_height] + 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 - c_bar_background = colours.column_bar_background + gui.set_label_hold = -1 - # if colours.lm: - # c_bar_background = [235, 110, 160, 255] + if inp.mouse_click: + gui.set_label_hold = h + gui.set_label_point = copy.deepcopy(inp.mouse_position) + if right_click: + set_menu.activate(h) + + if h != 0: + if tauon.coll(m_grip): + in_grip = True + if inp.mouse_click: + gui.set_hold = h + gui.set_point = inp.mouse_position[0] + gui.set_old = gui.pl_st[h - 1][1] + + if inp.mouse_down and gui.set_hold == h: + gui.pl_st[h - 1][1] = gui.set_old + (inp.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 inp.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: + tauon.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() - if gui.tracklist_center_mode: - ddt.rect((0, top, window_size[0], gui.set_height), c_bar_background) + elif gui.combo_mode: + if gui.radio_view: + radio_view.render() + elif gui.showcase_mode: + showcase.render() + # else: + # combo_pl_render.cache_render() else: - ddt.rect(rect, c_bar_background) + playlist_render.cache_render() - start = x + 16 * gui.scale - c_width = width - 16 * gui.scale + if gui.combo_mode and key_esc_press and is_level_zero(): + exit_combo() - run = 0 + 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] + tauon.fields.add(rect) + gui.delay_frame(0.26) + + if tauon.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(tauon=tauon) + if not tauon.coll(rect): + gui.bar_hover_timer.set() - for i, item in enumerate(gui.pl_st): + if gui.set_bar and gui.set_mode and not gui.combo_mode: - # if run > rect[2] - 55 * gui.scale: - # break + x = 0 + if gui.lsp: + x = gui.lspw - wid = item[1] + width = gui.plw - if run + wid > c_width: - wid = c_width - run + if gui.tracklist_center_mode: + x = gui.tracklist_highlight_left + width = gui.tracklist_highlight_width - if run > c_width - 22 * gui.scale: - break + rect = [x, top, width, gui.set_height] - # if run > c_width - 20 * gui.scale: - # run = run - 20 * gui.scale + c_bar_background = colours.column_bar_background - wid = max(0, wid) + # if colours.lm: + # c_bar_background = [235, 110, 160, 255] - # ddt.rect_r((run, 40, wid, 10), [255, 0, 0, 100]) - box = (start + run, rect[1], wid, rect[3]) + 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) - grip = (start + run, rect[1], 3 * gui.scale, rect[3]) + start = x + 16 * gui.scale + c_width = width - 16 * gui.scale - bg = c_bar_background + run = 0 - if coll(box) and gui.set_label_hold != -1: - bg = [39, 39, 39, 255] + for i, item in enumerate(gui.pl_st): - if i == gui.set_label_hold: - bg = [22, 22, 22, 255] + # if run > rect[2] - 55 * gui.scale: + # break - ddt.rect(box, bg) - ddt.rect(grip, colours.column_grip) + wid = item[1] - line = _(item[0]) - ddt.text_background_colour = bg + if run + wid > c_width: + wid = c_width - run - # # 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 run > c_width - 22 * gui.scale: + break - run += box[2] + # if run > c_width - 20 * gui.scale: + # run = run - 20 * gui.scale - 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 + wid = max(0, wid) - 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: + # ddt.rect_r((run, 40, wid, 10), [255, 0, 0, 100]) + box = (start + run, rect[1], wid, rect[3]) - do_minimize_button() + grip = (start + run, rect[1], 3 * gui.scale, rect[3]) - # edge_playlist.render(gui.playlist_left, gui.panelY, gui.plw, 2 * gui.scale) + bg = c_bar_background - bottom_playlist2.render(gui.playlist_left, window_size[1] - gui.panelBY, gui.plw, 25 * gui.scale, - bottom=True) - # -------------------------------------------- - # ALBUM ART + if tauon.coll(box) and gui.set_label_hold != -1: + bg = [39, 39, 39, 255] - # Right side panel drawing + if i == gui.set_label_hold: + bg = [22, 22, 22, 255] - if gui.rsp and not album_mode: - gui.showing_l_panel = False - target_track = pctl.show_object() + ddt.rect(box, bg) + ddt.rect(grip, colours.column_grip) - if middle_click: - if coll( - (window_size[0] - gui.rspw, gui.panelY, gui.rspw, - window_size[1] - gui.panelY - gui.panelBY)): + line = _(item[0]) + ddt.text_background_colour = bg - 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)): + # # 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) + + run += box[2] + + 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 tauon.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 < inp.mouse_position[0] and \ + inp.mouse_position[1] < gui.panelY and \ + inp.mouse_position[0] > top_panel.tabs_right_x and \ + inp.mouse_position[0] < window_size[0] - 130 * gui.scale - gui.offset_extra: + + window_menu.activate(None, (inp.mouse_position[0], 30 * gui.scale)) + + elif middle_click and top_panel.tabs_right_x < inp.mouse_position[0] and \ + inp.mouse_position[1] < gui.panelY and \ + inp.mouse_position[0] > top_panel.tabs_right_x and \ + inp.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 prefs.album_mode: + gui.showing_l_panel = False + target_track = pctl.show_object() + + if middle_click: + if tauon.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 tauon.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 tauon.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 tauon.coll((x, y, w, h)) and not tauon.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 tauon.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 prefs.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 = ((tauon.playlist_box.tab_h + tauon.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) + tauon.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 inp.quick_drag and not tauon.coll(panel_rect): + gui.force_side_on_drag = False + update_layout_do(tauon=tauon) - 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 inp.quick_drag and not coll_point(gui.drag_source_position_persist, panel_rect) and \ + not point_proximity_test( + gui.drag_source_position, + inp.mouse_position, + 10 * gui.scale): + gui.force_side_on_drag = True + if inp.mouse_up: + update_layout_do(tauon=tauon) - 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: + preview_queue = False + if inp.quick_drag and tauon.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 - 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 preview_queue: + pl_box_h = int(round(full * 5 / 6)) - 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 prefs.left_panel_mode != "queue": + tauon.playlist_box.draw(0, gui.panelY, gui.lspw, pl_box_h) + else: + pl_box_h = 0 + + if pctl.force_queue or preview_queue or not prefs.show_playlist_list or not prefs.hide_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) - if pctl.force_queue or preview_queue or not prefs.hide_queue: + # ------------------------------------------------ + # Scroll Bar - if h_estimate < half: - pl_box_h = h_estimate - else: - pl_box_h = half + # if not scroll_enable: + top = gui.panelY + if gui.artist_info_panel: + top += gui.artist_panel_height - if preview_queue: - pl_box_h = int(round(full * 5 / 6)) + 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) - if prefs.left_panel_mode != "queue": + width = 15 * gui.scale - playlist_box.draw(0, gui.panelY, gui.lspw, pl_box_h) - else: - pl_box_h = 0 + x = 0 + if gui.lsp: # Move left so it sits over panel divide - if pctl.force_queue or preview_queue or not prefs.show_playlist_list or not prefs.hide_queue: + 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 - 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) + # x = gui.plw + # width = round(14 * gui.scale) + # if gui.lsp: + # x += gui.lspw + # x -= width - # ------------------------------------------------ - # Scroll Bar + gui.scroll_hide_box = ( + x + 1 if not gui.maximized else x, top, 28 * gui.scale, window_size[1] - gui.panelBY - top) - # if not scroll_enable: - top = gui.panelY - if gui.artist_info_panel: - top += gui.artist_panel_height + tauon.fields.add(gui.scroll_hide_box) + if scroll_hide_timer.get() < 0.9 or ((tauon.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 tauon.search_over.active): - 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_opacity = 255 - width = 15 * gui.scale + if not gui.combo_mode: + sy = 31 * gui.scale + ey = window_size[1] - (30 + 22) * gui.scale - x = 0 - if gui.lsp: # Move left so it sits over panel divide + if len(pctl.default_playlist) < 50: + sbl = 85 * gui.scale + if len(pctl.default_playlist) == 0: + sbp = top + else: + sbl = 105 * 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 - - # 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 + tauon.fields.add((x + 2 * gui.scale, sbp, 20 * gui.scale, sbl)) + if tauon.coll((x, top, 28 * gui.scale, ey - top)) and ( + inp.mouse_down or right_click) \ + and coll_point(inp.click_location, (x, top, 28 * gui.scale, ey - top)): - 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: - gui.pl_update = 1 - if right_click: + sbp = inp.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(pctl.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(pctl.default_playlist): + # logging.info("END") + + # elif inp.mouse_position[1] < sbp: + # pctl.playlist_view_position -= 2 + # elif inp.mouse_position[1] > sbp + sbl: + # pctl.playlist_view_position += 2 + elif inp.mouse_click: + + if inp.mouse_position[1] < sbp: + gui.scroll_direction = -1 + elif inp.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 # inp.mouse_position[1] + scroll_point = inp.mouse_position[1] + scroll_bpoint = sbp + else: + # gui.update += 1 + if sbp < inp.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(pctl.default_playlist)) + + if sbp + sbl > ey: + sbp = ey - sbl + elif sbp < top: + sbp = top + + if not inp.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] - int(sbl / 2) + sbp = inp.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 (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)) + pctl.playlist_view_position = int(len(pctl.default_playlist) * per) + logging.debug("Position set by scroll bar (drag)") - if sbp + sbl > ey: - sbp = ey - sbl - elif sbp < top: - sbp = top - if not mouse_down: - scroll_hold = False + elif len(pctl.default_playlist) > 0: + per = pctl.playlist_view_position / len(pctl.default_playlist) + sbp = int((ey - top - sbl) * per) + top + 1 - 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 + bg = [255, 255, 255, 6] + fg = colours.scroll_colour - 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 colours.lm: + bg = [200, 200, 200, 100] + fg = [100, 100, 100, 200] + 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)) - elif len(default_playlist) > 0: - per = pctl.playlist_view_position / len(default_playlist) - sbp = int((ey - top - sbl) * per) + top + 1 + if (tauon.coll((x + 2 * gui.scale, sbp, 20 * gui.scale, sbl)) and inp.mouse_position[ + 0] != 0) or scroll_hold: + ddt.rect_a((x + 1 * gui.scale, sbp), (width, sbl), [255, 255, 255, 19]) - bg = [255, 255, 255, 6] - fg = colours.scroll_colour + # NEW TOP BAR + # C-TBR - if colours.lm: - bg = [200, 200, 200, 100] - fg = [100, 100, 100, 200] + if gui.mode == 1: + tauon.top_panel.render() + + # 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.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)) + gui.queue_frame_draw = None - 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]) + # BOTTOM BAR! + # C-BB - # NEW TOP BAR - # C-TBR + ddt.text_background_colour = colours.bottom_panel_colour - if gui.mode == 1: - top_panel.render() + if prefs.shuffle_lock: + bottom_bar_ao1.render() + else: + tauon.bottom_bar1.render() + + if prefs.art_bg and not prefs.bg_showcase_only: + style_overlay.display() + # if inp.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 - # 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)) + # Overlay GUI ---------------------- - gui.queue_frame_draw = None + if gui.rename_playlist_box: + rename_playlist_box.render() - # BOTTOM BAR! - # C-BB + if gui.preview_artist: - ddt.text_background_colour = colours.bottom_panel_colour + 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)) - if prefs.shuffle_lock: - bottom_bar_ao1.render() - else: - bottom_bar1.render() + artist_preview_render.draw(gui.preview_artist_location[0], gui.preview_artist_location[1]) + if inp.mouse_click or right_click or inp.mouse_wheel: + 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) + 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 - style_overlay.hole_punches.clear() + inp.key_return_press = False - 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 gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False - # Overlay GUI ---------------------- + tc = pctl.master_library[r_menu_index] - if gui.rename_playlist_box: - rename_playlist_box.render() + w = round(540 * gui.scale) + h = round(240 * gui.scale) + comment_mode = 0 - if 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 - 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)) + x = round((window_size[0] / 2) - (w / 2)) + y = round((window_size[1] / 2) - (h / 2)) - 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 = "" + x1 = int(x + 18 * gui.scale) + x2 = int(x + 98 * gui.scale) - 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 + value_font_a = 312 + value_font = 12 - inp.key_return_press = False + # if inp.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) - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False + # 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) - tc = pctl.master_library[r_menu_index] + 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 - w = round(540 * gui.scale) - h = round(240 * gui.scale) - comment_mode = 0 + if inp.mouse_click and not tauon.coll([x, y, w, h]): + track_box = False - 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 + else: + art_size = int(115 * gui.scale) - x = round((window_size[0] / 2) - (w / 2)) - y = round((window_size[1] / 2) - (h / 2)) + # 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)) - x1 = int(x + 18 * gui.scale) - x2 = int(x + 98 * gui.scale) + y -= int(24 * gui.scale) + y1 = int(y + (40 * gui.scale)) - value_font_a = 312 - value_font = 12 + ext_rect = [x + w - round(38 * gui.scale), y + round(44 * gui.scale), round(38 * gui.scale), + round(12 * gui.scale)] - # 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) + line = tc.file_ext + ex_colour = [130, 130, 130, 255] + if line in format_colours: + ex_colour = format_colours[line] - # 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) + # Spotify icon rendering + if line == "SPTY": + colour = [30, 215, 96, 255] + h, l, s = rgb_to_hls(colour[0], colour[1], colour[2]) - 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 + rect = (x + w - round(35 * gui.scale), y + round(30 * gui.scale), round(30 * gui.scale), + round(30 * gui.scale)) + tauon.fields.add(rect) + if tauon.coll(rect): + l += 0.1 + gui.cursor_want = 3 - if inp.mouse_click and not coll([x, y, w, h]): - track_box = False + 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) - else: - art_size = int(115 * gui.scale) + colour = hls_to_rgb(h, l, s) - # 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)) + gui.spot_info_icon.render(x + w - round(33 * gui.scale), y + round(35 * gui.scale), colour) - y -= int(24 * gui.scale) - y1 = int(y + (40 * gui.scale)) + # 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) - ext_rect = [x + w - round(38 * gui.scale), y + round(44 * gui.scale), round(38 * gui.scale), - round(12 * gui.scale)] - line = tc.file_ext - ex_colour = [130, 130, 130, 255] - if line in format_colours: - ex_colour = format_colours[line] + rect = [x1, y1 + int(2 * gui.scale), 450 * gui.scale, 14 * gui.scale] + tauon.fields.add(rect) + if tauon.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) - # Spotify icon rendering - if line == "SPTY": - colour = [30, 215, 96, 255] - h, l, s = rgb_to_hls(colour[0], colour[1], colour[2]) + if tauon.coll(rect): + ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.title, 314) - 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 + y1 += int(16 * gui.scale) + rect = [x1, y1 + (2 * gui.scale), 450 * gui.scale, 14 * gui.scale] + tauon.fields.add(rect) + if tauon.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 tauon.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] + tauon.fields.add(rect) + if tauon.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) + + if tauon.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] + tauon.fields.add(rect) + path = tc.fullpath + if msys: + path = path.replace("/", "\\") + if tauon.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 - (1 * gui.scale)), tc.artist, - value_colour, value_font_a, max_w=390 * 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): - ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.artist, value_font_a) + if tauon.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(16 * gui.scale) + y1 += int(15 * 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) - - 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) + if tc.samplerate != 0: + ddt.text((x1, y1), _("Samplerate"), key_colour_off, 212, max_w=70 * gui.scale) - q = ddt.text( - (x2, y1 - int(3 * gui.scale)), clean_string(path), - path_colour, 210, max_w=425 * gui.scale) + line = str(tc.samplerate) + " Hz" - 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() + 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.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.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) - off = ddt.text((x2, y1), line, value_colour, value_font) + # ----------- + 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] + tauon.fields.add(rect) + if tauon.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) - if tc.bit_depth > 0: - line = str(tc.bit_depth) + " bit" - ddt.text((x2 + off + 9 * gui.scale, y1), line, value_colour, 311) + q = ddt.text( + (x + (8 + 88) * gui.scale, y1), tc.album_artist, + value_colour, value_font, max_w=120 * gui.scale) + if tauon.coll(rect): + ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.album_artist, value_font) - y1 += int(15 * gui.scale) + x -= int(170 * gui.scale) - 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) + rect = [x1, y1, 150 * gui.scale, 16 * gui.scale] + tauon.fields.add(rect) + if tauon.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")) - 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) - - 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((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) - 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) - - 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 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] + tauon.fields.add(rect) + if tauon.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.album_artist) + copy_to_clipboard(tc.genre) 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) - - x -= int(170 * gui.scale) + 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(23 * gui.scale) + y1 += int(15 * gui.scale) - total = star_store.get(r_menu_index) + rect = [x1, y1 + (2 * gui.scale), 150 * gui.scale, 14 * gui.scale] + tauon.fields.add(rect) + if tauon.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) + + 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] + tauon.fields.add(rect) + if tauon.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 tauon.coll(rect): + ex_tool_tip(x2 + 185 * gui.scale, y1, q, tc.composer, value_font_a) - ratio = 0 + x -= int(170 * gui.scale) - if total > 0 and pctl.master_library[ - r_menu_index].length > 1: - ratio = total / (tc.length - 1) + y1 += int(23 * 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) + total = star_store.get(r_menu_index) - y1 += int(15 * gui.scale) + ratio = 0 - rect = [x1, y1, 150, 14] + if total > 0 and pctl.master_library[ + r_menu_index].length > 1: + ratio = total / (tc.length - 1) - 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 count"), key_colour_off, 212, max_w=70 * gui.scale) + ddt.text((x2, y1), str(int(ratio)), value_colour, value_font) - line = time.strftime("%H:%M:%S", time.gmtime(total)) + y1 += int(15 * gui.scale) - 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) + rect = [x1, y1, 150, 14] - # ------- - if tc.lyrics != "": + if tauon.coll(rect) and inp.key_shift_down and inp.mouse_wheel != 0: + star_store.add(r_menu_index, 60 * inp.mouse_wheel) - 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 + line = time.strftime("%H:%M:%S", time.gmtime(total)) - 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) - inp.mouse_click = False - else: - ddt.text((x1, y1), _("Comment"), key_colour_off, 212) - # ddt.draw_text((x1, y1), "Comment", key_colour_off, 12) + 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 "\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 tc.lyrics != "": - 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] + 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 - 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]) + tauon.fields.add(rect) + if tauon.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() + tauon.fields.add(link_rect) + if tauon.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 tauon.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 + tauon.fields.add(tool_rect) + if not gui.top_bar_mode2 or tauon.coll(tool_rect): + draw_window_tools(tauon) - 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 + + gui.level_2_click = False - ddt.text_background_colour = colours.box_background + 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 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_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) - p = ddt.text( - (x + 10 * gui.scale, y + 9 * gui.scale), _("Folder Modification"), colours.box_title_text, 213) + ddt.text_background_colour = 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 + if key_esc_press or ( + (inp.mouse_click or right_click or level_2_right_click) and not tauon.coll((x, y, w, h))): + gui.rename_folder_box = False - rename_folder.draw(x + 14 * gui.scale, y + 41 * gui.scale, colours.box_input_text, width=300) + p = ddt.text( + (x + 10 * gui.scale, y + 9 * gui.scale), _("Folder Modification"), colours.box_title_text, 213) - 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 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 - 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 + rename_folder.draw(x + 14 * gui.scale, y + 41 * gui.scale, colours.box_input_text, width=300) - 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 + 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 inp.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=inp.mouse_up, tooltip=tt): + if inp.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 inp.key_ctrl_down and key_v_press: - search_over.render() + # tauon.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 == "": + tauon.search_over.render() + + if keymaps.test("quick-find") and quick_search_mode is False: + if not tauon.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 (inp.key_ctrl_down and key_f_press)) and quick_search_mode is False: + # if not tauon.search_over.active: + # quick_search_mode = True + # if search_clear_timer.get() > 3: + # search_text.text = "" + # input_text = "" + # elif ((key_backslash_press or (inp.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 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 len(input_text) > 0: + search_index = -1 + + if inp.backspace_press and search_text.text == "": + quick_search_mode = False - if search_text.text[-1] == "/": - tt_title = search_text.text.replace("/", "") + 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 (inp.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: - 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(): + + 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 pctl.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) + + else: + search_terms = search_text.text.lower().split() + for item in pctl.default_playlist: + 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(pctl.default_playlist) - 1: + search_index += 1 + if search_index > len(pctl.default_playlist) - 1: + search_index = 0 + + search_terms = search_text.text.lower().split() + tr = pctl.get_track(pctl.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(pctl.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 inp.key_shiftr_down \ + and not inp.key_shift_down \ + and not inp.key_ctrl_down \ + and not inp.key_rctrl_down \ + and not inp.key_meta \ + and not inp.key_lalt \ + and not inp.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(pctl.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[pctl.default_playlist[search_index]].title.lower() + \ + pctl.master_library[pctl.default_playlist[search_index]].artist.lower() \ + + pctl.master_library[pctl.default_playlist[search_index]].album.lower() + \ + pctl.master_library[pctl.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): pctl.selected_in_playlist = search_index - if len(default_playlist) > 10 and search_index > 10: + if len(pctl.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() - 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 - - 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: - - 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 - - edge_playlist2.pulse() - - 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) 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(pctl.default_playlist[search_index], search_index) + if prefs.album_mode: + goto_album(pctl.playlist_playing_position) + quick_search_mode = False + search_clear_timer.set() + elif not tauon.search_over.active: + if key_up_press and (( + not inp.key_shiftr_down \ + and not inp.key_shift_down \ + and not inp.key_ctrl_down \ + and not inp.key_rctrl_down \ + and not inp.key_meta \ + and not inp.key_lalt \ + and not inp.key_ralt) or (keymaps.test("shift-up"))): + + pctl.show_selected() + gui.pl_update = 1 - 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 not keymaps.test("shift-up"): + if pctl.selected_in_playlist > 0: + pctl.selected_in_playlist -= 1 + r_menu_index = pctl.default_playlist[pctl.selected_in_playlist] + shift_selection = [] - 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 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") - scroll_hide_timer.set() - gui.frame_callback_list.append(TestTimer(0.9)) + scroll_hide_timer.set() + gui.frame_callback_list.append(TestTimer(0.9)) - pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(default_playlist)) + pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(pctl.default_playlist)) - 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.selected_in_playlist < len(pctl.default_playlist) and ( + (key_down_press and \ + not inp.key_shiftr_down \ + and not inp.key_shift_down \ + and not inp.key_ctrl_down \ + and not inp.key_rctrl_down \ + and not inp.key_meta \ + and not inp.key_lalt \ + and not inp.key_ralt) or keymaps.test("shift-down")): - pctl.show_selected() - gui.pl_update = 1 + pctl.show_selected() + gui.pl_update = 1 - 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 not keymaps.test("shift-down"): + if pctl.selected_in_playlist < len(pctl.default_playlist) - 1: + pctl.selected_in_playlist += 1 + r_menu_index = pctl.default_playlist[pctl.selected_in_playlist] + shift_selection = [] - 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.playlist_view_position < len( + pctl.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)) + scroll_hide_timer.set() + gui.frame_callback_list.append(TestTimer(0.9)) - pctl.selected_in_playlist = max(pctl.selected_in_playlist, 0) + pctl.selected_in_playlist = max(pctl.selected_in_playlist, 0) - 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 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(pctl.default_playlist) - 1: + pctl.selected_in_playlist = 0 + shift_selection = [] + if pctl.default_playlist: + pctl.jump(pctl.default_playlist[pctl.selected_in_playlist], pctl.selected_in_playlist) + if prefs.album_mode: + goto_album(pctl.playlist_playing_position) - elif gui.mode == 3: + elif gui.mode == 3: - 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 (inp.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: - 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 + mini_mode.render() - ww = 0 - if gui.lsp: - ww = gui.lspw - - rect = (ww + 5 * gui.scale, gui.panelY + 5 * gui.scale, 235 * gui.scale, 39 * gui.scale) - fields.add(rect) - - 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) + t = toast_love_timer.get() + if t < 1.8 and gui.toast_love_object is not None: + track = gui.toast_love_object - # fqo = copy.copy(pctl.force_queue[-1]) + ww = 0 + if gui.lsp: + ww = gui.lspw - ddt.text_background_colour = colours.queue_card_background + rect = (ww + 5 * gui.scale, gui.panelY + 5 * gui.scale, 235 * gui.scale, 39 * gui.scale) + tauon.fields.add(rect) - 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]) + if tauon.coll(rect): + toast_love_timer.force_set(10) 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.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 - 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) + # fqo = copy.copy(pctl.force_queue[-1]) - rect = (ww + 5 * gui.scale, gui.panelY + 5 * gui.scale, 215 * gui.scale, 39 * gui.scale) - fields.add(rect) + ddt.text_background_colour = colours.queue_card_background - if coll(rect): - queue_add_timer.force_set(10) - elif len(pctl.force_queue) > 0: - - fqo = copy.copy(pctl.force_queue[-1]) - - ddt.rect(grow_rect(rect, 2 * gui.scale), colours.box_border) - ddt.rect(rect, colours.queue_card_background) + 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 - 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] + 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) - 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 = queue_add_timer.get() + if t < 2.5 and gui.toast_queue_object: + track = pctl.get_track(gui.toast_queue_object.track_id) - 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) + ww = 0 + if gui.lsp: + ww = gui.lspw + if tauon.search_over.active: + ww = window_size[0] // 2 - (215 * gui.scale // 2) - t = toast_mode_timer.get() - if t < 0.98: + rect = (ww + 5 * gui.scale, gui.panelY + 5 * gui.scale, 215 * gui.scale, 39 * gui.scale) + tauon.fields.add(rect) - wid = ddt.get_text_w(gui.mode_toast_text, 313) - wid = max(round(68 * gui.scale), wid) + if tauon.coll(rect): + queue_add_timer.force_set(10) + elif len(pctl.force_queue) > 0: - ww = round(7 * gui.scale) - if gui.lsp and not gui.combo_mode: - ww += gui.lspw + fqo = copy.copy(pctl.force_queue[-1]) - rect = (ww + 8 * gui.scale, gui.panelY + 15 * gui.scale, wid + 20 * gui.scale, 25 * gui.scale) - fields.add(rect) + ddt.rect(grow_rect(rect, 2 * gui.scale), colours.box_border) + ddt.rect(rect, colours.queue_card_background) - 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) + 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] // 2), rect[1] + 4 * gui.scale, 2), gui.mode_toast_text, colours.grey(230), 313) + 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) - # Render Menus------------------------------- - for instance in Menu.instances: - instance.render() + 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) - if view_box.active: - view_box.render() + t = toast_mode_timer.get() + if t < 0.98: - tool_tip.render() - tool_tip2.render() + wid = ddt.get_text_w(gui.mode_toast_text, 313) + wid = max(round(68 * gui.scale), wid) - if console.show: - rect = (20 * gui.scale, 40 * gui.scale, 580 * gui.scale, 200 * gui.scale) - ddt.rect(rect, [0, 0, 0, 245]) + ww = round(7 * gui.scale) + if gui.lsp and not gui.combo_mode: + ww += gui.lspw - yy = rect[3] + 15 * gui.scale - u = False - for record in reversed(log.log_history): + rect = (ww + 8 * gui.scale, gui.panelY + 15 * gui.scale, wid + 20 * gui.scale, 25 * gui.scale) + tauon.fields.add(rect) - if yy < rect[1] + 5 * gui.scale: - break + if tauon.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) - text_colour = [60, 255, 60, 255] - message = log.format(record) + 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) - t = record.created - d = time.time() - t - dt = time.localtime(t) + # Render Menus------------------------------- + for instance in Menu.instances: + instance.render() - fade = 255 - if d > 2: - fade = 200 + if view_box.active: + view_box.render() - 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] + tool_tip.render() + tool_tip2.render() - time_colour = [255, 80, 160, fade] + if console.show: + rect = (20 * gui.scale, 40 * gui.scale, 580 * gui.scale, 200 * gui.scale) + ddt.rect(rect, [0, 0, 0, 245]) - 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]) + yy = rect[3] + 15 * gui.scale + u = False + for record in reversed(log.log_history): - 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 yy < rect[1] + 5 * gui.scale: + break - if draw.button("Copy", rect[0] + rect[2] - 55 * gui.scale, rect[1] + rect[3] - 30 * gui.scale): + text_colour = [60, 255, 60, 255] + message = log.format(record) - text = "" - for record in log.log_history[-50:]: t = record.created + d = time.time() - t 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: + 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 inp.quick_drag and inp.mouse_down and not point_proximity_test( + gui.drag_source_position, inp.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 inp.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]) - gui.cursor_is = gui.cursor_want + 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]) - 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]) + # gui.update += 1 + gui.update_on_drag = True + + # Drag pl tab next to cursor + if (tauon.playlist_box.drag) and inp.mouse_down and not point_proximity_test( + gui.drag_source_position, inp.mouse_position, 10 * gui.scale): + i_x, i_y = get_sdl_input.mouse() + gui.drag_source_position = (0, 0) 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]) + (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[tauon.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, inp.mouse_position, round(4 * gui.scale)): + ddt.rect(( + inp.mouse_position[0] + round(8 * gui.scale), inp.mouse_position[1] - round(8 * gui.scale), 48 * gui.scale, + 14 * gui.scale), colours.grey(70)) + if (gui.set_label_hold != -1) and inp.mouse_down: - 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_on_drag = True - # gui.update += 1 - gui.update_on_drag = True + if not point_proximity_test(gui.set_label_point, inp.mouse_position, 3): + i_x, i_y = get_sdl_input.mouse() + gui.set_label_point = (0, 0) - # 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: + 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.update_on_drag = True + input_text = "" + gui.update -= 1 - 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) + if pref_box.enabled: + ddt.rect((0, 0, gui.spec_w, gui.spec_h), [0, 0, 0, 90]) - gui.bar.y = 0 + gui.spec_h - item - gui.bar.h = item + SDL_SetRenderTarget(renderer, None) + SDL_RenderCopy(renderer, gui.spec1_tex, None, gui.spec1_rec) - SDL_RenderFillRect(renderer, gui.bar) + if gui.vis == 1: - gui.bar.x += round(4 * gui.scale) - - 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_meter_colour_mode == 1: - 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 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(tauon.bag.t_window): + 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") + bb_type = 0 -IMG_Quit() -SDL_QuitSubSystem(SDL_INIT_EVERYTHING) -SDL_Quit() -#logging.info("SDL unloaded") + # gui.scroll_hide_box = (0, gui.panelY, 28, window_size[1] - gui.panelBY - gui.panelY) -exit_timer = Timer() -exit_timer.set() + encoding_menu = False + enc_index = 0 + enc_setting = 0 + enc_field = "All" -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 + gen_menu = False - while lfm_scrobbler.running: - time.sleep(0.2) - lfm_scrobbler.running = False - if exit_timer.get() > 15: - logging.warning("Scrobble wait timeout") - break + transfer_setting = 0 -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…") + b_panel_size = 300 + b_info_bar = False + + + IMG_Quit() + SDL_QuitSubSystem(SDL_INIT_EVERYTHING) + SDL_Quit() + #logging.info("SDL unloaded") + + exit_timer = Timer() + exit_timer.set() + + if not tauon.quick_close: + while tauon.thread_manager.check_playback_running(): time.sleep(0.2) - logging.info("Clearing tmp cache") - shutil.rmtree(cache_dir) + 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 + + 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) -logging.info("Bye!") + logging.info("Bye!") diff --git a/src/tauon/t_modules/t_prefs.py b/src/tauon/t_modules/t_prefs.py index 7b861713a..9530b412e 100644 --- a/src/tauon/t_modules/t_prefs.py +++ b/src/tauon/t_modules/t_prefs.py @@ -1,418 +1,408 @@ from __future__ import annotations -from pathlib import Path +from dataclasses import dataclass, field +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from pathlib import Path + +@dataclass class Prefs: """Used to hold any kind of settings""" - def __init__( - self, *, user_directory: Path, music_directory: Path | None, cache_directory: Path, - macos: bool, phone: bool, left_window_control: bool, detect_macstyle: bool, - gtk_settings: Settings | None, discord_allow: bool, - flatpak_mode: bool, desktop: str | None, window_opacity: float, scale: float, - ) -> None: - self.colour_from_image: bool = False - self.dim_art: bool = False - self.prefer_side: bool = True # Saves whether side panel is shown or not - self.pause_fade_time: int = 400 - self.change_volume_fade_time: int = 400 - self.cross_fade_time: int = 700 - self.volume_wheel_increment: int = 2 - self.encoder_output: Path = user_directory / "encoder" - if music_directory is not None: - self.encoder_output: Path = music_directory / "encode-output" - self.rename_folder_template: str = "<albumartist> - <album>" - self.rename_tracks_template: str = "<tn>. <artist> - <title>.<ext>" - - self.enable_web: bool = False - self.allow_remote: bool = False - self.expose_web: bool = True - - self.enable_transcode: bool = True - self.show_rym: bool = False - self.show_band: bool = False - self.show_wiki: bool = False - self.show_transfer: bool = True - self.show_queue: bool = True - self.prefer_bottom_title: bool = True - self.append_date: bool = True - - self.transcode_codec: str = "opus" - self.transcode_mode: str = "single" - self.transcode_bitrate: int = 64 +# music_directory: Path | None + encoder_output: Path +# user_directory: Path +# cache_directory: Path + window_opacity: float + ui_scale: float + power_save: bool + flatpak_mode: bool + discord_allow: bool + left_window_control: bool + macstyle: bool + macos: bool + phone: bool + force_subpixel_text: bool + dc_device: bool + desktop: str + album_mode: bool = False + colour_from_image: bool = False + dim_art: bool = False + prefer_side: bool = True # Saves whether side panel is shown or not + pause_fade_time: int = 400 + change_volume_fade_time: int = 400 + cross_fade_time: int = 700 + volume_wheel_increment: int = 2 + rename_folder_template: str = "<albumartist> - <album>" + rename_tracks_template: str = "<tn>. <artist> - <title>.<ext>" + + enable_web: bool = False + allow_remote: bool = False + expose_web: bool = True + + enable_transcode: bool = True + show_rym: bool = False + show_band: bool = False + show_wiki: bool = False + show_transfer: bool = True + show_queue: bool = True + prefer_bottom_title: bool = True + append_date: bool = True + + transcode_codec: str = "opus" + transcode_mode: str = "single" + transcode_bitrate: int = 64 # self.line_style: int = 1 - self.device: int = 1 - self.device_name: str = "" - - self.cache_gallery: bool = True - self.gallery_row_scroll: bool = True - self.gallery_scroll_wheel_px: int = 90 - - self.playlist_font_size: int = 15 - self.playlist_row_height: int = 27 - - self.tag_editor_name: str = "" - self.tag_editor_target: str = "" - self.tag_editor_path: str = "" - - self.use_title: bool = False - self.auto_extract: bool = False - self.auto_del_zip: bool = False - self.pl_thumb: bool = False - - self.use_custom_fonts: bool = False - self.linux_font: str = "Noto Sans, Noto Sans CJK JP, Arial," - self.linux_font_semibold: str = "Noto Sans, Noto Sans CJK JP, Arial, Medium" - self.linux_font_bold: str = "Noto Sans, Noto Sans CJK JP, Bold" - self.linux_font_condensed: str = "Noto Sans, Extra-Condensed" - self.linux_font_condensed_bold: str = "Noto Sans, Extra-Condensed Bold" + device: int = 1 + device_name: str = "" - self.spec2_scroll: bool = True + cache_gallery: bool = True + gallery_row_scroll: bool = True + gallery_scroll_wheel_px: int = 90 - self.spec2_p_base: list[float] = [10, 10, 100] - self.spec2_p_multiply: list[float] = [0.5, 1, 1] + playlist_font_size: int = 15 + playlist_row_height: int = 27 - self.spec2_base: list[float] = [10, 10, 100] - self.spec2_multiply: list[float] = [0.5, 1, 1] - self.spec2_colour_setting: str = "custom" + tag_editor_name: str = "" + tag_editor_target: str = "" + tag_editor_path: str = "" - self.auto_lfm: bool = False - self.scrobble_mark: bool = False - self.enable_mpris: bool = True + use_title: bool = False + auto_extract: bool = False + auto_del_zip: bool = False + pl_thumb: bool = False - self.replay_gain: int = 0 # 0=off 1=track 2=album - self.replay_preamp: int = 0 # db - self.radio_page_lyrics: bool = True + use_custom_fonts: bool = False + linux_font: str = "Noto Sans, Noto Sans CJK JP, Arial," + linux_font_semibold: str = "Noto Sans, Noto Sans CJK JP, Arial, Medium" + linux_font_bold: str = "Noto Sans, Noto Sans CJK JP, Bold" + linux_font_condensed: str = "Noto Sans, Extra-Condensed" + linux_font_condensed_bold: str = "Noto Sans, Extra-Condensed Bold" - self.show_gimage: bool = False - self.end_setting: str = "stop" - self.show_gen: bool = False - self.show_lyrics_side: bool = True + spec2_scroll: bool = True - self.log_vol: bool = False + spec2_p_base: list[float] = field(default_factory=lambda: [10, 10, 100]) + spec2_p_multiply: list[float] = field(default_factory=lambda: [0.5, 1, 1]) - self.ui_scale: float = scale + spec2_base: list[float] = field(default_factory=lambda: [10, 10, 100]) + spec2_multiply: list[float] = field(default_factory=lambda: [0.5, 1, 1]) + spec2_colour_setting: str = "custom" - # if flatpak_mode: + auto_lfm: bool = False + scrobble_mark: bool = False + enable_mpris: bool = True - self.transcode_opus_as: bool = False + replay_gain: int = 0 # 0=off 1=track 2=album + replay_preamp: int = 0 # db + radio_page_lyrics: bool = True - self.discord_active: bool = False - self.discord_ready: bool = False - self.disconnect_discord: bool = False + show_gimage: bool = False + end_setting: str = "stop" + show_gen: bool = False + show_lyrics_side: bool = True - self.monitor_downloads: bool = True - self.extract_to_music: bool = False + log_vol: bool = False - self.enable_lb: bool = False - self.lb_token: str = "" + transcode_opus_as: bool = False - self.use_jump_crossfade: bool = True - self.use_transition_crossfade: bool = False - self.use_pause_fade: bool = True + discord_active: bool = False + discord_ready: bool = False + disconnect_discord: bool = False - self.show_notifications: bool = True + monitor_downloads: bool = True + extract_to_music: bool = False - self.true_shuffle: bool = True - self.append_total_time: bool = False - self.backend: int = 4 # 2 gstreamer, 4 phazor + enable_lb: bool = False + lb_token: str = "" - self.album_repeat_mode: bool = False # passed to pctl - self.album_shuffle_mode: bool = False # passed to pctl + use_jump_crossfade: bool = True + use_transition_crossfade: bool = False + use_pause_fade: bool = True - self.finish_current: bool = False # Finish current album when adding to queue + show_notifications: bool = True - self.reload_play_state: bool = False # Resume playback on app restart - self.resume_play_wake: bool = False # Resume playback on wake - self.reload_state: tuple[int, float] | None = None + true_shuffle: bool = True + append_total_time: bool = False + backend: int = 4 # 2 gstreamer, 4 phazor - self.mono: bool = False + album_repeat_mode: bool = False # passed to pctl + album_shuffle_mode: bool = False # passed to pctl - self.last_fm_token = None - self.last_fm_username = "" + finish_current: bool = False # Finish current album when adding to queue - self.use_card_style = True + reload_play_state: bool = False # Resume playback on app restart + resume_play_wake: bool = False # Resume playback on wake + reload_state: tuple[int, float] | None = None - self.plex_username = "" - self.plex_password = "" - self.plex_servername = "" + mono: bool = False - self.koel_username = "admin@example.com" - self.koel_password = "admin" - self.koel_server_url = "http://localhost:8050" + last_fm_token = None + last_fm_username = "" - self.auto_lyrics = False # Function has been disabled - self.jelly_username = "" - self.jelly_password = "" - self.jelly_server_url = "http://localhost:8096" + use_card_style = True - self.auto_lyrics_checked = [] + plex_username = "" + plex_password = "" + plex_servername = "" - self.show_side_art = True - self.always_pin_playlists = True + koel_username = "admin@example.com" + koel_password = "admin" + koel_server_url = "http://localhost:8050" - self.user_directory: Path = user_directory - self.cache_directory: Path = cache_directory + auto_lyrics = False # Function has been disabled + jelly_username = "" + jelly_password = "" + jelly_server_url = "http://localhost:8096" - self.window_opacity = window_opacity - self.gallery_single_click = True - self.custom_bg_opacity = 40 + auto_lyrics_checked: list = field(default_factory=list) - self.tabs_on_top = True - self.desktop = desktop + show_side_art = True + always_pin_playlists = True - self.dc_device = False # (BASS) Disconnect device on pause - if desktop == "KDE": - self.dc_device = True + gallery_single_click = True + custom_bg_opacity = 40 - self.showcase_vis = True - self.show_lyrics_showcase = True + tabs_on_top = True - self.spec2_colour_mode = 0 - self.flatpak_mode = flatpak_mode + showcase_vis = True + show_lyrics_showcase = True - self.device_buffer = 80 + spec2_colour_mode = 0 - self.eq = [0.0] * 10 - self.use_eq = False + device_buffer = 80 - self.bio_large = False - self.discord_allow = discord_allow - self.discord_show = False + eq = [0.0] * 10 + use_eq = False - self.min_to_tray = False + bio_large = False + discord_show = False - self.guitar_chords = False - self.prefer_synced_lyrics = True - self.sync_lyrics_time_offset = 0 + min_to_tray = False - self.playback_follow_cursor = False - self.short_buffer = False + guitar_chords = False + prefer_synced_lyrics = True + sync_lyrics_time_offset = 0 - self.gst_output = "rgvolume pre-amp=-2 fallback-gain=-6 ! autoaudiosink" + playback_follow_cursor = False + short_buffer = False - self.art_bg = False - self.art_bg_stronger = 1 - self.art_bg_opacity = 10 - self.art_bg_blur = 9 - self.art_bg_always_blur = False + gst_output = "rgvolume pre-amp=-2 fallback-gain=-6 ! autoaudiosink" - self.random_mode = False - self.repeat_mode = False + art_bg = False + art_bg_stronger = 1 + art_bg_opacity = 10 + art_bg_blur = 9 + art_bg_always_blur = False - self.failed_artists = [] - self.failed_background_artists = [] + random_mode = False + repeat_mode = False - self.artist_list = False - self.auto_sort = False + failed_artists: list = field(default_factory=list) + failed_background_artists: list = field(default_factory=list) - self.transcode_inplace = False + artist_list = False + auto_sort = False - self.bg_showcase_only = False + transcode_inplace = False - self.lyrics_enables = [] + bg_showcase_only = False - self.fatvap = "6b2a9499238ce6416783fc8129b8ac67" + lyrics_enables: list = field(default_factory=list) - self.fanart_notify = True - self.discogs_pat = "" + fatvap = "6b2a9499238ce6416783fc8129b8ac67" - self.artist_list_prefer_album_artist = True + fanart_notify = True + discogs_pat = "" - self.mini_mode_mode = 0 - self.dc_device_setting = "on" + artist_list_prefer_album_artist = True - self.download_dir1 = "" - self.dd_index = False + mini_mode_mode = 0 + dc_device_setting = "on" - self.metadata_page_port = 7590 + download_dir1 = "" + dd_index = False - self.custom_encoder_output = "" - self.column_aa_fallback_artist = False + metadata_page_port = 7590 - self.meta_persists_stop = False - self.meta_shows_selected = False - self.meta_shows_selected_always = False + custom_encoder_output = "" + column_aa_fallback_artist = False - self.left_align_album_artist_title = False - self.stop_notifications_mini_mode = False - self.scale_want = 1 - self.x_scale = True - self.hide_queue = True - self.show_playlist_list = True - self.thin_gallery_borders = False - self.show_current_on_transition = False + meta_persists_stop = False + meta_shows_selected = False + meta_shows_selected_always = False - self.force_subpixel_text = False - if gtk_settings and gtk_settings.get_property("gtk-xft-rgba") == "rgb": - self.force_subpixel_text = True + left_align_album_artist_title = False + stop_notifications_mini_mode = False + scale_want = 1 + x_scale = True + hide_queue = True + show_playlist_list = True + thin_gallery_borders = False + show_current_on_transition = False - self.chart_rows = 3 - self.chart_columns = 3 - self.chart_bg = [7, 7, 7] - self.chart_text = True - self.chart_font = "Monospace 10" - self.chart_tile = False + chart_rows = 3 + chart_columns = 3 + chart_bg: list[int] = field(default_factory=lambda: [7, 7, 7]) + chart_text = True + chart_font = "Monospace 10" + chart_tile = False - self.chart_cascade = False - self.chart_c1 = 5 - self.chart_c2 = 6 - self.chart_c3 = 10 - self.chart_d1 = 2 - self.chart_d2 = 2 - self.chart_d3 = 2 + chart_cascade = False + chart_c1 = 5 + chart_c2 = 6 + chart_c3 = 10 + chart_d1 = 2 + chart_d2 = 2 + chart_d3 = 2 - self.art_in_top_panel = True - self.always_art_header = False + art_in_top_panel = True + always_art_header = False # self.center_bg = True - self.ui_lang: str = "auto" - self.side_panel_layout = 0 - self.use_absolute_track_index = False + ui_lang: str = "auto" + side_panel_layout = 0 + use_absolute_track_index = False - self.hide_bottom_title = True - self.auto_goto_playing = False + hide_bottom_title = True + auto_goto_playing = False - self.diacritic_search = True - self.increase_gallery_row_spacing = False - self.center_gallery_text = False + diacritic_search = True + increase_gallery_row_spacing = False + center_gallery_text = False - self.tracklist_y_text_offset = 0 - self.theme_name = "Turbo" - self.left_panel_mode = "playlist" + tracklist_y_text_offset = 0 + theme_name = "Turbo" + theme: int = 0 + left_panel_mode = "playlist" - self.folder_tree_codec_colours = False + folder_tree_codec_colours = False - self.network_stream_bitrate = 0 # 0 is off + network_stream_bitrate = 0 # 0 is off - self.show_side_lyrics_art_panel = True + show_side_lyrics_art_panel = True - self.gst_use_custom_output = False + gst_use_custom_output = False - self.notify_include_album = True + notify_include_album = True - self.auto_dl_artist_data = False + auto_dl_artist_data = False - self.enable_fanart_artist = False - self.enable_fanart_bg = False - self.enable_fanart_cover = False + enable_fanart_artist = False + enable_fanart_bg = False + enable_fanart_cover = False - self.always_auto_update_playlists = False + always_auto_update_playlists = False - self.subsonic_server = "http://localhost:4040" - self.subsonic_user = "" - self.subsonic_password = "" - self.subsonic_password_plain = False + subsonic_server = "http://localhost:4040" + subsonic_user = "" + subsonic_password = "" + subsonic_password_plain = False - self.subsonic_playlists = {} + subsonic_playlists = {} - self.write_ratings = False - self.rating_playtime_stars = False + write_ratings = False + rating_playtime_stars = False - self.lyrics_subs = {} + lyrics_subs = {} - self.radio_urls = [] + radio_urls: list = field(default_factory=list) - self.lyric_metadata_panel_top = False - self.showcase_overlay_texture = False + lyric_metadata_panel_top = False + showcase_overlay_texture = False - self.sync_target = "" - self.sync_deletes = False - self.sync_playlist: int | None = None - self.download_playlist: int | None = None + sync_target = "" + sync_deletes = False + sync_playlist: int | None = None + download_playlist: int | None = None - self.sep_genre_multi = False - self.topchart_sorts_played = True + sep_genre_multi = False + topchart_sorts_played = True - self.spot_client = "" - self.spot_secret = "" - self.spot_username = "" - self.spot_password = "" - self.spot_mode = False - self.launch_spotify_web = False - self.launch_spotify_local = False - self.remove_network_tracks = False - self.bypass_transcode = False - self.force_hide_max_button = False - self.zoom_art = False - self.auto_rec = False - self.radio_record_codec = "OPUS" - self.pa_fast_seek = False - self.precache = False + spot_client = "" + spot_secret = "" + spot_username = "" + spot_password = "" + spot_mode = False + launch_spotify_web = False + launch_spotify_local = False + remove_network_tracks = False + bypass_transcode = False + force_hide_max_button = False + zoom_art = False + auto_rec = False + radio_record_codec = "OPUS" + pa_fast_seek = False + precache = False # TODO(Martin): cache_list isn't really used anywhere and will always be empty? - self.cache_list: list[str] = [] - self.cache_limit = 2000 # in mb - self.save_window_position = True - self.spotify_token = "" - self.always_ffmpeg = False - - self.use_libre_fm = False - self.back_restarts = False - - self.old_playlist_box_position = 0 - self.listenbrainz_url = "" - self.maloja_enable = False - self.maloja_url = "" - self.maloja_key = "" - - self.scrobble_hold = False - - self.artist_list_sort_mode = "alpha" - - self.phazor_device_selected = "Default" - self.phazor_devices = ["Default"] - self.bg_flips = set() - self.use_tray = False - self.tray_show_title = False - self.drag_to_unpin = True - self.enable_remote = False - - self.artist_list_style = 1 - self.discord_enable = False - self.stop_end_queue = False - - self.block_suspend = False - self.smart_bypass = True - self.seek_interval = 15 - self.shuffle_lock = False - self.album_shuffle_lock_mode = False - self.premium = False - self.power_save = False - if macos or phone: - self.power_save = True - self.left_window_control = macos or left_window_control - self.macstyle = macos or detect_macstyle - self.radio_thumb_bans = [] - self.show_nag = False - - self.playlist_exports = {} - self.show_chromecast = False - - self.samplerate = 48000 - self.resample = 1 - self.volume_power = 2 - - self.tmp_cache = True - - self.sat_url = "" - self.lyrics_font_size = 15 - - self.use_gamepad = True - self.avoid_resampling = False - self.use_scancodes = False - - self.artist_list_threshold = 4 - self.allow_video_formats = True - self.mini_mode_on_top = True - self.tray_theme = "pink" - - self.lastfm_pull_love = False - self.row_title_format = 1 - self.row_title_genre = False - self.row_title_separator_type = 1 - self.search_on_letter = True - - self.gallery_combine_disc = False - self.pipewire = False - self.tidal_quality = 1 + cache_list: list[str] = field(default_factory=list) + cache_limit = 2000 # in mb + save_window_position = True + spotify_token = "" + always_ffmpeg = False + + use_libre_fm = False + back_restarts = False + + old_playlist_box_position = 0 + listenbrainz_url = "" + maloja_enable = False + maloja_url = "" + maloja_key = "" + + scrobble_hold = False + + artist_list_sort_mode = "alpha" + + phazor_device_selected = "Default" + phazor_devices = ["Default"] + bg_flips = set() + use_tray = False + tray_show_title = False + drag_to_unpin = True + enable_remote = False + + artist_list_style = 1 + discord_enable = False + stop_end_queue = False + + block_suspend = False + smart_bypass = True + seek_interval = 15 + shuffle_lock = False + album_shuffle_lock_mode = False + premium = False + radio_thumb_bans: list = field(default_factory=list) + show_nag = False + + playlist_exports = {} + show_chromecast = False + + samplerate = 48000 + resample = 1 + volume_power = 2 + + tmp_cache = True + + sat_url = "" + lyrics_font_size = 15 + + use_gamepad = True + avoid_resampling = False + use_scancodes = False + + artist_list_threshold = 4 + allow_video_formats = True + mini_mode_on_top = True + tray_theme = "pink" + + lastfm_pull_love = False + row_title_format = 1 + row_title_genre = False + row_title_separator_type = 1 + search_on_letter = True + + gallery_combine_disc = False + pipewire = False + tidal_quality = 1 diff --git a/src/tauon/t_modules/t_stream.py b/src/tauon/t_modules/t_stream.py index 1c155f209..b9493e536 100644 --- a/src/tauon/t_modules/t_stream.py +++ b/src/tauon/t_modules/t_stream.py @@ -69,18 +69,14 @@ def __init__(self, tauon: Tauon) -> None: self.url = None def stop(self) -> None: - - try: + if self.tauon.radiobox.websocket: self.tauon.radiobox.websocket.close() logging.info("Websocket closed") - except Exception: - logging.exception("No socket to close?") self.abort = True self.tauon.radiobox.loaded_url = None def start_download(self, url: str) -> bool: - self.abort = True while self.download_running: time.sleep(0.01) @@ -129,7 +125,7 @@ class InterceptedHTTPResponse: r.add_header("Icy-MetaData", "1") r.add_header("User-Agent", self.tauon.t_agent) logging.info("Open URL.....") - r = urllib.request.urlopen(r, timeout=20, context=self.tauon.ssl_context) + r = urllib.request.urlopen(r, timeout=20, context=self.tauon.tls_context) logging.info("URL opened.") except Exception as e: From c3db17dea312bfd99b237a42667e4c85cb0aeae9 Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 16:37:31 +0100 Subject: [PATCH 02/13] Damn, it launches now --- src/tauon/t_modules/t_main.py | 441 ++++++++++++++++------------------ 1 file changed, 210 insertions(+), 231 deletions(-) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index d22f11735..c9c7223a7 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -566,6 +566,12 @@ def __init__(self, bag: Bag, tracklist_texture_rect: SDL_Rect, tracklist_texture self.s4_spec = [0] * 45 self.update_spec = 0 + self.playlist_hold_position = 0 + self.playlist_hold = False + self.selection_stage = 0 + + self.shift_selection: list[int] = [] + # self.spec_rect = [0, 5, 80, 20] # x = 72 + 24 - 6 - 10 self.spec4_array = [] @@ -802,6 +808,10 @@ def __init__(self, bag: Bag, tracklist_texture_rect: SDL_Rect, tracklist_texture self.column_sort_ani_direction = 1 self.column_sort_ani_x = 0 + self.inc_arrow = asset_loader(self.bag, self.bag.loaded_asset_dc, "inc.png", True) + self.dec_arrow = asset_loader(self.bag, self.bag.loaded_asset_dc, "dec.png", True) + self.corner_icon = asset_loader(self.bag, self.bag.loaded_asset_dc, "corner.png", True) + self.restore_showcase_view = False self.restore_radio_view = False @@ -1075,10 +1085,10 @@ def __init__(self, gui: GuiVar) -> None: self.mouse_down: bool = False self.mouse_up: bool = False self.right_down: bool = False - self.click_location = [200, 200] - self.last_click_location = [0, 0] - self.mouse_position = [0, 0] - self.mouse_up_position = [0, 0] + self.click_location = [200, 200] + self.last_click_location = [0, 0] + self.mouse_position = [0, 0] + self.mouse_up_position = [0, 0] self.drag_mode: bool = False self.quick_drag: bool = False self.clicked: bool = False @@ -1095,6 +1105,7 @@ def __init__(self, gui: GuiVar) -> None: self.key_lalt: bool = False self.media_key = "" + self.input_text = "" def m_key_play(self) -> None: self.media_key = "Play" @@ -1901,8 +1912,6 @@ def show_selected(self) -> int: if self.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: @@ -1994,8 +2003,6 @@ def title_text(self) -> str: return line def show(self) -> int | None: - global shift_selection - if not self.track_queue: return 0 return None @@ -2014,8 +2021,6 @@ def show_current( # logging.info("--------") logging.debug("Position set by show playing") - global shift_selection - if self.tauon.spot_ctl.coasting: sptr = self.tauon.dummy_track.misc.get("spotify-track-url") if sptr: @@ -2125,7 +2130,7 @@ def show_current( # logging.info("Run Over") if select: - shift_selection = [] + gui.shift_selection = [] self.render_playlist() @@ -2325,12 +2330,11 @@ def jump(self, index: int, pl_position: int = None, jump: bool = True) -> None: 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 self.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.gui.playlist_hold = False self.play_target(jump=jump) if pl_position is not None: @@ -2733,7 +2737,7 @@ def test_progress(self) -> None: i = max(i, 0) self.selected_in_playlist = i - shift_selection = [i] + gui.shift_selection = [i] self.jump(pp[i], i, jump=False) @@ -5079,9 +5083,9 @@ def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): self.prefs: Prefs = bag.prefs self.fields = Fields() self.artist_list_box = ArtistList(tauon=self) - self.search_over = SearchOverlay(tauon=self) self.radiobox = RadioBox(tauon=self) self.pctl: PlayerCtl = PlayerCtl(tauon=self) + self.search_over = SearchOverlay(tauon=self) self.deco = Deco(tauon=self) self.lfm_scrobbler: LastScrob = self.pctl.lfm_scrobbler self.star_store: StarStore = StarStore(tauon=self) @@ -8136,10 +8140,9 @@ def display(self, track: TrackClass, location, box, fast: bool = False, theme_on # temp fix global move_on_title - global playlist_hold inp.quick_drag = False move_on_title = False - playlist_hold = False + gui.playlist_hold = False except Exception: logging.exception("Image load error") @@ -8790,7 +8793,7 @@ def render(self): if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not tauon.coll((x, y, w, h))): self.active = False - select = list(set(shift_selection)) + select = list(set(gui.shift_selection)) if not select and pctl.selected_ready(): select = [pctl.selected_in_playlist] @@ -9009,7 +9012,7 @@ def render(self): if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not tauon.coll((x, y, w, h))): self.active = False - select = list(set(shift_selection)) + select = list(set(gui.shift_selection)) if not select and pctl.selected_ready(): select = [pctl.selected_in_playlist] @@ -9717,6 +9720,12 @@ def get_playlist(self, playlist_name: str | None = None, return_list: bool = Fal class SearchOverlay: def __init__(self, tauon: Tauon): + self.tauon = tauon + self.pctl = tauon.pctl + self.prefs = tauon.prefs + self.gui = tauon.gui + self.inp = tauon.gui.inp + self.active = False self.search_text = TextBox() @@ -9746,12 +9755,12 @@ def click_artist(self, name, get_list=False, search_lists=None): if search_lists is None: search_lists = [] - for pl in pctl.multi_playlist: + for pl in self.pctl.multi_playlist: search_lists.append(pl.playlist_ids) for pl in search_lists: for item in pl: - tr = pctl.master_library[item] + tr = self.pctl.master_library[item] n = name.lower() if tr.artist.lower() == n \ or tr.album_artist.lower() == n \ @@ -9762,40 +9771,40 @@ def click_artist(self, name, get_list=False, search_lists=None): if get_list: return playlist - pctl.multi_playlist.append(pl_gen( + self.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 + "\"" + switch_playlist(len(self.pctl.multi_playlist) - 1) + self.pctl.gen_codes[pl_to_id(len(self.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 pl in self.pctl.multi_playlist: for item in pl.playlist_ids: - if name in pctl.master_library[item].date: + if name in self.pctl.master_library[item].date: if item not in playlist: playlist.append(item) if get_list: return playlist - pctl.multi_playlist.append(pl_gen( + self.pctl.multi_playlist.append(pl_gen( title=_("Year: ") + name, playlist_ids=copy.deepcopy(playlist), hide_title=False)) - if gui.combo_mode: + if self.gui.combo_mode: exit_combo() switch_playlist(len(pctl.multi_playlist) - 1) - inp.key_return_press = False + self.inp.key_return_press = False def click_composer(self, name: str, get_list: bool = False): @@ -9899,46 +9908,46 @@ def click_genre(self, name: str, get_list: bool = False, search_lists=None): inp.key_return_press = False - def click_album(self, index): - + def click_album(self, index) -> None: pctl.jump(index) if gui.combo_mode: exit_combo() pctl.show_current() - inp.key_return_press = False def render(self): - global input_text + prefs = self.prefs + inp = self.inp + gui = self.gui + 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 \ + if prefs.search_on_letter and inp.input_text != "" and gui.layer_focus == 0 and \ not inp.key_lalt and not inp.key_ralt and \ not inp.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 gui.rename_folder_box and inp.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 < inp.mouse_position[0] < gui.lspw \ and gui.panelY < inp.mouse_position[1] < window_size[1] - gui.panelBY: - artist_list_box.locate_artist_letter(input_text) + artist_list_box.locate_artist_letter(inp.input_text) return activate_search_overlay() self.old_mouse = copy.deepcopy(inp.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 = "" + inp.input_text = "" if inp.backspace_press: # self.searched_text = "" @@ -10052,7 +10061,7 @@ def render(self): except Exception: logging.exception("Unknown error trying to release worker2_lock") - if input_text or key_backspace_press: + if inp.input_text or key_backspace_press: self.input_timer.set() gui.update += 1 @@ -13384,7 +13393,7 @@ def slide_control(self, x: int, y: int, label: str, units: str, value: int, lowe if colour_value(colours.box_background) > 300: abg = colours.box_sub_text - dec_arrow.render(x + 1 * gui.scale, y, abg) + gui.dec_arrow.render(x + 1 * gui.scale, y, abg) x += 33 * gui.scale @@ -13412,7 +13421,7 @@ def slide_control(self, x: int, y: int, label: str, units: str, value: int, lowe if colour_value(colours.box_background) > 300: abg = colours.box_sub_text - inc_arrow.render(x + 1 * gui.scale, y, abg) + gui.inc_arrow.render(x + 1 * gui.scale, y, abg) return value @@ -14127,13 +14136,13 @@ def render(self): modified = False gui.pl_update += 1 - for item in shift_selection: + for item in gui.shift_selection: pctl.multi_playlist[i].playlist_ids.append(pctl.default_playlist[item]) modified = True - if len(shift_selection) > 0: + if len(gui.shift_selection) > 0: modified = True self.adds.append( - [pctl.multi_playlist[i].uuid_int, len(shift_selection), Timer()]) # ID, num, timer + [pctl.multi_playlist[i].uuid_int, len(gui.shift_selection), Timer()]) # ID, num, timer if modified: pctl.after_import_flag = True @@ -14274,7 +14283,7 @@ def render(self): 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 inp.quick_drag and not point_proximity_test(gui.drag_source_position, inp.mouse_position, 15 * gui.scale): - for item in shift_selection: + for item in gui.shift_selection: if item < len(pctl.default_playlist) and pctl.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 @@ -14305,7 +14314,7 @@ def render(self): ddt.rect((x, y, 2 * gui.scale, gui.panelY2), [80, 200, 180, 255]) if inp.mouse_up: - drop_tracks_to_new_playlist(shift_selection) + drop_tracks_to_new_playlist(gui.shift_selection) # Draw end drag tab indicator if tauon.playlist_box.drag and inp.mouse_position[0] > x and inp.mouse_position[1] < gui.panelY: @@ -16516,12 +16525,7 @@ def full_render(self): global highlight_left global highlight_right - global playlist_hold - global playlist_hold_position - global shift_selection - global click_time - global selection_stage global r_menu_index global r_menu_position @@ -16697,11 +16701,11 @@ def full_render(self): drag_highlight = False # Shift selection highlight - if (track_position in shift_selection and len(shift_selection) > 1): + if (track_position in gui.shift_selection and len(gui.shift_selection) > 1): highlight = True # Tracks have been dropped? - if playlist_hold is True and tauon.coll(input_box): + if gui.playlist_hold is True and tauon.coll(input_box): if inp.mouse_up: move_on_title = True @@ -16732,11 +16736,11 @@ def full_render(self): 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.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( + if d_mouse_click and track_position in gui.shift_selection and coll_point( inp.last_click_location, (input_box)): click_time -= 1.5 pctl.jump(track_id, track_position) @@ -16750,17 +16754,17 @@ def full_render(self): if right_click: folder_menu.activate(track_id) r_menu_position = track_position - selection_stage = 2 + gui.selection_stage = 2 gui.pl_update = 1 - if track_position not in shift_selection: - shift_selection = [] + if track_position not in gui.shift_selection: + gui.shift_selection = [] pctl.selected_in_playlist = track_position u = track_position while u < len(pctl.default_playlist) and track_object.parent_folder_path == \ pctl.master_library[ pctl.default_playlist[u]].parent_folder_path: - shift_selection.append(u) + gui.shift_selection.append(u) u += 1 # Add folder to selection if clicked @@ -16770,29 +16774,29 @@ def full_render(self): set_drag_source() if not pl_is_locked(pctl.active_playlist_viewing) or inp.key_shift_down: - playlist_hold = True + gui.playlist_hold = True - selection_stage = 1 + gui.selection_stage = 1 temp = get_folder_tracks_local(track_position) pctl.selected_in_playlist = track_position - if len(shift_selection) > 0 and inp.key_shift_down: - if track_position < shift_selection[0]: + if len(gui.shift_selection) > 0 and inp.key_shift_down: + if track_position < gui.shift_selection[0]: for item in reversed(temp): - if item not in shift_selection: - shift_selection.insert(0, item) + if item not in gui.shift_selection: + gui.shift_selection.insert(0, item) else: for item in temp: - if item not in shift_selection: - shift_selection.append(item) + if item not in gui.shift_selection: + gui.shift_selection.append(item) else: - shift_selection = copy.copy(temp) + gui.shift_selection = copy.copy(temp) # Should draw drag highlight? - if inp.mouse_down and playlist_hold and tauon.coll(input_box) and track_position not in shift_selection: - if len(shift_selection) < 2 and not inp.key_shift_down: + if inp.mouse_down and gui.playlist_hold and tauon.coll(input_box) and track_position not in gui.shift_selection: + if len(gui.shift_selection) < 2 and not inp.key_shift_down: pass else: drag_highlight = True @@ -16865,26 +16869,26 @@ def full_render(self): 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.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 inp.mouse_up and line_over and not inp.key_shift_down and not inp.key_ctrl_down and point_proximity_test( - gui.drag_source_position, inp.mouse_position, 15): # and not playlist_hold: - shift_selection = [track_position] + if len(gui.shift_selection) > 1 and inp.mouse_up and line_over and not inp.key_shift_down and not inp.key_ctrl_down and point_proximity_test( + gui.drag_source_position, inp.mouse_position, 15): # and not gui.playlist_hold: + gui.shift_selection = [track_position] pctl.selected_in_playlist = track_position gui.pl_update = 1 gui.update = 2 # # Begin drag block selection - # if inp.mouse_down and line_over and track_position in shift_selection and len(shift_selection) > 1: + # if inp.mouse_down and line_over and track_position in gui.shift_selection and len(gui.shift_selection) > 1: # if not pl_is_locked(pctl.active_playlist_viewing): - # playlist_hold = True + # gui.playlist_hold = True # elif inp.key_shift_down: - # playlist_hold = True + # gui.playlist_hold = True # Begin drag single track if inp.mouse_click and line_hit and not gui.side_drag: @@ -16892,16 +16896,16 @@ def full_render(self): set_drag_source() # Shift Move Selection - if move_on_title or (inp.mouse_up and playlist_hold is True and tauon.coll(( + if move_on_title or (inp.mouse_up and gui.playlist_hold is True and tauon.coll(( left + highlight_left, line_y, highlight_width, gui.playlist_row_height))): - if len(shift_selection) > 1 or inp.key_shift_down: - if track_position not in shift_selection: # p_track != playlist_hold_position and + if len(gui.shift_selection) > 1 or inp.key_shift_down: + if track_position not in gui.shift_selection: # p_track != gui.playlist_hold_position and - if len(shift_selection) == 0: + if len(gui.shift_selection) == 0: - ref = pctl.default_playlist[playlist_hold_position] - pctl.default_playlist[playlist_hold_position] = "old" + ref = pctl.default_playlist[gui.playlist_hold_position] + pctl.default_playlist[gui.playlist_hold_position] = "old" if move_on_title: pctl.default_playlist.insert(track_position, "new") else: @@ -16915,14 +16919,14 @@ def full_render(self): else: ref = [] - selection_stage = 2 - for item in shift_selection: + gui.selection_stage = 2 + for item in gui.shift_selection: ref.append(pctl.default_playlist[item]) - for item in shift_selection: + for item in gui.shift_selection: pctl.default_playlist[item] = "old" - for item in shift_selection: + for item in gui.shift_selection: if move_on_title: pctl.default_playlist.insert(track_position, "new") else: @@ -16931,29 +16935,29 @@ def full_render(self): for b in reversed(range(len(pctl.default_playlist))): if pctl.default_playlist[b] == "old": del pctl.default_playlist[b] - shift_selection = [] + gui.shift_selection = [] for b in range(len(pctl.default_playlist)): if pctl.default_playlist[b] == "new": - shift_selection.append(b) + gui.shift_selection.append(b) pctl.default_playlist[b] = ref.pop(0) - pctl.selected_in_playlist = shift_selection[0] + pctl.selected_in_playlist = gui.shift_selection[0] gui.pl_update += 1 reload_albums(True) pctl.notify_change() # Test show drag indicator - if inp.mouse_down and playlist_hold and tauon.coll(input_box) and track_position not in shift_selection: - if len(shift_selection) > 1 or inp.key_shift_down: + if inp.mouse_down and gui.playlist_hold and tauon.coll(input_box) and track_position not in gui.shift_selection: + if len(gui.shift_selection) > 1 or inp.key_shift_down: drag_highlight = True # Right click menu activation if right_click and line_hit and inp.mouse_position[0] > gui.playlist_left + 10: - if len(shift_selection) > 1 and track_position in shift_selection: + if len(gui.shift_selection) > 1 and track_position in gui.shift_selection: selection_menu.activate(pctl.default_playlist[track_position]) - selection_stage = 2 + gui.selection_stage = 2 else: r_menu_index = pctl.default_playlist[track_position] r_menu_position = track_position @@ -16961,45 +16965,45 @@ def full_render(self): gui.pl_update += 1 gui.update += 1 - if track_position not in shift_selection: + if track_position not in gui.shift_selection: pctl.selected_in_playlist = track_position - shift_selection = [pctl.selected_in_playlist] + gui.shift_selection = [pctl.selected_in_playlist] if line_over and inp.mouse_click: - if track_position in shift_selection: + if track_position in gui.shift_selection: pass else: - selection_stage = 2 + gui.selection_stage = 2 if inp.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() + if y not in gui.shift_selection: + gui.shift_selection.append(y) + gui.shift_selection.sort() pctl.selected_in_playlist = track_position elif inp.key_ctrl_down: - shift_selection.append(track_position) + gui.shift_selection.append(track_position) else: pctl.selected_in_playlist = track_position - shift_selection = [pctl.selected_in_playlist] + gui.shift_selection = [pctl.selected_in_playlist] if not pl_is_locked(pctl.active_playlist_viewing) or inp.key_shift_down: - playlist_hold = True - playlist_hold_position = track_position + gui.playlist_hold = True + gui.playlist_hold_position = track_position # Activate drag if shift key down if inp.quick_drag and pl_is_locked(pctl.active_playlist_viewing) and inp.mouse_down: if inp.key_shift_down: - playlist_hold = True + gui.playlist_hold = True else: - playlist_hold = False + gui.playlist_hold = False # Multi Select Highlight - if track_position in shift_selection or track_position == pctl.selected_in_playlist: + if track_position in gui.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] == \ @@ -17262,7 +17266,7 @@ def full_render(self): # Blue drop line - if drag_highlight: # playlist_hold_position != p_track: + if drag_highlight: # gui.playlist_hold_position != p_track: ddt.rect( [left + highlight_left, line_y + gui.playlist_row_height - 1 * gui.scale, highlight_width, @@ -17638,7 +17642,7 @@ def full_render(self): SDL_RenderCopy(self.renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) if inp.mouse_down is False: - playlist_hold = False + gui.playlist_hold = False ddt.pretty_rect = None ddt.alpha_bg = False @@ -19191,12 +19195,12 @@ def draw(self, x, y, w, h): modified = False gui.pl_update += 1 - for item in shift_selection: + for item in gui.shift_selection: pctl.multi_playlist[i].playlist_ids.append(pctl.default_playlist[item]) modified = True - if len(shift_selection) > 0: + if len(gui.shift_selection) > 0: self.adds.append( - [pctl.multi_playlist[i].uuid_int, len(shift_selection), Timer()]) # ID, num, timer + [pctl.multi_playlist[i].uuid_int, len(gui.shift_selection), Timer()]) # ID, num, timer modified = True if modified: pctl.after_import_flag = True @@ -19313,7 +19317,7 @@ def draw(self, x, y, w, h): ddt.rect((tab_start, yy + (self.tab_h - self.indicate_w), tab_width, self.indicate_w), [80, 160, 200, 255]) elif inp.quick_drag and not point_proximity_test(gui.drag_source_position, inp.mouse_position, 15 * gui.scale): - for item in shift_selection: + for item in gui.shift_selection: if len(pctl.default_playlist) > item and pctl.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 @@ -19356,7 +19360,7 @@ def draw(self, x, y, w, h): if inp.quick_drag: ddt.rect((tab_start, yy, tab_width, self.indicate_w), [80, 160, 200, 255]) if inp.mouse_up: - drop_tracks_to_new_playlist(shift_selection) + drop_tracks_to_new_playlist(gui.shift_selection) if right_click: extra_tab_menu.activate(pctl.active_playlist_viewing) @@ -19974,7 +19978,7 @@ def draw_card(self, artist, x, y, w): pctl.jump(pctl.default_playlist[select], pl_position=select) pctl.playlist_view_position = select pctl.selected_in_playlist = select - shift_selection.clear() + gui.shift_selection.clear() self.d_click_timer.force_set(10) else: # Goto next artist section in playlist @@ -20446,8 +20450,8 @@ def render(self, x, y, w, h): # Hold highlight while dragging folder if inp.quick_drag and not point_proximity_test(gui.drag_source_position, inp.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( + if gui.shift_selection: + if pctl.get_track(pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids[gui.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: @@ -20626,8 +20630,7 @@ def render(self, x, y, w, h): if self.click_drag_source and not point_proximity_test(gui.drag_source_position, inp.mouse_position, 15) and \ pctl.default_playlist is pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids: inp.quick_drag = True - global playlist_hold - playlist_hold = True + gui.playlist_hold = True self.dragging_name = self.click_drag_source[0] logging.info(self.dragging_name) @@ -20635,15 +20638,15 @@ def render(self, x, y, w, h): if "/" in self.dragging_name: self.dragging_name = os.path.basename(self.dragging_name) - shift_selection.clear() + gui.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) + gui.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) + gui.shift_selection.append(p) self.click_drag_source = None if self.dragging_name and not inp.quick_drag: @@ -20816,7 +20819,7 @@ def make_as_playlist(self): hide_title=False)) def drop_tracks_insert(self, insert_position): - if not shift_selection: + if not gui.shift_selection: return # remove incomplete album from queue @@ -20826,14 +20829,14 @@ def drop_tracks_insert(self, insert_position): playlist_index = pctl.active_playlist_viewing playlist_id = pl_to_id(pctl.active_playlist_viewing) - main_track_position = shift_selection[0] + main_track_position = gui.shift_selection[0] main_track_id = pctl.default_playlist[main_track_position] inp.quick_drag = False - if len(shift_selection) > 1: + if len(gui.shift_selection) > 1: # if shift selection contains only same folder - for position in shift_selection: + for position in gui.shift_selection: if pctl.get_track(pctl.default_playlist[position]).parent_folder_path != pctl.get_track( main_track_id).parent_folder_path or inp.key_ctrl_down: break @@ -20843,11 +20846,11 @@ def drop_tracks_insert(self, insert_position): insert_position, queue_item_gen(main_track_id, main_track_position, playlist_id, 1)) return - if len(shift_selection) == 1: + if len(gui.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): + for position in reversed(gui.shift_selection): pctl.force_queue.insert( insert_position, queue_item_gen(pctl.default_playlist[position], position, playlist_id)) @@ -21324,7 +21327,7 @@ def draw(self, x: int, y: int, w: int, h: int): self.draw_card(x, y, w, h, yyy, track, fqo, draw_back=True) # Drag and drop tracks from main playlist into queue - if inp.quick_drag and inp.mouse_up and tauon.coll(box_rect) and shift_selection: + if inp.quick_drag and inp.mouse_up and tauon.coll(box_rect) and gui.shift_selection: self.drop_tracks_insert(len(fq)) # Right click context menu in blank space @@ -23373,7 +23376,9 @@ def scan(self): class Fader: - def __init__(self): + def __init__(self, tauon: Tauon) -> None: + self.tauon = tauon + self.window_size = tauon.bag.window_size self.total_timer = Timer() self.timer = Timer() @@ -23381,8 +23386,7 @@ def __init__(self): self.state = 0 # 0 = Want off, 1 = Want fade on self.a = 0 # The fade progress (0-1) - def render(self): - + def render(self) -> None: if self.total_timer.get() > self.ani_duration: self.a = self.state elif self.state == 0: @@ -23394,20 +23398,18 @@ def render(self): 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)]) + rect = [0, 0, self.window_size[0], self.window_size[1]] + self.tauon.bag.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.tauon.gui.update += 1 + def rise(self) -> None: self.state = 1 self.timer.hit() self.total_timer.set() - def fall(self): - + def fall(self) -> None: self.state = 0 self.timer.hit() self.total_timer.set() @@ -25960,7 +25962,13 @@ def draw_window_tools(tauon: Tauon) -> None: else: tauon.top_panel.restore_button.render(rect[0] + 8 * gui.scale, rect[1] + 9 * gui.scale, fg_off) -def draw_window_border(): +def draw_window_border(tauon: Tauon) -> None: + ddt = tauon.bag.ddt + colours = tauon.bag.colours + gui = tauon.gui + corner_icon = tauon.gui.corner_icon + window_size = tauon.bag.window_size + corner_icon.render(window_size[0] - corner_icon.w, window_size[1] - corner_icon.h, colours.corner_icon) corner_rect = (window_size[0] - 20 * gui.scale, window_size[1] - 20 * gui.scale, 20, 20) @@ -27013,8 +27021,8 @@ def show_in_playlist(tauon: Tauon): 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) + gui.shift_selection.clear() + gui.shift_selection.append(pctl.selected_in_playlist) pctl.render_playlist() def open_folder_stem(path): @@ -28548,7 +28556,7 @@ def delete_playlist(index: int, force: bool = False, check_lock: bool = False) - 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] + gui.shift_selection = [pctl.selected_in_playlist] if prefs.album_mode: reload_albums(True) @@ -31113,7 +31121,7 @@ def s_copy(): global cargo cargo = [] if pctl.default_playlist: - for item in shift_selection: + for item in gui.shift_selection: cargo.append(pctl.default_playlist[item]) if not cargo and -1 < pctl.selected_in_playlist < len(pctl.default_playlist): @@ -31147,7 +31155,7 @@ def lightning_paste(): _("This function can only move one folder at a time."), mode="info") return - match_track = pctl.get_track(pctl.default_playlist[shift_selection[0]]) + match_track = pctl.get_track(pctl.default_playlist[gui.shift_selection[0]]) match_path = match_track.parent_folder_path if pctl.playing_state > 0 and move: @@ -31237,7 +31245,7 @@ def lightning_paste(): 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] + insert = gui.shift_selection[0] old_insert = insert while insert < len(pctl.default_playlist) and pctl.master_library[ pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids[insert]].parent_folder_name == \ @@ -31405,20 +31413,18 @@ def refind_playing(): break def del_selected(force_delete: bool = False): - global shift_selection - gui.update += 1 gui.pl_update = 1 - if not shift_selection: - shift_selection = [pctl.selected_in_playlist] + if not gui.shift_selection: + gui.shift_selection = [pctl.selected_in_playlist] if not pctl.default_playlist: return li = [] - for item in reversed(shift_selection): + for item in reversed(gui.shift_selection): if item > len(pctl.default_playlist) - 1: return @@ -31458,7 +31464,7 @@ def del_selected(force_delete: bool = False): pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(pctl.default_playlist) - 1) - shift_selection = [pctl.selected_in_playlist] + gui.shift_selection = [pctl.selected_in_playlist] gui.pl_update += 1 refind_playing() pctl.notify_change() @@ -31568,7 +31574,7 @@ def add_selected_to_queue(): def add_selected_to_queue_multi(): if prefs.stop_end_queue: pctl.auto_stop = False - for index in shift_selection: + for index in gui.shift_selection: pctl.force_queue.append( queue_item_gen(pctl.default_playlist[index], index, @@ -31832,7 +31838,6 @@ def rename_folders_disable_test(index: int) -> bool: def rename_folders(index: int): global track_box global rename_index - global input_text track_box = False rename_index = index @@ -31842,12 +31847,11 @@ def rename_folders(index: int): return gui.rename_folder_box = True - input_text = "" - shift_selection.clear() + inp.input_text = "" + gui.shift_selection.clear() - global playlist_hold inp.quick_drag = False - playlist_hold = False + gui.playlist_hold = False def move_folder_up(index: int, do: bool = False) -> bool | None: track = pctl.master_library[index] @@ -32042,7 +32046,7 @@ def reload_metadata(input, keep_star: bool = True) -> None: def reload_metadata_selection(tauon: Tauon) -> None: cargo = [] - for item in shift_selection: + for item in gui.shift_selection: cargo.append(pctl.default_playlist[item]) for k in cargo: @@ -32058,7 +32062,7 @@ def editor(index: int | None) -> None: todo = [index] obs = [pctl.master_library[index]] elif index is None: - for item in shift_selection: + for item in gui.shift_selection: todo.append(pctl.default_playlist[item]) obs.append(pctl.master_library[pctl.default_playlist[item]]) if len(todo) > 0: @@ -32202,7 +32206,7 @@ def launch_editor(index: int): mini_t.start() def launch_editor_selection_disable_test(index: int): - for position in shift_selection: + for position in gui.shift_selection: if pctl.get_track(pctl.default_playlist[position]).is_network: return True return False @@ -32330,7 +32334,7 @@ def intel_moji(index: int): def sel_to_car(): cargo = [] - for item in shift_selection: + for item in gui.shift_selection: cargo.append(pctl.default_playlist[item]) def cut_selection(): @@ -32442,12 +32446,12 @@ def add_to_spotify_library(track_id: int) -> None: def selection_queue_deco(): total = 0 - for item in shift_selection: + for item in gui.shift_selection: total += pctl.get_track(pctl.default_playlist[item]).length total = get_hms_time(total) - text = (_("Queue {N}").format(N=len(shift_selection))) + f" [{total}]" + text = (_("Queue {N}").format(N=len(gui.shift_selection))) + f" [{total}]" return [colours.menu_text, colours.menu_background, text] @@ -33268,7 +33272,6 @@ def check_auto_update_okay(code, pl=None): def switch_playlist(number, cycle=False, quiet=False): global search_index - global shift_selection # Close any active menus # for instance in Menu.instances: @@ -33322,7 +33325,7 @@ def switch_playlist(number, cycle=False, quiet=False): 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] + gui.shift_selection = [pctl.selected_in_playlist] id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int @@ -33837,7 +33840,7 @@ def locate_artist() -> None: if next: pctl.selected_in_playlist = start pctl.playlist_view_position = start - shift_selection.clear() + gui.shift_selection.clear() break if pctl.selected_in_playlist == start: @@ -33847,7 +33850,7 @@ def locate_artist() -> None: else: pctl.selected_in_playlist = block_starts[0] pctl.playlist_view_position = block_starts[0] - shift_selection.clear() + gui.shift_selection.clear() tree_view_box.show_track(pctl.get_track(pctl.default_playlist[pctl.selected_in_playlist])) else: @@ -39473,14 +39476,8 @@ def main(holder: Holder): update_title = False - playlist_hold_position = 0 - playlist_hold = False - selection_stage = 0 - selected_in_playlist = -1 - shift_selection = [] - gen_codes: dict[int, str] = {} # Control Variables-------------------------------------------------------------------------- @@ -41762,10 +41759,6 @@ def dev_mode_disable_save_state() -> None: pref_box = Over(bag=bag, gui=gui) - inc_arrow = asset_loader(bag, loaded_asset_dc, "inc.png", True) - dec_arrow = asset_loader(bag, loaded_asset_dc, "dec.png", True) - corner_icon = asset_loader(bag, loaded_asset_dc, "corner.png", True) - bottom_bar_ao1 = BottomBarType_ao1(bag=bag, gui=gui) mini_mode = MiniMode(bag=bag, gui=gui) mini_mode2 = MiniMode2(bag=bag, gui=gui) @@ -41819,7 +41812,7 @@ def dev_mode_disable_save_state() -> None: tauon.dl_mon = dl_mon dl_menu.add(MenuItem("Dismiss", dismiss_dl)) - fader = Fader() + fader = Fader(tauon=tauon) edge_playlist2 = EdgePulse2() bottom_playlist2 = EdgePulse2() gallery_pulse_top = EdgePulse2() @@ -41885,8 +41878,8 @@ def dev_mode_disable_save_state() -> None: 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("-"): + 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() @@ -41967,7 +41960,6 @@ def dev_mode_disable_save_state() -> None: # Resize menu widths to text length (length can vary due to translations) for menu in Menu.instances: - w = 0 icon_space = 0 @@ -42101,7 +42093,6 @@ def dev_mode_disable_save_state() -> None: # gui.update = 2 while SDL_PollEvent(ctypes.byref(event)) != 0: - # if event.type == SDL_SYSWMEVENT: # logging.info(event.syswm.msg.contents) # Not implemented by pysdl2 @@ -42212,7 +42203,6 @@ def dev_mode_disable_save_state() -> None: reset_render = True if event.type == SDL_DROPTEXT: - power += 5 link = event.drop.file.decode() @@ -42256,7 +42246,6 @@ def dev_mode_disable_save_state() -> None: drop_file(target) if event.type == SDL_DROPFILE: - power += 5 dropped_file_sdl = event.drop.file #logging.info(dropped_file_sdl) @@ -42288,13 +42277,11 @@ def dev_mode_disable_save_state() -> None: gui.update += 1 elif event.type == SDL_MOUSEMOTION: - inp.mouse_position[0] = int(event.motion.x / logical_size[0] * window_size[0]) inp.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: - inp.k_input = True focused = True power += 5 @@ -42413,7 +42400,6 @@ def dev_mode_disable_save_state() -> None: key_focused = 1 elif event.type == SDL_KEYUP: - inp.k_input = True power += 5 gui.update += 2 @@ -42452,7 +42438,6 @@ def dev_mode_disable_save_state() -> None: inp.mouse_wheel += event.wheel.y gui.update += 1 elif event.type == SDL_WINDOWEVENT: - power += 5 #logging.info(event.window.event) @@ -42524,7 +42509,6 @@ def dev_mode_disable_save_state() -> None: # tauon.thread_manager.sleep() elif event.window.event == SDL_WINDOWEVENT_RESTORED: - gui.lowered = False gui.maximized = False gui.pl_update = 1 @@ -42604,7 +42588,6 @@ def dev_mode_disable_save_state() -> None: i -= 1 if animate_monitor_timer.get() < 1 or load_orders: - if cursor_blink_timer.get() > 0.65: cursor_blink_timer.set() TextBox.cursor ^= True @@ -42617,7 +42600,7 @@ def dev_mode_disable_save_state() -> None: SDL_Delay(3) power = 1000 - if inp.mouse_wheel or inp.k_input or gui.pl_update or gui.update or top_panel.adds: # or mouse_moved: + if inp.mouse_wheel or inp.k_input or gui.pl_update or gui.update or tauon.top_panel.adds: # or mouse_moved: power = 1000 if prefs.art_bg and core_timer.get() < 3: @@ -42631,7 +42614,6 @@ def dev_mode_disable_save_state() -> None: gui.pl_update += 1 if pctl.wake_past_time: - if get_real_time() > pctl.wake_past_time: pctl.wake_past_time = 0 power = 1000 @@ -42733,7 +42715,7 @@ def dev_mode_disable_save_state() -> None: key_end_press = False inp.mouse_wheel = 0 pref_box.scroll = 0 - input_text = "" + inp.input_text = "" inp.level_2_enter = False if c_yax != 0: @@ -43132,7 +43114,7 @@ def dev_mode_disable_save_state() -> None: gui.pl_update = 1 pctl.selected_in_playlist = pctl.playlist_view_position logging.debug("Position changed by page key") - shift_selection.clear() + gui.shift_selection.clear() if keymaps.test("pageup"): if len(pctl.default_playlist) > 0: pctl.playlist_view_position -= gui.playlist_view_length - 4 @@ -43140,7 +43122,7 @@ def dev_mode_disable_save_state() -> None: gui.pl_update = 1 pctl.selected_in_playlist = pctl.playlist_view_position logging.debug("Position changed by page key") - shift_selection.clear() + gui.shift_selection.clear() 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: @@ -43166,7 +43148,7 @@ def dev_mode_disable_save_state() -> None: if key_a_press and inp.key_ctrl_down: gui.pl_update = 1 - shift_selection = range(len(pctl.default_playlist)) # TODO(Martin): This can under some circumstances end up doing a range.clear() + gui.shift_selection = range(len(pctl.default_playlist)) # TODO(Martin): This can under some circumstances end up doing a range.clear() if keymaps.test("revert"): pctl.revert() @@ -43194,30 +43176,30 @@ def dev_mode_disable_save_state() -> None: if pctl.selected_in_playlist > len(pctl.default_playlist) - 1: pctl.selected_in_playlist = 0 - if not shift_selection: - shift_selection.append(pctl.selected_in_playlist) + if not gui.shift_selection: + gui.shift_selection.append(pctl.selected_in_playlist) if pctl.selected_in_playlist < len(pctl.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) + if pctl.selected_in_playlist not in gui.shift_selection: + gui.shift_selection.append(pctl.selected_in_playlist) else: - shift_selection.remove(r) + gui.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(pctl.default_playlist) - 1: pctl.selected_in_playlist = 0 - if not shift_selection: - shift_selection.append(pctl.selected_in_playlist) + if not gui.shift_selection: + gui.shift_selection.append(pctl.selected_in_playlist) if pctl.selected_in_playlist < len(pctl.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) + if pctl.selected_in_playlist not in gui.shift_selection: + gui.shift_selection.insert(0, pctl.selected_in_playlist) else: - shift_selection.remove(r) + gui.shift_selection.remove(r) if keymaps.test("toggle-shuffle"): # pctl.random_mode ^= True @@ -43854,42 +43836,39 @@ def dev_mode_disable_save_state() -> None: # ddt.rect_r((x - 7, y - 7, bag.album_mode_art_size + 14, bag.album_mode_art_size + extend + 55), [80, 80, 80, 80], True) # Quick drag and drop - if inp.mouse_up and (playlist_hold and m_in) and not gui.side_drag and shift_selection: - + if inp.mouse_up and (gui.playlist_hold and m_in) and not gui.side_drag and gui.shift_selection: info = get_album_info(album_dex[album_on]) if info[1]: - track_position = info[1][0] - if track_position > shift_selection[0]: + if track_position > gui.shift_selection[0]: track_position = info[1][-1] + 1 ref = [] - for item in shift_selection: + for item in gui.shift_selection: ref.append(pctl.default_playlist[item]) - for item in shift_selection: + for item in gui.shift_selection: pctl.default_playlist[item] = "old" - for item in shift_selection: + for item in gui.shift_selection: pctl.default_playlist.insert(track_position, "new") for b in reversed(range(len(pctl.default_playlist))): if pctl.default_playlist[b] == "old": del pctl.default_playlist[b] - shift_selection = [] + gui.shift_selection = [] for b in range(len(pctl.default_playlist)): if pctl.default_playlist[b] == "new": - shift_selection.append(b) + gui.shift_selection.append(b) pctl.default_playlist[b] = ref.pop(0) - pctl.selected_in_playlist = shift_selection[0] + pctl.selected_in_playlist = gui.shift_selection[0] gui.pl_update += 1 - playlist_hold = False + gui.playlist_hold = False reload_albums(True) pctl.notify_change() - elif not gui.side_drag and is_level_zero(): if coll_point(inp.click_location, rect) and gui.panelY < inp.mouse_position[1] < \ window_size[1] - gui.panelBY: @@ -43911,8 +43890,8 @@ def dev_mode_disable_save_state() -> None: info = get_album_info(album_dex[album_on]) inp.quick_drag = True if not pl_is_locked(pctl.active_playlist_viewing) or inp.key_shift_down: - playlist_hold = True - shift_selection = info[1] + gui.playlist_hold = True + gui.shift_selection = info[1] gui.pl_update += 1 inp.click_location = [0, 0] @@ -43975,17 +43954,17 @@ def dev_mode_disable_save_state() -> None: else: pctl.selected_in_playlist = album_dex[album_on] # playlist_position = pctl.playlist_selected - shift_selection = [pctl.selected_in_playlist] + gui.shift_selection = [pctl.selected_in_playlist] gallery_menu.activate(pctl.default_playlist[pctl.selected_in_playlist]) r_menu_position = pctl.selected_in_playlist - shift_selection = [] + gui.shift_selection = [] u = pctl.selected_in_playlist while u < len(pctl.default_playlist) and pctl.master_library[ pctl.default_playlist[u]].parent_folder_path == \ pctl.master_library[ pctl.default_playlist[pctl.selected_in_playlist]].parent_folder_path: - shift_selection.append(u) + gui.shift_selection.append(u) u += 1 pctl.render_playlist() @@ -44236,7 +44215,7 @@ def dev_mode_disable_save_state() -> None: style_overlay.hole_punches.append(rect) # # Drag over highlight - # if inp.quick_drag and playlist_hold and inp.mouse_down: + # if inp.quick_drag and gui.playlist_hold and inp.mouse_down: # rect = (x, y, bag.album_mode_art_size, bag.album_mode_art_size + extend * gui.scale) # m_in = tauon.coll(rect) and gui.panelY < inp.mouse_position[1] < window_size[1] - gui.panelBY # if m_in: @@ -45846,7 +45825,7 @@ def dev_mode_disable_save_state() -> None: draw_window_tools(tauon) if not gui.fullscreen and not gui.maximized: - draw_window_border() + draw_window_border(tauon) fader.render() if pref_box.enabled: @@ -46218,7 +46197,7 @@ def dev_mode_disable_save_state() -> None: if pctl.selected_in_playlist > 0: pctl.selected_in_playlist -= 1 r_menu_index = pctl.default_playlist[pctl.selected_in_playlist] - shift_selection = [] + gui.shift_selection = [] if pctl.playlist_view_position > 0 and pctl.selected_in_playlist < pctl.playlist_view_position + 2: pctl.playlist_view_position -= 1 @@ -46246,7 +46225,7 @@ def dev_mode_disable_save_state() -> None: if pctl.selected_in_playlist < len(pctl.default_playlist) - 1: pctl.selected_in_playlist += 1 r_menu_index = pctl.default_playlist[pctl.selected_in_playlist] - shift_selection = [] + gui.shift_selection = [] if pctl.playlist_view_position < len( pctl.default_playlist) and pctl.selected_in_playlist > pctl.playlist_view_position + gui.playlist_view_length - 3 - gui.row_extra: @@ -46262,7 +46241,7 @@ def dev_mode_disable_save_state() -> None: gui.pl_update = 1 if pctl.selected_in_playlist > len(pctl.default_playlist) - 1: pctl.selected_in_playlist = 0 - shift_selection = [] + gui.shift_selection = [] if pctl.default_playlist: pctl.jump(pctl.default_playlist[pctl.selected_in_playlist], pctl.selected_in_playlist) if prefs.album_mode: @@ -46495,7 +46474,7 @@ def dev_mode_disable_save_state() -> None: x_offset = round(20 * gui.scale) y_offset = round(1 * gui.scale) - if len(shift_selection) == 1: # Single track + if len(gui.shift_selection) == 1: # Single track ddt.rect((i_x + x_offset, i_y + y_offset, block_size, block_size), [160, 140, 235, 240]) elif inp.key_ctrl_down: # Add to queue undrouped small_block = round(6 * gui.scale) From 9c1acbe53163a3ffc11776aa290d13ac93eca03c Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 17:00:08 +0100 Subject: [PATCH 03/13] Even less crashes! --- src/tauon/t_modules/t_main.py | 192 +++++++++++++++++----------------- 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index c9c7223a7..2327e2e4e 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -71,6 +71,7 @@ from collections import OrderedDict from ctypes import Structure, byref, c_char_p, c_double, c_int, c_uint32, c_void_p, pointer from dataclasses import dataclass, field +from functools import partial from pathlib import Path from typing import TYPE_CHECKING @@ -1078,6 +1079,7 @@ class Input: def __init__(self, gui: GuiVar) -> None: self.gui = gui self.mouse_click: bool = False + self.middle_click: bool = False self.right_click: bool = False self.level_2_enter: bool = False self.backspace_press: int = 0 @@ -1522,20 +1524,22 @@ def __init__(self) -> None: class GetSDLInput: - def __init__(self): + def __init__(self, tauon: Tauon) -> None: + self.logical_size = tauon.bag.logical_size + self.window_size = tauon.bag.window_size 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): + def mouse(self) -> tuple[int, int]: 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]) + return (int(self.i_x.contents.value / self.logical_size[0] * self.window_size[0]), + int(self.i_y.contents.value / self.logical_size[0] * self.window_size[0])) - def test_capture_mouse(self): + def test_capture_mouse(self) -> None: if not self.mouse_capture and self.mouse_capture_want: SDL_CaptureMouse(SDL_TRUE) self.mouse_capture = True @@ -2160,7 +2164,7 @@ def toggle_mute(self) -> None: self.set_volume() def set_volume(self, notify: bool = True) -> None: - if (self.tauon.spot_ctl.coasting or self.tauon.spot_ctl.playing) and not self.tauon.spot_ctl.local and self.tauon.inp.mouse_down: + if (self.tauon.spot_ctl.coasting or self.tauon.spot_ctl.playing) and not self.tauon.spot_ctl.local and self.tauon.gui.inp.mouse_down: # Rate limit network volume change t = self.volume_update_timer.get() if t < 0.3: @@ -4546,7 +4550,7 @@ def render(self): to_call = i if self.items[i].set_ref is not None: self.reference = self.items[i].set_ref - self.tauon.inp.mouse_down = False + self.tauon.gui.inp.mouse_down = False else: self.clicked = False @@ -5081,7 +5085,7 @@ def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): self.strings: Strings = strings self.gui: GuiVar = gui self.prefs: Prefs = bag.prefs - self.fields = Fields() + self.fields = Fields(tauon=self) self.artist_list_box = ArtistList(tauon=self) self.radiobox = RadioBox(tauon=self) self.pctl: PlayerCtl = PlayerCtl(tauon=self) @@ -5092,6 +5096,7 @@ def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): self.bottom_bar1 = BottomBarType1(tauon=self) self.top_panel = TopPanel(tauon=self) self.playlist_box = PlaylistBox(tauon=self) + self.radio_view = RadioView(tauon=self) self.cache_directory: Path = bag.dirs.cache_directory self.user_directory: Path | None = bag.dirs.user_directory self.music_directory: Path | None = bag.dirs.music_directory @@ -5143,6 +5148,8 @@ def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): self.extra_menu = Menu(self, 175, show_icons=True) self.shuffle_menu = Menu(self, 120) self.repeat_menu = Menu(self, 120) + self.tab_menu = Menu(self, 160, show_icons=True) + self.tray_lock = threading.Lock() self.tray_releases = 0 @@ -13585,17 +13592,18 @@ def render(self): ddt.text_background_colour = colours.box_background class Fields: - def __init__(self) -> None: + def __init__(self, tauon: Tauon) -> None: + self.tauon = tauon self.id = [] self.last_id = [] self.field_array = [] self.force = False - def add(self, rect, callback=None): + def add(self, rect, callback=None) -> None: self.field_array.append((rect, callback)) - def test(self): + def test(self) -> bool: if self.force: self.force = False return True @@ -13605,7 +13613,7 @@ def test(self): self.id = [] for f in self.field_array: - if tauon.coll(f[0]): + if self.tauon.coll(f[0]): self.id.append(1) # += "1" if f[1] is not None: # Call callback if present f[1]() @@ -13614,11 +13622,9 @@ def test(self): if self.last_id == self.id: return False - return True - def clear(self): - + def clear(self) -> None: self.field_array = [] class TopPanel: @@ -13794,7 +13800,7 @@ def render(self): update_layout = True gui.update += 1 - if middle_click: + if inp.middle_click: toggle_left_last() update_layout = True gui.update += 1 @@ -14113,7 +14119,7 @@ def render(self): gui.update += 1 # Delete playlist on wheel click - elif tab_menu.active is False and middle_click: + elif tauon.tab_menu.active is False and inp.middle_click: # delete_playlist(i) delete_playlist_ask(i) break @@ -14123,7 +14129,7 @@ def render(self): if gui.radio_view: radio_tab_menu.activate(copy.deepcopy(i)) else: - tab_menu.activate(copy.deepcopy(i)) + tauon.tab_menu.activate(copy.deepcopy(i)) gui.tab_menu_pl = i # Quick drop tracks @@ -14151,11 +14157,11 @@ def render(self): tree_view_box.clear_target_pl(i) tauon.thread_manager.ready("worker") - if inp.mouse_up and radio_view.drag: - pctl.radio_playlists[i].stations.append(radio_view.drag) + if inp.mouse_up and tauon.radio_view.drag: + pctl.radio_playlists[i].stations.append(tauon.radio_view.drag) toast(_("Added station to: ") + pctl.radio_playlists[i].name) - radio_view.drag = None + tauon.radio_view.drag = None x += tab_width + self.tab_spacing @@ -14232,7 +14238,7 @@ def render(self): 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 tauon.playlist_box.drag): + tauon.tab_menu.active is True and tauon.tab_menu.reference == i) or (tauon.tab_menu.active is False and tab_hit and not tauon.playlist_box.drag): bg = colours.tab_highlight elif i == pctl.active_playlist_playing: bg = colours.tab_background @@ -14332,7 +14338,7 @@ def render(self): if inp.mouse_up: inp.quick_drag = False tauon.playlist_box.drag = False - radio_view.drag = None + tauon.radio_view.drag = None # Scroll anywhere on panel to cycle playlist # (This is a bit complicated because we need to skip over hidden playlists) @@ -14747,7 +14753,7 @@ def render(self): tauon.fields.add(self.seek_bar_position + self.seek_bar_size) if tauon.coll(self.seek_bar_position + self.seek_bar_size): - if middle_click and pctl.playing_state > 0: + if inp.middle_click and pctl.playing_state > 0: gui.seek_cur_show = True clicked = True @@ -15268,7 +15274,7 @@ def render(self): gui.mode_toast_text = _("Shuffle Off") toast_mode_timer.set() gui.delay_frame(1) - if middle_click: + if inp.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") @@ -15302,7 +15308,7 @@ def render(self): gui.mode_toast_text = _("Repeat Off") toast_mode_timer.set() gui.delay_frame(1) - if middle_click: + if inp.middle_click: pctl.revert() gui.tool_tip_lock_off_b = True if not gui.tool_tip_lock_off_b: @@ -15829,7 +15835,7 @@ def render(self): gui.mode_toast_text = _("Shuffle Off") toast_mode_timer.set() gui.delay_frame(1) - if middle_click: + if inp.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") @@ -16710,7 +16716,7 @@ def full_render(self): move_on_title = True # Ignore click in ratings box - click_title = (inp.mouse_click or right_click or middle_click) and tauon.coll(input_box) + click_title = (inp.mouse_click or right_click or inp.middle_click) and tauon.coll(input_box) if click_title and gui.show_album_ratings: if inp.mouse_position[0] > (input_box[0] + input_box[2]) - 80 * gui.scale: click_title = False @@ -16720,7 +16726,7 @@ def full_render(self): gui.pl_update += 1 # Add folder to queue if middle click - if middle_click and is_level_zero(): + if inp.middle_click and is_level_zero(): if inp.key_ctrl_down: # Add as ungrouped tracks i = track_position parent = pctl.get_track(pctl.default_playlist[i]).parent_folder_path @@ -16830,7 +16836,7 @@ def full_render(self): line_hit = False if tauon.coll(input_box) and inp.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())): + if (inp.mouse_click or right_click or (inp.middle_click and is_level_zero())): line_hit = True gui.pl_update += 1 @@ -16864,7 +16870,7 @@ def full_render(self): this_line_playing = True # Add to queue on middle click - if middle_click and line_hit: + if inp.middle_click and line_hit: pctl.force_queue.append( queue_item_gen(track_id, track_position, pl_to_id(pctl.active_playlist_viewing))) @@ -17648,7 +17654,7 @@ def full_render(self): ddt.alpha_bg = False def cache_render(self): - SDL_RenderCopy(self.renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) + SDL_RenderCopy(self.renderer, self.gui.tracklist_texture, None, self.gui.tracklist_texture_rect) class ArtBox: @@ -18615,8 +18621,8 @@ def draw_list(self): text_colour = colours.tab_text_active ddt.rect(rect, bg) - if radio_view.drag: - if station == radio_view.drag: + if tauon.radio_view.drag: + if station == tauon.radio_view.drag: text_colour = colours.box_sub_text bg = [255, 255, 255, 10] ddt.rect(rect, bg) @@ -18631,15 +18637,15 @@ def draw_list(self): if gui.level_2_click: # self.drag = p # self.click_point = copy.copy(inp.mouse_position) - radio_view.drag = station - radio_view.click_point = copy.copy(inp.mouse_position) + tauon.radio_view.drag = station + tauon.radio_view.click_point = copy.copy(inp.mouse_position) if inp.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, inp.mouse_position, round(4 * gui.scale)): + if point_proximity_test(tauon.radio_view.click_point, inp.mouse_position, round(4 * gui.scale)): self.start(station) - if middle_click: + if inp.middle_click: to_delete = p if level_2_right_click: self.right_clicked_station = station @@ -19140,10 +19146,10 @@ def draw(self, x, y, w, h): if gui.radio_view: radio_tab_menu.activate(i, inp.mouse_position) else: - tab_menu.activate(i, inp.mouse_position) + tauon.tab_menu.activate(i, inp.mouse_position) gui.tab_menu_pl = i - if tab_menu.active is False and middle_click: + if tauon.tab_menu.active is False and inp.middle_click: delete_pl = i # delete_playlist(i) # break @@ -19238,7 +19244,7 @@ def draw(self, x, y, w, h): 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): + if i == pctl.active_playlist_viewing or (tauon.tab_menu.active and tauon.tab_menu.reference == i): # bg = [255, 255, 255, 25] # Adjust highlight for different background brightnesses @@ -20051,7 +20057,7 @@ def draw_card(self, artist, x, y, w): if prefs.album_mode: goto_album(select) - if middle_click: + if inp.middle_click: self.click_ref = artist self.click_highlight_timer.set() create_artist_pl(artist) @@ -20476,14 +20482,10 @@ def render(self, x, y, w, h): inp.quick_drag and not point_proximity_test(gui.drag_source_position, inp.mouse_position, 15)) if mouse_in and not tree_view_scroll.held: - - if middle_click: + if inp.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("/")): @@ -20503,7 +20505,6 @@ def render(self, x, y, w, h): elif inp.mouse_click: # inp.quick_drag = True - if not self.click_drag_source: self.click_drag_source = item set_drag_source() @@ -21197,7 +21198,7 @@ def draw(self, x: int, y: int, w: int, h: int): self.right_click_id = fq[i].uuid_int qb_right_click = 2 - if middle_click and tauon.coll(rect): + if inp.middle_click and tauon.coll(rect): pctl.force_queue.remove(fq[i]) gui.pl_update += 1 @@ -25775,6 +25776,7 @@ def do_minimize_button(): def draw_window_tools(tauon: Tauon) -> None: bag = tauon.bag gui = tauon.gui + inp = tauon.gui.inp colours = tauon.bag.colours window_size = tauon.bag.window_size ddt = tauon.bag.ddt @@ -28251,7 +28253,7 @@ def pin_playlist_toggle(pl: int) -> None: pctl.multi_playlist[pl].hidden ^= True 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 and tauon.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")] @@ -37898,13 +37900,22 @@ def display_friend_heart(x: int, yy: int, name: str, just: int = 0) -> None: 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]) -def hit_callback(win, point, data): +def hit_callback_wrapper(win, point, data, tauon: Tauon): + """A wrapper function that captures Tauon""" + return hit_callback(tauon, win, point, data) + +def hit_callback(win, point, data, tauon: Tauon): + logical_size = tauon.bag.logical_size + window_size = tauon.bag.window_size + gui = tauon.gui + prefs = tauon.prefs + inp = tauon.gui.inp + 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 inp.key_shift_down or inp.key_shiftr_down: return SDL_HITTEST_NORMAL @@ -37929,7 +37940,6 @@ def hit_callback(win, point, data): y1 = window_size[1] - 79 * gui.scale if y0 < y < y1 and not tauon.search_over.active: return SDL_HITTEST_DRAGGABLE - return SDL_HITTEST_NORMAL # Standard player mode @@ -37944,9 +37954,7 @@ def hit_callback(win, point, data): # 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 @@ -37954,20 +37962,17 @@ def hit_callback(win, point, data): 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: + if tauon.top_panel.drag_zone_start_x > x or tauon.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 inp.mouse_up or inp.mouse_down: # mouse up/down is workaround for Wayland + if tauon.top_panel.drag_zone_start_x < x < window_size[0] - (gui.offset_extra + 5): + if tauon.tab_menu.active or inp.mouse_up or inp.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)): + 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: @@ -38005,7 +38010,7 @@ def reload_scale(bag: Bag): menu.rescale() bottom_bar1.__init__() bottom_bar_ao1.__init__() - top_panel.__init__() + tauon.top_panel.__init__() view_box.__init__(reload=True) queue_box.recalc() tauon.playlist_box.recalc() @@ -38191,10 +38196,10 @@ def update_layout_do(tauon: Tauon): # if system != 'windows': # if draw_border: # gui.panelY = 30 * gui.scale + 3 * gui.scale - # top_panel.ty = 3 * gui.scale + # tauon.top_panel.ty = 3 * gui.scale # else: # gui.panelY = 30 * gui.scale - # top_panel.ty = 0 + # tauon.top_panel.ty = 0 if gui.set_bar and gui.set_mode: gui.playlist_top = gui.playlist_top_bk + gui.set_height - 6 * gui.scale @@ -38645,8 +38650,8 @@ def save_state() -> None: gui.show_album_ratings, prefs.radio_urls, gui.showcase_mode, # gui.combo_mode, - top_panel.prime_tab, - top_panel.prime_side, + tauon.top_panel.prime_tab, + tauon.top_panel.prime_side, prefs.sync_playlist, prefs.spot_client, prefs.spot_secret, @@ -38796,9 +38801,9 @@ def drop_file(target): #logging.info(event.drop) 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 = tauon.top_panel.tabs_left_x + for tab in tauon.top_panel.shown_tabs: + wid = tauon.top_panel.tab_text_spaces[tab] + tauon.top_panel.tab_extra_width if x < i_x < x + wid: gui.drop_playlist_target = tab @@ -41061,7 +41066,7 @@ def update(self, force=False): # playlist_menu.add('Paste', append_here, paste_deco) # Create playlist tab menu - tab_menu = Menu(tauon, 160, show_icons=True) + tab_menu = tauon.tab_menu tab_menu.add(MenuItem(_("Rename"), rename_playlist, pass_ref=True, hint="Ctrl+R")) radio_tab_menu = Menu(tauon, 160, show_icons=True) @@ -41804,7 +41809,6 @@ def dev_mode_disable_save_state() -> None: 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(tauon=tauon) showcase = Showcase() cctest = ColourPulse2() view_box = ViewBox(tauon=tauon) @@ -41820,7 +41824,7 @@ def dev_mode_disable_save_state() -> None: lyric_side_top_pulse = EdgePulse2() lyric_side_bottom_pulse = EdgePulse2() - c_hit_callback = SDL_HitTest(hit_callback) + c_hit_callback = SDL_HitTest(partial(hit_callback, tauon=tauon)) SDL_SetWindowHitTest(t_window, c_hit_callback, 0) # -------------------------------------------------------------------------------------------- @@ -41925,7 +41929,7 @@ def dev_mode_disable_save_state() -> None: gal_left = False gal_right = False - get_sdl_input = GetSDLInput() + get_sdl_input = GetSDLInput(tauon=tauon) SDL_StartTextInput() @@ -42053,7 +42057,7 @@ def dev_mode_disable_save_state() -> None: right_click = False level_2_right_click = False inp.mouse_click = False - middle_click = False + inp.middle_click = False inp.mouse_up = False inp.key_return_press = False key_down_press = False @@ -42306,7 +42310,7 @@ def dev_mode_disable_save_state() -> None: inp.mouse_down = True elif event.button.button == SDL_BUTTON_MIDDLE: if not tauon.search_over.active: - middle_click = True + inp.middle_click = True gui.update += 1 elif event.button.button == SDL_BUTTON_X1: keymaps.hits.append("MB4") @@ -42638,11 +42642,10 @@ def dev_mode_disable_save_state() -> None: power += 400 if power < 500: - time.sleep(0.03) - 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 tauon.transcode_list and not gui.frame_callback_list: + 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 tauon.transcode_list and not gui.frame_callback_list: pass else: sleep_timer.set() @@ -42692,7 +42695,7 @@ def dev_mode_disable_save_state() -> None: # right_click = False # level_2_right_click = False # inp.mouse_click = False - # middle_click = False + # inp.middle_click = False inp.mouse_up = False inp.key_return_press = False key_down_press = False @@ -43804,7 +43807,7 @@ def dev_mode_disable_save_state() -> None: extend = 40 * gui.scale # Process inputs first - if (inp.mouse_click or right_click or middle_click or inp.mouse_down or inp.mouse_up) and pctl.default_playlist: + if (inp.mouse_click or right_click or inp.middle_click or inp.mouse_down or inp.mouse_up) and pctl.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: @@ -43919,7 +43922,7 @@ def dev_mode_disable_save_state() -> None: 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(): + elif inp.middle_click and is_level_zero(): # Middle click to add album to queue if inp.key_ctrl_down: # Add to queue ungrouped @@ -44432,7 +44435,7 @@ def dev_mode_disable_save_state() -> None: if right_click: lightning_menu.activate(item, position=( window_size[0] - 180 * gui.scale, rect[1] + rect[3] + 5 * gui.scale)) - if middle_click: + if inp.middle_click: path_stem_to_playlist(item.path, item.name) ddt.rect(rect, item.colour) @@ -44700,7 +44703,7 @@ def dev_mode_disable_save_state() -> None: gui.pl_update -= 1 if gui.combo_mode: if gui.radio_view: - radio_view.render() + tauon.radio_view.render() elif gui.showcase_mode: showcase.render() @@ -44713,7 +44716,7 @@ def dev_mode_disable_save_state() -> None: elif gui.combo_mode: if gui.radio_view: - radio_view.render() + tauon.radio_view.render() elif gui.showcase_mode: showcase.render() # else: @@ -44850,16 +44853,16 @@ def dev_mode_disable_save_state() -> None: (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 < inp.mouse_position[0] and \ + elif right_click and tauon.top_panel.tabs_right_x < inp.mouse_position[0] and \ inp.mouse_position[1] < gui.panelY and \ - inp.mouse_position[0] > top_panel.tabs_right_x and \ + inp.mouse_position[0] > tauon.top_panel.tabs_right_x and \ inp.mouse_position[0] < window_size[0] - 130 * gui.scale - gui.offset_extra: window_menu.activate(None, (inp.mouse_position[0], 30 * gui.scale)) - elif middle_click and top_panel.tabs_right_x < inp.mouse_position[0] and \ + elif inp.middle_click and tauon.top_panel.tabs_right_x < inp.mouse_position[0] and \ inp.mouse_position[1] < gui.panelY and \ - inp.mouse_position[0] > top_panel.tabs_right_x and \ + inp.mouse_position[0] > tauon.top_panel.tabs_right_x and \ inp.mouse_position[0] < window_size[0] - gui.offset_extra: do_minimize_button() @@ -44877,7 +44880,7 @@ def dev_mode_disable_save_state() -> None: gui.showing_l_panel = False target_track = pctl.show_object() - if middle_click: + if inp.middle_click: if tauon.coll( (window_size[0] - gui.rspw, gui.panelY, gui.rspw, window_size[1] - gui.panelY - gui.panelBY)): @@ -46246,11 +46249,8 @@ def dev_mode_disable_save_state() -> None: pctl.jump(pctl.default_playlist[pctl.selected_in_playlist], pctl.selected_in_playlist) if prefs.album_mode: goto_album(pctl.playlist_playing_position) - - elif gui.mode == 3: - - if (inp.key_shift_down and inp.mouse_click) or middle_click: + if (inp.key_shift_down and inp.mouse_click) or inp.middle_click: if prefs.mini_mode_mode == 4: prefs.mini_mode_mode = 1 window_size[0] = int(330 * gui.scale) @@ -46511,7 +46511,7 @@ def dev_mode_disable_save_state() -> None: (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[tauon.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, inp.mouse_position, round(4 * gui.scale)): + if tauon.radio_view.drag and not point_proximity_test(tauon.radio_view.click_point, inp.mouse_position, round(4 * gui.scale)): ddt.rect(( inp.mouse_position[0] + round(8 * gui.scale), inp.mouse_position[1] - round(8 * gui.scale), 48 * gui.scale, 14 * gui.scale), colours.grey(70)) From 316054c30a443239afef9960e89cdbfa4dc58175 Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 17:19:22 +0100 Subject: [PATCH 04/13] Small fixes --- src/tauon/t_modules/t_main.py | 46 ++++++++++++----------------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index 2327e2e4e..876b26266 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -361,13 +361,14 @@ class LoadImageAsset: def __init__(self, *, bag: Bag, path: str, is_full_path: bool = False, reload: bool = False, scale_name: str = "") -> None: if not reload: self.assets.append(self) - + self.bag = bag + self.renderer = bag.renderer self.path = path self.scale_name = scale_name self.scaled_asset_directory: Path = bag.dirs.scaled_asset_directory raw_image = IMG_Load(self.path.encode()) - self.sdl_texture = SDL_CreateTextureFromSurface(bag.renderer, raw_image) + self.sdl_texture = SDL_CreateTextureFromSurface(self.renderer, raw_image) p_w = pointer(c_int(0)) p_h = pointer(c_int(0)) @@ -385,12 +386,12 @@ 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=self.scaled_asset_directory, path=self.path, reload=True, scale_name=self.scale_name) + self.__init__(bag=self.bag, path=self.path, reload=True, scale_name=self.scale_name) - def render(self, x: int, y: int, colour=None) -> None: + def render(self, x: int, y: int, colour: list[int] | None = None) -> None: self.rect.x = round(x) self.rect.y = round(y) - SDL_RenderCopy(renderer, self.sdl_texture, None, self.rect) + SDL_RenderCopy(self.renderer, self.sdl_texture, None, self.rect) class WhiteModImageAsset: assets: list[WhiteModImageAsset] = [] @@ -418,9 +419,9 @@ 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=self.scaled_asset_directory, path=self.path, reload=True, scale_name=self.scale_name) + self.__init__(bag=self.bag, path=self.path, reload=True, scale_name=self.scale_name) - def render(self, x: int, y: int, colour) -> None: + def render(self, x: int, y: int, colour: list[int]) -> None: if colour != self.colour: SDL_SetTextureColorMod(self.sdl_texture, colour[0], colour[1], colour[2]) SDL_SetTextureAlphaMod(self.sdl_texture, colour[3]) @@ -449,7 +450,7 @@ def update_layout(self) -> None: 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): + def delay_frame(self, t: float) -> None: self.frame_callback_list.append(TestTimer(t)) def destroy_textures(self): @@ -1131,13 +1132,13 @@ def m_key_previous(self) -> None: class KeyMap: - def __init__(self, bag: Bag): + def __init__(self, bag: Bag, inp: Input): self.bag = bag + self.inp = inp self.hits = [] # The keys hit this frame self.maps = {} # Loaded from input.txt def load(self): - path = self.bag.dirs.config_directory / "input.txt" with path.open(encoding="utf_8") as f: content = f.read().splitlines() @@ -1173,24 +1174,21 @@ def load(self): else: self.maps[function] = [(key, mod)] - def test(self, function): - + def test(self, function) -> bool: + inp = self.inp 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 = (inp.key_ctrl_down or inp.key_rctrl_down) * 1 shift = (inp.key_shift_down or inp.key_shiftr_down) * 10 alt = (inp.key_lalt or inp.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: @@ -1816,7 +1814,7 @@ def radio_progress(self) -> None: self.lfm_scrobbler.scrob_full_track(copy.deepcopy(self.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) + new_pool = copy.deepcopy(self.multi_playlist[self.id_to_pl(pl_id)].playlist_ids) random.shuffle(new_pool) self.shuffle_pools[pl_id] = new_pool logging.info("Refill shuffle pool") @@ -2406,7 +2404,6 @@ def back(self) -> None: self.gui.pl_update += 1 def stop(self, block: bool = False, run : bool = False) -> None: - self.playerCommand = "stop" if run: self.playerCommand = "runstop" @@ -2460,7 +2457,6 @@ def stop(self, block: bool = False, run : bool = False) -> None: return previous_state def pause(self) -> None: - if self.tauon.spotc and self.tauon.spotc.running and self.tauon.spot_ctl.playing: if self.playing_state == 1: self.playerCommand = "pauseon" @@ -2559,7 +2555,6 @@ def seek_time(self, new: float) -> None: self.mpris.seek_do(self.playing_time) def play(self) -> None: - if self.tauon.spot_ctl.playing: if self.playing_state == 2: self.play_pause() @@ -3180,7 +3175,6 @@ def advance( # 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: @@ -3402,8 +3396,7 @@ def auth1(self) -> None: webbrowser.open(self.url, new=2, autoraise=True) def auth2(self) -> None: - - # This is step 2 where the user clicks "Done" + """This is step 2 where the user clicks "Done"""" if self.sg is None: show_message(_("You need to log in first")) @@ -3437,7 +3430,6 @@ def auth3(self) -> None: show_message(_("Logout will complete on app restart.")) def connect(self, m_notify: bool = True) -> bool | None: - if not last_fm_enable: return False @@ -3453,7 +3445,6 @@ def connect(self, m_notify: bool = True) -> bool | 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=self.prefs.last_fm_token) # , username=lfm_username, password_hash=lfm_hash) @@ -3504,7 +3495,6 @@ def no_user_connect(self) -> bool: return False def get_all_scrobbles_estimate_time(self) -> float | None: - if not self.connected: self.connect(False) if not self.connected or not self.prefs.last_fm_username: @@ -3518,7 +3508,6 @@ def get_all_scrobbles_estimate_time(self) -> float | None: return 0 def get_all_scrobbles(self) -> None: - if not self.connected: self.connect(False) if not self.connected or not self.prefs.last_fm_username: @@ -3573,7 +3562,6 @@ def get_all_scrobbles(self) -> None: 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, "", "" @@ -3682,8 +3670,6 @@ def scrobble(self, track_object: TrackClass, timestamp: float | None = None) -> # Pull loved status self.sync_pull_love(track_object) - - else: logging.warning("Not sent, incomplete metadata") @@ -39713,7 +39699,7 @@ def main(holder: Holder): # Functions for reading and setting play counts inp = gui.inp - keymaps = KeyMap(bag=bag) + keymaps = KeyMap(bag=bag, inp=inp) # This is legacy. New settings are added straight to the save list (need to overhaul) view_prefs = { From 4dfd41b4dba64298956b2af3716d47ad2b5b3548 Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 17:42:38 +0100 Subject: [PATCH 05/13] More refactors --- src/tauon/t_modules/t_main.py | 433 +++++++++++++++++----------------- 1 file changed, 215 insertions(+), 218 deletions(-) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index 876b26266..c62f04fba 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -1079,22 +1079,23 @@ class Input: def __init__(self, gui: GuiVar) -> None: self.gui = gui - self.mouse_click: bool = False - self.middle_click: bool = False - self.right_click: bool = False - self.level_2_enter: bool = False - self.backspace_press: int = 0 - self.mouse_wheel: int = 0 - self.mouse_down: bool = False - self.mouse_up: bool = False - self.right_down: bool = False - self.click_location = [200, 200] - self.last_click_location = [0, 0] - self.mouse_position = [0, 0] - self.mouse_up_position = [0, 0] - self.drag_mode: bool = False - self.quick_drag: bool = False - self.clicked: bool = False + self.mouse_click: bool = False + self.middle_click: bool = False + self.right_click: bool = False + self.level_2_right_click: bool = False + self.level_2_enter: bool = False + self.backspace_press: int = 0 + self.mouse_wheel: int = 0 + self.mouse_down: bool = False + self.mouse_up: bool = False + self.right_down: bool = False + self.click_location = [200, 200] + self.last_click_location = [0, 0] + self.mouse_position = [0, 0] + self.mouse_up_position = [0, 0] + self.drag_mode: bool = False + self.quick_drag: bool = False + self.clicked: bool = False self.k_input: bool = True self.key_return_press: bool = False @@ -1601,7 +1602,7 @@ def __init__(self, tauon: Tauon): self.install_directory: Path = self.bag.dirs.install_directory self.loading_in_progress: bool = False - + self.cargo: list[int] = [] # Database self.master_count: int = self.bag.master_count @@ -2132,11 +2133,11 @@ def show_current( # logging.info("Run Over") if select: - gui.shift_selection = [] + self.gui.shift_selection = [] self.render_playlist() - if prefs.album_mode and not quiet: + if self.prefs.album_mode and not quiet: if highlight: self.gui.gallery_animate_highlight_on = goto_album(self.selected_in_playlist) gallery_select_animate_timer.set() @@ -3396,7 +3397,7 @@ def auth1(self) -> None: webbrowser.open(self.url, new=2, autoraise=True) def auth2(self) -> None: - """This is step 2 where the user clicks "Done"""" + """This is step 2 where the user clicks \"Done\"""" if self.sg is None: show_message(_("You need to log in first")) @@ -4317,7 +4318,12 @@ def rescale(self): self.w += 15 def __init__(self, tauon: Tauon, width: int, show_icons: bool = False) -> None: - self.tauon = tauon + self.tauon = tauon + self.gui = tauon.gui + self.inp = tauon.gui.inp + self.ddt = tauon.bag.ddt + self.window_size = tauon.bag.window_size + self.base_v_size = 22 self.active = False self.request_width: int = width @@ -4375,7 +4381,7 @@ def add_to_sub(self, sub_menu_index: int, menu_item: MenuItem) -> None: def test_item_active(self, item): if item.show_test is not None: - if item.show_test(1) is False: + if item.show_test(1, self.tauon) is False: return False return True @@ -4440,8 +4446,8 @@ def render_icon(self, x, y, icon, selected, fx): icon.base_asset.render(x, y) def render(self): + gui = self.gui if self.active: - if Menu.switch != self.id: self.active = False @@ -4711,7 +4717,8 @@ def render(self): # 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): + def activate(self, in_reference: int = 0, position: list[int] | None = None) -> None: + inp = self.inp Menu.active = True @@ -4726,7 +4733,7 @@ def activate(self, in_reference=0, position=None): # 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]: + if self.pos[0] + self.w > self.window_size[0]: self.pos[0] -= round(self.w + 3 * gui.scale) # Get height size of menu @@ -4742,7 +4749,7 @@ def activate(self, in_reference=0, position=None): shown_h += self.h # Flip menu up if would intersect with bottom of window - if self.pos[1] + full_h > window_size[1]: + if self.pos[1] + full_h > self.window_size[1]: self.pos[1] -= shown_h # Prevent moving outside top of window @@ -5125,16 +5132,18 @@ def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): self.encode_folder_name = encode_folder_name self.encode_track_name = encode_track_name # Create top menu - self.x_menu = Menu(self, 190, show_icons=True) - self.set_menu = Menu(self, 150) - self.field_menu = Menu(self, 140) - self.dl_menu = Menu(self, 90) + self.x_menu = Menu(self, 190, show_icons=True) + self.set_menu = Menu(self, 150) + self.field_menu = Menu(self, 140) + self.dl_menu = Menu(self, 90) - self.cancel_menu = Menu(self, 100) - self.extra_menu = Menu(self, 175, show_icons=True) - self.shuffle_menu = Menu(self, 120) - self.repeat_menu = Menu(self, 120) - self.tab_menu = Menu(self, 160, show_icons=True) + self.cancel_menu = Menu(self, 100) + self.extra_menu = Menu(self, 175, show_icons=True) + self.shuffle_menu = Menu(self, 120) + self.repeat_menu = Menu(self, 120) + self.tab_menu = Menu(self, 160, show_icons=True) + self.playlist_menu = Menu(self, 130) + self.spotify_playlist_menu = Menu(self, 175) self.tray_lock = threading.Lock() @@ -6295,7 +6304,7 @@ def render(self, index: int, x: int, y: int, side_panel: bool = False, w: int = self.ready = False self.generate(pctl.master_library[index]) - if right_click and x and y and tauon.coll((x, y, w, h)): + if inp.right_click and x and y and tauon.coll((x, y, w, h)): showcase_menu.activate(pctl.master_library[index]) if not self.ready: @@ -6461,7 +6470,7 @@ def draw( # Activate Menu if tauon.coll(rect): - if right_click or level_2_right_click: + if inp.right_click or inp.level_2_right_click: field_menu.activate(self) if width > 0 and active: @@ -6882,7 +6891,7 @@ def draw( # Activate Menu if tauon.coll(rect): - if right_click or level_2_right_click: + if inp.right_click or inp.level_2_right_click: field_menu.activate(self) if click and field_menu.active: @@ -8601,7 +8610,7 @@ def render(self): 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 tauon.coll((x, y, w, h))): + if key_esc_press or ((inp.mouse_click or inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): rename_track_box.active = False r_todo = [] @@ -8783,7 +8792,7 @@ def render(self): 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 tauon.coll((x, y, w, h))): + if key_esc_press or ((inp.mouse_click or inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): self.active = False select = list(set(gui.shift_selection)) @@ -9002,7 +9011,7 @@ def render(self): 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 tauon.coll((x, y, w, h))): + if key_esc_press or ((inp.mouse_click or inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): self.active = False select = list(set(gui.shift_selection)) @@ -9235,7 +9244,7 @@ def render(self): 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 tauon.coll((x, y, w, h))): + if key_esc_press or ((inp.mouse_click or inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): self.active = False gui.box_over = False @@ -9337,7 +9346,7 @@ def render(self) -> None: 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 tauon.coll( + if key_esc_press or ((inp.mouse_click or gui.level_2_click or inp.right_click or inp.level_2_right_click) and not tauon.coll( (x, y, w, h))): self.active = False gui.box_over = False @@ -10314,7 +10323,7 @@ def render(self): clear = True - if level_2_right_click: + if inp.level_2_right_click: show = True clear = True @@ -10449,7 +10458,7 @@ def get_rect(self): def render(self): - if inp.mouse_click or inp.key_return_press or right_click or key_esc_press or inp.backspace_press \ + if inp.mouse_click or inp.key_return_press or inp.right_click or key_esc_press or inp.backspace_press \ or keymaps.test("quick-find") or (inp.k_input and message_box_min_timer.get() > 1.2): if not key_focused and message_box_min_timer.get() > 0.4: @@ -11666,7 +11675,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -11684,7 +11693,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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( @@ -11721,7 +11730,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -11737,7 +11746,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -11800,7 +11809,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -11816,7 +11825,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -11875,7 +11884,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -11889,7 +11898,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -11903,7 +11912,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -11936,7 +11945,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -11950,7 +11959,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -11964,7 +11973,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -12009,7 +12018,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -12023,7 +12032,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -12037,7 +12046,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -12066,7 +12075,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -12080,7 +12089,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -12094,7 +12103,7 @@ def last_fm_box(self, x0, y0, w0, h0): y += round(19 * gui.scale) rect1 = (x + 0 * gui.scale, y, field_width, round(17 * gui.scale)) tauon.fields.add(rect1) - if tauon.coll(rect1) and (self.click or level_2_right_click): + if tauon.coll(rect1) and (self.click or inp.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 @@ -14111,7 +14120,7 @@ def render(self): break # Activate menu on right click - elif right_click: + elif inp.right_click: if gui.radio_view: radio_tab_menu.activate(copy.deepcopy(i)) else: @@ -14391,7 +14400,7 @@ def render(self): # if colours.lm: # colour = [40, 40, 40, 255] if dl > 0 or watching > 0: - if right_click: + if inp.right_click: tauon.dl_menu.activate(position=(inp.mouse_position[0], gui.panelY)) if dl > 0: if inp.mouse_click: @@ -14462,7 +14471,7 @@ def render(self): text = _("Extracting Archive...") else: text = _("Importing... ") + str(to_got) # + "/" + str(to_get) - if right_click and tauon.coll([x, y, 180 * gui.scale, 18 * gui.scale]): + if inp.right_click and tauon.coll([x, y, 180 * gui.scale, 18 * gui.scale]): tauon.cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) elif tauon.after_scan: # bg = colours.status_info_text @@ -14509,7 +14518,7 @@ def render(self): elif gui.sync_progress and not tauon.transcode_list: text = gui.sync_progress bg = [100, 200, 100, 255] - if right_click and tauon.coll([x, y, 280 * gui.scale, 18 * gui.scale]): + if inp.right_click and tauon.coll([x, y, 280 * gui.scale, 18 * gui.scale]): cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) elif tauon.transcode_list and gui.tc_cancel: bg = [150, 150, 150, 255] @@ -14551,7 +14560,7 @@ def render(self): # if inp.key_ctrl_down and key_c_press: # del transcode_list[1:] # gui.tc_cancel = True - if right_click and tauon.coll([x, y, 280 * gui.scale, 18 * gui.scale]): + if inp.right_click and tauon.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 @@ -14875,8 +14884,8 @@ def render(self): min_h = round(4 * gui.scale) spacing = round(5 * gui.scale) - if right_click and tauon.coll((h_rect[0], h_rect[1], h_rect[2] + 50 * gui.scale, h_rect[3])): - if right_click: + if inp.right_click and tauon.coll((h_rect[0], h_rect[1], h_rect[2] + 50 * gui.scale, h_rect[3])): + if inp.right_click: pctl.toggle_mute() for bar in range(8): @@ -15192,7 +15201,7 @@ def render(self): inp.mouse_click = False tool_tip2.test(33 * gui.scale, y - 35 * gui.scale, _("Play, RC: Go to playing")) - if right_click: + if inp.right_click: pctl.show_current(highlight=True) self.play_button.render(29 * gui.scale, window_size[1] - self.control_line_bottom, play_colour) @@ -15213,7 +15222,7 @@ def render(self): pause_colour = colours.media_buttons_over if inp.mouse_click: pctl.pause() - if right_click: + if inp.right_click: pctl.show_current(highlight=True) tool_tip2.test(x, y - 35 * gui.scale, _("Pause")) @@ -15229,7 +15238,7 @@ def render(self): stop_colour = colours.media_buttons_over if inp.mouse_click: pctl.stop() - if right_click: + if inp.right_click: pctl.auto_stop ^= True tool_tip2.test(x, y - 35 * gui.scale, _("Stop, RC: Toggle auto-stop")) @@ -15248,7 +15257,7 @@ def render(self): if inp.mouse_click: pctl.advance() gui.tool_tip_lock_off_f = True - if right_click: + if inp.right_click: # pctl.random_mode ^= True toggle_random() gui.tool_tip_lock_off_f = True @@ -15283,7 +15292,7 @@ def render(self): if inp.mouse_click: pctl.back() gui.tool_tip_lock_off_b = True - if right_click: + if inp.right_click: toggle_repeat() gui.tool_tip_lock_off_b = True # if window_size[0] < 600 * gui.scale: @@ -15770,7 +15779,7 @@ def render(self): inp.mouse_click = False tool_tip2.test(33 * gui.scale, y - 35 * gui.scale, _("Play, RC: Go to playing")) - if right_click: + if inp.right_click: pctl.show_current(highlight=True) self.play_button.render(29 * gui.scale, window_size[1] - self.control_line_bottom, play_colour) @@ -15791,7 +15800,7 @@ def render(self): pause_colour = colours.media_buttons_over if inp.mouse_click: pctl.pause() - if right_click: + if inp.right_click: pctl.show_current(highlight=True) tool_tip2.test(x, y - 35 * gui.scale, _("Pause")) @@ -15809,7 +15818,7 @@ def render(self): if inp.mouse_click: pctl.advance() gui.tool_tip_lock_off_f = True - if right_click: + if inp.right_click: # pctl.random_mode ^= True toggle_random() gui.tool_tip_lock_off_f = True @@ -15878,7 +15887,7 @@ def render(self): mouse_in = tauon.coll(detect_mouse_rect) # Play / Pause when right clicking below art - if right_click: # and inp.mouse_position[1] > y1: + if inp.right_click: # and inp.mouse_position[1] > y1: pctl.play_pause() # Volume change on scroll @@ -16105,7 +16114,7 @@ def render(self): mouse_in = tauon.coll(detect_mouse_rect) # Play / Pause when right clicking below art - if right_click: # and inp.mouse_position[1] > y1: + if inp.right_click: # and inp.mouse_position[1] > y1: pctl.play_pause() # Volume change on scroll @@ -16254,7 +16263,7 @@ def render(self): mouse_in = tauon.coll(detect_mouse_rect) # Play / Pause when right clicking below art - if right_click: # and inp.mouse_position[1] > y1: + if inp.right_click: # and inp.mouse_position[1] > y1: pctl.play_pause() # Volume change on scroll @@ -16702,7 +16711,7 @@ def full_render(self): move_on_title = True # Ignore click in ratings box - click_title = (inp.mouse_click or right_click or inp.middle_click) and tauon.coll(input_box) + click_title = (inp.mouse_click or inp.right_click or inp.middle_click) and tauon.coll(input_box) if click_title and gui.show_album_ratings: if inp.mouse_position[0] > (input_box[0] + input_box[2]) - 80 * gui.scale: click_title = False @@ -16743,7 +16752,7 @@ def full_render(self): goto_album(pctl.playlist_playing_position) # Show selection menu if right clicked after select - if right_click: + if inp.right_click: folder_menu.activate(track_id) r_menu_position = track_position gui.selection_stage = 2 @@ -16822,10 +16831,9 @@ def full_render(self): line_hit = False if tauon.coll(input_box) and inp.mouse_position[1] < window_size[1] - gui.panelBY: line_over = True - if (inp.mouse_click or right_click or (inp.middle_click and is_level_zero())): + if (inp.mouse_click or inp.right_click or (inp.middle_click and is_level_zero())): line_hit = True gui.pl_update += 1 - else: line_hit = False else: @@ -16945,8 +16953,7 @@ def full_render(self): drag_highlight = True # Right click menu activation - if right_click and line_hit and inp.mouse_position[0] > gui.playlist_left + 10: - + if inp.right_click and line_hit and inp.mouse_position[0] > gui.playlist_left + 10: if len(gui.shift_selection) > 1 and track_position in gui.shift_selection: selection_menu.activate(pctl.default_playlist[track_position]) gui.selection_stage = 2 @@ -17626,9 +17633,8 @@ def full_render(self): gui.playlist_current_visible_tracks_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int if (inp.right_click and gui.playlist_top + 5 * gui.scale + gui.playlist_row_height * len(list_items) < - inp.mouse_position[1] < window_size[ - 1] - 55 and width + left > inp.mouse_position[0] > gui.playlist_left + 15): - playlist_menu.activate() + inp.mouse_position[1] < window_size[1] - 55 and width + left > inp.mouse_position[0] > gui.playlist_left + 15): + tauon.playlist_menu.activate() SDL_SetRenderTarget(self.renderer, gui.main_texture) SDL_RenderCopy(self.renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) @@ -18633,7 +18639,7 @@ def draw_list(self): self.start(station) if inp.middle_click: to_delete = p - if level_2_right_click: + if inp.level_2_right_click: self.right_clicked_station = station self.right_clicked_station_p = p radio_entry_menu.activate(station) @@ -19000,7 +19006,7 @@ def render(self): # 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 tauon.coll(rect)): + or ((inp.mouse_click or inp.level_2_right_click) and not tauon.coll(rect)): gui.rename_playlist_box = False if self.edit_generator: @@ -19128,7 +19134,7 @@ def draw(self, x, y, w, h): tab_on += 1 if tauon.coll((tab_start, yy - 1, tab_width, (self.tab_h + 1))): - if right_click: + if inp.right_click: if gui.radio_view: radio_tab_menu.activate(i, inp.mouse_position) else: @@ -19354,7 +19360,7 @@ def draw(self, x, y, w, h): if inp.mouse_up: drop_tracks_to_new_playlist(gui.shift_selection) - if right_click: + if inp.right_click: extra_tab_menu.activate(pctl.active_playlist_viewing) # Move tab to end playlist if dragged past end @@ -20048,7 +20054,7 @@ def draw_card(self, artist, x, y, w): self.click_highlight_timer.set() create_artist_pl(artist) - if right_click: + if inp.right_click: self.click_ref = artist self.click_highlight_timer.set() @@ -20132,7 +20138,7 @@ def render(self, x, y, w, h): 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, + len(self.current_artists) - range, r_click=inp.right_click, jump_distance=35, extend_field=6 * gui.scale) if not self.current_artists: @@ -20394,7 +20400,7 @@ def render(self, x, y, w, h): 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) + max_scroll, r_click=inp.right_click, jump_distance=40) self.scroll_positions[pl_id] = scroll_position @@ -20461,7 +20467,7 @@ def render(self, x, y, w, h): if light_mode: text_colour = [0, 0, 0, 255] - if right_click: + if inp.right_click: mouse_in = tauon.coll(rect) and is_level_zero(False) else: mouse_in = tauon.coll(rect) and focused and not ( @@ -20470,7 +20476,7 @@ def render(self, x, y, w, h): if mouse_in and not tree_view_scroll.held: if inp.middle_click: stem_to_new_playlist(full_folder_path) - elif right_click: + elif inp.right_click: if item[3]: for p, id in enumerate(pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids): if msys: @@ -21036,7 +21042,7 @@ def draw(self, x: int, y: int, w: int, h: int): self.scroll_position += inp.mouse_wheel * -1 self.scroll_position = max(self.scroll_position, 0) - if right_click: + if inp.right_click: qb_right_click = 1 # text_colour = [255, 255, 255, 91] @@ -21371,16 +21377,16 @@ def l_panel(self, x, y, w, h, track, top_border: bool = True): 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 (inp.mouse_click or inp.right_click) and is_level_zero(False): if tauon.coll(border_rect): if inp.mouse_click: album_art_gen.cycle_offset(target_track) - if right_click: + if inp.right_click: picture_menu.activate(in_reference=target_track) elif tauon.coll(rect): if inp.mouse_click: pctl.show_current() - if right_click: + if inp.right_click: showcase_menu.activate(track) ddt.rect(border_rect, border_colour) @@ -21417,7 +21423,7 @@ def lyrics(self, x, y, w, h, track: TrackClass): # Test for show lyric menu on right ckick if tauon.coll((x + 10, y, w - 10, h)): - if right_click: # and 3 > pctl.playing_state > 0: + if inp.right_click: # and 3 > pctl.playing_state > 0: gui.force_showcase_index = -1 showcase_menu.activate(track) @@ -21476,7 +21482,7 @@ def draw(self, x, y, w, h, track=None): # Test for show lyric menu on right ckick if tauon.coll((x + 10, y, w - 10, h)): - if right_click: # and 3 > pctl.playing_state > 0: + if inp.right_click: # and 3 > pctl.playing_state > 0: gui.force_showcase_index = -1 showcase_menu.activate(track) @@ -21789,7 +21795,7 @@ def draw(self, x, y, w, h): wait = False # Activate menu - if right_click and tauon.coll((x, y, w, h)): + if inp.right_click and tauon.coll((x, y, w, h)): artist_info_menu.activate(in_reference=artist) background = colours.artist_bio_background @@ -26953,8 +26959,8 @@ def toggle_shuffle_layout(albums: bool = False): def toggle_shuffle_layout_albums(): toggle_shuffle_layout(albums=True) -def exit_shuffle_layout(_): - return prefs.shuffle_lock +def exit_shuffle_layout(_: int, tauon: Tauon) -> bool: + return tauon.prefs.shuffle_lock def bio_set_large(): # if window_size[0] >= round(1000 * gui.scale): @@ -28024,32 +28030,31 @@ def ser_gimage(track_object: TrackClass): track_object.artist + " " + track_object.album) webbrowser.open(line, new=2, autoraise=True) -def append_here(): - global cargo - pctl.default_playlist += cargo +def append_here(pctl: PlayerCtl) -> None: + pctl.default_playlist += pctl.cargo -def paste_deco(): +def paste_deco(tauon: Tauon) -> list[list[int] | str | None]: active = False line = None - if len(cargo) > 0: + if len(tauon.pctl.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/"): + elif tauon.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") if active: - line_colour = colours.menu_text + line_colour = tauon.bag.colours.menu_text else: - line_colour = colours.menu_text_disabled + line_colour = tauon.bag.colours.menu_text_disabled - return [line_colour, colours.menu_background, line] + return [line_colour, tauon.bag.colours.menu_background, line] -def lightning_move_test(discard): - return gui.lightning_copy and prefs.show_transfer +def lightning_move_test(_: int, tauon: Tauon) -> bool: + return tauon.gui.lightning_copy and tauon.prefs.show_transfer # def copy_deco(): # line = "Copy" @@ -28643,8 +28648,7 @@ def s_append(index: int): paste(playlist_no=index) def append_playlist(index: int): - global cargo - pctl.multi_playlist[index].playlist_ids += cargo + pctl.multi_playlist[index].playlist_ids += pctl.cargo gui.pl_update = 1 reload() @@ -29886,11 +29890,11 @@ def make_auto_sorting(pl: int) -> None: _("OK. This playlist will automatically sort on import from now on"), _("You remove or edit this behavior by going \"Misc...\" > \"Edit generator...\""), mode="done") -def spotify_show_test(_): - return prefs.spot_mode +def spotify_show_test(_: int, tauon: Tauon) -> bool: + return tauon.prefs.spot_mode -def jellyfin_show_test(_): - return prefs.jelly_password and prefs.jelly_username +def jellyfin_show_test(_: int, tauon: Tauon) -> bool: + return tauon.prefs.jelly_password and tauon.prefs.jelly_username def upload_jellyfin_playlist(pl: TauonPlaylist) -> None: if jellyfin.scanning: @@ -31024,13 +31028,12 @@ def convert_folder(index: int): transcode_list.append(folder) tauon.thread_manager.ready("worker") -def transfer(index: int, args) -> None: - global cargo - old_cargo = copy.deepcopy(cargo) +def transfer(index: int, args: list[int]) -> None: + old_cargo = copy.deepcopy(pctl.cargo) if args[0] == 1 or args[0] == 0: # copy if args[1] == 1: # single track - cargo.append(index) + pctl.cargo.append(index) if args[0] == 0: # cut del pctl.default_playlist[pctl.selected_in_playlist] @@ -31038,7 +31041,7 @@ def transfer(index: int, args) -> None: for b in range(len(pctl.default_playlist)): if pctl.master_library[pctl.default_playlist[b]].parent_folder_name == pctl.master_library[ index].parent_folder_name: - cargo.append(pctl.default_playlist[b]) + pctl.cargo.append(pctl.default_playlist[b]) if args[0] == 0: # cut for b in reversed(range(len(pctl.default_playlist))): if pctl.master_library[pctl.default_playlist[b]].parent_folder_name == pctl.master_library[ @@ -31046,7 +31049,7 @@ def transfer(index: int, args) -> None: del pctl.default_playlist[b] elif args[1] == 3: # playlist - cargo += pctl.default_playlist + pctl.cargo += pctl.default_playlist if args[0] == 0: # cut pctl.default_playlist = [] @@ -31062,8 +31065,8 @@ def transfer(index: int, args) -> None: else: insert += 1 - while len(cargo) > 0: - pctl.default_playlist.insert(insert, cargo.pop()) + while len(pctl.cargo) > 0: + pctl.default_playlist.insert(insert, pctl.cargo.pop()) elif args[1] == 2: # After insert = pctl.selected_in_playlist @@ -31072,18 +31075,17 @@ def transfer(index: int, args) -> None: pctl.master_library[index].parent_folder_name: insert += 1 - while len(cargo) > 0: - pctl.default_playlist.insert(insert, cargo.pop()) + while len(pctl.cargo) > 0: + pctl.default_playlist.insert(insert, pctl.cargo.pop()) elif args[1] == 3: # End - pctl.default_playlist += cargo - # cargo = [] + pctl.default_playlist += pctl.cargo + # pctl.cargo = [] - cargo = old_cargo + pctl.cargo = old_cargo reload() -def temp_copy_folder(ref): - global cargo - cargo = [] +def temp_copy_folder(ref: int, pctl: PlayerCtl) -> None: + pctl.cargo = [] transfer(ref, args=[1, 2]) def activate_track_box(index: int): @@ -31106,19 +31108,18 @@ def s_copy(): if "file://" in clip: copy_to_clipboard("") - global cargo - cargo = [] + pctl.cargo = [] if pctl.default_playlist: for item in gui.shift_selection: - cargo.append(pctl.default_playlist[item]) + pctl.cargo.append(pctl.default_playlist[item]) - if not cargo and -1 < pctl.selected_in_playlist < len(pctl.default_playlist): - cargo.append(pctl.default_playlist[pctl.selected_in_playlist]) + if not pctl.cargo and -1 < pctl.selected_in_playlist < len(pctl.default_playlist): + pctl.cargo.append(pctl.default_playlist[pctl.selected_in_playlist]) tauon.copied_track = None - if len(cargo) == 1: - tauon.copied_track = cargo[0] + if len(pctl.cargo) == 1: + tauon.copied_track = pctl.cargo[0] def directory_size(path: str) -> int: total = 0 @@ -31133,10 +31134,10 @@ def lightning_paste(): # if not inp.key_shift_down: # move = False - move_track = pctl.get_track(cargo[0]) + move_track = pctl.get_track(pctl.cargo[0]) move_path = move_track.parent_folder_path - for item in cargo: + for item in pctl.cargo: if move_path != pctl.get_track(item).parent_folder_path: show_message( _("More than one folder is in the clipboard"), @@ -31264,7 +31265,7 @@ def lightning_paste(): prep_gal() reload_albums(True) - cargo.clear() + pctl.cargo.clear() gui.lightning_copy = False def paste(playlist_no=None, track_id=None): @@ -31303,7 +31304,7 @@ def paste(playlist_no=None, track_id=None): clip = False elif "spotify" in clip: - cargo.clear() + pctl.cargo.clear() for link in clip.split("\n"): logging.info(link) link = link.strip() @@ -31312,7 +31313,7 @@ def paste(playlist_no=None, track_id=None): 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) + pctl.cargo.extend(l) elif clip.startswith("https://open.spotify.com/playlist/"): tauon.spot_ctl.playlist(link) if prefs.album_mode: @@ -32033,11 +32034,11 @@ def reload_metadata(input, keep_star: bool = True) -> None: tauon.thread_manager.ready("worker") def reload_metadata_selection(tauon: Tauon) -> None: - cargo = [] + pctl.cargo = [] for item in gui.shift_selection: - cargo.append(pctl.default_playlist[item]) + pctl.cargo.append(pctl.default_playlist[item]) - for k in cargo: + for k in pctl.cargo: if tauon.pctl.master_library[k].is_cue == False: tauon.to_scan.append(k) tauon.thread_manager.ready("worker") @@ -32320,10 +32321,10 @@ def intel_moji(index: int): show_message(_("Autodetect failed")) def sel_to_car(): - cargo = [] + pctl.cargo = [] for item in gui.shift_selection: - cargo.append(pctl.default_playlist[item]) + pctl.cargo.append(pctl.default_playlist[item]) def cut_selection(): sel_to_car() @@ -33002,7 +33003,6 @@ def toggle_galler_text(mode: int = 0) -> bool: # Jump to playing album if prefs.album_mode and gui.first_in_grid is not None: - if gui.first_in_grid < len(pctl.default_playlist): goto_album(gui.first_in_grid, force=True) @@ -33401,20 +33401,21 @@ def new_playlist_deco(): text = _("New Playlist") return [colours.menu_text, colours.menu_background, text] -def clean_db_show_test(_): - return gui.suggest_clean_db +def clean_db_show_test(_: int, tauon: Tauon) -> bool: + return tauon.gui.suggest_clean_db -def clean_db_fast(): +def clean_db_fast(tauon: Tauon) -> None: + pctl = tauon.pctl 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 + tauon.gui.show_message(_("Done! {N} old items were removed.").format(N=len(keys)), mode="done") + tauon.gui.suggest_clean_db = False -def clean_db_deco(): - return [colours.menu_text, [30, 150, 120, 255], _("Clean Database!")] +def clean_db_deco(tauon: Tauon) -> list[list[int] | str]: + return [tauon.bag.colours.menu_text, [30, 150, 120, 255], _("Clean Database!")] def import_spotify_playlist() -> None: clip = copy_from_clipboard() @@ -33433,8 +33434,8 @@ def import_spotify_playlist_deco(): 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 +def show_import_music(_: int, tauon: Tauon) -> bool: + return tauon.gui.add_music_folder_ready def import_music(): pl = pl_gen(_("Music")) @@ -33933,16 +33934,16 @@ def spot_import_playlists() -> None: else: show_message(_("Please wait until current job is finished")) -def spot_import_playlist_menu() -> None: +def spot_import_playlist_menu(tauon: Tauon) -> None: if not tauon.spot_ctl.spotify_com: playlists = tauon.spot_ctl.get_playlist_list() - spotify_playlist_menu.items.clear() + tauon.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])) + tauon.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=(tauon.extra_menu.pos[0], window_size[1] - gui.panelBY)) + tauon.spotify_playlist_menu.add(MenuItem(_("> Import All Playlists"), spot_import_playlists)) + tauon.spotify_playlist_menu.activate(position=(tauon.extra_menu.pos[0], window_size[1] - gui.panelBY)) else: show_message(_("Please wait until current job is finished")) @@ -34137,7 +34138,8 @@ def lastfm_colour() -> list[int] | None: return [250, 50, 50, 255] return None -def lastfm_menu_test(a) -> bool: +def lastfm_menu_test(_: int, tauon: Tauon) -> bool: + prefs = tauon.prefs if (prefs.auto_lfm and prefs.last_fm_token is not None) or prefs.enable_lb or prefs.maloja_enable: return True return False @@ -34404,8 +34406,8 @@ def open_donate_link() -> None: 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 show_stop_quick_add(_: int, tauon: Tauon) -> bool: + return tauon. pctl.quick_add_target is not None def view_tracks(tauon: Tauon) -> None: # if gui.show_playlist is False: @@ -39532,7 +39534,6 @@ def main(holder: Holder): Archive_Formats = Archive_Formats ) - cargo = [] # --------------------------------------------------------------------- # Player variables @@ -40892,7 +40893,7 @@ def update(self, force=False): tool_tip_instant = ToolTip3(bag, gui) # Create empty area menu - playlist_menu = Menu(tauon, 130) + playlist_menu = tauon.playlist_menu radio_entry_menu = Menu(tauon, 125) showcase_menu = Menu(tauon, 135) center_info_menu = Menu(tauon, 125) @@ -40908,7 +40909,7 @@ def update(self, force=False): 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) + spotify_playlist_menu = tauon.spotify_playlist_menu radio_context_menu = Menu(tauon, 175) #chrome_menu = Menu(tauon, 175) @@ -42040,8 +42041,8 @@ def dev_mode_disable_save_state() -> None: keymaps.hits.clear() d_mouse_click = False - right_click = False - level_2_right_click = False + inp.right_click = False + inp.level_2_right_click = False inp.mouse_click = False inp.middle_click = False inp.mouse_up = False @@ -42282,7 +42283,7 @@ def dev_mode_disable_save_state() -> None: continue if event.button.button == SDL_BUTTON_RIGHT: - right_click = True + inp.right_click = True inp.right_down = True #logging.info("RIGHT DOWN") elif event.button.button == SDL_BUTTON_LEFT: @@ -42670,7 +42671,7 @@ def dev_mode_disable_save_state() -> None: if inp.k_input: # TODO(Martin): Check if commenting this out is the correct thing to do - #if inp.mouse_click or right_click or inp.mouse_up: + #if inp.mouse_click or inp.right_click or inp.mouse_up: # last_click_location = copy.deepcopy(inp.click_location) # click_location = copy.deepcopy(inp.inp.mouse_position) @@ -42678,8 +42679,8 @@ def dev_mode_disable_save_state() -> None: keymaps.hits.clear() # d_mouse_click = False - # right_click = False - # level_2_right_click = False + # inp.right_click = False + # inp.level_2_right_click = False # inp.mouse_click = False # inp.middle_click = False inp.mouse_up = False @@ -42914,7 +42915,7 @@ def dev_mode_disable_save_state() -> None: 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( + if len(pctl.cargo) == 1 and tauon.copied_track is not None and -1 < pctl.selected_in_playlist < len( pctl.default_playlist): fr = pctl.get_track(tauon.copied_track) to = pctl.get_track(pctl.default_playlist[pctl.selected_in_playlist]) @@ -43063,8 +43064,8 @@ def dev_mode_disable_save_state() -> None: inp.mouse_click = False gui.level_2_click = True - if right_click: - level_2_right_click = True + if inp.right_click: + inp.level_2_right_click = True if pref_box.enabled: @@ -43072,8 +43073,8 @@ def dev_mode_disable_save_state() -> None: if inp.mouse_click: # and not gui.message_box: pref_box.click = True inp.mouse_click = False - if right_click: - right_click = False + if inp.right_click: + inp.right_click = False pref_box.right_click = True pref_box.scroll = inp.mouse_wheel @@ -43081,14 +43082,14 @@ def dev_mode_disable_save_state() -> None: else: if inp.mouse_click: pref_box.close() - if right_click: + if inp.right_click: pref_box.close() if pref_box.lock is False: pass - if right_click and ( + if inp.right_click and ( radiobox.active or rename_track_box.active or gui.rename_playlist_box or gui.rename_folder_box or tauon.search_over.active): - right_click = False + inp.right_click = False if inp.mouse_wheel != 0: gui.update += 1 @@ -43497,7 +43498,7 @@ def dev_mode_disable_save_state() -> None: gui.layer_focus = 0 - if inp.mouse_click or inp.mouse_wheel or right_click: + if inp.mouse_click or inp.mouse_wheel or inp.right_click: inp.mouse_position[0], inp.mouse_position[1] = get_sdl_input.mouse() if inp.mouse_click: @@ -43777,7 +43778,7 @@ def dev_mode_disable_save_state() -> None: gui.album_scroll_px + album_v_slide_value, max_scroll + album_v_slide_value, jump_distance=1400 * gui.scale, - r_click=right_click, + r_click=inp.right_click, extend_field=15 * gui.scale) - album_v_slide_value if last_row != row_len: @@ -43793,7 +43794,7 @@ def dev_mode_disable_save_state() -> None: extend = 40 * gui.scale # Process inputs first - if (inp.mouse_click or right_click or inp.middle_click or inp.mouse_down or inp.mouse_up) and pctl.default_playlist: + if (inp.mouse_click or inp.right_click or inp.middle_click or inp.mouse_down or inp.mouse_up) and pctl.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: @@ -43923,7 +43924,7 @@ def dev_mode_disable_save_state() -> None: else: # Add to queue grouped add_album_to_queue(pctl.default_playlist[album_dex[album_on]]) - elif right_click: + elif inp.right_click: if pctl.quick_add_target: pl = id_to_pl(pctl.quick_add_target) if pl is not None: @@ -44386,9 +44387,7 @@ def dev_mode_disable_save_state() -> None: block_gap = 1 if tauon.coll(hot_r) or gui.pt > 0: - for i, item in enumerate(gui.power_bar): - if run_y + block_h > top + h: break @@ -44396,9 +44395,8 @@ def dev_mode_disable_save_state() -> None: i_rect = [window_size[0] - 36 * gui.scale, run_y, 34 * gui.scale, block_h] tauon.fields.add(i_rect) - if (tauon.coll(i_rect) or ( - lightning_menu.active and lightning_menu.reference == item)) and item.peak_x == 9 * gui.scale: - + if (tauon.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: minx = 100 * gui.scale @@ -44418,7 +44416,7 @@ def dev_mode_disable_save_state() -> None: if inp.mouse_click: goto_album(item.position) - if right_click: + if inp.right_click: lightning_menu.activate(item, position=( window_size[0] - 180 * gui.scale, rect[1] + rect[3] + 5 * gui.scale)) if inp.middle_click: @@ -44556,9 +44554,9 @@ def dev_mode_disable_save_state() -> None: if gui.lsp: left = gui.lspw rect = [left, top, gui.plw, 12 * gui.scale] - if right_click and tauon.coll(rect): + if inp.right_click and tauon.coll(rect): set_menu_hidden.activate() - right_click = False + inp.right_click = False width = gui.plw if gui.set_bar and gui.set_mode: @@ -44639,7 +44637,7 @@ def dev_mode_disable_save_state() -> None: if inp.mouse_click: gui.set_label_hold = h gui.set_label_point = copy.deepcopy(inp.mouse_position) - if right_click: + if inp.right_click: set_menu.activate(h) if h != 0: @@ -44835,11 +44833,11 @@ def dev_mode_disable_save_state() -> None: gui.column_sort_up_icon.render(x, round(y), [255, 255, 255, 90]) # Switch Vis: - if right_click and tauon.coll( + if inp.right_click and tauon.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 tauon.top_panel.tabs_right_x < inp.mouse_position[0] and \ + elif inp.right_click and tauon.top_panel.tabs_right_x < inp.mouse_position[0] and \ inp.mouse_position[1] < gui.panelY and \ inp.mouse_position[0] > tauon.top_panel.tabs_right_x and \ inp.mouse_position[0] < window_size[0] - 130 * gui.scale - gui.offset_extra: @@ -44917,7 +44915,7 @@ def dev_mode_disable_save_state() -> None: gui.panelY + 25 * gui.scale, side_panel=True, w=gui.rspw, h=window_size[1] - gui.panelY - gui.panelBY) - if right_click and tauon.coll( + if inp.right_click and tauon.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) @@ -44978,7 +44976,7 @@ def dev_mode_disable_save_state() -> None: # 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 tauon.coll((x, y, w, h)) and target_track: + if inp.right_click and tauon.coll((x, y, w, h)) and target_track: center_info_menu.activate(target_track) else: @@ -45015,13 +45013,13 @@ def dev_mode_disable_save_state() -> None: if gui.art_drawn_rect: coll_border = gui.art_drawn_rect - if right_click and tauon.coll((x, y, w, h)) and not tauon.coll(coll_border): + if inp.right_click and tauon.coll((x, y, w, h)) and not tauon.coll(coll_border): if is_level_zero(include_menus=False) and target_track: center_info_menu.activate(target_track) else: text_y = y + round(h * 0.40) - if right_click and tauon.coll((x, y, w, h)) and target_track: + if inp.right_click and tauon.coll((x, y, w, h)) and target_track: center_info_menu.activate(target_track) ww = w - 25 * gui.scale @@ -45192,12 +45190,11 @@ def dev_mode_disable_save_state() -> None: tauon.fields.add((x + 2 * gui.scale, sbp, 20 * gui.scale, sbl)) if tauon.coll((x, top, 28 * gui.scale, ey - top)) and ( - inp.mouse_down or right_click) \ + inp.mouse_down or inp.right_click) \ and coll_point(inp.click_location, (x, top, 28 * gui.scale, ey - top)): gui.pl_update = 1 - if right_click: - + if inp.right_click: sbp = inp.mouse_position[1] - int(sbl / 2) if sbp + sbl > ey: sbp = ey - sbl @@ -45364,11 +45361,11 @@ def dev_mode_disable_save_state() -> None: artist_preview_render.size[0] + border * 2), (20, 20, 20, 255)) artist_preview_render.draw(gui.preview_artist_location[0], gui.preview_artist_location[1]) - if inp.mouse_click or right_click or inp.mouse_wheel: + if inp.mouse_click or inp.right_click or inp.mouse_wheel: gui.preview_artist = "" if track_box: - if inp.key_return_press or right_click or key_esc_press or inp.backspace_press or keymaps.test( + if inp.key_return_press or inp.right_click or key_esc_press or inp.backspace_press or keymaps.test( "quick-find"): track_box = False @@ -45841,7 +45838,7 @@ def dev_mode_disable_save_state() -> None: 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 tauon.coll((x, y, w, h))): + (inp.mouse_click or inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): gui.rename_folder_box = False p = ddt.text( From 5c1109427179e88c0408bb74446f5b9e5c332cc7 Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 17:45:58 +0100 Subject: [PATCH 06/13] Moar ref --- src/tauon/t_modules/t_main.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index c62f04fba..f2ee934a3 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -4322,6 +4322,7 @@ def __init__(self, tauon: Tauon, width: int, show_icons: bool = False) -> None: self.gui = tauon.gui self.inp = tauon.gui.inp self.ddt = tauon.bag.ddt + self.colours = tauon.bag.colours self.window_size = tauon.bag.window_size self.base_v_size = 22 @@ -4359,7 +4360,7 @@ def deco(_=_): def click(self) -> None: self.clicked = True # cheap hack to prevent scroll bar from being activated when closing menu - inp.click_location = [0, 0] + self.inp.click_location = [0, 0] def add(self, menu_item: MenuItem) -> None: if menu_item.render_func is None: @@ -4392,12 +4393,10 @@ def is_item_disabled(self, item): 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 @@ -4405,10 +4404,8 @@ def render_icon(self, x, y, icon, selected, fx): 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 @@ -4425,7 +4422,6 @@ def render_icon(self, x, y, icon, selected, fx): # 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 @@ -4445,8 +4441,13 @@ def render_icon(self, x, y, icon, selected, fx): else: icon.base_asset.render(x, y) - def render(self): - gui = self.gui + def render(self) -> None: + tauon = self.tauon + gui = self.gui + ddt = self.ddt + inp = self.inp + colours = self.colours + if self.active: if Menu.switch != self.id: self.active = False @@ -4506,9 +4507,9 @@ def render(self): # 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) + fx = self.items[i].render_func(self.reference, self.tauon) else: - fx = self.items[i].render_func() + fx = self.items[i].render_func(tauon=self.tauon) else: fx = self.deco() @@ -4598,9 +4599,9 @@ def render(self): y_run += self.h - if y_run > window_size[1] - self.h: + if y_run > self.window_size[1] - self.h: direc = 1 - if self.pos[0] > window_size[0] // 2: + if self.pos[0] > self.window_size[0] // 2: direc = -1 x_run += self.w * direc y_run = self.pos[1] @@ -4612,14 +4613,14 @@ def render(self): 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]: + if sub_pos[0] + sub_w > self.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 + minY = self.window_size[1] - self.h * len(self.subs[self.sub_active]) - 15 * gui.scale sub_pos[1] = min(sub_pos[1], minY) xoff = 0 From 50ae3582338ffa65f45bd881f19ca1497226c3c2 Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 17:59:18 +0100 Subject: [PATCH 07/13] Move get_album_info and deglobalize further --- src/tauon/t_modules/t_main.py | 143 +++++++++++++++++----------------- 1 file changed, 71 insertions(+), 72 deletions(-) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index f2ee934a3..563fdd688 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -639,6 +639,9 @@ def __init__(self, bag: Bag, tracklist_texture_rect: SDL_Rect, tracklist_texture self.present = False self.drag_source_position = (0, 0) self.drag_source_position_persist = (0, 0) + self.old_album_pos: int = -55 + self.album_playlist_width: int = 430 + self.album_tab_mode = False self.main_art_box = (0, 0, 10, 10) self.gall_tab_enter = False @@ -2088,7 +2091,7 @@ def show_current( 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] + ap = self.tauon.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: @@ -5112,6 +5115,8 @@ def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): self.switch_playlist = None self.open_uri = open_uri self.love = love + self.album_info_cache = {} + self.album_info_cache_key = (-1, -1) self.album_mode: bool = False self.snap_mode: bool = bag.snap_mode self.console = bag.console @@ -5212,6 +5217,49 @@ def start_remote(self) -> None: self.web_thread.start() self.web_running = True + def get_album_info(self, position: int, pl: int | None = None): + pctl = self.pctl + playlist = pctl.default_playlist + prefs = self.prefs + if pl is not None: + playlist = pctl.multi_playlist[pl].playlist_ids + + if self.album_info_cache_key != (pctl.selected_in_playlist, pctl.playing_object()): # Premature optimisation? + self.album_info_cache.clear() + self.album_info_cache_key = (pctl.selected_in_playlist, pctl.playing_object()) + + if position in self.album_info_cache: + return self.album_info_cache[position] + + if album_dex and prefs.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 + + self.album_info_cache[position] = playing, album, select + return playing, album, select + 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" @@ -31541,18 +31589,22 @@ def spot_heart_menu_colour(): return [30, 215, 96, 255] return None -def add_to_queue(ref): +def add_to_queue(ref: int) -> None: 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(): +def add_selected_to_queue(tauon: Tauon) -> None: + gui = tauon.gui + prefs = tauon.prefs + pctl = tauon.pctl + gui.pl_update += 1 if prefs.stop_end_queue: pctl.auto_stop = False if gui.album_tab_mode: - add_album_to_queue(pctl.default_playlist[get_album_info(pctl.selected_in_playlist)[1][0]], pctl.selected_in_playlist) + add_album_to_queue(pctl.default_playlist[tauon.get_album_info(pctl.selected_in_playlist)[1][0]], pctl.selected_in_playlist) queue_timer_set() else: pctl.force_queue.append( @@ -33177,16 +33229,14 @@ def goto_album(playlist_no: int, down: bool = False, force: bool = False) -> lis def toggle_album_mode(tauon: Tauon, force_on: bool = False) -> None: global update_layout - global album_playlist_width - global old_album_pos gui = tauon.gui pctl = tauon.pctl gui.gall_tab_enter = False if prefs.album_mode is True: prefs.album_mode = False - # album_playlist_width = gui.playlist_width - # old_album_pos = gui.album_scroll_px + # gui.album_playlist_width = gui.playlist_width + # gui.old_album_pos = gui.album_scroll_px gui.rspw = gui.pref_rspw gui.rsp = prefs.prefer_side gui.album_tab_mode = False @@ -34473,7 +34523,7 @@ def standard_view_deco(): # gui.show_playlist = False # update_layout = True # gui.rspw = window_size[0] -# album_playlist_width = gui.playlist_width +# gui.album_playlist_width = gui.playlist_width # #gui.playlist_width = -19 def toggle_library_mode() -> None: @@ -36197,49 +36247,6 @@ def cache_paths() -> tuple[dict, dict]: #logging.info("DONE LOADING") break -def get_album_info(position, pl: int | None = None): - playlist = pctl.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 prefs.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 - def get_folder_list(index: int): playlist = [] @@ -36281,18 +36288,18 @@ def gal_jump_select(up=False, num=1): pctl.selected_in_playlist = old_selected return - alb = get_album_info(pctl.selected_in_playlist) + alb = tauon.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) + alb = tauon.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) + pctl.selected_in_playlist = max(tauon.get_album_info(on)[1][0], 0) num -= 1 def gen_power2(): @@ -36369,7 +36376,6 @@ def key(tag): def reload_albums(tauon: Tauon, quiet: bool = False, return_playlist: int = -1, custom_list=None) -> list[int] | None: global album_dex global update_layout - global old_album_pos if tauon.cm_clean_db: # Doing reload while things are being removed may cause crash @@ -36422,7 +36428,7 @@ def reload_albums(tauon: Tauon, quiet: bool = False, return_playlist: int = -1, return dex album_dex = dex - album_info_cache.clear() + tauon.album_info_cache.clear() gui.update += 2 gui.pl_update = 1 update_layout = True @@ -38562,7 +38568,7 @@ def save_state() -> None: prefs.show_lyrics_side, None, #prefs.last_device, album_mode, - None, # album_playlist_width + None, # gui.album_playlist_width prefs.transcode_opus_as, gui.star_mode, prefs.prefer_side, # gui.rsp, @@ -39429,8 +39435,6 @@ def main(holder: Holder): # gui.offset_extra = 0 - old_album_pos = -55 - album_dex = [] album_artist_dict = {} row_len = 5 @@ -39466,8 +39470,6 @@ def main(holder: Holder): source = None - album_playlist_width = 430 - update_title = False selected_in_playlist = -1 @@ -39913,7 +39915,7 @@ def main(holder: Holder): if save[66] is not None: gui.restart_album_mode = save[66] if save[67] is not None: - album_playlist_width = save[67] + gui.album_playlist_width = save[67] if save[68] is not None: prefs.transcode_opus_as = save[68] if save[69] is not None: @@ -41720,10 +41722,7 @@ def dev_mode_disable_save_state() -> None: 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) @@ -43828,7 +43827,7 @@ def dev_mode_disable_save_state() -> None: # Quick drag and drop if inp.mouse_up and (gui.playlist_hold and m_in) and not gui.side_drag and gui.shift_selection: - info = get_album_info(album_dex[album_on]) + info = tauon.get_album_info(album_dex[album_on]) if info[1]: track_position = info[1][0] @@ -43863,7 +43862,7 @@ def dev_mode_disable_save_state() -> None: elif not gui.side_drag and is_level_zero(): if coll_point(inp.click_location, rect) and gui.panelY < inp.mouse_position[1] < \ window_size[1] - gui.panelBY: - info = get_album_info(album_dex[album_on]) + info = tauon.get_album_info(album_dex[album_on]) if m_in and inp.mouse_up and prefs.gallery_single_click: if is_level_zero() and gui.d_click_ref == album_dex[album_on]: @@ -43878,7 +43877,7 @@ def dev_mode_disable_save_state() -> None: pctl.jump(pctl.default_playlist[album_dex[album_on]], album_dex[album_on]) pctl.show_current() elif inp.mouse_down and not m_in: - info = get_album_info(album_dex[album_on]) + info = tauon.get_album_info(album_dex[album_on]) inp.quick_drag = True if not pl_is_locked(pctl.active_playlist_viewing) or inp.key_shift_down: gui.playlist_hold = True @@ -43887,7 +43886,7 @@ def dev_mode_disable_save_state() -> None: inp.click_location = [0, 0] if m_in: - info = get_album_info(album_dex[album_on]) + info = tauon.get_album_info(album_dex[album_on]) if inp.mouse_click: if prefs.gallery_single_click: gui.d_click_ref = album_dex[album_on] @@ -43914,7 +43913,7 @@ def dev_mode_disable_save_state() -> None: # Middle click to add album to queue if inp.key_ctrl_down: # Add to queue ungrouped - album = get_album_info(album_dex[album_on])[1] + album = tauon.get_album_info(album_dex[album_on])[1] for item in album: pctl.force_queue.append( queue_item_gen(pctl.default_playlist[item], item, pl_to_id( @@ -44003,7 +44002,7 @@ def dev_mode_disable_save_state() -> None: track = pctl.master_library[pctl.default_playlist[album_dex[album_on]]] - info = get_album_info(album_dex[album_on]) + info = tauon.get_album_info(album_dex[album_on]) album = info[1] # info = (0, 0, 0) From 901e532d9a213bf06278060b3c725c9dc76979f9 Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 18:04:50 +0100 Subject: [PATCH 08/13] Migrate more input to Input --- src/tauon/t_modules/t_main.py | 151 +++++++++++++++++----------------- 1 file changed, 77 insertions(+), 74 deletions(-) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index 563fdd688..b069ca87e 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -1103,6 +1103,12 @@ def __init__(self, gui: GuiVar) -> None: self.k_input: bool = True self.key_return_press: bool = False self.key_tab_press: bool = False + self.key_down_press: bool = False + self.key_up_press: bool = False + self.key_right_press: bool = False + self.key_left_press: bool = False + self.key_esc_press: bool = False + self.key_shift_down: bool = False self.key_shiftr_down: bool = False self.key_ctrl_down: bool = False @@ -4704,7 +4710,7 @@ def render(self) -> None: else: self.items[to_call].func() - if self.clicked or key_esc_press or self.close_next_frame: + if self.clicked or inp.key_esc_press or self.close_next_frame: self.close_next_frame = False self.active = False self.clicked = False @@ -6559,7 +6565,7 @@ def d(): d() # Ctrl + left to move cursor back a word - elif (inp.key_ctrl_down or inp.key_rctrl_down) and key_left_press: + elif (inp.key_ctrl_down or inp.key_rctrl_down) and inp.key_left_press: while g() == " ": self.cursor_position += 1 if not inp.key_shift_down: @@ -6575,7 +6581,7 @@ def d(): break # Ctrl + right to move cursor forward a word - elif (inp.key_ctrl_down or inp.key_rctrl_down) and key_right_press: + elif (inp.key_ctrl_down or inp.key_rctrl_down) and inp.key_up_press: while g2() == " ": self.cursor_position -= 1 if not inp.key_shift_down: @@ -6603,13 +6609,13 @@ def d(): self.eliminate_selection() # Left and right arrow keys to move cursor - if key_right_press: + if inp.key_up_press: if self.cursor_position > 0: self.cursor_position -= 1 if not inp.key_shift_down and not inp.key_shiftr_down: self.selection = self.cursor_position - if key_left_press: + if inp.key_left_press: if self.cursor_position < len(self.text): self.cursor_position += 1 if not inp.key_shift_down and not inp.key_shiftr_down: @@ -6978,7 +6984,7 @@ def d(): d() # Ctrl + left to move cursor back a word - elif (inp.key_ctrl_down or inp.key_rctrl_down) and key_left_press: + elif (inp.key_ctrl_down or inp.key_rctrl_down) and inp.key_left_press: while g() == " ": self.cursor_position += 1 if not inp.key_shift_down: @@ -6994,7 +7000,7 @@ def d(): break # Ctrl + right to move cursor forward a word - elif (inp.key_ctrl_down or inp.key_rctrl_down) and key_right_press: + elif (inp.key_ctrl_down or inp.key_rctrl_down) and inp.key_up_press: while g2() == " ": self.cursor_position -= 1 if not inp.key_shift_down: @@ -7022,13 +7028,13 @@ def d(): self.eliminate_selection() # Left and right arrow keys to move cursor - if key_right_press: + if inp.key_up_press: if self.cursor_position > 0: self.cursor_position -= 1 if not inp.key_shift_down and not inp.key_shiftr_down: self.selection = self.cursor_position - if key_left_press: + if inp.key_left_press: if self.cursor_position < len(self.text): self.cursor_position += 1 if not inp.key_shift_down and not inp.key_shiftr_down: @@ -8659,7 +8665,7 @@ def render(self): 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 inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): + if inp.key_esc_press or ((inp.mouse_click or inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): rename_track_box.active = False r_todo = [] @@ -8841,7 +8847,7 @@ def render(self): 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 inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): + if inp.key_esc_press or ((inp.mouse_click or inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): self.active = False select = list(set(gui.shift_selection)) @@ -9060,7 +9066,7 @@ def render(self): 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 inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): + if inp.key_esc_press or ((inp.mouse_click or inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): self.active = False select = list(set(gui.shift_selection)) @@ -9293,7 +9299,7 @@ def render(self): 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 inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): + if inp.key_esc_press or ((inp.mouse_click or inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): self.active = False gui.box_over = False @@ -9395,7 +9401,7 @@ def render(self) -> None: 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 inp.right_click or inp.level_2_right_click) and not tauon.coll( + if inp.key_esc_press or ((inp.mouse_click or gui.level_2_click or inp.right_click or inp.level_2_right_click) and not tauon.coll( (x, y, w, h))): self.active = False gui.box_over = False @@ -10011,7 +10017,7 @@ def render(self): self.searched_text = "" return - if key_esc_press: + if inp.key_esc_press: if self.delay_enter: self.delay_enter = False else: @@ -10136,16 +10142,14 @@ def render(self): yy = 110 * gui.scale - if key_down_press: - + if inp.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(inp.mouse_position) - if key_up_press: - + if inp.key_up_press: if self.force_select > -1: self.force_select -= 1 self.force_select = max(self.force_select, 0) @@ -10168,7 +10172,6 @@ def render(self): 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 @@ -10507,7 +10510,7 @@ def get_rect(self): def render(self): - if inp.mouse_click or inp.key_return_press or inp.right_click or key_esc_press or inp.backspace_press \ + if inp.mouse_click or inp.key_return_press or inp.right_click or inp.key_esc_press or inp.backspace_press \ or keymaps.test("quick-find") or (inp.k_input and message_box_min_timer.get() > 1.2): if not key_focused and message_box_min_timer.get() > 0.4: @@ -13497,7 +13500,7 @@ def render(self): if self.init2done is False: self.init2() - if key_esc_press: + if inp.key_esc_press: self.close() tab_width = 115 * gui.scale @@ -18475,7 +18478,7 @@ def render(self) -> None: 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 tauon.coll((x, y, w, h))): + if inp.key_esc_press or (gui.level_2_click and not tauon.coll((x, y, w, h))): self.active = False if self.add_mode: @@ -18503,7 +18506,7 @@ def render(self) -> None: ddt.text_background_colour = colours.box_background - if key_esc_press or (gui.level_2_click and not tauon.coll((x, y, w, h))): + if inp.key_esc_press or (gui.level_2_click and not tauon.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) @@ -19054,7 +19057,7 @@ def render(self): # 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) \ + if inp.key_return_press or (inp.key_esc_press and len(editline) == 0) \ or ((inp.mouse_click or inp.level_2_right_click) and not tauon.coll(rect)): gui.rename_playlist_box = False @@ -19160,7 +19163,7 @@ def draw(self, x, y, w, h): draw_pin_indicator = False # prefs.tabs_on_top # if not gui.album_tab_mode: - # if key_left_press or key_right_press: + # if inp.key_left_press or inp.key_up_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: @@ -22763,9 +22766,9 @@ def render(self): x -= 100 * gui.scale w = window_size[0] - x - 30 * gui.scale - if key_up_press and not (inp.key_ctrl_down or inp.key_shift_down or inp.key_shiftr_down): + if inp.key_up_press and not (inp.key_ctrl_down or inp.key_shift_down or inp.key_shiftr_down): lyrics_ren.lyrics_position += 35 * gui.scale - if key_down_press and not (inp.key_ctrl_down or inp.key_shift_down or inp.key_shiftr_down): + if inp.key_down_press and not (inp.key_ctrl_down or inp.key_shift_down or inp.key_shiftr_down): lyrics_ren.lyrics_position -= 35 * gui.scale lyrics_ren.test_update(track) @@ -42047,11 +42050,11 @@ def dev_mode_disable_save_state() -> None: inp.middle_click = False inp.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 + inp.key_down_press = False + inp.key_up_press = False + inp.key_up_press = False + inp.key_left_press = False + inp.key_esc_press = False key_del = False inp.backspace_press = 0 key_backspace_press = False @@ -42174,19 +42177,19 @@ def dev_mode_disable_save_state() -> None: elif is_level_zero(): pctl.stop() else: - key_esc_press = True + inp.key_esc_press = True if event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_UP: - key_up_press = True + inp.key_up_press = True if event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_DOWN: - key_down_press = True + inp.key_down_press = True if event.cbutton.button == SDL_CONTROLLER_BUTTON_DPAD_LEFT: if gui.album_tab_mode: - key_left_press = True + inp.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 + inp.key_up_press = True elif is_level_zero() or quick_search_mode: cycle_playlist_pinned(-1) @@ -42364,13 +42367,13 @@ def dev_mode_disable_save_state() -> None: elif event.key.keysym.sym == SDLK_LALT: inp.key_lalt = True elif event.key.keysym.sym == SDLK_DOWN: - key_down_press = True + inp.key_down_press = True elif event.key.keysym.sym == SDLK_UP: - key_up_press = True + inp.key_up_press = True elif event.key.keysym.sym == SDLK_LEFT: - key_left_press = True + inp.key_left_press = True elif event.key.keysym.sym == SDLK_RIGHT: - key_right_press = True + inp.key_up_press = True elif event.key.keysym.sym == SDLK_LSHIFT: inp.key_shift_down = True elif event.key.keysym.sym == SDLK_RSHIFT: @@ -42685,11 +42688,11 @@ def dev_mode_disable_save_state() -> None: # inp.middle_click = False inp.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 + inp.key_down_press = False + inp.key_up_press = False + inp.key_right_press = False + inp.key_left_press = False + inp.key_esc_press = False key_del = False inp.backspace_press = 0 key_backspace_press = False @@ -42711,9 +42714,9 @@ def dev_mode_disable_save_state() -> None: if c_yax != 0: if c_yax_timer.get() >= 0: if c_yax == -1: - key_up_press = True + inp.key_up_press = True if c_yax == 1: - key_down_press = True + inp.key_down_press = True c_yax_timer.force_set(-0.01) gui.delay_frame(0.02) inp.k_input = True @@ -42752,12 +42755,12 @@ def dev_mode_disable_save_state() -> None: n += 1 if keymaps.test("cycle-playlist-left"): - if gui.album_tab_mode and key_left_press: + if gui.album_tab_mode and inp.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: + if gui.album_tab_mode and inp.key_up_press: pass elif is_level_zero() or quick_search_mode: cycle_playlist_pinned(-1) @@ -42797,7 +42800,7 @@ def dev_mode_disable_save_state() -> None: update_layout_do(tauon=tauon) if keymaps.test("escape"): - key_esc_press = True + inp.key_esc_press = True if inp.key_ctrl_down: gui.pl_update += 1 @@ -42805,7 +42808,7 @@ def dev_mode_disable_save_state() -> None: if mouse_enter_window: inp.key_return_press = False - if gui.fullscreen and key_esc_press: + if gui.fullscreen and inp.key_esc_press: gui.fullscreen = False SDL_SetWindowFullscreen(t_window, 0) @@ -42817,18 +42820,18 @@ def dev_mode_disable_save_state() -> None: and not inp.key_ctrl_down \ and not inp.key_meta \ and not inp.key_lalt: - if key_left_press: + if inp.key_left_press: gal_left = True - key_left_press = False - if key_right_press: + inp.key_left_press = False + if inp.key_up_press: gal_right = True - key_right_press = False - if key_up_press: + inp.key_up_press = False + if inp.key_up_press: gal_up = True - key_up_press = False - if key_down_press: + inp.key_up_press = False + if inp.key_down_press: gal_down = True - key_down_press = False + inp.key_down_press = False if not tauon.search_over.active: if key_del: @@ -42836,7 +42839,7 @@ def dev_mode_disable_save_state() -> None: del_selected() # Arrow keys to change playlist - if (key_left_press or key_right_press) and len(pctl.multi_playlist) > 1: + if (inp.key_left_press or inp.key_up_press) and len(pctl.multi_playlist) > 1: gui.pl_update = 1 gui.update += 1 @@ -43129,11 +43132,11 @@ def dev_mode_disable_save_state() -> None: # These need to be disabled when text fields are active if not tauon.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 + inp.key_up_press = False pctl.advance() if keymaps.test("previous"): - key_left_press = False + inp.key_left_press = False pctl.back() if key_a_press and inp.key_ctrl_down: @@ -44708,7 +44711,7 @@ def dev_mode_disable_save_state() -> None: else: playlist_render.cache_render() - if gui.combo_mode and key_esc_press and is_level_zero(): + if gui.combo_mode and inp.key_esc_press and is_level_zero(): exit_combo() if not gui.set_bar and gui.set_mode and not gui.combo_mode: @@ -45365,7 +45368,7 @@ def dev_mode_disable_save_state() -> None: gui.preview_artist = "" if track_box: - if inp.key_return_press or inp.right_click or key_esc_press or inp.backspace_press or keymaps.test( + if inp.key_return_press or inp.right_click or inp.key_esc_press or inp.backspace_press or keymaps.test( "quick-find"): track_box = False @@ -45837,7 +45840,7 @@ def dev_mode_disable_save_state() -> None: ddt.text_background_colour = colours.box_background - if key_esc_press or ( + if inp.key_esc_press or ( (inp.mouse_click or inp.right_click or inp.level_2_right_click) and not tauon.coll((x, y, w, h))): gui.rename_folder_box = False @@ -45946,7 +45949,7 @@ def dev_mode_disable_save_state() -> None: 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): + inp.key_esc_press and len(editline) == 0)) or (inp.mouse_click and quick_search_mode is True): quick_search_mode = False search_text.text = "" @@ -45957,7 +45960,7 @@ def dev_mode_disable_save_state() -> None: # search_text.text = "" # input_text = "" # elif ((key_backslash_press or (inp.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: + # inp.key_esc_press and len(editline) == 0)) or input.mouse_click and quick_search_mode is True: # quick_search_mode = False # search_text.text = "" @@ -46067,7 +46070,7 @@ def dev_mode_disable_save_state() -> None: 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 \ + if (len(input_text) > 0 and not gui.search_error) or inp.key_down_press is True or inp.backspace_press \ or gui.force_search: gui.pl_update = 1 @@ -46114,12 +46117,12 @@ def dev_mode_disable_save_state() -> None: search_index = oi if len(input_text) > 0 or gui.force_search: gui.search_error = True - if key_down_press: + if inp.key_down_press: bottom_playlist2.pulse() gui.force_search = False - if key_up_press is True \ + if inp.key_up_press is True \ and not inp.key_shiftr_down \ and not inp.key_shift_down \ and not inp.key_ctrl_down \ @@ -46167,7 +46170,7 @@ def dev_mode_disable_save_state() -> None: quick_search_mode = False search_clear_timer.set() elif not tauon.search_over.active: - if key_up_press and (( + if inp.key_up_press and (( not inp.key_shiftr_down \ and not inp.key_shift_down \ and not inp.key_ctrl_down \ @@ -46195,7 +46198,7 @@ def dev_mode_disable_save_state() -> None: pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(pctl.default_playlist)) if pctl.selected_in_playlist < len(pctl.default_playlist) and ( - (key_down_press and \ + (inp.key_down_press and \ not inp.key_shiftr_down \ and not inp.key_shift_down \ and not inp.key_ctrl_down \ From 23ef0d7c1897bddb7efa2d0b3584cf2572ab3693 Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 18:21:55 +0100 Subject: [PATCH 09/13] Refactor more functions --- src/tauon/t_modules/t_main.py | 169 ++++++++++++++++------------------ 1 file changed, 79 insertions(+), 90 deletions(-) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index b069ca87e..341220fe9 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -1468,7 +1468,7 @@ def light_mode(self): self.gallery_artist_line = self.grey(40) self.pluse_colour = [212, 66, 244, 255] - # view_box.off_colour = self.grey(200) + # tauon.view_box.off_colour = self.grey(200) class TrackClass: """This is the fundamental object/data structure of a track""" @@ -4402,6 +4402,8 @@ def is_item_disabled(self, item): return item.disable_test() def render_icon(self, x, y, icon, selected, fx): + colours = self.colours + gui = self.gui if colours.lm: selected = True @@ -4414,7 +4416,7 @@ def render_icon(self, x, y, icon, selected, fx): 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() + colour = icon.colour_callback(tauon=self.tauon) elif selected and fx[0] != colours.menu_text_disabled: colour = icon.colour @@ -4516,7 +4518,7 @@ def render(self) -> None: # 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, self.tauon) + fx = self.items[i].render_func(self.reference, tauon=self.tauon) else: fx = self.items[i].render_func(tauon=self.tauon) else: @@ -4624,8 +4626,8 @@ def render(self) -> None: if sub_pos[0] + sub_w > self.window_size[0]: sub_pos[0] = x_run - sub_w - if view_box.active: - sub_pos[0] -= view_box.w + if tauon.view_box.active: + sub_pos[0] -= tauon.view_box.w fx = self.deco() @@ -4706,9 +4708,9 @@ def render(self) -> 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) + self.items[to_call].func(self.reference, tauon=tauon) else: - self.items[to_call].func() + self.items[to_call].func(tauon=tauon) if self.clicked or inp.key_esc_press or self.close_next_frame: self.close_next_frame = False @@ -5100,6 +5102,7 @@ def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): self.top_panel = TopPanel(tauon=self) self.playlist_box = PlaylistBox(tauon=self) self.radio_view = RadioView(tauon=self) + self.view_box = ViewBox(tauon=self) self.cache_directory: Path = bag.dirs.cache_directory self.user_directory: Path | None = bag.dirs.user_directory self.music_directory: Path | None = bag.dirs.music_directory @@ -8494,9 +8497,8 @@ def display(self) -> None: class ToolTip: - def __init__(self, bag: Bag, gui: GuiVar) -> None: - self.bag = bag - self.gui = gui + def __init__(self, tauon: Tauon) -> None: + self.gui = tauon.gui self.text = "" self.h = 24 * self.gui.scale self.w = 62 * self.gui.scale @@ -8509,7 +8511,6 @@ def __init__(self, bag: Bag, gui: GuiVar) -> None: 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() @@ -8527,11 +8528,8 @@ def test(self, x, y, text): 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( @@ -8543,20 +8541,19 @@ def render(self) -> None: else: self.timer.set() self.a = False - self.called = False class ToolTip3: - def __init__(self, bag: Bag, gui: GuiVar) -> None: - self.gui = gui + def __init__(self, tauon: Tauon) -> None: + self.gui = tauon.gui self.x = 0 self.y = 0 self.text = "" self.font = None self.show = False self.width = 0 - self.height = 24 * gui.scale + self.height = 24 * self.gui.scale self.timer = Timer() self.pl_position = 0 self.click_exclude_point = (0, 0) @@ -11288,7 +11285,7 @@ def funcs(self, x0, y0, w0, h0): _("Show artist info panel"), subtitle=_("You can also toggle this with ctrl+o")) if new != old: - view_box.artist_info(True) + tauon.view_box.artist_info(True) y += 38 * gui.scale @@ -14431,7 +14428,7 @@ def render(self): if x > window_size[0] - (210 * gui.scale): xx = window_size[0] - round(210 * gui.scale) tauon.x_menu.activate(position=(xx + round(12 * gui.scale), gui.panelY)) - view_box.activate(xx) + tauon.view_box.activate(xx) # if True: # border = round(3 * gui.scale) @@ -22912,6 +22909,7 @@ class ViewBox: def __init__(self, tauon: Tauon, reload: bool = False) -> None: self.tauon = tauon + self.gui = tauon.gui self.colours = tauon.bag.colours self.x = 0 self.y = tauon.gui.panelY @@ -22977,8 +22975,8 @@ def activate(self, x): self.col_force_off = False - # gui.level_2_click = False - gui.update = 2 + # self.gui.level_2_click = False + self.gui.update = 2 def button(self, x, y, asset, test, colour_get=None, name="Unknown", animate=True, low=0, high=0): @@ -26994,7 +26992,7 @@ def toggle_shuffle_layout(albums: bool = False): gui.shuffle_was_repeat = pctl.repeat_mode if not gui.combo_mode: - view_box.lyrics(hit=True) + tauon.view_box.lyrics(hit=True) pctl.random_mode = True pctl.repeat_mode = False if albums: @@ -28417,8 +28415,8 @@ def export_xspf(pl: int, direc: str | None = None, relative: bool = False, show: return target -def reload(): - if prefs.album_mode: +def reload(tauon: Tauon) -> None: + if tauon.prefs.album_mode: reload_albums(quiet=True) # tree_view_box.clear_all() @@ -28446,7 +28444,7 @@ def clear_playlist(tauon: Tauon, index: int) -> None: del pctl.multi_playlist[index].playlist_ids[:] if pctl.active_playlist_viewing == index: pctl.default_playlist = tauon.pctl.multi_playlist[index].playlist_ids - reload() + reload(tauon=tauon) # pctl.playlist_playing = 0 pctl.multi_playlist[index].position = 0 @@ -28692,17 +28690,17 @@ def re_import2(pl: int) -> None: if paths: show_message(_("Rescanning folders..."), mode="info") -def rescan_all_folders(): +def rescan_all_folders(pctl: PlayerCtl) -> None: for i, p in enumerate(pctl.multi_playlist): re_import2(i) -def s_append(index: int): +def s_append(index: int) -> None: paste(playlist_no=index) -def append_playlist(index: int): - pctl.multi_playlist[index].playlist_ids += pctl.cargo +def append_playlist(tauon: Tauon, index: int) -> None: + tauon.pctl.multi_playlist[index].playlist_ids += tauon.pctl.cargo - gui.pl_update = 1 + tauon.gui.pl_update = 1 reload() def index_key(index: int): @@ -31080,7 +31078,8 @@ def convert_folder(index: int): transcode_list.append(folder) tauon.thread_manager.ready("worker") -def transfer(index: int, args: list[int]) -> None: +def transfer(tauon: Tauon, index: int, args: list[int]) -> None: + pctl = tauon.pctl old_cargo = copy.deepcopy(pctl.cargo) if args[0] == 1 or args[0] == 0: # copy @@ -31088,7 +31087,6 @@ def transfer(index: int, args: list[int]) -> None: pctl.cargo.append(index) if args[0] == 0: # cut del pctl.default_playlist[pctl.selected_in_playlist] - elif args[1] == 2: # folder for b in range(len(pctl.default_playlist)): if pctl.master_library[pctl.default_playlist[b]].parent_folder_name == pctl.master_library[ @@ -31099,7 +31097,6 @@ def transfer(index: int, args: list[int]) -> None: if pctl.master_library[pctl.default_playlist[b]].parent_folder_name == pctl.master_library[ index].parent_folder_name: del pctl.default_playlist[b] - elif args[1] == 3: # playlist pctl.cargo += pctl.default_playlist if args[0] == 0: # cut @@ -31107,7 +31104,6 @@ def transfer(index: int, args: list[int]) -> None: elif args[0] == 2: # Drop if args[1] == 1: # Before - insert = pctl.selected_in_playlist while insert > 0 and pctl.master_library[pctl.default_playlist[insert]].parent_folder_name == \ pctl.master_library[index].parent_folder_name: @@ -31123,8 +31119,8 @@ def transfer(index: int, args: list[int]) -> None: elif args[1] == 2: # After insert = pctl.selected_in_playlist - while insert < len(pctl.default_playlist) and pctl.master_library[pctl.default_playlist[insert]].parent_folder_name == \ - pctl.master_library[index].parent_folder_name: + while insert < len(pctl.default_playlist) \ + and pctl.master_library[pctl.default_playlist[insert]].parent_folder_name == pctl.master_library[index].parent_folder_name: insert += 1 while len(pctl.cargo) > 0: @@ -31134,11 +31130,11 @@ def transfer(index: int, args: list[int]) -> None: # pctl.cargo = [] pctl.cargo = old_cargo - reload() + reload(tauon=tauon) -def temp_copy_folder(ref: int, pctl: PlayerCtl) -> None: +def temp_copy_folder(tauon: Tauon, ref: int) -> None: pctl.cargo = [] - transfer(ref, args=[1, 2]) + transfer(tauon, ref, args=[1, 2]) def activate_track_box(index: int): global track_box @@ -31320,7 +31316,9 @@ def lightning_paste(): pctl.cargo.clear() gui.lightning_copy = False -def paste(playlist_no=None, track_id=None): +def paste(tauon: Tauon, playlist_no: int | None = None, track_id: int | None = None) -> None: + gui = tauon.gui + pctl = tauon.pctl clip = copy_from_clipboard() logging.info(clip) if "tidal.com/album/" in clip: @@ -31392,12 +31390,11 @@ def paste(playlist_no=None, track_id=None): found = True if not found: - if playlist_no is None: if track_id is None: - transfer(0, (2, 3)) + transfer(tauon, 0, (2, 3)) else: - transfer(track_id, (2, 2)) + transfer(tauon, track_id, (2, 2)) else: append_playlist(playlist_no) @@ -31500,7 +31497,7 @@ def del_selected(force_delete: bool = False): else: undo.bk_tracks(pctl.active_playlist_viewing, li) - reload() + reload(tauon=tauon) tree_view_box.clear_target_pl(pctl.active_playlist_viewing) pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(pctl.default_playlist) - 1) @@ -31701,7 +31698,7 @@ def delete_track(track_ref): logging.exception("Error deleting file") show_message(_("Error deleting file"), fullpath, mode="error") - reload() + reload(tauon=tauon) refind_playing() pctl.notify_change() @@ -32991,7 +32988,7 @@ def sort_ass(h, invert=False, custom_list=None, custom_name=""): gui.pl_update = 1 elif custom_list is not None: playlist.sort(key=key, reverse=invert) - reload() + reload(tauon=tauon) def sort_dec(h): sort_ass(h, True) @@ -33387,7 +33384,7 @@ def switch_playlist(number, cycle=False, quiet=False): pctl.show_current(this_only=True, playing=False, highlight=True, no_switch=True) if prefs.shuffle_lock: - view_box.lyrics(hit=True) + tauon.view_box.lyrics(hit=True) if pctl.active_playlist_viewing: pctl.active_playlist_playing = pctl.active_playlist_viewing random_track() @@ -33443,13 +33440,14 @@ def activate_radio_box(): radiobox.radio_field.clear() radiobox.radio_field_title.clear() -def new_playlist_colour_callback(): - if gui.radio_view: +def new_playlist_colour_callback(tauon: Tauon) -> list[int]: + if tauon.gui.radio_view: return [120, 90, 245, 255] return [237, 80, 221, 255] -def new_playlist_deco(): - if gui.radio_view: +def new_playlist_deco(tauon: Tauon): + colours = tauon.bag.colours + if tauon.gui.radio_view: text = _("New Radio List") else: text = _("New Playlist") @@ -38009,7 +38007,7 @@ def reload_scale(bag: Bag): bottom_bar1.__init__() bottom_bar_ao1.__init__() tauon.top_panel.__init__() - view_box.__init__(reload=True) + tauon.view_box.__init__(reload=True) queue_box.recalc() tauon.playlist_box.recalc() @@ -40890,13 +40888,13 @@ def update(self, force=False): - tool_tip = ToolTip(bag, gui) - tool_tip2 = ToolTip(bag, gui) + tool_tip = ToolTip(tauon) + tool_tip2 = ToolTip(tauon) tool_tip2.trigger = 1.8 track_box_path_tool_timer = Timer() - columns_tool_tip = ToolTip3(bag, gui) - tool_tip_instant = ToolTip3(bag, gui) + columns_tool_tip = ToolTip3(tauon) + tool_tip_instant = ToolTip3(tauon) # Create empty area menu playlist_menu = tauon.playlist_menu @@ -41801,7 +41799,6 @@ def dev_mode_disable_save_state() -> None: showcase = Showcase() cctest = ColourPulse2() - view_box = ViewBox(tauon=tauon) dl_mon = DLMon() tauon.dl_mon = dl_mon dl_menu.add(MenuItem("Dismiss", dismiss_dl)) @@ -42975,34 +42972,34 @@ def dev_mode_disable_save_state() -> None: if keymaps.test("cycle-layouts"): - if view_box.tracks(): - view_box.side(True) - elif view_box.side(): - view_box.gallery1(True) - elif view_box.gallery1(): - view_box.lyrics(True) + if tauon.view_box.tracks(): + tauon.view_box.side(True) + elif tauon.view_box.side(): + tauon.view_box.gallery1(True) + elif tauon.view_box.gallery1(): + tauon.view_box.lyrics(True) else: - view_box.tracks(True) + tauon.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) + if tauon.view_box.tracks(): + tauon.view_box.lyrics(True) + elif tauon.view_box.lyrics(): + tauon.view_box.gallery1(True) + elif tauon.view_box.gallery1(): + tauon.view_box.side(True) else: - view_box.tracks(True) + tauon.view_box.tracks(True) if keymaps.test("toggle-columns"): - view_box.col(True) + tauon.view_box.col(True) if keymaps.test("toggle-artistinfo"): - view_box.artist_info(True) + tauon.view_box.artist_info(True) if keymaps.test("toggle-showcase"): - view_box.lyrics(True) + tauon.view_box.lyrics(True) if keymaps.test("toggle-gallery-keycontrol"): toggle_gallery_keycontrol() @@ -43048,11 +43045,11 @@ def dev_mode_disable_save_state() -> None: instance.click() inp.mouse_click = False ab_click = True - if view_box.active: - view_box.clicked = True + if tauon.view_box.active: + tauon.view_box.clicked = True if inp.mouse_click and ( - prefs.show_nag or gui.box_over or radiobox.active or tauon.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: + prefs.show_nag or gui.box_over or radiobox.active or tauon.search_over.active or gui.rename_folder_box or gui.rename_playlist_box or rename_track_box.active or tauon.view_box.active or trans_edit_box.active): # and not gui.message_box: inp.mouse_click = False gui.level_2_click = True else: @@ -43485,7 +43482,7 @@ def dev_mode_disable_save_state() -> None: # inp.mouse_position[0], inp.mouse_position[1] = get_sdl_input.mouse() gui.showed_title = False - 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: + if not gui.mouse_in_window and not tauon.bottom_bar1.volume_bar_being_dragged and not tauon.bottom_bar1.volume_hit and not tauon.bottom_bar1.seek_hit: inp.mouse_position[0] = -300 inp.mouse_position[1] = -300 @@ -44168,7 +44165,6 @@ def dev_mode_disable_save_state() -> None: 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: @@ -44216,7 +44212,6 @@ def dev_mode_disable_save_state() -> None: if gui.gallery_show_text: c_index = pctl.default_playlist[album_dex[album_on]] - if c_index in album_artist_dict: pass else: @@ -44236,7 +44231,6 @@ def dev_mode_disable_save_state() -> None: pctl.master_library[ pctl.default_playlist[album_dex[album_on]]].artist: album_artist_dict[c_index] = _("Various Artists") - break i += 1 else: @@ -44259,9 +44253,7 @@ def dev_mode_disable_save_state() -> None: x += round(6 * gui.scale) if card_mode: - if line2 == "": - ddt.text( (x, y + bag.album_mode_art_size + 8 * gui.scale, text_align), line, @@ -44269,7 +44261,6 @@ def dev_mode_disable_save_state() -> None: 310, bag.album_mode_art_size - 18 * gui.scale) else: - ddt.text( (x, y + bag.album_mode_art_size + 7 * gui.scale, text_align), line2, @@ -44284,7 +44275,6 @@ def dev_mode_disable_save_state() -> None: 10, bag.album_mode_art_size - 18 * gui.scale) elif line2 == "": - ddt.text( (x, y + bag.album_mode_art_size + 9 * gui.scale, text_align), line, @@ -44292,7 +44282,6 @@ def dev_mode_disable_save_state() -> None: 311, bag.album_mode_art_size - 5 * gui.scale) else: - ddt.text( (x, y + bag.album_mode_art_size + 8 * gui.scale, text_align), line2, @@ -44480,7 +44469,7 @@ def dev_mode_disable_save_state() -> None: gui.pl_update += 2 if order.notify and gui.message_box and len(load_orders) == 1: show_message(_("Rescan folders complete."), mode="done") - reload() + reload(tauon=tauon) tree_view_box.clear_target_pl(target_pl) if order.play and order.tracks: @@ -46362,8 +46351,8 @@ def dev_mode_disable_save_state() -> None: for instance in Menu.instances: instance.render() - if view_box.active: - view_box.render() + if tauon.view_box.active: + tauon.view_box.render() tool_tip.render() tool_tip2.render() From 0bf514f7fe93b713e5c14947cee83de5e9f402c9 Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 21:43:47 +0100 Subject: [PATCH 10/13] More refactor --- src/tauon/t_modules/t_main.py | 351 ++++++++++++++++-------------- src/tauon/t_modules/t_webserve.py | 6 +- 2 files changed, 185 insertions(+), 172 deletions(-) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index 341220fe9..ce74b09b0 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -1913,7 +1913,7 @@ def playing_ready(self) -> bool: return len(self.track_queue) > 0 def selected_ready(self) -> bool: - return pctl.default_playlist and self.selected_in_playlist < len(pctl.default_playlist) + return self.default_playlist and self.selected_in_playlist < len(self.default_playlist) def render_playlist(self) -> None: if taskbar_progress and self.msys and self.windows_progress: @@ -2036,7 +2036,7 @@ def show_current( if self.tauon.spot_ctl.coasting: sptr = self.tauon.dummy_track.misc.get("spotify-track-url") if sptr: - for p in pctl.default_playlist: + for p in self.default_playlist: tr = self.get_track(p) if tr.misc.get("spotify-track-url") == sptr: index = tr.index @@ -4363,7 +4363,8 @@ def __init__(self, tauon: Tauon, width: int, show_icons: bool = False) -> None: Menu.instances.append(self) @staticmethod - def deco(_=_): + def deco(_=_, tauon: Tauon | None = None): + colours = tauon.bag.colours return [colours.menu_text, colours.menu_background, None] def click(self) -> None: @@ -4522,7 +4523,7 @@ def render(self) -> None: else: fx = self.items[i].render_func(tauon=self.tauon) else: - fx = self.deco() + fx = self.deco(tauon=self.tauon) if fx[2] is not None: label = fx[2] @@ -5068,7 +5069,7 @@ def path(self, track: TrackClass) -> str: class Tauon: """Root class for everything Tauon""" - def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): + def __init__(self, holder: Holder, bag: Bag, gui: GuiVar): self.bag = bag self.t_window = holder.t_window self.t_title = holder.t_title @@ -5087,12 +5088,26 @@ def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): self.cachement: player4.Cachement | None = None self.dummy_event: SDL_Event = SDL_Event() self.translate = _ - self.strings: Strings = strings + self.strings = Strings() self.gui: GuiVar = gui self.prefs: Prefs = bag.prefs + # Create top menu + self.x_menu = Menu(self, 190, show_icons=True) + self.set_menu = Menu(self, 150) + self.field_menu = Menu(self, 140) + self.dl_menu = Menu(self, 90) + + self.cancel_menu = Menu(self, 100) + self.extra_menu = Menu(self, 175, show_icons=True) + self.shuffle_menu = Menu(self, 120) + self.repeat_menu = Menu(self, 120) + self.tab_menu = Menu(self, 160, show_icons=True) + self.playlist_menu = Menu(self, 130) + self.spotify_playlist_menu = Menu(self, 175) self.fields = Fields(tauon=self) self.artist_list_box = ArtistList(tauon=self) self.radiobox = RadioBox(tauon=self) + self.dummy_track = self.radiobox.dummy_track self.pctl: PlayerCtl = PlayerCtl(tauon=self) self.search_over = SearchOverlay(tauon=self) self.deco = Deco(tauon=self) @@ -5103,6 +5118,8 @@ def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): self.playlist_box = PlaylistBox(tauon=self) self.radio_view = RadioView(tauon=self) self.view_box = ViewBox(tauon=self) + self.pref_box = Over(tauon=self) + self.fader = Fader(tauon=self) self.cache_directory: Path = bag.dirs.cache_directory self.user_directory: Path | None = bag.dirs.user_directory self.music_directory: Path | None = bag.dirs.music_directory @@ -5146,19 +5163,6 @@ def __init__(self, holder: Holder, bag: Bag, strings: Strings, gui: GuiVar): self.listen_alongers = {} self.encode_folder_name = encode_folder_name self.encode_track_name = encode_track_name - # Create top menu - self.x_menu = Menu(self, 190, show_icons=True) - self.set_menu = Menu(self, 150) - self.field_menu = Menu(self, 140) - self.dl_menu = Menu(self, 90) - - self.cancel_menu = Menu(self, 100) - self.extra_menu = Menu(self, 175, show_icons=True) - self.shuffle_menu = Menu(self, 120) - self.repeat_menu = Menu(self, 120) - self.tab_menu = Menu(self, 160, show_icons=True) - self.playlist_menu = Menu(self, 130) - self.spotify_playlist_menu = Menu(self, 175) self.tray_lock = threading.Lock() @@ -5218,10 +5222,33 @@ def coll(self, r: list[int]) -> bool: inp = self.gui.inp return r[0] < inp.mouse_position[0] <= r[0] + r[2] and r[1] <= inp.mouse_position[1] <= r[1] + r[3] + def toggle_enable_web(self, mode: int = 0) -> bool | None: + prefs = self.prefs + gui = self.gui + 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), self.strings, self]) + webThread.daemon = True + webThread.start() + show_message(_("Web server starting"), _("External connections will be accepted."), mode="done") + + elif prefs.enable_web is False: + if self.radio_server is not None: + self.radio_server.shutdown() + gui.web_running = False + + time.sleep(0.25) + return None + 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]) + target=webserve2, args=[pctl, prefs, gui, album_art_gen, str(install_directory), self.strings, self]) self.web_thread.daemon = True self.web_thread.start() self.web_running = True @@ -10652,13 +10679,17 @@ def __init__(self): self.ani_timer.force_set(10) class Over: - def __init__(self, bag: Bag, gui: GuiVar): - - global window_size - self.dirs = bag.dirs - self.prefs = bag.prefs - self.gui = gui - self.init2done = False + def __init__(self, tauon: Tauon) -> None: + bag = tauon.bag + self.tauon = tauon + self.colours = tauon.bag.colours + self.dirs = tauon.bag.dirs + self.prefs = tauon.bag.prefs + self.gui = tauon.gui + self.inp = tauon.gui.inp + self.ddt = tauon.bag.ddt + self.window_size = tauon.bag.window_size + self.init2done = False self.about_image = asset_loader(bag, bag.loaded_asset_dc, "v4-a.png") self.about_image2 = asset_loader(bag, bag.loaded_asset_dc, "v4-b.png") @@ -11248,7 +11279,11 @@ def view2(self, x0, y0, w0, h0): x + 25 * gui.scale, y, _("Thumbnail size"), "px", bag.album_mode_art_size, 70, 400, 10, img_slide_update_gall) def funcs(self, x0, y0, w0, h0): - + tauon = self.tauon + prefs = self.prefs + gui = self.gui + ddt = self.ddt + colours = self.colours x = x0 + 25 * gui.scale y = y0 - 10 * gui.scale @@ -11259,10 +11294,9 @@ def funcs(self, x0, y0, w0, h0): 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): + x, y, tauon.toggle_enable_web, _("Enable Listen Along"), subtitle=_("Start server for remote web playback")) + if tauon.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", @@ -11394,7 +11428,6 @@ def funcs(self, x0, y0, w0, h0): 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")) @@ -11555,6 +11588,8 @@ def button2(self, x, y, text, width=0, center_text=False, force_on=False): return hit def toggle_square(self, x, y, function, text: str , click: bool = False, subtitle: str = "") -> bool: + gui = self.gui + colours = self.colours x = round(x) y = round(y) @@ -11565,19 +11600,19 @@ def toggle_square(self, x, y, function, text: str , click: bool = False, subtitl 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) + le = self.ddt.text((x + 20 * gui.scale, y - 1 * gui.scale), text, colours.box_text, 13) + se = self.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) + le = self.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) + self.ddt.rect_a((x, y), (full_w, full_w), colours.box_check_border) # Inner background - ddt.rect_a( + self.ddt.rect_a( (x + border, y + border), (gap * 2 + inner_square, gap * 2 + inner_square), alpha_blend([255, 255, 255, 14], colours.box_background)) @@ -11602,7 +11637,7 @@ def toggle_square(self, x, y, function, text: str , click: bool = False, subtitl # 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) + self.ddt.rect_a((x + border + gap, y + border + gap), (inner_square, inner_square), colours.toggle_box_on) return active @@ -13292,7 +13327,6 @@ def stats(self, x0, y0, w0, h0): 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 @@ -13482,18 +13516,23 @@ def slide_control(self, x: int, y: int, label: str, units: str, value: int, lowe # prefs.line_style = 1 def inside(self): - return tauon.coll((self.box_x, self.box_y, self.w, self.h)) + return self.tauon.coll((self.box_x, self.box_y, self.w, self.h)) - def init2(self): + def init2(self) -> None: self.init2done = True - def close(self): + def close(self) -> None: self.enabled = False fader.fall() if gui.opened_config_file: reload_config_file() - def render(self): + def render(self) -> None: + tauon = self.tauon + inp = self.inp + gui = self.gui + ddt = self.ddt + colours = self.colours if self.init2done is False: self.init2() @@ -13506,7 +13545,7 @@ def render(self): header_width = 0 top_mode = False - if window_size[0] < 700 * gui.scale: + if self.window_size[0] < 700 * gui.scale: top_mode = True side_width = 0 * gui.scale header_width = round(48 * gui.scale) # 48 @@ -13519,8 +13558,8 @@ def render(self): 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) + x = int(self.window_size[0] / 2) - int(full_width / 2) + y = int(self.window_size[1] / 2) - int(full_height / 2) self.box_x = x self.box_y = y @@ -13621,8 +13660,7 @@ def render(self): 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) + ddt.text((box[0] + (tab_width // 2), yy, 2), item[0], tab_text, 213) current_tab += 1 @@ -22908,8 +22946,11 @@ def get(self, hit, on, off, low_hls, high_hls): class ViewBox: def __init__(self, tauon: Tauon, reload: bool = False) -> None: - self.tauon = tauon - self.gui = tauon.gui + self.tauon = tauon + self.x_menu = tauon.x_menu + self.prefs = tauon.prefs + self.gui = tauon.gui + self.ddt = tauon.bag.ddt self.colours = tauon.bag.colours self.x = 0 self.y = tauon.gui.panelY @@ -22978,29 +23019,27 @@ def activate(self, x): # self.gui.level_2_click = False self.gui.update = 2 - def button(self, x, y, asset, test, colour_get=None, name="Unknown", animate=True, low=0, high=0): - + def button(self, x, y, asset, test, colour_get=None, name: str = "Unknown", animate: bool = True, low: int = 0, high: int = 0): on = test() - rect = [x - 8 * gui.scale, - y - 8 * gui.scale, - asset.w + 16 * gui.scale, - asset.h + 16 * gui.scale] - tauon.fields.add(rect) + rect = [ + x - 8 * self.gui.scale, + y - 8 * self.gui.scale, + asset.w + 16 * self.gui.scale, + asset.h + 16 * self.gui.scale] + self.tauon.fields.add(rect) if on: colour = self.on_colour - else: colour = self.off_colour fun = None col = False - if tauon.coll(rect): - - tool_tip.test(x + asset.w + 10 * gui.scale, y - 15 * gui.scale, name) + if self.tauon.coll(rect): + tool_tip.test(x + asset.w + 10 * self.gui.scale, y - 15 * self.gui.scale, name) col = True - if gui.level_2_click: + if self.gui.level_2_click: fun = test if colour_get is None: colour = self.over_colour @@ -23018,109 +23057,104 @@ def button(self, x, y, asset, test, colour_get=None, name="Unknown", animate=Tru return fun - def tracks(self, hit=False): - + def tracks(self, hit: bool =False) -> bool | None: if hit is False: - return prefs.album_mode is False and \ - gui.combo_mode is False and \ - gui.rsp is False + return self.prefs.album_mode is False and \ + self.gui.combo_mode is False and \ + self.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 + self.gui.combo_mode is False and \ + self.gui.rsp is False): + if self.x_menu.active: + self.x_menu.close_next_frame = True view_tracks() - def side(self, hit=False): - + def side(self, hit: bool = False) -> bool | None: if hit is False: - return prefs.album_mode is False and \ - gui.combo_mode is False and \ - gui.rsp is True - if not (prefs.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 + return self.prefs.album_mode is False and \ + self.gui.combo_mode is False and \ + self.gui.rsp is True + if not (self.prefs.album_mode is False and \ + self.gui.combo_mode is False and \ + self.gui.rsp is True): + if self.x_menu.active: + self.x_menu.close_next_frame = True view_standard_meta() def gallery1(self, hit: bool = False) -> bool | None: - if hit is False: - return prefs.album_mode is True # and gui.show_playlist is True - - if prefs.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 + return self.prefs.album_mode is True # and self.gui.show_playlist is True + + if self.prefs.album_mode and not self.gui.combo_mode: + self.gui.hide_tracklist_in_gallery ^= True + self.gui.rspw = self.gui.pref_gallery_w + self.gui.update_layout() + # self.x_menu.active = False + self.x_menu.close_next_frame = True # Menu.active = False return None - if x_menu.active: - x_menu.close_next_frame = True + if self.x_menu.active: + self.x_menu.close_next_frame = True force_album_view() - def radio(self, hit=False): - + def radio(self, hit: bool = False) -> bool | None: if hit is False: - return gui.radio_view + return self.gui.radio_view - if not gui.radio_view: - enter_radio_view(tauon=tauon) + if not self.gui.radio_view: + enter_radio_view(tauon=self.tauon) else: exit_combo(restore=True) - if x_menu.active: - x_menu.close_next_frame = True - - def lyrics(self, hit=False): + if self.x_menu.active: + self.x_menu.close_next_frame = True + def lyrics(self, hit: bool = False) -> bool | None: if hit is False: - return gui.showcase_mode + return self.gui.showcase_mode - if not gui.showcase_mode: - if gui.radio_view: - gui.was_radio = True + if not self.gui.showcase_mode: + if self.gui.radio_view: + self.gui.was_radio = True enter_showcase_view() - elif gui.was_radio: - enter_radio_view(tauon=tauon) + elif self.gui.was_radio: + enter_radio_view(tauon=self.tauon) else: exit_combo(restore=True) - if x_menu.active: - x_menu.close_next_frame = True - - def col(self, hit=False): + if self.x_menu.active: + self.x_menu.close_next_frame = True + def col(self, hit: bool = False) -> bool | None: if hit is False: - return gui.set_mode + return self.gui.set_mode - if not gui.set_mode: - if gui.combo_mode: + if not self.gui.set_mode: + if self.gui.combo_mode: exit_combo() - if prefs.album_mode and gui.plw < 550 * gui.scale: - toggle_album_mode(tauon=tauon) + if self.prefs.album_mode and self.gui.plw < 550 * self.gui.scale: + toggle_album_mode(tauon=self.tauon) toggle_library_mode() - def artist_info(self, hit=False): - + def artist_info(self, hit: bool = False) -> bool: if hit is False: - return gui.artist_info_panel - - gui.artist_info_panel ^= True - gui.update_layout() + return self.gui.artist_info_panel - def render(self): + self.gui.artist_info_panel ^= True + self.gui.update_layout() - if prefs.shuffle_lock: + def render(self) -> None: + gui = self.gui + ddt = self.ddt + colours = self.colours + if self.prefs.shuffle_lock: self.active = False self.clicked = False return @@ -23268,11 +23302,11 @@ def render(self): if func is not None: func(True) - if gui.level_2_click and tauon.coll(vr): + if gui.level_2_click and self.tauon.coll(vr): x_menu.clicked = False gui.level_2_click = False - if not x_menu.active: + if not self.x_menu.active: self.active = False class DLMon: @@ -33431,14 +33465,14 @@ def cycle_playlist_pinned(step): break on += 1 -def activate_info_box(): - fader.rise() - pref_box.enabled = True +def activate_info_box(tauon: Tauon) -> None: + tauon.fader.rise() + tauon.pref_box.enabled = True -def activate_radio_box(): - radiobox.active = True - radiobox.radio_field.clear() - radiobox.radio_field_title.clear() +def activate_radio_box(tauon: Tauon) -> None: + tauon.radiobox.active = True + tauon.radiobox.radio_field.clear() + tauon.radiobox.radio_field_title.clear() def new_playlist_colour_callback(tauon: Tauon) -> list[int]: if tauon.gui.radio_view: @@ -36688,27 +36722,6 @@ def toggle_auto_artist_dl(mode: int = 0) -> bool | 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 @@ -40402,7 +40415,6 @@ def SMTC_button_callback(button: int) -> None: QuickThumbnail.renderer = holder.renderer - strings = Strings() signal.signal(signal.SIGINT, signal_handler) if system == "Windows" or msys: @@ -40415,10 +40427,8 @@ def SMTC_button_callback(button: int) -> None: tauon = Tauon( holder=holder, bag=bag, - strings=strings, gui=gui) radiobox = tauon.radiobox - tauon.dummy_track = radiobox.dummy_track star_store=tauon.star_store pctl = tauon.pctl pctl.default_playlist = multi_playlist[0].playlist_ids @@ -41750,7 +41760,7 @@ def dev_mode_disable_save_state() -> None: tauon.remote_limited = False # -------------------------------------------------------------- - pref_box = Over(bag=bag, gui=gui) + pref_box = tauon.pref_box bottom_bar_ao1 = BottomBarType_ao1(bag=bag, gui=gui) mini_mode = MiniMode(bag=bag, gui=gui) @@ -41803,7 +41813,7 @@ def dev_mode_disable_save_state() -> None: tauon.dl_mon = dl_mon dl_menu.add(MenuItem("Dismiss", dismiss_dl)) - fader = Fader(tauon=tauon) + fader = tauon.fader edge_playlist2 = EdgePulse2() bottom_playlist2 = EdgePulse2() gallery_pulse_top = EdgePulse2() @@ -44892,20 +44902,23 @@ def dev_mode_disable_save_state() -> None: 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) + 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) + 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) + 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) if inp.right_click and tauon.coll( (window_size[0] - gui.rspw, gui.panelY + 25 * gui.scale, gui.rspw, window_size[1] - (gui.panelBY + gui.panelY))): diff --git a/src/tauon/t_modules/t_webserve.py b/src/tauon/t_modules/t_webserve.py index 98b3e2406..96bdc600d 100644 --- a/src/tauon/t_modules/t_webserve.py +++ b/src/tauon/t_modules/t_webserve.py @@ -34,7 +34,7 @@ from tauon.t_modules.t_extra import Timer if TYPE_CHECKING: - from tauon.t_modules.t_main import AlbumArt, GuiVar, PlayerCtl, Prefs, Tauon, TrackClass + from tauon.t_modules.t_main import AlbumArt, GuiVar, PlayerCtl, Prefs, Strings, Tauon, TrackClass def send_file(path: str, mime: str, server) -> None: @@ -75,7 +75,7 @@ def send_file(path: str, mime: str, server) -> None: break server.wfile.write(data) -def webserve(pctl: PlayerCtl, prefs: Prefs, gui: GuiVar, album_art_gen: AlbumArt, install_directory: str, strings, tauon: Tauon) -> int | None: +def webserve(pctl: PlayerCtl, prefs: Prefs, gui: GuiVar, album_art_gen: AlbumArt, install_directory: str, strings: Strings, tauon: Tauon) -> int | None: if prefs.enable_web is False: return 0 @@ -231,7 +231,7 @@ class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): logging.exception("Failed starting radio page server!") -def webserve2(pctl: PlayerCtl, prefs: Prefs, gui: GuiVar, album_art_gen: AlbumArt, install_directory: str, strings, tauon: Tauon) -> None: +def webserve2(pctl: PlayerCtl, prefs: Prefs, gui: GuiVar, album_art_gen: AlbumArt, install_directory: str, strings: Strings, tauon: Tauon) -> None: play_timer = Timer() From 56ac4c88a39364e854e9e8467089661c82fd8e1b Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 21:58:59 +0100 Subject: [PATCH 11/13] Remove more globals --- src/tauon/t_modules/t_bootstrap.py | 6 ++-- src/tauon/t_modules/t_main.py | 51 ++++++++++++------------------ 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/src/tauon/t_modules/t_bootstrap.py b/src/tauon/t_modules/t_bootstrap.py index 01417be42..57bf88979 100644 --- a/src/tauon/t_modules/t_bootstrap.py +++ b/src/tauon/t_modules/t_bootstrap.py @@ -7,7 +7,7 @@ from collections.abc import Callable from io import TextIOWrapper from pathlib import Path - from sdl2 import render, video + from sdl2 import SDL_Renderer, SDL_Window from tauon.__main__ import LogHistoryHandler @@ -15,8 +15,8 @@ class Holder: """Class that holds variables for forwarding them from __main__.py to t_main.py""" - t_window: video.LP_SDL_Window # SDL_CreateWindow() return type - renderer: render.LP_SDL_Renderer # SDL_CreateRenderer() return type + t_window: SDL_Window # SDL_CreateWindow() return type + renderer: SDL_Renderer # SDL_CreateRenderer() return type logical_size: list[int] # X Y res window_size: list[int] # X Y res maximized: bool diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index ce74b09b0..b66a055ff 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -86,6 +86,7 @@ from dataclasses import dataclass from PIL import Image, ImageDraw, ImageEnhance, ImageFilter from sdl2 import ( + SDL_Renderer, SDL_BLENDMODE_BLEND, SDL_BLENDMODE_NONE, SDL_BUTTON_LEFT, @@ -568,6 +569,7 @@ def __init__(self, bag: Bag, tracklist_texture_rect: SDL_Rect, tracklist_texture self.s4_spec = [0] * 45 self.update_spec = 0 + self.new_playlist_cooldown = False self.playlist_hold_position = 0 self.playlist_hold = False self.selection_stage = 0 @@ -1676,6 +1678,7 @@ def __init__(self, tauon: Tauon): self.left_time = 0 self.left_index = 0 self.player_volume: float = self.bag.volume + self.volume_store: float = 50 # Used to save the previous volume when muted self.new_time = 0 self.time_to_get = [] self.a_time = 0 @@ -2162,12 +2165,11 @@ def show_current( return 0 def toggle_mute(self) -> None: - global volume_store if self.player_volume > 0: - volume_store = self.player_volume + self.volume_store = self.player_volume self.player_volume = 0 else: - self.player_volume = volume_store + self.player_volume = self.volume_store self.set_volume() @@ -14763,7 +14765,6 @@ def update(self): # self.seek_bar_size[0] = window_size[0] def render(self): - global volume_store global clicked window_size = self.window_size @@ -15061,10 +15062,10 @@ def render(self): self.volume_bar_size[1] + 20 * gui.scale)): if pctl.player_volume > 0: - volume_store = pctl.player_volume + pctl.volume_store = pctl.player_volume pctl.player_volume = 0 else: - pctl.player_volume = volume_store + pctl.player_volume = pctl.volume_store pctl.set_volume() @@ -15608,7 +15609,6 @@ def update(self): # self.seek_bar_size[0] = window_size[0] def render(self): - global volume_store global clicked ddt.rect_a((0, self.window_size[1] - self.gui.panelBY), (self.window_size[0], self.gui.panelBY), colours.bottom_panel_colour) @@ -15673,10 +15673,10 @@ def render(self): if inp.right_click and tauon.coll((h_rect[0], h_rect[1], h_rect[2] + 50 * gui.scale, h_rect[3])): if inp.right_click: if pctl.player_volume > 0: - volume_store = pctl.player_volume + pctl.volume_store = pctl.player_volume pctl.player_volume = 0 else: - pctl.player_volume = volume_store + pctl.player_volume = pctl.volume_store pctl.set_volume() @@ -23639,7 +23639,7 @@ class Bag: dirs: Directories prefs: Prefs formats: Formats - renderer: renderer + renderer: SDL_Renderer ddt: TDraw fonts: Fonts tls_context: ssl.SSLContext @@ -23919,8 +23919,8 @@ def auto_size_columns(): gui.pl_update += 1 update_set() -def set_colour(colour): - SDL_SetRenderDrawColor(renderer, colour[0], colour[1], colour[2], colour[3]) +def set_colour(colour: list[int]) -> None: + SDL_SetRenderDrawColor(tauon.bag.renderer, colour[0], colour[1], colour[2], colour[3]) def get_themes(dirs: Directories, deco: bool = False) -> list[str] | dict[str, str]: themes: list[str] = [] # full, name @@ -38164,8 +38164,6 @@ def update_layout_do(tauon: Tauon): # input.mouse_click = False - global renderer - if prefs.spec2_colour_mode == 0: prefs.spec2_base = [10, 10, 100] prefs.spec2_multiply = [0.5, 1, 1] @@ -38427,14 +38425,13 @@ def update_layout_do(tauon: Tauon): # -------------------------------------------------------------------- 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.tracklist_texture_rect = SDL_Rect(0, 0, gui.max_window_tex, gui.max_window_tex) - + renderer = tauon.bag.renderer SDL_DestroyTexture(gui.tracklist_texture) SDL_RenderClear(renderer) gui.tracklist_texture = SDL_CreateTexture( @@ -38786,8 +38783,6 @@ def is_level_zero(include_menus: bool = True) -> bool: and not trans_edit_box.active def drop_file(target): - global new_playlist_cooldown - if system != "windows" and sdl_version >= 204: gmp = get_global_mouse() gwp = get_window_position() @@ -38809,7 +38804,7 @@ def drop_file(target): gui.drop_playlist_target = 0 #logging.info(event.drop) - if i_y < gui.panelY and not new_playlist_cooldown and gui.mode == 1: + if i_y < gui.panelY and not gui.new_playlist_cooldown and gui.mode == 1: x = tauon.top_panel.tabs_left_x for tab in tauon.top_panel.shown_tabs: wid = tauon.top_panel.tab_text_spaces[tab] + tauon.top_panel.tab_extra_width @@ -38825,15 +38820,13 @@ def drop_file(target): x += wid else: logging.info("MISS") - if new_playlist_cooldown: + if gui.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 - + gui.new_playlist_cooldown = True elif gui.lsp and gui.panelY < i_y < window_size[1] - gui.panelBY and i_x < gui.lspw and gui.mode == 1: - y = gui.panelY y += 5 * gui.scale y += tauon.playlist_box.tab_h + tauon.playlist_box.gap @@ -38848,12 +38841,12 @@ def drop_file(target): break y += tauon.playlist_box.tab_h + tauon.playlist_box.gap else: - if new_playlist_cooldown: + if gui.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 + gui.new_playlist_cooldown = True else: @@ -38883,7 +38876,7 @@ def drop_file(target): inp.mouse_down = False inp.drag_mode = False -def main(holder: Holder): +def main(holder: Holder) -> None: t_window = holder.t_window renderer = holder.renderer logical_size = holder.logical_size @@ -39461,7 +39454,6 @@ def main(holder: Holder): b_info_y = int(window_size[1] * 0.7) # For future possible panel below playlist - volume_store = 50 # Used to save the previous volume when muted # row_alt = False @@ -42075,7 +42067,7 @@ def dev_mode_disable_save_state() -> None: key_end_press = False inp.mouse_wheel = 0 pref_box.scroll = 0 - new_playlist_cooldown = False + gui.new_playlist_cooldown = False input_text = "" inp.level_2_enter = False @@ -42649,13 +42641,12 @@ def dev_mode_disable_save_state() -> None: if sleep_timer.get() > 2: SDL_WaitEventTimeout(None, 1000) continue - else: power = 0 gui.pl_update = min(gui.pl_update, 2) - new_playlist_cooldown = False + gui.new_playlist_cooldown = False if prefs.auto_extract and prefs.monitor_downloads: dl_mon.scan() From 4bec4549329db50336f7a36eebe1828887fa8f52 Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 22:13:09 +0100 Subject: [PATCH 12/13] Remove more globals --- src/tauon/t_modules/t_main.py | 222 ++++++++++++++---------------- src/tauon/t_modules/t_topchart.py | 4 +- 2 files changed, 102 insertions(+), 124 deletions(-) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index b66a055ff..8f7ac651d 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -696,6 +696,8 @@ def __init__(self, bag: Bag, tracklist_texture_rect: SDL_Rect, tracklist_texture self.show_hearts = True + self.search_index: int = 0 + self.cursor_is = 0 self.cursor_want = 0 # 0 standard @@ -2321,12 +2323,12 @@ def update_change(self) -> None: self.lfm_scrobbler.start_queue() - if (prefs.album_mode or not self.gui.rsp) and (self.gui.theme_name == "Carbon" or self.prefs.colour_from_image): + if (self.prefs.album_mode or not self.gui.rsp) and (self.gui.theme_name == "Carbon" or self.prefs.colour_from_image): target = self.playing_object() if target and self.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) + self.tauon.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: self.lfm_scrobbler.start_queue() @@ -4776,26 +4778,26 @@ def activate(self, in_reference: int = 0, position: list[int] | None = None) -> class GallClass: def __init__(self, tauon: Tauon, size: int = 250, save_out: bool = True) -> None: - self.gui = tauon.gui - self.prefs = tauon.prefs - self.search_over = tauon.search_over - 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 + self.gui = tauon.gui + self.prefs = tauon.prefs + self.search_over = tauon.search_over + self.album_art_gen = tauon.album_art_gen + 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) + sources = self.album_art_gen.get_sources(track_object) if len(sources) == 0: return False, 0 - offset = album_art_gen.get_offset(track_object.fullpath, sources) + offset = self.album_art_gen.get_offset(track_object.fullpath, sources) return sources[offset], offset def worker_render(self) -> None: @@ -5042,11 +5044,12 @@ def render(self, track: TrackClass, location, size=None, force_offset=None) -> b return False class ThumbTracks: - def __init__(self) -> None: - pass + def __init__(self, tauon: Tauon) -> None: + self.tauon = tauon + self.album_art_gen = tauon.album_art_gen def path(self, track: TrackClass) -> str: - source, offset = tauon.gall_ren.get_file_source(track) + source, offset = self.tauon.gall_ren.get_file_source(track) if source is False: # No art return None @@ -5059,7 +5062,7 @@ def path(self, track: TrackClass) -> str: if os.path.isfile(t_path): return t_path - source_image = album_art_gen.get_source_raw(0, 0, track, subsource=source) + source_image = self.album_art_gen.get_source_raw(0, 0, track, subsource=source) with Image.open(source_image) as im: if im.mode != "RGB": @@ -5122,6 +5125,7 @@ def __init__(self, holder: Holder, bag: Bag, gui: GuiVar): self.view_box = ViewBox(tauon=self) self.pref_box = Over(tauon=self) self.fader = Fader(tauon=self) + self.album_art_gen = AlbumArt(tauon=self) self.cache_directory: Path = bag.dirs.cache_directory self.user_directory: Path | None = bag.dirs.user_directory self.music_directory: Path | None = bag.dirs.music_directory @@ -5153,7 +5157,7 @@ def __init__(self, holder: Holder, bag: Bag, gui: GuiVar): self.pl_gen = pl_gen self.gall_ren = GallClass(tauon=self, size=bag.album_mode_art_size) self.QuickThumbnail = QuickThumbnail - self.thumb_tracks = ThumbTracks() + self.thumb_tracks = ThumbTracks(tauon=self) self.chunker = Chunker() self.thread_manager: ThreadManager | None = None # Avoid NameError self.thread_manager: ThreadManager = ThreadManager(tauon=self) @@ -5234,7 +5238,7 @@ def toggle_enable_web(self, mode: int = 0) -> bool | None: if prefs.enable_web and not gui.web_running: webThread = threading.Thread( - target=webserve, args=[pctl, prefs, gui, album_art_gen, str(install_directory), self.strings, self]) + target=webserve, args=[pctl, prefs, gui, self.album_art_gen, str(install_directory), self.strings, self]) webThread.daemon = True webThread.start() show_message(_("Web server starting"), _("External connections will be accepted."), mode="done") @@ -5250,7 +5254,7 @@ def toggle_enable_web(self, mode: int = 0) -> bool | None: 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), self.strings, self]) + target=webserve2, args=[pctl, prefs, gui, self.album_art_gen, str(install_directory), self.strings, self]) self.web_thread.daemon = True self.web_thread.start() self.web_running = True @@ -7263,7 +7267,8 @@ def __init__(self) -> None: self.format = "" class AlbumArt: - def __init__(self): + def __init__(self, tauon: Tauon) -> None: + self.gui = tauon.gui 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", @@ -7289,14 +7294,12 @@ def __init__(self): 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_image = self.get_source_raw(0, 0, track, subsource=subsource) self.downloaded_track = track self.download_in_progress = False - gui.update += 1 + self.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 @@ -7323,7 +7326,6 @@ def get_info(self, track_object: TrackClass) -> list[tuple[str, int, int, int, s 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 @@ -7453,7 +7455,6 @@ def fast_display(self, index, location, box, source: list[tuple[int, str]], offs return 0 def open_external(self, track_object: TrackClass) -> int: - index = track_object.index source = self.get_sources(track_object) @@ -7466,7 +7467,7 @@ def open_external(self, track_object: TrackClass) -> int: show_message(_("Saving network images not implemented")) return 0 if source[offset][0] > 0: - pic = album_art_gen.get_embed(track_object) + pic = self.get_embed(track_object) if not pic: show_message(_("Image save error."), _("No embedded album art."), mode="warning") return 0 @@ -7500,7 +7501,6 @@ def open_external(self, track_object: TrackClass) -> int: 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: @@ -7521,12 +7521,10 @@ 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 @@ -7536,7 +7534,6 @@ def get_offset(self, filepath: str, source: list[tuple[int, str]]) -> int: return folder_image_offsets[parent_folder] def get_embed(self, track: TrackClass): - # cached = self.embed_cached # if cached[0] == track: # #logging.info("used cached") @@ -7595,7 +7592,6 @@ def get_embed(self, track: TrackClass): 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: @@ -7606,7 +7602,6 @@ def get_source_raw(self, offset: int, sources: list[tuple[int, str]] | int, trac 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": @@ -7628,17 +7623,14 @@ def get_source_raw(self, offset: int, sources: list[tuple[int, str]] | int, trac 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 @@ -8315,9 +8307,7 @@ def __init__(self): 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: @@ -8333,7 +8323,7 @@ def worker(self) -> None: self.current_track_album = track.album try: - self.im = album_art_gen.get_blur_im(track) + self.im = tauon.album_art_gen.get_blur_im(track) except Exception: logging.exception("Blur blackground error") raise @@ -8404,7 +8394,7 @@ def display(self) -> None: self.a_texture = c self.a_rect = dst - self.a_type = album_art_gen.loaded_bg_type + self.a_type = tauon.album_art_gen.loaded_bg_type self.stage = 2 self.radio_meta = None @@ -11245,7 +11235,7 @@ def view2(self, x0, y0, w0, h0): 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() + tauon.album_art_gen.clear_cache() global update_layout y += 35 * gui.scale @@ -13815,16 +13805,16 @@ def render(self): 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)) + tauon.album_art_gen.display(tr, (window_size[0] - gui.panelY - 1, 0), (gui.panelY, gui.panelY)) if pctl.loading_in_progress or \ tauon.to_scan or \ tauon.cm_clean_db or \ - lastfm.scanning_friends or \ + tauon.lastfm.scanning_friends or \ tauon.after_scan or \ tauon.move_in_progress or \ tauon.plex.scanning or \ tauon.transcode_list or tauon.spot_ctl.launching_spotify or tauon.spot_ctl.spotify_com or tauon.subsonic.scanning or \ - tauon.koel.scanning or gui.sync_progress or lastfm.scanning_scrobbles: + tauon.koel.scanning or gui.sync_progress or tauon.lastfm.scanning_scrobbles: ddt.rect( (window_size[0] - (gui.panelY + 20), gui.panelY - gui.panelY2, gui.panelY + 25, gui.panelY2), colours.top_panel_background) @@ -14813,7 +14803,7 @@ def render(self): # 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])) + # tauon.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]) @@ -15625,7 +15615,7 @@ def render(self): # 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])) + # tauon.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]) @@ -15998,9 +15988,8 @@ def render(self): 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)) + tauon.album_art_gen.display(track, (0, 0), (w, w)) line1c = colours.mini_mode_text_1 line2c = colours.mini_mode_text_2 @@ -16220,9 +16209,8 @@ def render(self): track = pctl.playing_object() if track is not None: - # Render album art - album_art_gen.display(track, (0, 0), (h, h)) + tauon.album_art_gen.display(track, (0, 0), (h, h)) text_hit_area = (x1, 0, w, h) @@ -16383,7 +16371,7 @@ def render(self): 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)) + tauon.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 @@ -17776,8 +17764,8 @@ def draw(self, x, y, w, h, target_track=None, tight_border=False, default_border 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), gui.side_drag) - showc = album_art_gen.get_info(target_track) + result = tauon.album_art_gen.display(target_track, (rect[0], rect[1]), (box_w, box_h), gui.side_drag) + showc = tauon.album_art_gen.get_info(target_track) # Draw faint border on album art if tight_border: @@ -17805,12 +17793,9 @@ def draw(self, x, y, w, h, target_track=None, tight_border=False, default_border # Input for album art if target_track: - # Cycle images on click - if tauon.coll(gui.main_art_box) and inp.mouse_click is True and key_focused == 0: - - album_art_gen.cycle_offset(target_track) + tauon.album_art_gen.cycle_offset(target_track) if pctl.mpris: pctl.mpris.update(force=True) @@ -18137,7 +18122,7 @@ def start(self, station: RadioStation): self.dummy_track.date = "" pctl.radio_meta_on = "" - album_art_gen.clear_cache() + tauon.album_art_gen.clear_cache() if not tauon.test_ffmpeg(): prefs.auto_rec = False @@ -21467,7 +21452,7 @@ def l_panel(self, x, y, w, h, track, top_border: bool = True): if (inp.mouse_click or inp.right_click) and is_level_zero(False): if tauon.coll(border_rect): if inp.mouse_click: - album_art_gen.cycle_offset(target_track) + tauon.album_art_gen.cycle_offset(target_track) if inp.right_click: picture_menu.activate(in_reference=target_track) elif tauon.coll(rect): @@ -21478,11 +21463,11 @@ def l_panel(self, x, y, w, h, track, top_border: bool = True): 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])) + tauon.album_art_gen.display(track, (art_rect[0], art_rect[1]), (art_rect[2], art_rect[3])) tauon.fields.add(border_rect) if tauon.coll(border_rect) and is_level_zero(True): - showc = album_art_gen.get_info(target_track) + showc = tauon.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) @@ -22522,7 +22507,7 @@ def render(self): 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])) + r = tauon.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: @@ -22677,7 +22662,6 @@ def render(self): index = gui.force_showcase_index track = pctl.master_library[index] else: - if pctl.playing_state == 3: track = radiobox.dummy_track else: @@ -22685,7 +22669,6 @@ def render(self): 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( @@ -22696,12 +22679,12 @@ def render(self): style_overlay.hole_punches.append(rect) # Draw album art in box - album_art_gen.display(track, (x, y), (box, box)) + tauon.album_art_gen.display(track, (x, y), (box, box)) # Click art to cycle if tauon.coll((x, y, box, box)): if inp.mouse_click is True: - album_art_gen.cycle_offset(track) + tauon.album_art_gen.cycle_offset(track) if inp.right_click: picture_menu.activate(in_reference=track) inp.right_click = False @@ -26334,8 +26317,7 @@ def img_slide_update_gall(value, pause: bool = True) -> None: prefs.thin_gallery_borders = False def clear_img_cache(delete_disk: bool = True) -> None: - global album_art_gen - album_art_gen.clear_cache() + tauon.album_art_gen.clear_cache() prefs.failed_artists.clear() prefs.failed_background_artists.clear() tauon.gall_ren.key_list = [] @@ -26395,7 +26377,7 @@ def clear_track_image_cache(track: TrackClass): tauon.gall_ren.key_list.remove(key) gui.halt_image_rendering = False - album_art_gen.clear_cache() + tauon.album_art_gen.clear_cache() def trunc_line(line: str, font: str, px: int, dots: bool = True) -> str: """This old function is slow and should be avoided""" @@ -27703,7 +27685,7 @@ def save_embed_img(track_object: TrackClass): return try: - pic = album_art_gen.get_embed(track_object) + pic = tauon.album_art_gen.get_embed(track_object) if not pic: show_message(_("Image save error."), _("No embedded album art found file."), mode="warning") @@ -27733,7 +27715,7 @@ def save_embed_img(track_object: TrackClass): 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) + info = tauon.album_art_gen.get_info(track_object) if info is None: return [colours.menu_text_disabled, colours.menu_background, None] @@ -27749,12 +27731,12 @@ def open_image_disable_test(track_object: TrackClass): 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) + tauon.album_art_gen.open_external(track_object) 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) + info = tauon.album_art_gen.get_info(track_object) if info is None: return [colours.menu_text_disabled, colours.menu_background, None] @@ -27767,7 +27749,7 @@ def extract_image_deco(track_object: TrackClass): return [line_colour, colours.menu_background, None] def cycle_image_deco(track_object: TrackClass): - info = album_art_gen.get_info(track_object) + info = tauon.album_art_gen.get_info(track_object) if pctl.playing_state != 0 and (info is not None and info[1] > 1): line_colour = colours.menu_text @@ -27779,7 +27761,7 @@ def cycle_image_deco(track_object: TrackClass): 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) + info = tauon.album_art_gen.get_info(track_object) if info is not None and info[1] > 1: line_colour = colours.menu_text @@ -27791,12 +27773,12 @@ def cycle_image_gal_deco(track_object: TrackClass): 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) + tauon.album_art_gen.cycle_offset(track_object) 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) + tauon.album_art_gen.cycle_offset_reverse(track_object) def dl_art_deco(track_object: TrackClass): if type(track_object) is int: @@ -28044,9 +28026,9 @@ def remove_embed_picture(track_object: TrackClass, dry: bool = True) -> int | No def delete_file_image(track_object: TrackClass): try: - showc = album_art_gen.get_info(track_object) + showc = tauon.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] + source = tauon.album_art_gen.get_sources(track_object)[showc[2]][1] os.remove(source) # clear_img_cache() clear_track_image_cache(track_object) @@ -28058,7 +28040,7 @@ def delete_file_image(track_object: TrackClass): 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) + info = tauon.album_art_gen.get_info(track_object) text = _("Delete Image File") line_colour = colours.menu_text @@ -28085,7 +28067,7 @@ def delete_track_image(track_object: TrackClass): track_object = pctl.master_library[track_object] if track_object.is_network: return - info = album_art_gen.get_info(track_object) + info = tauon.album_art_gen.get_info(track_object) if info and info[0] == 0: delete_file_image(track_object) elif info and info[0] == 1: @@ -33343,9 +33325,7 @@ def check_auto_update_okay(code, pl=None): "tmix\"" not in code and "r" not in cmds) -def switch_playlist(number, cycle=False, quiet=False): - global search_index - +def switch_playlist(number: int, cycle: bool = False, quiet: bool = False) -> None: # Close any active menus # for instance in Menu.instances: # instance.active = False @@ -33362,7 +33342,7 @@ def switch_playlist(number, cycle=False, quiet=False): gui.previous_playlist_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int gui.pl_update = 1 - search_index = 0 + gui.search_index = 0 gui.column_d_click_on = -1 gui.search_error = False if quick_search_mode: @@ -35922,7 +35902,6 @@ def cache_paths() -> tuple[dict, dict]: #logging.info(pctl.master_library) - global album_art_gen global to_got global to_get @@ -36193,7 +36172,7 @@ def cache_paths() -> tuple[dict, dict]: 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")) + tauon.album_art_gen.save_thumb(pctl.get_track(folder_items[0]), (1080, 1080), str(output_dir / "cover")) #logging.info(transcode_list[0]) @@ -37005,7 +36984,7 @@ def reload_backend() -> None: def gen_chart() -> None: try: - topchart = t_topchart.TopChart(tauon, album_art_gen) + topchart = t_topchart.TopChart(tauon) tracks = [] @@ -38782,7 +38761,7 @@ def is_level_zero(include_menus: bool = True) -> bool: and not gui.box_over \ and not trans_edit_box.active -def drop_file(target): +def drop_file(target: str) -> None: if system != "windows" and sdl_version >= 204: gmp = get_global_mouse() gwp = get_window_position() @@ -39574,7 +39553,6 @@ def main(holder: Holder) -> None: playlist_active: int = 0 quick_search_mode = False - search_index = 0 # ---------------------------------------- # Playlist right click menu @@ -40861,7 +40839,7 @@ def update(self, force=False): temp_dest = SDL_Rect(0, 0) - album_art_gen = AlbumArt() + album_art_gen = tauon.album_art_gen # 0 - blank # 1 - preparing first @@ -45973,7 +45951,7 @@ def dev_mode_disable_save_state() -> None: ddt.rect(rect, colours.box_background) if len(input_text) > 0: - search_index = -1 + gui.search_index = -1 if inp.backspace_press and search_text.text == "": quick_search_mode = False @@ -46069,21 +46047,21 @@ def dev_mode_disable_save_state() -> None: gui.pl_update = 1 if gui.force_search: - search_index = 0 + gui.search_index = 0 if inp.backspace_press: - search_index = 0 + gui.search_index = 0 if len(search_text.text) > 0 and search_text.text[0] != "/": - oi = search_index + oi = gui.search_index - while search_index < len(pctl.default_playlist) - 1: - search_index += 1 - if search_index > len(pctl.default_playlist) - 1: - search_index = 0 + while gui.search_index < len(pctl.default_playlist) - 1: + gui.search_index += 1 + if gui.search_index > len(pctl.default_playlist) - 1: + gui.search_index = 0 search_terms = search_text.text.lower().split() - tr = pctl.get_track(pctl.default_playlist[search_index]) + tr = pctl.get_track(pctl.default_playlist[gui.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() @@ -46093,9 +46071,9 @@ def dev_mode_disable_save_state() -> None: if all(word in line for word in search_terms): - pctl.selected_in_playlist = search_index - if len(pctl.default_playlist) > 10 and search_index > 10: - pctl.playlist_view_position = search_index - 7 + pctl.selected_in_playlist = gui.search_index + if len(pctl.default_playlist) > 10 and gui.search_index > 10: + pctl.playlist_view_position = gui.search_index - 7 logging.debug("Position changed by search") else: pctl.playlist_view_position = 0 @@ -46107,7 +46085,7 @@ def dev_mode_disable_save_state() -> None: break else: - search_index = oi + gui.search_index = oi if len(input_text) > 0 or gui.force_search: gui.search_error = True if inp.key_down_press: @@ -46125,25 +46103,25 @@ def dev_mode_disable_save_state() -> None: and not inp.key_ralt: gui.pl_update = 1 - oi = search_index + oi = gui.search_index - while search_index > 1: - search_index -= 1 - search_index = min(search_index, len(pctl.default_playlist) - 1) + while gui.search_index > 1: + gui.search_index -= 1 + gui.search_index = min(gui.search_index, len(pctl.default_playlist) - 1) search_terms = search_text.text.lower().split() - line = pctl.master_library[pctl.default_playlist[search_index]].title.lower() + \ - pctl.master_library[pctl.default_playlist[search_index]].artist.lower() \ - + pctl.master_library[pctl.default_playlist[search_index]].album.lower() + \ - pctl.master_library[pctl.default_playlist[search_index]].filename.lower() + line = pctl.master_library[pctl.default_playlist[gui.search_index]].title.lower() + \ + pctl.master_library[pctl.default_playlist[gui.search_index]].artist.lower() \ + + pctl.master_library[pctl.default_playlist[gui.search_index]].album.lower() + \ + pctl.master_library[pctl.default_playlist[gui.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(pctl.default_playlist) > 10 and search_index > 10: - pctl.playlist_view_position = search_index - 7 + pctl.selected_in_playlist = gui.search_index + if len(pctl.default_playlist) > 10 and gui.search_index > 10: + pctl.playlist_view_position = gui.search_index - 7 logging.debug("Position changed by search") else: pctl.playlist_view_position = 0 @@ -46151,13 +46129,13 @@ def dev_mode_disable_save_state() -> None: pctl.show_selected() break else: - search_index = oi + gui.search_index = oi edge_playlist2.pulse() - if inp.key_return_press is True and search_index > -1: + if inp.key_return_press is True and gui.search_index > -1: gui.pl_update = 1 - pctl.jump(pctl.default_playlist[search_index], search_index) + pctl.jump(pctl.default_playlist[gui.search_index], gui.search_index) if prefs.album_mode: goto_album(pctl.playlist_playing_position) quick_search_mode = False diff --git a/src/tauon/t_modules/t_topchart.py b/src/tauon/t_modules/t_topchart.py index 83e914ee8..69a4751b8 100644 --- a/src/tauon/t_modules/t_topchart.py +++ b/src/tauon/t_modules/t_topchart.py @@ -36,12 +36,12 @@ class TopChart: - def __init__(self, tauon: Tauon, album_art_gen: AlbumArt) -> None: + def __init__(self, tauon: Tauon) -> None: self.pctl = tauon.pctl self.cache_dir = tauon.cache_directory self.user_dir = tauon.user_directory - self.album_art_gen = album_art_gen + self.album_art_gen = tauon.album_art_gen def generate( self, tracks: list[TrackClass], bg: tuple[int, int, int] = (10,10,10), rows: int = 3, columns: int = 3, From d2c5d2ce9fb9053cef2703879824e0f368bde82c Mon Sep 17 00:00:00 2001 From: Martin Rys <martin@rys.rs> Date: Sat, 8 Feb 2025 22:23:53 +0100 Subject: [PATCH 13/13] Burn another global --- src/tauon/t_modules/t_main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index 8f7ac651d..ab7275587 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -33148,15 +33148,14 @@ def enter_radio_view(tauon: Tauon) -> None: tauon.gui.update_layout() def standard_size(tauon: Tauon) -> None: - global window_size global update_layout prefs.album_mode = False tauon.gui.rsp = True - window_size = window_default_size + tauon.bag.window_size = window_default_size SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - gui.rspw = 80 + int(window_size[0] * 0.18) + gui.rspw = 80 + int(tauon.bag.window_size[0] * 0.18) update_layout = True bag.album_mode_art_size = 130 # clear_img_cache()